Lewati ke konten utama

Poslite Service — API Reference

Port internal: 8096
Proxy publik: POST|GET /merchant/poslite/* via core_api (JWT required)
Auth internal: X-Internal-API-Key + X-Merchant-ID (di-inject core_api proxy)

Semua path di bawah ini adalah path setelah prefix /merchant distrip oleh core_api. Mobile memanggil https://api-merchant.kesles.com/merchant/poslite/...; core_api forward ke http://127.0.0.1:8096/poslite/....


Catalog — Outlets

GET /poslite/catalog/outlets

List semua outlet merchant.

Response 200:

{
"outlets": [
{
"id": "uuid",
"merchant_id": "uuid",
"code": "OUTLET-01",
"name": "Toko Pusat",
"address_line": "Jl. Sudirman 1",
"city": "Makassar",
"province": "Sulawesi Selatan",
"phone": "0411-123456",
"nmid": "ID1234567890123",
"qris_static_payload": "00020101...",
"is_active": true,
"created_at": "...",
"updated_at": "..."
}
]
}

POST /poslite/catalog/outlets

Buat outlet baru.

Body:

{
"code": "OUTLET-01",
"name": "Toko Pusat",
"address_line": "Jl. Sudirman 1",
"city": "Makassar",
"province": "Sulawesi Selatan",
"phone": "0411-123456"
}

code dan name wajib. Conflict uk_outlets_merchant_code → 409 code_conflict.

Response 201: Objek outlet.

GET /poslite/catalog/outlets/{id}

Ambil detail outlet.

Response 200: Objek outlet. 404 not_found jika tidak ada.

PATCH /poslite/catalog/outlets/{id}

Update outlet (semua field opsional).

Body:

{
"name": "Toko Baru",
"address_line": "...",
"city": "...",
"province": "...",
"phone": "...",
"is_active": false,
"nmid": "ID...",
"qris_static_payload": "00020101..."
}

nmid dan qris_static_payload di-pull dari merchant.merchant_outlets — update manual hanya untuk sinkronisasi manual atau koreksi.

Response 200: Objek outlet terupdate.

DELETE /poslite/catalog/outlets/{id}

Soft-delete outlet (deleted_at = now()). Response 204.


Catalog — Categories

GET /poslite/catalog/categories

List semua kategori merchant.

Response 200: { "categories": [...] }

POST /poslite/catalog/categories

Buat kategori baru. Field name wajib.

Body: { "name": "...", "parent_id": null, "sort_order": 0, "icon_name": "category" }

Response 201: Objek kategori.

GET /poslite/catalog/categories/{id}

Ambil detail satu kategori.

Response 200: Objek kategori. 404 jika tidak ada.

PATCH /poslite/catalog/categories/{id}

Update kategori (semua field opsional).

Body: { "name": "...", "parent_id": "...", "sort_order": 1, "icon_name": "...", "is_active": true }

Response 200: Objek kategori.

DELETE /poslite/catalog/categories/{id}

Soft-delete. Response 204.

GET /poslite/catalog/categories/defaults?business_type_name=<nama>

Template kategori default dari db_reference berdasarkan jenis usaha.

Response 200: { "business_type_name": "...", "categories": [{ "name": "...", "icon_name": "...", "sort_order": 0 }] }

503 reference_db_unavailable jika DB_REFERENCE_POSTGRES_DSN tidak dikonfigurasi.

POST /poslite/catalog/categories/provision

Provisi kategori awal merchant dari daftar pilihan. Idempotent — jika merchant sudah punya kategori, return existing tanpa insert baru.

Body:

{
"selected": [{ "name": "Makanan", "icon_name": "food" }],
"custom": [{ "name": "Lainnya", "icon_name": "category" }]
}

Response 200: { "categories": [...] }


Catalog — Products

GET /poslite/catalog/products

List produk. Query params opsional: category_id, q, active=true, limit, offset.

Response 200: { "products": [...] } — setiap produk menyertakan barcode (dari default variant, atau null jika belum di-set).

POST /poslite/catalog/products

Buat produk baru.

Body:

{
"name": "Nasi Goreng",
"sku": "NAGO-01",
"category_id": "uuid",
"product_type": "good",
"sell_price_amount": 15000,
"cost_price_amount": 8000,
"unit_label": "porsi",
"description": "",
"image_url": "",
"tax_rate_bps": 0,
"track_stock": false,
"barcode": "8999909096004"
}

name + sku wajib. product_type: good (default) atau service. Auto-buat 1 default variant transparan (untuk referensi variant_id di sales).

Conflict:

  • 409 sku_conflict — SKU sudah dipakai merchant
  • 409 barcode_conflict — Barcode sudah dipakai produk lain (unique global)

Response 201: Objek produk.

GET /poslite/catalog/products/{id}

Ambil produk termasuk barcode. Response 200. 404 jika tidak ada.

PATCH /poslite/catalog/products/{id}

Update produk (semua field opsional).

Body (semua opsional):

{
"name": "...",
"category_id": "...",
"description": "...",
"image_url": "...",
"unit_label": "...",
"sell_price_amount": 18000,
"cost_price_amount": 9000,
"tax_rate_bps": 0,
"track_stock": false,
"is_active": true,
"sort_order": 1,
"barcode": "8999909096004"
}

barcode: null → tidak ubah. barcode: "" → hapus barcode. barcode: "123..." → set baru. Update barcode ditulis ke catalog.product_variants (default variant). Conflict → 409 barcode_conflict.

Response 200: Objek produk dengan barcode terbaru.

DELETE /poslite/catalog/products/{id}

Soft-delete. Response 204.


Sync

GET /poslite/sync/pull?entity=<e>&since=<millis>&limit=200

Tarik perubahan katalog ke device (incremental by updated_at).

Entity yang didukung: outlets | categories | products | variants | product_stock | customers

Response 200:

{
"entity": "products",
"rows": [{ "id": "...", "name": "...", ... }],
"server_time": 1749470000000,
"has_more": false
}
  • since kosong → full pull
  • has_more: true → loop dengan since=server_time dari response sebelumnya
  • Timestamp di rows = epoch millis; bool = int (0/1) — disesuaikan format SQLite mobile

POST /poslite/sync/push

Kirim batch mutasi dari device ke server. Idempotent by outbox_id — pengiriman ulang aman.

Body:

{
"items": [
{
"outbox_id": "uuid",
"entity_type": "sales_order",
"entity_id": "uuid",
"op": "create",
"payload": { ... }
}
]
}

Entity type yang didukung: sales_order | stock_movement | customer

sales_order payload:

{
"id": "uuid", "merchant_id": "uuid", "outlet_id": "uuid",
"receipt_number": "RCP-00042", "status": "completed", "source": "offline",
"transacted_at": 1749470000000,
"subtotal_amount": 32000, "total_amount": 32000, "paid_amount": 50000, "change_amount": 18000,
"items": [
{ "id": "uuid", "line_no": 1, "variant_id": "uuid",
"sku_snapshot": "NAGO-01", "name_snapshot": "Nasi Goreng",
"quantity": 2, "unit_price_amount": 16000, "line_total_amount": 32000 }
],
"payments": [
{ "id": "uuid", "method": "cash", "amount": 50000 }
]
}

Response 200:

{
"accepted": ["outbox_id_1", "outbox_id_2"],
"rejected": { "outbox_id_3": "entity_type tidak dikenal: unknown" }
}

Max 500 items per batch.


Sales — Orders

GET /poslite/sales/orders

List order merchant. Query params opsional: outlet_id, status, date_from, date_to, limit (max 200, default 50), offset.

Status: open | completed | voided | refunded

Response 200: { "orders": [...] } — tanpa items dan payments (header saja).

GET /poslite/sales/orders/{id}

Detail order termasuk items dan payments. 404 jika tidak ada.

PATCH /poslite/sales/orders/{id}

Aksi pada order. Saat ini hanya mendukung void.

Body:

{
"action": "void",
"void_reason": "Pesanan dibatalkan pelanggan"
}

void_reason opsional — boleh string kosong. Mobile saat ini mengirim "".

Response 200: Objek order dengan status: "voided" dan voided_at terisi.

Error:

  • 400 bad_requestaction bukan "void"
  • 404 not_found — order tidak ada
  • 500 internal_error: "order sudah dalam status voided" — sudah dibatalkan sebelumnya

Mobile flow: device memanggil PATCH endpoint ini, lalu menandai order sebagai voided di SQLite lokal dan me-reload daftar riwayat.


Kode Error Umum

HTTPCodeKeterangan
400bad_requestField wajib kosong atau body tidak valid
401unauthorizedX-Internal-API-Key salah atau kosong
404not_foundResource tidak ditemukan atau bukan milik merchant
405Method tidak didukung untuk path ini
409sku_conflictSKU sudah dipakai
409barcode_conflictBarcode sudah dipakai produk lain
409code_conflictKode outlet sudah dipakai
503reference_db_unavailabledb_reference tidak terhubung
500internal_errorError tak terduga — lihat log service