# Axios One centralized client (`app/services/http.ts`), created lazily per request context. Everything API-related goes through it; components and pages never touch Axios directly (page → composable → repository → client). ## Transport - **Default (recommended): same-origin proxy.** `baseURL` is empty and the browser calls `/api/...` on the Nuxt origin; `nuxt.config.ts` proxies `/api/**` to the backend via a Nitro `routeRules` proxy (`NUXT_API_PROXY_TARGET`, default `http://100.64.0.5:8080`). This avoids CORS entirely (the running backend does not emit CORS headers — verified 2026-08-12). - **Alternative: direct cross-origin.** Set `NUXT_PUBLIC_API_BASE_URL` to the backend origin. The backend must then allow this app's origin in its `CORS_ORIGINS` (`.env.prod`); exact scheme+host+port, no trailing slash. ## Headers - `Authorization: Bearer ` is attached by a request interceptor from a module-level token slot (`setAuthToken`), populated by `useAuth` on login/me and cleared on logout. No other auth logic lives in interceptors. - Per the spec, **every endpoint except `POST /api/v1/auth/login`, `GET /health`, `GET /ready` requires a bearer token** — including the public catalog, which means guest browsing against the real backend currently 401s. ## Response envelope The backend wraps payloads in `{ "data": , "meta": { … } }`. Lists return `meta.pagination` = `{ current_page, per_page, total, last_page }` and paginate with **`page` + `per_page`** (max 100, default 20). The repository layer unwraps it (`data()` helper in `app/services/http/repos.ts`) and maps responses to `Paginated` (`paginated()` helper); outgoing frontend `pageSize` is translated to `per_page` (`pageParams()`). ## Errors `normalizeApiError` (`app/services/apiError.ts`) converts transport failures and API error bodies into a stable shape surfaced by composables/pages. The spec's error envelope is `{ error: { code, message, details? } }` with string codes (`AUTHENTICATION_REQUIRED`, `VALIDATION_ERROR`, `INTERNAL_ERROR`, …). ## Endpoint status (2026-08-12, OpenAPI-backed) Contract source: `docs/keluargamart-openapi.yaml` — the KeluargaMart backend's OpenAPI 3.0.3 spec (85 paths under `/api/v1`), downloaded from `http://100.64.0.5:8080/docs` on 2026-08-12 and archived in this repo. Column **Probe** = live response from the running backend (through the `/api` proxy, bearer token) the same day. ### Confirmed & wired in `app/services/http/repos.ts` | Group | Path | Notes | Probe | |---|---|---|---| | Auth | `POST /api/v1/auth/login` | `{ data: { token, user } }` | 200 | | Auth | `GET /api/v1/auth/me` | `{ data: { user, employee?, permissions? } }` | 200 | | Auth | `PUT /api/v1/auth/me` | update own name/email/password — not wired (storefront profile is customer-scoped) | — | | Catalog | `GET /api/v1/products` | params `page, per_page, search, status, sort, order` | 200 — real data (e.g. `MIN-20260810-001` Air Mineral 600ml) | | Catalog | `GET /api/v1/products/search` | lightweight POS lookup `{id, code, name, selling_price, stock}` — used by `getProductBySku/Slug` adapter | 404 (in spec, not implemented) | | Catalog | `GET /api/v1/products/categories` | | 404 (in spec, not implemented) | | Catalog | `GET/POST/PUT/DELETE /api/v1/products/{id}` | ProductInput: `name, description, category_id, unit_id, purchase_price, selling_price, min_stock, is_active` | 200/404 | | Catalog | `GET/POST /api/v1/categories`, `GET/PUT/DELETE …/{id}` | CategoryInput: `name, description, is_active` | 200 | | Customers | `GET /api/v1/customers` | params `page, per_page, search, status` | 200 — 6 customers, paginated | | Customers | `GET /api/v1/customers/search` | | in spec, not implemented | | Customers | `GET/PUT/DELETE /api/v1/customers/{id}` | CustomerInput; **PUT not PATCH** | 200 | | Orders | `GET /api/v1/sales-orders` | params `page, per_page, status, customer_id, date_from, date_to` | 200 — 4 orders, paginated | | Orders | `POST /api/v1/sales-orders` | SalesOrderInput `{customer_id, order_date, items[{product_id, quantity, unit_price}]}` — not wired (no customer binding) | in spec | | Orders | `GET/PUT/DELETE /api/v1/sales-orders/{id}` | draft only for PUT/DELETE | 200 | | Orders | `POST /api/v1/sales-orders/{id}/confirm` | `POST …/cancel` — cancel body-free; storefront reason dropped | in spec | | Invoices | `GET /api/v1/invoices`, `GET /api/v1/invoices/{id}` | `{id}/print`, `{id}/send` also defined | 200 — 2 invoices | | Payments | `GET /api/v1/payments` | params `page, per_page, status, date_from, date_to` | 200 — 1 payment | | Payments | `POST /api/v1/payments` | PaymentInput `{invoice_id, amount, payment_method(cash/transfer/giro/cheque)}` + optional `Idempotency-Key` header — not wired | in spec | | Admin | `GET /api/v1/dashboard/kpis` | ERP shape: `month_sales, month_purchase, receivables, payables, low_stock_count, recent_activity, sales_chart` | 200 | Follow-up probe 2026-08-12 (same day): `sales-orders`, `invoices`, `payments`, `customers`, `dashboard/kpis` were `500 INTERNAL_ERROR` at first probe and are now `200` with real data — the backend was fixed mid-day. **Money/quantity serialize as JSON strings** on the live backend (e.g. `"87500"`, `"25000000"`) despite the spec's "JSON number" claim; the adapters coerce with `Number()` before arithmetic/comparison. ### In the spec but ERP/HR scope — excluded per AGENTS.md `units`, `suppliers`, `warehouses`, `inventory/*` (stock, movements, stock-in/out, transfers, opnames), `purchase-orders`, `goods-receipts`, `vouchers` (purchasing scope), `cashflows`, `reports/*`, `employees`, `hr/*` (attendance, clock-in/out, leaves, payroll, performance, sales visits), `users` + `users/{id}/toggle` (staff accounts), `pos`, `invoices/receivables`, `purchase-orders/debts`. The frontend does not consume these. ### Storefront-only — paths the ERP spec does not list Cart, checkout, account profile/addresses/team users/notification preferences, notifications, support tickets, returns, brands, promotions, order templates/recurring/reorder, shipping methods, product sub-resources (`/products/slug/:slug`, featured/new/recommended/suggestions), payment methods (`/payments/methods`), payment proof upload/partial, admin order-management (`/admin/orders/*`), customer approval/status, admin analytics, auth flows (register/forgot/reset). The backend contract grew to cover these after the ERP spec was written, so absence from that spec is no longer evidence a path is missing — `app/services/http/repos.ts` is the authority and calls them for real. Where a buyer-facing list genuinely does not exist server-side, the repo returns an honest empty state rather than inventing rows: see `voucherRepo.getAvailable` (vouchers are admin-managed and redeemed by code) and `discountRepo.getActiveRules`. #### Verified against the running backend (2 Oct 2026) All 52 paths `app/services/http/repos.ts` calls were probed on the local stack. **None are missing.** Each returned: | Status | Count | Meaning | |---|---|---| | 200 | 19 | served | | 405 | 16 | path mounted; probed with GET, real call uses POST/PUT/PATCH/DELETE | | 403 | 12 | mounted, buyer token lacks the permission (admin-only routes) | | 404 | 3 | buyer-scoped routes with no owned record yet — see below | | 400 / 503 | 2 | empty probe body / gateway absent, i.e. the handler ran | The three 404s are the only ones worth naming, because a 404 on a `/me`-scoped route is *not* a missing endpoint: - `GET /sales-orders/me/{id}` → 404 until the caller owns an order, then 200. - `GET /invoices/me/{id}/print` → the order must be invoiced first. The handler returns 404 rather than 403 when the invoice is not the caller's (`internal/finance/handler.go`, `MyPrintInvoice`), so an id belonging to someone else is indistinguishable from one that does not exist. - `GET /memberships/me` → 404 until the buyer has a membership record. `membershipRepo.getProfile` already treats that as `null`. Re-run this probe before trusting the table again; the storefront surface moves as the backend grows. #### Repo methods that fabricate instead of calling `app/services/http/repos.ts` still satisfies part of its interface without a backend call. None of these is reachable from a rendered control today, but each will hand a caller made-up data the moment a page wires it up: | Method | Returns | Why it is inert | |---|---|---| | `supportRepo.createTicket` | a ticket with a `TICK-` number | `/support` is a WhatsApp page; no caller | | `supportRepo.getTickets/replyTicket/getFaqs` | `[]` / `null` / throws | no caller | | `ordersRepo.reorder` | an empty cart | `reorderEligible` is hardcoded `false`, so the button never renders | | `ordersRepo.getTemplates/getRecurring/saveTemplate` | `[]` / `{}` | no page reaches these | | `voucherRepo.getAvailable` | `[]` | vouchers are admin-managed, redeemed by code | | `discountRepo.getActiveRules` | `[]` | no rule engine on the backend | | `ordersRepo.getShippingMethods` | a 3-row courier list | see below | `getShippingMethods` is the one a caller *does* reach. Its `cost` values are invented, but they never reach a buyer: `POST /cart/checkout` takes no shipping method, returns `shipping_cost 0`, and the checkout page shows the courier as "Ditagih terpisah oleh kurir" with `orderTotal` taken from the server cart. Do not start summing that `cost` client-side. Before wiring any of these to a control, either add the backend route or render the honest empty state — per `AGENTS.md`, client-invented prices, statuses and ids are never authoritative. ### Field adapters (backend → storefront) The ERP payloads map into the storefront domain types in `app/services/http/repos.ts` (`mapProduct`, `mapSalesOrder`, `mapInvoice`, `mapPayment`, `mapCustomer`, `mapCategory`). Notes: - Backend `Product` has no sku/slug/availability/MOQ/images[] — `code` doubles as sku/slug, availability defaults to `in_stock`; TODO(Phase 00). - `SalesOrderStatus` (`draft/confirmed/picking/shipped/delivered/invoiced/ paid/cancelled`) maps onto the storefront lifecycle; payment status is **derived** from `paid_amount` vs `total_amount`. - `InvoiceStatus` has no `overdue` storefront counterpart → `pending`. - `PaymentMethod` is a fixed enum (`cash/transfer/giro/cheque`); the storefront `kind` union collapses non-transfer methods to `bank_transfer` while labels stay accurate. - Everything inferred carries a `TODO(Phase 00)` in the code. There is no mock repository layer. `getRepos()` returns `createHttpRepos()` unconditionally, so every page reads live API state and a gap in the backend shows as an error or empty state rather than invented data.