# PHASE X4.11 — CASHIER RECEIPT PRINT READ-ONLY AUDIT

Date: 2026-10-07

## 1. Executive Summary

Arsitektur print yang ada **sudah sepenuhnya siap untuk CASHIER** di sisi
infrastruktur: `print_jobs` punya destination `RECEIPT`, device type
`PRINTER_KASIR` sudah dipetakan server ke `RECEIPT`, dan jalur itu sudah
**terbukti produksi** (voucher slip X4.18-F1 dicetak lewat RECEIPT).
Yang belum ada hanya **pemicu job receipt saat payment sukses** dan
**builder payload teks struk** — keduanya perubahan server kecil, tanpa
migration, tanpa perubahan Android APK, tanpa perubahan core Bluetooth/renderer.

## 2. Current Cashier Flow

| Layer | File | Function/Class | Evidence | Status |
|---|---|---|---|---|
| Cashier UI | `views/cashier/order.php` | form BAYAR → `POST /cashier/orders/{id}/payment` | baris 291-347 | AKTIF |
| Payment | `app/Modules/Payment/PaymentController.php` + `PaymentService::record` | validasi server (shift, lock order+shift, amount ≤ remaining, TUNAI received ≥ amount, change server-side) | `PaymentService.php:139-223` | AKTIF |
| Order finalization | `OrderService::completeOrder` (dipanggil saat paid ≥ total) | stok, loyalty, COMPLETED, release table | `PaymentService.php:217-219` | AKTIF |
| Receipt generation | `ReceiptService::build` (read-only, COMPLETED-only, snapshot) | view model lengkap | `ReceiptService.php:44-160` | AKTIF |
| Receipt UI | `views/receipts/show.php` | HTML + tombol `data-print` | baris 1-6 | AKTIF |
| Print trigger | `app.js` `[data-print]` → **`window.print()` (browser only)** | "printer fisik masih deferred" | `app.js:55-57` | AKTIF (browser) |
| Print queue | `print_jobs` + `PrintJobService::create/claim/reportResult` | destination ENUM `KITCHEN,BAR,PACKING,RECEIPT,OTHER` | migration 002 + 012 | SIAP |
| ACK | `DeviceApiController::reportJobResult` | SUCCESS/FAILURE, STALE_CLAIM, idempotent | `DeviceApiController.php:163-217` | SIAP |
| Reprint | Belum ada jalur thermal | browser print ulang = read-only | — | BELUM ADA |

Redirect setelah bayar: `PaymentController::record` → order COMPLETED →
`/receipts/{id}` (SIMPAN → STRUK, Phase 3M).

## 3. Payment Semantics

- **Q1 SUCCESS =** baris `payments` ACTIVE tersimpan + paid ≥ total →
  `payment_status=PAID` → `completeOrder` — semua dalam satu transaksi
  (`PaymentService::record`). Bukan event printer.
- **Q2** Receipt `build()` hanya untuk order COMPLETED → YA, receipt hanya
  bisa setelah payment sukses (`ReceiptService:65-67`).
- **Q3** Printer mati TIDAK memengaruhi payment — tidak ada satu pun
  pemanggilan print di jalur payment; job receipt (nanti) akan enqueued
  SETELAH transaksi payment commit. **Payment tetap SUCCESS.**
- **Q4** Kegagalan printer TIDAK bisa rollback payment/order — tidak ada
  mekanisme dan tidak boleh ada (financial authority ≠ printer).
- **Q5** Refresh `/receipts/{id}` = GET read-only (`ReceiptService` tanpa
  side effect — docblock "READ-ONLY, side-effect free") → TIDAK membuat
  receipt/job baru. Aman.
- **Q6/Q7** Submit ganda ditolak server: amount > remaining →
  `INVALID_PAYMENT: nominal melebihi sisa tagihan` (`PaymentService:177-179`);
  completeOrder juga idempotent (stock movement unique per order_item).
  Evidence tes: `PaymentTest::testInvalidPaymentsRejected`,
  `testPartialThenPaid`.

## 4. Receipt Generation

Source of truth = **snapshot transaksi** (`orders`, `order_items`,
`payments`, `corrections`, loyalty snapshot) — model **B: immutable
transaction data**, dirender dinamis (A) dari snapshot yang tak berubah.
Konsekuensi: reprint kapan pun menghasilkan konten identik tanpa
recalculation; tidak butuh tabel receipt baru.

