# API Contract Requests — keluargamart ERP Frontend Frontend rework per `local://ERP-REWORK-PLAN.md` / `local://paste-2.md` is **complete and builds** (`npm run build` ✓). This document lists the **backend endpoints that are requested (CONTRACT REQ)** because the UI already renders graceful degradations for them but cannot show real data until the backend exposes them. Every other screen uses existing live endpoints. Legend: ✅ live · ⚠️ partial · 🔲 CONTRACT REQ (not yet implemented) ## Live endpoints used (verified in `cmd/server/main.go`) - ✅ `GET /dashboard/kpis`, `GET /dashboard/charts` - ✅ `GET /reports/profit-loss` (revenue, cogs, gross_profit, gross_margin, net_profit, months[], expense_by_category) - ✅ `GET /sales-orders` (+ `/{id}`, `?store_id=`) - ✅ `GET /delivery-orders` (+ `?sales_order_id=`) - ✅ `GET /purchase-orders` (+ `/{id}`, `/debts`) - ✅ `GET /goods-receipts` (no `purchase_order_id` filter → filtered client-side) - ✅ `GET /stores` (+ `/{id}`, `/{id}/users`) - ✅ `GET /cashflows`, `GET /invoices` (+ `/receivables`), `POST /invoices/{id}/pay` - ✅ `GET /reports/{profit-loss,balance-sheet,sales,stock,fakturisasi,summary,analytics}` ## CONTRACT REQ — requested endpoints ### 1. Branch / Store overview — `GET /stores/{id}/overview` Powers `BranchDetailPage` (route `/system/stores/:id`) and the per-branch rows in `ConsolidatedPage`. ```json { "profile": { "id": 1, "code": "JKT01", "name": "Toko Jakarta", "is_active": true, "address": "...", "phone": "...", "owner": null, "city": null, "opened_at": null, "opening_hours": null }, "last_order_date": "2026-08-20", "order_count": 142, "order_frequency_30d": 12, "outstanding_orders": 3500000, "revenue": 125000000, "expenses": 40000000, "cashflow": 85000000, "inventory_value": 22000000, "receivables": 9000000, "payables": 12000000, "recent_orders": [ { "so_number": "...", "order_date": "...", "status": "...", "total_amount": 0, "remaining_amount": 0 } ], "alerts": [ { "severity": "Outstanding", "tone": "warning", "label": "..." } ] } ``` Note: the `Store` model today only has `id, code, name, address, phone, is_active`. The `owner / city / opened_at / opening_hours` fields and the aggregate block require a backend change (extend `StoreInput` + add `GetStoreOverview`). ### 2. Sales dashboard aggregates — `GET /sales/dashboard?period=` Powers `SalesDashboardPage` (NOO count, EC pending-API, AO count, rep performance, AO performance). Today the page is built from list endpoints (`/sales-orders`, `/customers`); a pre-aggregated endpoint would remove client-side fan-out. ```json { "noo": { "count": 30, "new_today": 4, "new_7d": 18 }, "ec": { "pending_api": 12, "pending_approval": 5 }, "ao": { "count": 9, "active": 7 }, "salesperson_performance": [ { "salesperson": "...", "orders": 0, "revenue": 0 } ], "ao_performance": [ { "ao": "...", "stores": 0, "orders": 0 } ], "recent_noo": [ { "so_number": "...", "customer": "...", "order_date": "..." } ] } ``` ### 3. Sales Order `source` enum (NOO / EC / AO) `SO` payloads need a `source` field classifying the order origin. Currently the sales model has no `source`. Add a `source` enum (`noo | ec | ao`) to the `SalesOrder` model + filter `?source=` on `GET /sales-orders`. ### 4. Serah Terima Barang document endpoint — `GET /delivery-orders/{id}/handover` Powers the printable goods-handover sheet in `WarehousePage` (`/warehouse`, tab "Serah Terima Barang"). This is a warehouse→driver handover receipt, **not** LHPP (see #8) — the two were conflated under the "LHPP" label before this was corrected (2026-09-13). Today the sheet is rendered client-side from the DO payload (`items[].requested/delivered/remaining`, `shipments[]`). A server-rendered/PDF endpoint would guarantee a signed, audited document. ### 5. Finance: journals / assets / expenses Powers the Journal / Assets / Expenses tabs in `FinanceHubPage` (`/finance/hub`). Today these render an empty state (degrade) because no endpoint exists. ```json GET /journals → [ { "entry_no": "...", "date": "...", "account": "...", "debit": 0, "credit": 0, "memo": "..." } ] GET /assets → [ { "code": "...", "name": "...", "cost": 0, "accum_depr": 0, "book_value": 0 } ] GET /expenses → [ { "category": "...", "period": "2026-08", "amount": 0 } ] ``` ### 6. Per-branch report breakdown `GET /reports/profit-loss?store_id=` and `/reports/{sales,stock}?store_id=` to feed true per-cabang rows in `ConsolidatedPage` (currently derived from order activity only). ### 7. Store geolocation — `latitude` / `longitude` on `StoreInput` Powers the map location picker in `StoresPage` (`/system/stores`, Tambah/Edit Toko → "Titik Lokasi Toko"). The frontend already sends `latitude` and `longitude` (decimal degrees, WGS84, `null` when unset) in the `POST /stores` and `PUT /stores/{id}` payloads and reads them back from `GET /stores` (+ `/{id}`). Today the backend silently drops them — a new store round-trips with `latitude: null`. Backend change: add nullable `latitude` / `longitude` (`float64` / `DECIMAL(10,7)`) to the `Store` model + `StoreInput`, and include them in the store serializer. Once available, the picker's coordinates persist and the Peta Aktivitas Sales map can plot registered stores at their true position. ### 8. LHPP (Laporan Harian Penagihan Piutang) — `GET /lhpp`, `GET /lhpp/{id}`, `POST /lhpp/{id}/payments` Powers the `LhppPage` (`/sales/lhpp`). An LHPP groups every DO that shares the same dispatched driver + delivery date into one printable billing sheet for the sales/collector to bill the customer against. Grouping is automatic: as soon as a DO is dispatched to a driver (`POST /delivery-orders/{id}/dispatch`), it joins that driver's LHPP for that date — there is no separate "create LHPP" step. Today this is entirely mocked (`src/mocks/controllers/lhpp.js`). **Ledger unification (2026-09-15):** tagihan/terbayar/sisa per item are read straight from the invoice tied to the DO's sales order (`invoices[].sales_order_id`), not a separate store. `POST /lhpp/{id}/payments` resolves `do_id → sales_order_id → invoice_id` and delegates to `financeController.createPayment` — the exact same path `POST /invoices/{id}/pay` uses — so the payment lands in the one `payments` table, updates the invoice + mirrors the sales order, and posts a `cashflows` entry, regardless of whether it was collected from `LhppPage` or `ReceivablesPage`. There is no more `lhppPayments` store. If a DO's sales order has no invoice yet, its item has `invoice_id: null` and cannot be paid until the invoice is issued. ```json GET /lhpp → { "data": [ { "id": 1, "lhpp_number": "LHPP-20260913-001", "lhpp_date": "2026-09-13", "driver_name": "Slamet", "fleet_name": "Hino Dutro", "plate_number": "B 8211 KMA", "do_count": 3, "total_tagihan": 0, "total_terbayar": 0, "total_sisa": 0, "status": "belum_ditagih|sebagian|lunas" } ], "meta": { "page": 1, "per_page": 15, "total": 0, "total_pages": 1 } } GET /lhpp/{id} → { "data": { ...header, "items": [ { "do_id": 1, "do_number": "DO-...", "customer_name": "...", "shipping_address": "...", "invoice_id": 1, "invoice_number": "INV-...", "tagihan": 0, "terbayar": 0, "sisa": 0 } ] } } POST /lhpp/{id}/payments body: { "do_id": 1, "amount": 0, "payment_method": "cash", "notes": "" } → { "data": } ``` **Backend status (2026-09-15): implemented** in `keluargamart-backend` (`internal/lhpp`). `GET /lhpp` / `GET /lhpp/{id}` are computed on read from `delivery_orders` + `dispatches` + `sales_orders` + `invoices` (no LHPP table); `POST /lhpp/{id}/payments` resolves the invoice from `do_id` and calls `finance.Service.PayInvoice` — the exact same code path `POST /invoices/{id}/pay` uses — so LHPP collections and `ReceivablesPage` can never drift apart. Group IDs are deterministic (`YYYYMMDD * 1e9 + driver_id`), not stored, since a group has no identity beyond driver+date. Permissions `sales.lhpp.view` / `sales.lhpp.pay` were added to the catalog (both covered by the existing `sales.*` wildcard already granted to the `sales` role — no new role-grant migration needed). `LhppPage.vue` was refactored onto `useListPage` (the same composable `CashflowPage`/`InvoicesPage` use) instead of its old hand-rolled `load()`/`meta` reactive object. That fixes a real shape gap: the mock's `paginate()` returns flat `meta.{page,per_page,total,total_pages}`, but the real backend nests pagination under `meta.pagination.{current_page,per_page, total,last_page}` — the old hand-rolled code only ever read the flat shape, so it would have silently shown an empty/broken pagination bar against the real API. `useListPage` already resolves `res?.meta?.pagination ?? res?.meta`, so the page now works unmodified against either backend. Item field names (`do_id`, `invoice_id`, `tagihan`, …) and amounts (decimal strings from the Go backend, coerced through `formatRupiah`/`Number()`) were already shape-compatible without changes. Switching this feature from mock to live is now just the existing global `VITE_USE_MOCKS` toggle in `src/services/http.js` — no LHPP-specific code path left to flip. Still open: a DO with multiple line-item DOs against one invoice (partial shipment) is expected — paying against one DO's item pays the invoice, and the remaining amount updates everywhere that invoice appears; there's no frontend affordance yet that flags "this invoice also appears in another LHPP" when that happens. **Dispatch UI (2026-09-15): added.** Until now there was no click-through way to get a DO into an LHPP at all — `DistributionPage.vue` was read-only and neither shipment nor dispatch creation had a button anywhere in the ERP, so the only way to populate LHPP was a raw API call. Added "Kirim ke Driver" on `DistributionPage.vue` (Distribusi → Delivery Order): a modal picking armada + driver + qty per item, posting once to `POST /delivery-orders/{id}/shipments` with an embedded `dispatch` object (fleet_id, driver_id, priority) — the backend creates the shipment and dispatch (status `dispatched` immediately) in one transaction, which is what makes the DO join its LHPP group. ### 8b. LHPP Sales — `GET /lhpp/sales`, `GET /lhpp/sales/me`, `GET /lhpp/sales/{id}`, `POST /lhpp/sales/{id}/payments` Client feedback (2026-09-20): the driver-keyed LHPP above only covers the warehouse→driver delivery flow. A sales rep whose billing (penagihan) visit is scheduled by Finance via **Jadwal Kunjungan** (`visit_type: 'billing'`, `sales_tracking.visits.create.billing` — see §"Jadwal Kunjungan & Definisi AO" below) had no LHPP of their own to print, because that visit is never tied to a delivery order/driver at all. LHPP Sales is the same computed- on-read pattern as §8, grouped by **sales rep + visit date** instead of driver + DO date, joined to that customer's **open invoices** (`status IN ('unpaid','partial','overdue')`) instead of a DO's single invoice — a billing visit collects against whatever the customer currently owes, not against one delivery. Implemented in `src/mocks/controllers/lhpp.js` (`buildSalesGroups`) mirroring backend `internal/lhpp` (`buildSalesGroups`, `SalesGroup`/`SalesItem` in `model.go`). Surfaced in `LhppPage.vue` (`/sales/lhpp`) as a second tab, "LHPP Sales (Saya)", next to the existing "LHPP Supir" tab — one menu, not a new page, per the client's simplicity preference. The tab always calls `GET /lhpp/sales/me` (self-scoped to the logged-in sales rep, resolved from the auth token the same way `GET /sales-tracking/visits/me` does), so Finance/ admin users see it empty (they aren't a sales employee) by design; `GET /lhpp/sales` (all reps) exists for a future Finance-facing view but has no UI wired to it yet. ```json GET /lhpp/sales → { "data": [ { "id": 1, "lhpp_number": "LHPP-SLS-20260920-007", "lhpp_date": "2026-09-20", "sales_id": 7, "sales_name": "Andi Kurniawan", "visit_count": 3, "total_tagihan": 0, "total_terbayar": 0, "total_sisa": 0, "status": "belum_ditagih|sebagian|lunas" } ], "meta": { "page": 1, "per_page": 15, "total": 0, "total_pages": 1 } } GET /lhpp/sales/me → same shape, filtered to the caller's own sales_id GET /lhpp/sales/{id} → { "data": { ...header, "items": [ { "visit_id": 1, "customer_id": 1, "customer_name": "...", "shipping_address": "...", "purpose": "Penagihan invoice jatuh tempo", "invoice_id": 1, "invoice_number": "INV-...", "tagihan": 0, "terbayar": 0, "sisa": 0 } ] } } POST /lhpp/sales/{id}/payments body: { "invoice_id": 1, "amount": 0, "payment_method": "cash", "notes": "" } → { "data": } ``` Payments post through the identical `financeController.createPayment` / `finance.Service.PayInvoice` path §8 uses, so a payment collected on a penagihan visit lands on the same invoice/cashflow ledger `ReceivablesPage` reads — matched by `invoice_id` (not `do_id`: a billing visit has no DO, the invoice is the payable unit directly). Group IDs reuse §8's `encodeGroupID(id, date)` scheme with `sales_id` in place of `driver_id` — safe since the two live in separate URL namespaces (`/lhpp/{id}` vs `/lhpp/sales/{id}`), so no collision risk despite the shared numeric space. **Assumption, not enforced:** one billing visit per sales rep + customer + date. A duplicate schedule to the same customer on the same day would sum that customer's open-invoice total twice in the group; this is treated as a scheduling data error (Finance shouldn't double-book), not guarded against. **Backend status (2026-09-20): implemented** in `keluargamart-backend` (`internal/lhpp`, `ListLHPPSalesCandidateRows` in `db/query/lhpp.sql`). Reuses the existing `sales.lhpp.view` / `sales.lhpp.pay` permissions (no new grant needed — same `sales.*` wildcard as §8). `GET /lhpp/sales/me` reuses the same user→employee `EmployeeLookup` closure as `salestracking.Service` (wired once in `main.go`, passed to both services). Mock seed: `src/mocks/data/tracking.js` seeds a handful of `visit_type: 'billing'` visits against customers that actually carry an open invoice (picked from `db.invoices`), so the printed sheet shows non-zero tagihan out of the box instead of an empty worksheet. New endpoint `GET /distribution/drivers` (`internal/distribution`, permission `distribution.dispatches.create`) backs the driver dropdown. There is no "driver" role in the schema — any active user is a valid candidate — so this intentionally does NOT reuse `GET /users` (gated behind `system.users.view`, which a warehouse dispatcher role does not have). Note for manual testing: `seed.sql` §27's demo dispatch (`DO-DEMO-0001`) was inserted directly with `status = 'assigned'`, not `'dispatched'` — it will NOT show up in LHPP as-is (LHPP filters strictly on `'dispatched'`, matching what the real `CreateDispatch`/`CreateShipment` service path always writes). Use the new "Kirim ke Driver" button on a fresh DO instead of relying on that seed row, or update it manually if you need the seed data specifically to appear. ## Non-blocking notes - `GET /goods-receipts` has no `purchase_order_id` filter; the PO detail view fetches all GRs and filters client-side. A server filter is preferred at scale. - PO lifecycle: `PurchaseOrder` has `created_by` only (no `approved_by` / `revision_count`); the stepper is derived from `status + GR + debt + paid`. - No `PATCH /purchase-orders/{id}` route exists; status transitions (approve / cancel) are limited to what the backend supports.