# LLM APIs: End-to-End Business Logic & Specifications

This document defines the internal business logic, calculations, and rules for the APIs in the [llm](file:///c:/laragon/www/tjs/app_api/llm) directory.

---

## 1. Global Authentication Failure (HTTP 401)

All endpoints require authentication by including the [auth.php](file:///c:/laragon/www/tjs/app_api/llm/auth.php) gateway.

If the query parameter `token` is missing or does not match the static key `84aux1j1yuliqy8i95oaprjy`, execution terminates immediately:

*   **HTTP Status**: `401 Unauthorized`
*   **Response Body**:
    ```json
    {
      "status": "Failed",
      "message": "Unauthorized"
    }
    ```

### Edge Cases & Vulnerabilities
*   **Missing Token**: If the request does not specify `?token=...`, the parameter resolves as an empty string (`$_GET['token'] ?? ''`), triggering a 401 error.
*   **Security Vulnerability (GET Parameters)**: Passing tokens in the URL is insecure because they can be logged in web server logs, proxy logs, and browser histories.

---

## 2. API Business Logic Specification

### 2.1. Location-to-Branch Lookup: [cek_cabang.php](file:///c:/laragon/www/tjs/app_api/llm/cek_cabang.php)

Resolves geographic location keywords to their corresponding branch code (`kode_cabang`) and branch name.

#### Core Business Rules
*   **Tiered Geographic Resolution (Search Hierarchy)**: 
    Instead of searching all geographic levels simultaneously, lookup follows a strict priority order:
    1.  **Primary Search (Kabupaten)**: Checks if the search term matches any regency (`A.kabupaten LIKE '%$search%'`). If matches are found, it returns them immediately.
    2.  **Secondary Search (Propinsi/Kecamatan)**: If and only if no kabupaten matches are found, it falls back to querying matches against province (`A.propinsi`) or district (`A.kecamatan`).
*   **Active Branch Association**: Joins the resolved location with `tblmastercabang` to retrieve the registered branch name where `status = 1`.
*   **Branch Name Fallback**: If the associated branch name does not exist or is inactive, the API falls back to using the raw `kode_cabang` as the branch name.
*   **Search Input Filtering**: The `search` parameter is trimmed and sanitized. If it evaluates to empty or the string `"0"`, the request is rejected with `400 Bad Request`.
*   **Response Limit**: Constrained to return a maximum of 50 unique geographic matches sorted alphabetically by regency.

---

### 2.2. Customer Profile & Address Lookup: [datadiri.php](file:///c:/laragon/www/tjs/app_api/llm/datadiri.php)

Retrieves a customer's profile details and associated shipping address book.

#### Core Business Rules
*   **Indonesian Phone Number Normalization**: Converts various input phone number formats to a standardized E.164-style format starting with `+62`:
    1.  Strips spaces (` `), parentheses (`(`, `)`), and dots (`.`).
    2.  If the number starts with `+62`, it is kept.
    3.  If the number starts with `0` (local Indonesian format), `0` is replaced with `+62` (e.g. `0812...` $\rightarrow$ `+62812...`).
    4.  If it starts with any other characters or formats, it prepends `+62`.
*   **Active Registration Check (`aktif = 1`)**: Only retrieves customer profiles where `aktif = 1` from `tblregister`. (Note: `tblregister.status` is not used yet; filtering relies entirely on the `aktif` column for active accounts).
*   **Primary Address Priority**: Retrieves all active addresses (`status = 1`) associated with the customer, sorting primary addresses (`keterangan = 'primary'`) to the top of the list.
*   **Geographic Enrichment**: Joins each address with the master regional JNE table (`tbljne`) on `idkp = jnecode` to map the specific province, regency, and district associated with the shipping address.

---

### 2.3. Product Detail & Price Lookup: [detail_barang.php](file:///c:/laragon/www/tjs/app_api/llm/detail_barang.php)

Retrieves active catalog product details, images, and branch-specific pricing tiers for a specific product code (`kode_barang`) and branch (`kode_cabang`).

#### How It Works (Step-by-Step E2E Business Flow)
1.  **Branch Check & Filtering**:
    *   Verifies if the requested `kode_cabang` is registered as an active branch in `tblmastercabang` where `status = 1`. If not found or inactive, terminates with a `404 Not Found` response.
2.  **Product Fetch & Inner Join**:
    *   Queries `tblmasterbarang` for the active product (`status = 1`) matching the exact `kode_barang`.
    *   Performs an `INNER JOIN` with `tblmasterharga` matching the product code and the requested `kode_cabang`. If the product is not registered or not active in the requested branch, terminates with a `404 Not Found` response.
    *   Performs a `LEFT JOIN` on `tblmasterbarang` as parent to fetch parent variation names (`Parent.nama_barang AS nama_varian`).
3.  **Product Images Fetch**:
    *   Retrieves all associated image paths from `tblmasterbarang_img` sorted by `urutan` ascending, mapped to absolute URLs: `https://trigunajayasentosaplastik.com/{imgpath}`.
4.  **Shopee Links Fetch**:
    *   Retrieves associated Shopee store links (`shopee_pack`, `shopee_gojek`, `shopee_jaskir`, `shopee_kurirtoko`) from `tblmasterbarang_link`.
5.  **Stock Location Resolution**:
    *   Resolves the transaction stock storage location:
        *   **Office Stock (`kode_kantor`)**: Resolved if the product acts as a parent bundle (has no conversion parent `konv_code` but child items reference it).
        *   **Warehouse Stock (`kode_gudang`)**: Resolved for standard, single-pack, and child items.
    *   Aggregates transaction quantities (`tbltransaksibarang` where `status = 1`) for the product in the resolved location to determine final branch stock quantity.
6.  **Markup Price Calculation**:
    *   Applies the branch-specific markup percentages (`persen_pack`, `persen_cargo_triguna`, `persen_pack_ol`, `persen_ol_tp`, `persen_ol_sp`, `persen_ol_lzd`) to the base price (`harga` from `tblmasterharga`) to compute final rounded pricing tiers.
    *   *Conversion quantity fallback*: If `konv_qty` is `<= 0`, it defaults to `1` to prevent division-by-zero errors.
7.  **Flat Output Format Assembly**:
    *   Constructs a flat product object including fields such as `detail_referral`, `uom` (defaulting to 'pcs'), `sku_gudang`, and `images` (array of strings). The object is wrapped in a single-element array under the `data` key.

---

### 2.4. Checkout & Free Shipping Simulation: [freeongkir.php](file:///c:/laragon/www/tjs/app_api/llm/freeongkir.php)

Simulates shipping costs, checks free shipping eligibility for Cargo Triguna, calculates the required add-on purchase if not eligible, and provides JNE Trucking (JTR) fallbacks.

> **v2 (2026-08-13)**: the endpoint is now a thin projection of the shared website-parity engine (see section 2.12) — same contract fields, values sourced from `helper/jaskir.php` (`jaskir_cargo_triguna` / `jaskir_cargo_jne`). `cargo_triguna_threshold`/`addon_needed` keep the original semantics (`threshold = round(ongkir / kali_cargo_triguna)`, addon = threshold − subtotal_cargo_triguna) computed from the website-parity ongkir. The step-by-step description below documents the v1 engine and remains accurate for destination resolution (steps 1–2); steps 3–6 are superseded by 2.12's pipeline.

#### Core Business Rules & Execution Steps

1.  **Coordinate & Input Resolution**:
    *   Attempts to parse and resolve coordinates from `kord` (which can be a Google Maps URL, shortlink, or raw coordinates).
    *   If `kord` is missing, but customer phone number (`nohp`) is provided, retrieves the primary address coordinates from `tblregister_alamat` where `status = 1` and `reg_id = customer_id`.
2.  **Optimized Geographical Branch Mapping**:
    To prevent high server CPU/DB load when scanning all 82,806 rows in `tbljne`:
    *   Query `tbljne` within a bounding box starting at `±0.1` degrees (approx. 11 km) of the resolved destination coordinates.
    *   If no matching rows are found, expand the search bounding box by `0.2` degrees progressively up to a maximum of `1.5` degrees.
    *   Compute the exact Haversine distance in PHP on only those candidate rows to find the geographically closest JNE area.
    *   Extract the resolved `jnecode`, regional details, coordinates, and associated serving branch `kode_cabang`.
    *   **Lookup Fallback**: If coordinates cannot be resolved, fall back to the explicit `jnecode` parameter or customer profile registered `jnecode`. If a branch is still not resolved or inactive, default to the `TGR` (Tangerang) branch.
3.  **Cart Calculations (Wholesale & Reseller Tiering)**:
    *   For each item in the cart, queries `tblmasterharga` for the serving branch `kode_cabang`.
    *   Calculates the wholesale quantity discount price based on quantity tiers (`mingrs1` to `mingrs5` and `hrggrs1` to `hrggrs5`).
    *   Based on the normal grand total, resolves the customer's reseller level and looks up reseller tier pricing limits (`limrsl` to `limrsl7` in the resolved branch, mapping to `harga_rsl` to `harga_rsl7`).
    *   Chooses the minimum of the wholesale quantity discount price and the reseller tier price as the final item price.
    *   Computes the totals: `subtotal`, `subtotal_cargo_triguna` (using `harga_cargo_triguna` from `tblmasterharga`), `subtotal_cargo_jne` (using `harga_cargo_jne`), `total_berat_bts`, and `total_berat_jne`.
4.  **Biteship Rate Resolution (Cargo Triguna)**:
    *   Queries Biteship pricing for the courier `grab` (Instant) from the branch coordinates to the resolved destination.
    *   If Grab Instant is unavailable or if the cost multiplied by `kali_grab_u_mobil_box` exceeds the branch limit `mobil_box_triguna_max`, queries Biteship for `deliveree` (Economy).
    *   Applies branch markups (`kali_grab_u_cargo` or `kali_deliveree_u_cargo`) to determine the raw Cargo Triguna shipping cost.
    *   If the cost exceeds the branch maximum `cargo_triguna_max`, the route is considered unreachable (`cargo_triguna_reachable = 0`).
5.  **JNE JTR Fallback Rate Resolution**:
    *   Queries JNE pricedev API for `JTR` (JNE Trucking) using the resolved `jnecode` and computed JNE weight.
    *   Applies province-specific JTR discounts and branch-subsidized Triguna cargo discounts (`kali_cargo_jne` and `cargo_jne_disc_max`).
6.  **Free Shipping Threshold Evaluation**:
    *   If Cargo Triguna is reachable, sets the threshold: `cargo_triguna_threshold = round(ongkir_cargo_triguna / kali_cargo_triguna)`.
    *   If `subtotal_cargo_triguna >= threshold`, sets `cargo_triguna_freeongkir_achieved = 1`.
### 2.5. Track Order: [lacak_pesanan.php](file:///c:/laragon/www/tjs/app_api/llm/lacak_pesanan.php)

Queries order status and compiles corresponding tracking history from JNE, Biteship, or Kurir Triguna.

#### Core Business Rules & Execution Steps

1.  **Input Parameters**:
    *   Accepts optional `nohp` (WhatsApp phone number) and `no_inv` (Invoice number).
    *   If both are missing, rejects request with `400 Bad Request`.
2.  **Order Selection & Status Filtering**:
    *   If `no_inv` is provided, queries matching order from `tblorder_master` directly with no status filtering.
    *   If `nohp` is provided, normalizes the phone number and queries the active registered customer profile (`tblregister` where `aktif = 1`). Finds the customer's orders in `tblorder_master` matching `status IN (1, 3, 4)` (active/running or arrived orders, excluding completed `status = 5` and cancelled/rejected `status = 0 or 2` orders).
3.  **Status Classification Mapping**:
    *   `1` $\rightarrow$ `order_baru` (Masih order baru belum diproses)
    *   `3` $\rightarrow$ `diproses` (Masih diproses belum dikirim)
    *   `4` and `tgl_terima = '1900-01-01 00:00:00'` $\rightarrow$ `dikirim` (Orderan sedang dikirim)
    *   `4` and `tgl_terima != '1900-01-01 00:00:00'` $\rightarrow$ `tiba` (Orderan telah tiba)
    *   `5` $\rightarrow$ `selesai` (Pesanan selesai)
    *   `0` or `2` $\rightarrow$ `batal` (Pesanan dibatalkan / ditolak)
4.  **Carrier-Specific Data Tracking Construction**:
    *   **JNE Trucking (JTR) / JNE Reguler**: Fetches `jne_user` and `jne_api` credentials for the order's branch. Queries corporate resis in `tblorder_resi_corporate` (falls back to the order's main resi if none exist). For each resi, queries `jne_trace_tracking()` and formats the timeline, appending proof photos or signature links if present.
    *   **Kurir Triguna / Cargo Triguna / Sameday Car**: Fetches the driver (sopir) name and phone number from `tblmastersopir` and estimated arrival time (`eta_kurir`).
    *   **Biteship Couriers (Grab, Deliveree, Gojek)**: Queries driver details and tracking links from the latest `tblorder_biteship` entry.
    *   **Ambil Sendiri (Sendiri)**: If arrived or completed, shows the receiver name and signature photo links (absolute URLs resolved dynamically using the request's protocol and host).

---

### 2.6. Gold Points Lookup: [poin_emas.php](file:///c:/laragon/www/tjs/app_api/llm/poin_emas.php)

Queries active customer gold points by phone number.

#### Core Business Rules & Execution Steps

1. **Input Parameters**:
   * Accepts `nohp` (WhatsApp phone number).
   * If missing, rejects request with `400 Bad Request`.
2. **Customer Validation**:
   * Normalizes the phone number using `normalizePhoneNum` helper.
   * Checks if an active customer profile exists in `tblregister` where `aktif = 1`.
   * If not registered or found, returns `{"status": "Success", "data": []}`.
3. **Gold Points Summation**:
   * Queries `tblorder_poin_emas` for `IFNULL(SUM(poin), 0)` where `status <> '0'` and `reg_id` is matched.
   * Returns `{"status": "Success", "data": [{"poin_emas": X}]}`.
### 2.7. Self-Pickup OTP Retrieval: [get_order_otp.php](file:///c:/laragon/www/tjs/app_api/llm/get_order_otp.php)

Queries the self-pickup (Ambil Sendiri) OTP code for an order.

#### Core Business Rules & Execution Steps

1. **Input Parameters**:
   * Accepts `no_inv` (or `no_invoice`) and `no_wa` (or `nohp`).
   * If either is missing, rejects request with `400 Bad Request`.
2. **Customer & Phone Validation**:
   * Normalizes the input phone number using `normalizePhoneNum` helper.
   * Queries `tblorder_master` joined with `tblregister` where `no_inv` is matched.
   * If the order is not found, or the associated customer's normalized phone number does not match the input phone number, returns a standard customer warning message with `422 Unprocessable Entity`:
     `Invoice untuk {$no_wa} tidak ditemukan. Perhatian: untuk meminta kode ulang harus pakai no_wa yang sama dengan saat memesan.`
3. **Shipping Method Validation**:
   * Inspects the order's shipping method using `order_get_jakir_name`.
   * If the shipping carrier is not "Sendiri" (Ambil Sendiri), rejects the request with a `422 Unprocessable Entity` and message: `Pesanan ini tidak menggunakan metode Ambil Sendiri.`
4.  **OTP Existence Check**:
    *   If the OTP code in the database is empty, rejects the request with a `422 Unprocessable Entity` and message: `Kode OTP belum dibuat untuk pesanan ini. Silakan hubungi admin.`
    *   Otherwise, returns the minimal payload: `{"status": "Success", "data": {"no_inv": "...", "otp": "..."}}`.

### 2.8. Parent Variation Lookup: [varian_barang.php](file:///c:/laragon/www/tjs/app_api/llm/varian_barang.php)

Searches for variation parent products by name or code. Space-separated words are treated as distinct keywords that must all match in the parent product name.

#### Core Business Rules & Execution Steps

1. **Input Parameters**:
   * Accepts `varian` (required): Keyword or product code to search parent variations.
2. **Parent Resolution**:
   * Checks for products where `status = 1`, `var_kode = ''`, and the `kode_barang` is referenced by at least one child product (`var_kode <> ''`). Matches all space-separated keywords in the name.
3. **Response Payload**:
   * Returns a flat array containing `kode_barang`, `nama_barang`, and the absolute product `url`.

### 2.9. Product Catalog Search: [cari_barang.php](file:///c:/laragon/www/tjs/app_api/llm/cari_barang.php)

Performs a search on active catalog products by name (per-word) with a description-phrase fallback, and groups active branches.

#### Core Business Rules & Execution Steps

1. **Input Parameters**:
   * Accepts `nama_barang` (required): Search query.
   * Accepts `kode_cabang` (optional): Filter to restrict matches to a specific branch.
2. **Name Matching (per word, AND, order-agnostic)**:
   * `nama_barang` is split on whitespace into keywords. Each keyword must independently appear in `MB.nama_barang` (`LIKE '%keyword%'`), all AND'd together, so `"nasi box"` and `"box nasi"` match the same set of names.
3. **Description Fallback (whole phrase)**:
   * The full, unsplit search string is also matched against `MB.detail` as one literal phrase (`LIKE '%nama_barang%'`), not split into keywords.
4. **Row Selection & Dedup Precedence**:
   * A product qualifies if it matches by name OR by detail. Each product appears at most once: if it matches by name (regardless of whether it also matches by detail), `matched_by = "nama_barang"`; otherwise `matched_by = "detail"`. Name matches are ordered before detail-only matches.
5. **Branch Visibility Grouping**:
   * Collects unique matching products. For each product, aggregates all branch codes (`kode_cabang`) where the product is visible (`tampil = 1` in `tblmasterharga`).
6. **Response Fields**:
   * Adds `matched_by` (`"nama_barang"` | `"detail"`) to each row, alongside the existing `kode_barang`, `nama_barang`, `var_kode`, `nama_varian`, `kode_cabang`.

---

### 2.10. JNE Destination Code Search: [jnecode.php](file:///c:/laragon/www/tjs/app_api/llm/jnecode.php)

Resolves customer locations to JNE destination codes (`jnecode`) from the `tbljne` master table (kelurahan granularity, ~82,000 rows). Registration stores this code as `idkp` on `tblregister` and `tblregister_alamat`.

#### Core Business Rules & Execution Steps

1. **Input Parameters** (all optional, trimmed and sanitized):
   * `search`: Global keyword — matched with `OR` across `propinsi`, `kabupaten`, `kecamatan`, `kelurahan`, `kodepos`, and `jnecode` (all `LIKE '%term%'`).
   * `propinsi`, `kabupaten`, `kecamatan`, `kelurahan`, `kodepos`: Field-specific partial-match filters.
2. **Filter Composition**:
   * All provided parameters (including `search`) are combined with `AND`. Empty-string parameters are treated as absent.
3. **Two Execution Modes**:
   * **Filtered mode** (≥1 parameter present): Returns at most **5** `DISTINCT` rows (`LIMIT 5`), sorted by `propinsi`, `kabupaten`, `kecamatan`, `kelurahan` ascending. No match → `data: []` (soft "not found", still HTTP 200).
   * **Dump mode** (no parameters): Returns **all** rows with no limit. The response (~13 MB) is streamed row-by-row (`MYSQLI_USE_RESULT`, incremental `json_encode` per row) to avoid buffering the full result set in PHP memory; `set_time_limit(300)` guards the long-running response.
4. **Row Shape**:
   * `jnecode`, `propinsi`, `kabupaten`, `kecamatan`, `kelurahan`, `kodepos`, `kode_cabang`. Note that one `jnecode` typically covers several kelurahan rows.

---

### 2.11. Customer Registration: [register.php](file:///c:/laragon/www/tjs/app_api/llm/register.php)

API twin of the web registration flow ([func/register.php](file:///c:/laragon/www/tjs/func/register.php)): inserts `tblregister` plus a primary `tblregister_alamat` row, without the web session/cookie side effects.

#### Core Business Rules & Execution Steps

1. **Input Parameters** (read from `$_REQUEST`, so GET and POST both work; aliases in parentheses):
   * Required: `nama` (`nmuser`), `nohp` (`hp`), `alamat` (`address`), `jnecode` (`idkp`).
   * Optional: `kelurahan` (`kel`), `kord`, `pwd`, `kode_referral`.
2. **Phone Normalization**: Same rules as the other endpoints (strip ` ( ) . -`, map `0`/`62`/bare prefix to `+62`), then validated against `/^\+62\d{7,15}$/` → `422` if invalid. Note: unlike the web form, dashes (`-`) are also stripped.
3. **Referral Validation** (only when provided): normalized like a phone number; must differ from the registrant's number and must exist in `tblregister` with `status = 1` → otherwise `422`.
4. **Duplicate Guard**: An active account (`status = 1`) with the same normalized `nohp` → `422`.
5. **JNE Code Validation**: `jnecode` must exist in `tbljne` → otherwise `422` (message points the AI to `jnecode.php`). The matching row(s) supply `propinsi`/`kabupaten`/`kecamatan` for the response.
   * **Kelurahan resolution**: explicit `kelurahan` parameter wins; if omitted and the code covers exactly **one** kelurahan, it is auto-filled; if ambiguous, stored as empty string.
6. **Coordinate Resolution (`kord`)**: Uses the shared [kord_helper.php](file:///c:/laragon/www/tjs/app_api/llm/kord_helper.php) (also used by freeongkir.php): accepts raw `lat,lng` or Google Maps links/shortlinks (resolved via cURL redirects, Google-domain-restricted). Resolved → stored as `https://www.google.com/maps/place/lat,lng` with `is_temp_kord = 0`. Unresolvable input → `422`. Absent → stored `''` with `is_temp_kord = 1` (matching the web form default).
7. **Password**: `pwd` used as-is when provided; otherwise an 8-char random alphanumeric (ambiguous chars `0/O/1/l/I` excluded) is generated. Stored as `md5(pwd)` (legacy scheme shared with the web login). Generated passwords are returned in the response (`password`, `password_generated: true`); caller-provided passwords are never echoed.
8. **Inserts** (identical columns to the web flow):
   * `tblregister (nohp, sandi, nama, kode_referral, alamat, idkp)`;
   * `tblregister_alamat (reg_id, pengingat='Alamat Utama', nama_penerima, hp_penerima, alamat, kelurahan, idkp, kord, is_temp_kord, keterangan='primary')`.
9. **Response**: `data` array with one object: `reg_id`, normalized `nohp`, geographic enrichment from `tbljne`, `kode_cabang` via `cabang_find_cbg()`, `kord`, `is_temp_kord`, and the generated password when applicable.

---

### 2.12. Order Simulation: [simulasi_order.php](file:///c:/laragon/www/tjs/app_api/llm/simulasi_order.php)

Superset of the freeongkir simulation for the AI CS flow (FLOW 14.9). API twin of the website's simulasi endpoint ([func/ajax/simulasi-jaskir.php](file:///c:/laragon/www/tjs/func/ajax/simulasi-jaskir.php)).

#### Core Business Rules & Execution Steps

1. **Shared Engine (v2, website parity)**: Both this endpoint and `freeongkir.php` call `ongkir_simulate()` in [lib/ongkir_engine.php](file:///c:/laragon/www/tjs/app_api/llm/lib/ongkir_engine.php). The LLM-specific input side is kept (items from a cart link via `ongkir_parse_items()`, customer lookup by phone, Google-Maps-link coordinate resolution, tbljne bounding-box destination lookup). Everything after destination resolution mirrors `func/ajax/simulasi-jaskir.php` step-for-step:
   * **Cart pricing** via `order_detail_cust_id()` ([helper/order.php](file:///c:/laragon/www/tjs/helper/order.php)) — the items are staged as short-lived `tblcart` rows under a unique `llm-<uniqid>` cust id (the same way `view/simulasi.php` loads a cart link) and deleted immediately after pricing, before any external rate call.
   * **Serving branch** from the destination's `tbljne.kode_cabang`, falling back to the main branch (`cabang_get_utama()`), with the same column list as the website endpoint.
   * **JNE family priced from the main branch**: when the serving branch differs from utama, a second `order_detail_cust_id()` pass prices the cart at utama; `jaskir_cargo_jne`/`jaskir_jne_reguler` consume that.
   * **Biteship** called twice like the website: weight 1 for the Triguna family, real `subberatbts` for Grab; Deliveree queried only when Grab is unavailable or exceeds the mobil-box cap.
   * **The 7 options** come from the shared calculators in [helper/jaskir.php](file:///c:/laragon/www/tjs/helper/jaskir.php): `cargotriguna`, `jne`, `jnr`, `mobilboxtriguna`, `instanttriguna`, `grabins`, `grabsd`; `jaskir_termurah()` picks the cheapest per column (`free` / `murah`).
   * **WA-channel Grab filter**: `simulasi_order.php` drops `grabins`/`grabsd` from the engine's list (and recomputes `termurah_*` on the filtered set) before building the response — owner policy, see `docs/wa_channel_policy.md` §1. The engine and `freeongkir.php` are unaffected.
   Verified: for the same cart + destination, every option's `tersedia`/`ongkir_final`/`total` and the cart `subtotal`/`hemat` are identical to the website endpoint's response.
2. **Item Breakdown**: per item `harga_satuan` (= `inv_det['harga']`, min of grosir/reseller tier), `harga_normal`, `total_item`, and the serving branch `stock` computed by `order_detail_cust_id(should_compute_stock: true)`.
3. **Discount**: `diskon = |subtotal_normal - subtotal|` — identical to the website's `hemat`.
4. **Payment totals per option**: `total_transfer = total - round(total * trfman%)`, `total_cod = total + round(total * biaya_cod%)` where the percents come from the option's **sending branch** (JNE family → utama) and the COD percent is `biaya_cod_jne_jtr` for `jne`/`jnr`, else `biaya_cod_ambil_kurir`. Grab options are `bisa_cod = 0`.
5. **No side effects**: unlike the website endpoint, nothing is written to `tblsimulasi` (and the temp `tblcart` rows never outlive the request).
6. **Soft Failures (422)**: no cart item valid in the serving branch, or all 7 options unavailable.

---

### 2.13. Gold Points Redemption: [tukar_poin_emas.php](file:///c:/laragon/www/tjs/app_api/llm/tukar_poin_emas.php)

API twin of the web redemption ([func/func.php](file:///c:/laragon/www/tjs/func/func.php) `act=tukarpoin_emas`). **Writes a pending redemption row.**

#### Core Business Rules & Execution Steps

1. **Confirmation Latch**: `konfirmasi=1` is required (`400` otherwise) so lookup-style calls can never create a redemption. Balance checks belong to `poin_emas.php`.
2. **Customer Gate**: `tblregister` matched on normalized `nohp` with `status = 1` (same gate as the web flow; note the read-only `poin_emas.php` uses `aktif = 1`).
2b. **Bank Account (`norek`)**: optional `norek` parameter (max 100 chars, `400` beyond) is saved to `tblregister.norek` — the same field the datadiri page edits via `func/profile.php` — and the write persists even if the redemption then fails, so the customer never resends it. After the optional save, an **empty profile norek rejects the redemption** with `422` ("Nomor rekening belum terdaftar... Kirim nomor rekening beserta nama pemiliknya"), because the payout needs a bank account (FLOW 11.2).
3. **Balance**: `SUM(poin)` over `tblorder_poin_emas` where `status <> 0` — pending redemptions (negative rows, status `2`) already reduce the balance, which naturally guards against double redemption.
4. **Tier Selection**: highest `tblemasantam` row with `poin <= balance` (no status filter, parity with the web flow). None → `422 Poin tidak mencukupi...`.
5. **Trx Number**: `RDM<yymm><6-digit counter>` continuing the year's sequence (`LIKE 'RDM<yy>%'`), identical scheme to the web flow.
6. **Insert**: `tblorder_poin_emas (no_inv, tanggal, reg_id, poin = -tier, otp = random 6 digits, keterangan = reward, status = '2' pending)`.
7. **Response**: includes `norek_terdaftar` (profile bank account) so the AI can confirm payout data per FLOW 11.2, plus the recomputed `sisa_poin` and the 1-month processing note.

---

### 2.14. Reseller Referral Link Generator: [link_reseller_markup.php](file:///c:/laragon/www/tjs/app_api/llm/link_reseller_markup.php)

Builds the `/​+62...-<markup>` referral link resolved by [index.php](file:///c:/laragon/www/tjs/index.php) + [helper/referral_ibukota.php](file:///c:/laragon/www/tjs/helper/referral_ibukota.php).

#### Core Business Rules & Execution Steps

1. **Markup Validation**: numeric 0–100, default **5** (FLOW 10.4). Formatted without trailing zeros (`7.50` → `7.5`). `referral_get_markup()` treats the numeric path suffix as percent (`1 + markup/100`).
2. **Customer Gate**: normalized `nohp` must exist in `tblregister` with `status = 1` — the same predicate `cust_from_nohp()` applies when the link is visited; an unregistered number would produce a dead link, hence `422`.
3. **Link Assembly**: `https://trigunajayasentosaplastik.com/<normalized nohp>` plus `-<markup>` suffix when markup > 0. The stored profile `nohp` (already `+62...`) is used verbatim as the path segment.

---

### 2.15. Unpaid Transfer Order Follow-up: [followup_customer.php](file:///c:/laragon/www/tjs/app_api/llm/followup_customer.php)

API twin of the admin *Follow Up WA* button ([control/tnewordertrf1.php](file:///c:/laragon/www/tjs/control/tnewordertrf1.php) `followup()`).

#### Core Business Rules & Execution Steps

1. **Order Scope**: `tblorder_master.status = 1` (order baru, belum diproses) `AND payment LIKE 'trf%'` — i.e. transfer orders whose payment has not been validated yet. Unlike the admin page (which is split per shipping type), all shipping types are included.
2. **Worklist Mode (default)**: with neither `nohp` nor `no_inv`, ALL matching orders are returned (this is the pool `control/cron/autobatal.php` cancels at 72h) — ordered `tanggal ASC` (oldest first), `LIMIT` capped at 200. Optional narrowing: `kode_cabang`, `min_umur_jam` (`tanggal <= NOW() - INTERVAL n HOUR`). `nohp`/`no_inv` narrow to one customer/invoice as before.
3. **Deadline Fields**: `umur_jam = TIMESTAMPDIFF(MINUTE, tanggal, NOW())/60` (1 decimal), `batas_bayar = tanggal + 72h`, `jam_tersisa = max(0, 72 - umur_jam)` — 72 mirrors the autobatal cron's constant.
4. **Name Cleanup**: same as the admin JS — the customer name is cut at `" - "` and any parenthesized suffix is stripped.
5. **Branch Data**: the hardcoded Tangerang contact block of the admin JS is replaced with the order branch's `tblmastercabang` `name`/`no_wa`, plus `norek` (the branch transfer account, multi-line) exposed as `rekening_cabang` for payment-instruction reminders (cached per branch within the request).
6. **Tracking Link**: `<baseurl>/lacak?nohp=<urlencoded +62...>&link=%2Forder`; `baseurl` is read inline from `tblsetting.baseurl` (same block in `link_reseller_markup.php` and `copy_harga_web_olshop.php`), falling back to the production domain when empty.
7. **Response**: one row per order with raw fields plus the assembled `followup_text`; empty `data` = nothing to follow up (not an error). **No send-tracking exists** — the caller must log which orders it already messaged.

---

### 2.16. Copy Harga Web Onlineshop: [copy_harga_web_olshop.php](file:///c:/laragon/www/tjs/app_api/llm/copy_harga_web_olshop.php)

API twin of the detail page's *Copy Harga Web Onlineshop* text ([view/detail.php](file:///c:/laragon/www/tjs/view/detail.php) `detail_generate_wa_text()`).

#### Core Business Rules & Execution Steps

1. **Branch Gate**: `cabang_get_data()` must return an active branch → `404` otherwise.
2. **Product Fetch**: `tblmasterbarang` (`status = 1`) inner-joined to `tblmasterharga` on the requested branch → `404` when absent. Needs `harga`, `harga_rsl7`, `konv_kode`, `url`.
3. **Web Price Text**: `Rp<harga_rsl7> - Rp<harga>` when the two differ, else a single `Rp<harga>` (thousands-formatted, parity with the page).
4. **Shopee Price**: `round(harga * (persen_ol_sp/100 + 1))` using the requested branch's `persen_ol_sp`.
5. **Dus vs Pack**: non-empty `konv_kode` → *Per Dus* labels + `shopee_jaskir` link; otherwise *Per Pack* labels + `shopee_pack` link (`tblmasterbarang_link`).
6. **Deviation from the page**: the page derives the branch from the session and uses the main-branch fallback (`mharga_query_str`); the API takes `kode_cabang` explicitly and joins that branch only (same contract as `detail_barang.php`).

---


## 3. Vocabulary Guide for LLM Prompts

If you want to ask an AI to map these details out in the future, you can use terms like:
*   **"Response Matrix / Response Payload Catalog"** (to list all status codes and exact success/failure JSON schemas).
*   **"Execution Branches / Decision Trees / Logical Paths"** (to map out every `if/else` statement and conditional check).
*   **"Input Pre-processing & Normalization Rules"** (to trace raw parameters to their final queried state).
*   **"Query Constraints & Side Effects"** (to identify silent failures, joins, and truncation).