## 5. Existing Print Architecture

- `print_jobs` (migration 002+012): branch_id, destination ENUM (termasuk
  **RECEIPT**), payload TEXT, status, claim_token, lease, attempt_count,
  kind/reprint — **generic, tidak KITCHEN-specific**.
- Claim: `POST /api/v1/device/print-jobs/claim` — destination ditentukan
  server dari tipe device: `PRINTER_KASIR → RECEIPT`,
  `PRINTER_KITCHEN/KITCHEN_PRINT_BRIDGE → KITCHEN` (`DeviceApiController:68-73`).
- Pembuat job RECEIPT sudah ada & produksi: `LotteryVoucherAdminController:69`
  (`PrintJobService::create(branch, 'RECEIPT', payload)`) — job 49-55.
- Retry/lease/STALE_CLAIM/rate-limit: identik untuk semua destination.

## 6. Android Agent Audit

Agent (`android-print-agent/`) **tidak punya konsep destination** —
`PrintPipeline` meng-claim apa pun yang server berikan untuk device
token-nya (grep: nol referensi destination/KITCHEN/RECEIPT di service/
pipeline). Destination = properti REGISTRASI DEVICE di server.

> **OPTION A — Agent sudah dapat menangani CASHIER tanpa source change.**
> Cukup daftarkan device baru bertipe `PRINTER_KASIR` (cabang kasir),
> generate token, install APK yang SAMA di tablet kasir, isi konfigurasi.
> (Catatan: satu instalasi = satu device/token — tablet kasir terpisah
> dari tablet dapur, atau switch manual device.)

## 7. Renderer Audit

`EscPosRenderer` = generic plain-text → ESC/POS (init, wrap per CPL,
center header, separator, cut, CP437 + UTF-8 passthrough). Receipt payload
cukup dibangun sebagai teks polos → **renderer reusable tanpa perubahan**.

## 8. Cashier Printer Evidence

**UNKNOWN** — repository tidak memuat spesifikasi printer kasir
(brand/model/MAC/UUID/paper width). Yang diketahui: printer dapur MP-581
RPP02N (Bluetooth SPP). Printer kasir harus disurvei: SPP? 58/80mm?
ESC/POS? → data input operator sebelum validasi hardware.

## 9. Reprint Semantics

Belum ada reprint thermal. Konsep yang konsisten dengan arsitektur
existing: **ORIGINAL = 1 job otomatis saat payment sukses; REPRINT =
tombol eksplisit membuat job BARU (payload sama)** — persis pola voucher
(X4.18-F1). Reprint = read-only terhadap transaksi; audit otomatis via
`print_job.create`.

## 10. Failure Semantics

| Scenario | Current Behavior | Risk | Evidence |
|---|---|---|---|
| A. Payment sukses, printer online | receipt dirender; (nanti) job RECEIPT dicetak | — | PaymentService + (desain) |
| B. Payment sukses, printer offline | payment tetap SUCCESS; job PENDING menunggu agent | struk tertunda — diterima | tidak ada coupling payment-printer |
| C. Agent offline | job persist PENDING; lease/retry server | struk tertunda | PrintJobService |
| D. Print gagal | ACK FAILURE → FAILED → re-claim ≤3 attempt | retry bounded | reportResult |
| E. Printer terima data, ACK gagal total | job PROCESSING → lease 90s → re-claim → **duplikat fisik mungkin** (at-least-once) | duplikat langka, terdokumentasi | X4.22 skenario 2 |
| F. Refresh halaman sukses | GET read-only — tidak ada job baru | aman | ReceiptService docblock |
| G. Klik bayar 2× | ditolak: amount > remaining | aman | PaymentService:177 |

## 11. Duplicate Receipt Risk

- **Transaction duplicate:** mustahil via submit ganda (server cap remaining);
  idempotensi completeOrder.
- **Physical print duplicate:** dimungkinkan hanya pada jendela
  E (print-OK + ACK-lost + lease expiry) — semantik **at-least-once**
  (sama persis kitchen/voucher). Bukan exactly-once — tidak diklaim.
- Pembuatan job receipt harus terjadi **sekali** di dalam/mengikuti
  transaksi payment (bukan di halaman receipt) agar refresh tidak
  menghasilkan job ganda.

## 12. Server vs Android Responsibilities

Model target sudah tercapai arsitektur existing:
- **Server:** payment authority, order authority, receipt data authority
  (snapshot), print job authority (create/claim/lease/ACK).
- **Android:** Bluetooth transport, ESC/POS delivery, retry transport,
  ACK reporting. **Printer:** physical output.
Tidak ada tanggung jawab yang tumpang tindih; cukup tambahkan "receipt
job trigger" di sisi server.

## 13. Reuse Matrix

| Component | Reuse for CASHIER? | Modification Needed? | Risk |
|---|---|---|---|
| Payment | YA (tak tersentuh) | TIDAK | none |
| Receipt data | YA (ReceiptService snapshot) | tambah builder payload teks | low |
| print_jobs | YA penuh | TIDAK | none |
| claim/lease | YA penuh | TIDAK | none |
| ACK | YA penuh | TIDAK | none |
| PrintPipeline | YA penuh | TIDAK | none |
| EscPosRenderer | YA penuh | TIDAK | none |
| BluetoothPrinter | YA penuh | TIDAK | none |
| Android Service | YA (APK sama) | TIDAK | none |
| Device configuration | YA (daftar `PRINTER_KASIR`) | registrasi device baru | none |
| Reprint | BELUM ADA | tambah action eksplisit (opsional MVP) | low |

## 14. Minimal Target Architecture

```text
KASIR (browser POS)
   │ POST /cashier/orders/{id}/payment
   ▼
PAYMENT SUCCESS (transaksi atomik — otoritas keuangan)
   │ enqueue (sekali, di jalur yang sama dengan completeOrder)
   ▼
print_jobs  destination=RECEIPT  payload=teks struk (snapshot)
   │ claim (token device PRINTER_KASIR → server menetapkan RECEIPT)
   ▼
Android Print Agent (APK existing, tablet kasir, config device KASIR)
   │ EscPosRenderer → Bluetooth SPP
   ▼
PRINTER KASIR (thermal ESC/POS)
   │ ACK SUCCESS/FAILURE → print_jobs PRINTED/FAILED (audit)
```

## 15. Required Changes (MVP)

1. **Server (kecil):** `ReceiptService::buildPrintPayload(order)` — builder
   teks struk dari snapshot (no. struk, waktu, branch, kasir, item, subtotal,
   event/reward, total, metode, uang diterima, kembalian).
2. **Server (kecil):** pemicu job — setelah payment sukses (jalur
   `PaymentService::record` setelah `completeOrder`, satu `PrintJobService::
   create(branch,'RECEIPT',payload)`; TIDAK di halaman receipt).
3. **Opsional MVP:** tombol "Cetak Ulang" di halaman receipt (job baru,
   payload sama) + endpoint kecil.
4. **Operasional:** daftar device `PRINTER_KASIR` cabang kasir + install
   APK existing di tablet kasir + pairing printer kasir.

## 16. Optional Changes

- Auto-print flag per cabang (setting) bila owner ingin mematikan cetak otomatis.
- Indikator status print di halaman receipt (polling read-only).

## 17. Not Required

Migration/tabel baru (print_jobs + snapshot cukup) · perubahan
BluetoothPrinter/PrintPipeline/renderer · APK baru · perubahan payment/
completeOrder · perubahan claim/lease/ACK · dependency baru · Redis/
queue/WebSocket/cloud.

## 18. Risks

1. Duplikat fisik pada jendela ACK-lost (at-least-once) — sama dengan
   kitchen; terdokumentasi, diterima.
2. Printer kasir belum diketahui (SPP? lebar kertas?) — hardware survey
   diperlukan sebelum validasi.
3. Struk tertunda bila agent/printer offline — job antre PENDING (bukan
   loss); keputusan bisnis apakah kasir boleh menunggu/diabaikan.
4. Satu APK = satu device — tablet kasir butuh instalasi terpisah dari
   tablet dapur (atau pemisahan destination via dua device di tablet
   berbeda).

## 19. Recommended Next Phase

**X4.11-I1 — RECEIPT PRINT MVP (server-only + ops):** builder payload +
pemicu job di payment success (+ cetak ulang opsional) + tes targeted +
regresi; registrasi device PRINTER_KASIR + survey printer kasir. Setelah
itu validasi hardware dengan APK existing.

---

## FINAL VERDICT

**PASS — READY FOR IMPLEMENTATION DESIGN**
(evidence lengkap; satu-satunya gap = spesifikasi printer kasir → hardware survey)
