"""Chapters 5-12: stack, application inventory, backend, ERP, POS, B2B, NextReport, shared packages."""
from pathlib import Path

from helpers import Builder, ACCENT, MUTED, WARN, OK, INFO
import facts as F


def ch5_stack(b: Builder) -> None:
    b.h1("5. Technology stack and dependency overview")
    b.h2("5.1 Backend")
    b.table(["Component", "Version / choice", "Role"],
            [["Language", "Go 1.26.5 (go.mod)", "Backend implementation"],
             ["Router", "github.com/go-chi/chi/v5 v5.3.1", "Route tree, middleware chaining, URL params"],
             ["Database driver", "github.com/jackc/pgx/v5 v5.10.0", "pgxpool + typed queries"],
             ["Cache/client", "github.com/redis/go-redis/v9 v9.22.0", "Rate limiting, idempotency"],
             ["Decimals", "github.com/shopspring/decimal v1.4.0", "All money and quantity arithmetic"],
             ["Password hashing", "golang.org/x/crypto/bcrypt", "User password verification"],
             ["Query generation", "sqlc v2 config (apps/backend/sqlc.yaml)", "531 named queries → internal/db"],
             ["Migrations", "Custom runner (internal/dbmigrate) + embedded SQL", "Applied at boot inside one transaction per file"],
             ["Structured logging", "log/slog (stdlib)", "JSON-ish key/value logs with request IDs"]],
            caption="Backend dependencies", widths=[1.5, 2.5, 2.5], font_size=8.0)

    b.h2("5.2 Frontend applications")
    b.table(["Library", "erp", "pos", "b2b", "Purpose"],
            [["vue", "3.5.41", "3.5.41", "3.5.41", "UI framework"],
             ["vite", "8.2.1", "8.2.1", "—", "Dev server and bundler"],
             ["nuxt", "—", "—", "4.x", "SSR framework with Nitro server"],
             ["vue-router", "5.2.0", "5.2.0", "Nuxt router", "Client routing"],
             ["pinia", "4.0.2", "4.0.2", "4.0.2", "State stores"],
             ["axios", "1.19.0", "1.19.0", "1.7.0", "HTTP client"],
             ["primevue", "4.5.5", "—", "—", "ERP component library (themed)"],
             ["@nuxt/ui", "—", "—", "4.x", "B2B component library"],
             ["tailwindcss", "4.3.3", "4.3.3", "(via @nuxt/ui)", "Utility CSS"],
             ["chart.js / vue-chartjs", "4.5.1 / 5.3.4", "—", "4.5.1 / 5.3.4", "Dashboard charts"],
             ["leaflet", "1.9.4", "—", "1.9.4", "Maps (OSM tiles)"],
             ["zxing-wasm", "3.1.3", "3.1.3", "—", "Barcode decoding in WASM"],
             ["barcode-detector", "3.2.2", "3.2.2", "—", "BarcodeDetector polyfill"],
             ["exceljs", "4.4.0", "4.4.0", "—", "Client-side XLSX export"],
             ["@phosphor-icons/vue", "2.2.1", "2.2.1", "—", "Icon set"]],
            caption="Frontend dependency matrix (declared versions)", widths=[1.5, 1.1, 1.1, 1.1, 1.7], font_size=7.8)

    b.h2("5.3 Reporting engine")
    b.p("apps/nextreport is a 260-line Node HTTP server. It accepts POST /render with a "
        "JSON template and data, runs the vendored NextReport engine (engine/index.js) to "
        "produce HTML, and — for format=pdf — renders that HTML through Puppeteer/Chromium "
        "with printBackground and zero margins. GET /health and / returns a version banner; "
        "POST /validate checks a template against the engine's schema. It has no "
        "authentication of its own; it is reachable only on the internal container network.")

    b.h2("5.4 Build-time tooling")
    b.table(["Tool", "Version", "Used by"],
            [["Turbo", "^2.1.0", "Workspace task orchestration"],
             ["TypeScript", "^5.9.3", "Package typings and b2b typecheck"],
             ["Python 3.11 (CI) / 3.14 (audit host)", "—", "Drift guard and contract generators"],
             ["PyYAML", "installed by CI", "scripts/check-drift.py, scripts/gen-*.py"],
             ["puppeteer", "^24.40.0", "NextReport PDF rendering (and, locally, the diagram renderer used for this document)"]],
            caption="Build-time tooling", widths=[2.0, 1.8, 2.7])


def ch6_inventory(b: Builder) -> None:
    b.h1("6. Application inventory")
    b.p("Five applications are present and implemented. All five have a Dockerfile, a "
        "package manifest and an entry point. No application in the repository is a stub.")
    b.table(
        ["App", "Entry point", "Dev command", "Prod execution", "Status"],
        [
            ["backend", "cmd/server/main.go → run() → router()", "pnpm dev:backend", "Static binary /app/server, non-root uid 10001", "Implemented; 35 internal packages"],
            ["erp", "src/main.js → App.vue → router/index.js", "pnpm --filter @keluargamart/erp dev", "nginx 1.29-alpine serving dist/, /api and /uploads proxied", "Implemented; 72 pages, 90 components"],
            ["pos", "src/main.js → App.vue → router/index.js", "pnpm --filter @keluargamart/pos dev", "nginx 1.29-alpine serving dist/, /api and /uploads proxied", "Implemented; 8 pages, 42 components"],
            ["b2b", "app/app.vue + Nuxt file routing + server/api/v1/[...path].ts", "pnpm --filter @keluargamart/b2b dev", "Nitro node server (.output/server)", "Implemented; 42 pages, 17 components"],
            ["nextreport", "server.mjs", "pnpm --filter @keluargamart/nextreport dev", "node server.mjs in node:22-slim with Chromium", "Implemented; 3 HTTP endpoints"],
        ],
        caption="Application inventory", widths=[0.8, 1.85, 1.4, 1.6, 1.35], font_size=7.8)

    b.h2("6.1 Modules that are libraries, not applications")
    b.table(["Directory", "Nature", "Why it is not an application"],
            [["apps/backend/cmd/seedctl", "One-off CLI", "Applies seed.sql or runs a verification query; no server"],
             ["apps/backend/cmd/importer", "One-off CLI", "Data import helper"],
             ["apps/backend/internal/parity", "Test-only package", "Compares Laravel-era expectations; only *_test.go"],
             ["apps/backend/internal/apidocs", "Handler package", "Serves the Scalar API reference and openapi.yaml"],
             ["apps/backend/internal/reportproxy", "Handler package", "Proxies render requests to NextReport and shapes the data"]],
            caption="Non-application code that looks like an application", widths=[1.9, 1.3, 3.3])


def ch7_backend(b: Builder) -> None:
    b.h1("7. Backend architecture")
    b.h2("7.1 Boot sequence")
    b.p("main() loads config, builds a slog logger, then run() performs a fixed sequence. "
        "The order matters: the server deliberately stays up when dependencies are down, "
        "so that /health can answer while /ready reports the failure.")
    b.numbered([
        "config.Load() reads every environment variable (pkg/config/config.go).",
        "database.New() opens a pgxpool with MaxConns=25, MinConns=2, MaxConnLifetime=1h, MaxConnIdleTime=15m.",
        "If DATABASE_URL is empty the process logs a warning and continues in health-only mode — no data routes are mounted.",
        "If a pool exists and AUTO_MIGRATE is true, dbmigrate.Run() applies every pending embedded migration. A failure aborts startup; the server never serves a wrong schema.",
        "redis.New() connects; a nil client disables rate limiting (fail-open) but leaves the login limiter fail-closed.",
        "storage.NewLocal(UPLOAD_DIR) prepares the local object store.",
        "reportproxy.EnsureLocalEngine() starts the bundled NextReport process in development/standalone mode and waits for readiness.",
        "http.Server is created with ReadHeaderTimeout 10s, ReadTimeout 30s, WriteTimeout 60s, IdleTimeout 120s.",
        "SIGINT/SIGTERM triggers a 10-second graceful shutdown.",
    ])
    b.note("Verified runtime behaviour",
           "Starting the backend with a migrated database logged 'dbmigrate: schema up "
           "to date', started the embedded NextReport engine on 127.0.0.1:5489, then "
           "logged 'server_started addr=:18080 env=development'. GET /health returned "
           "{\"env\":\"development\",\"status\":\"ok\"} and GET /ready returned "
           "{\"database\":\"connected\",\"redis\":\"unavailable\",\"status\":\"degraded\"} "
           "when Redis was not yet running.", "verified")

    b.h2("7.2 Application architecture")
    b.figure(Path("../diagrams/rendered/fig-application-architecture.png"),
             "Application and package architecture. Dashed edges are declared dependencies "
             "with no import in the source; everything else is a verified request path or a "
             "real import.")
    b.p("The backend is the only component that touches PostgreSQL or Redis. Frontends "
        "reach it over HTTP; the ERP and POS nginx containers proxy /api and /uploads "
        "same-origin, and B2B proxies through a Nitro catch-all route. Shared packages are "
        "plain ES modules with no build step: the contracts and permissions packages are "
        "generated text files plus a thin wrapper.")

    b.h2("7.3 Request pipeline")
    b.figure(Path("../diagrams/rendered/fig-request-lifecycle.png"),
             "Request lifecycle for a representative authenticated write. The 401 and 403 "
             "branches are the two points where authorisation fails; both return the "
             "standard error envelope with the request id.")

    b.table(["Middleware", "File", "Behaviour"],
            [["RequestID", "pkg/middleware/requestid.go", "Accepts a client X-Request-Id only if it passes a printable/length allowlist; otherwise generates one. Sets the response header and the context value."],
             ["Logging", "pkg/middleware/logging.go", "Wraps the ResponseWriter, logs method, path, status, bytes and duration with the request id."],
             ["Recovery", "pkg/middleware/recovery.go", "Catches panics, logs the last 8 KiB of the stack, returns a generic 500 envelope."],
             ["SecurityHeaders", "pkg/middleware/security.go", "X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, X-XSS-Protection, Permissions-Policy."],
             ["BodyLimit", "pkg/middleware/security.go", "413 on a declared Content-Length above the limit; http.MaxBytesReader otherwise."],
             ["CORS", "pkg/middleware/cors.go", "Allowlist match; '*' never echoes the origin and never sets Allow-Credentials."],
             ["Rate limiter (generic)", "pkg/middleware/ratelimit.go", "Per-IP across /api/v1, default 300/min, fail-open on Redis errors."],
             ["Rate limiter (login)", "pkg/middleware/ratelimit.go", "Per-IP for login/register/forgot/reset, default 10/min, fail-closed (503 when Redis is down)."],
             ["Idempotency", "pkg/middleware/idempotency.go", "Mandatory key on money endpoints; scoped to user + method + route; replay on same payload, 409 on different payload, fail-closed without Redis."],
             ["Activity log", "internal/system/middleware.go", "Records mutations into activity_logs, skipping the two noisy paths it is given."]],
            caption="Middleware chain, in mount order", widths=[1.35, 1.9, 3.25], font_size=7.8)

    b.h2("7.4 Module inventory")
    b.table(["Package", "Handlers", "Responsibility"],
            [["account", "4", "Buyer address book (B2B account)"],
             ["accounting", "22", "COA, departments, periods, journals, posting, reversal, ledger, advances"],
             ["auth", "10", "Login, register, password reset, me, logout, tokens, permission resolution"],
             ["cart", "6", "E-commerce cart and checkout"],
             ["cashier", "8", "Shift open/close, drawer entries, cash movements"],
             ["catalog", "27", "Products, categories, units, brands, product units, barcodes"],
             ["distribution", "20", "Delivery orders, dispatches, shipments, tracking, fleet, LHPP"],
             ["finance", "24", "Invoices, payments, cashflows, payment vouchers, proof upload, Midtrans"],
             ["galon", "11", "Water-container transactions and loans"],
             ["geo", "2", "Geocoding and routing"],
             ["hr", "28", "Employees, attendance, leave, payroll, performance"],
             ["inventory", "13", "Stock, movements, stock-in/out, opname, transfers"],
             ["lhpp", "12", "LHPP groups and settlement"],
             ["membership", "14", "Memberships, tiers, points, enrolment"],
             ["partners", "13", "Suppliers and customers"],
             ["purchasing", "19", "Purchase orders, goods receipts, vouchers, supplier debt"],
             ["reports", "15", "Analytical reports and report templates"],
             ["sales", "35", "Sales orders, POS, returns, vouchers, Midtrans, snap sweep"],
             ["salestargets", "5", "Targets per period/role/scope"],
             ["salestracking", "15", "Visits, activities, routes, GPS locations"],
             ["settings", "2", "Key/value application settings"],
             ["system", "25", "Users, roles, role-permissions, stores, activity log, notifications"],
             ["tickets", "5", "Support tickets and comments"],
             ["uploads", "4", "File upload and object serving"],
             ["warehousedeposit", "5", "Driver cash deposits"],
             ["warehouses", "6", "Warehouse master and options"],
             ["reportproxy", "3", "PDF/HTML render proxy"],
             ["health", "2", "/health and /ready"],
             ["apidocs", "2", "/docs and /openapi.yaml"]],
            caption="Backend module inventory (handler counts from the handler.go files)",
            widths=[1.3, 0.8, 4.4], font_size=7.8)

    b.h2("7.5 Layering")
    b.p("Every module follows the same shape: handler.go decodes HTTP and maps errors; "
        "service.go holds the rules and the transaction boundary; repository.go wraps the "
        "sqlc Querier (or, for the newest modules, hand-written SQL); model.go holds the "
        "domain and wire types. Handlers never touch the pool directly except for a few "
        "read-only convenience queries in main.go wiring.")
    b.note("Inconsistency", "internal/tickets, internal/salestargets, internal/reports/templates "
           "and internal/warehousedeposit use hand-written SQL instead of sqlc because their "
           "tables are missing from apps/backend/db/schema.sql (finding F-19). The layering "
           "is the same; only the query source differs.", "inferred")

    b.h2("7.6 Transactions and concurrency")
    b.bullets([
        "Financial writes, stock moves and state transitions run inside pool.Begin() … Commit() with a deferred Rollback.",
        "Order rows are locked with SELECT … FOR UPDATE before a status transition (OrderByIDForUpdate, GetShiftByIDForUpdate, LockJournal).",
        "Stock rows are locked before reservation/deduction (GetSalesStockRowForUpdate). Items are sorted by product_id before deduction so two concurrent carts lock in the same order.",
        "Document numbers are allocated with a single INSERT … ON CONFLICT DO UPDATE … RETURNING counter inside the caller's transaction (pkg/seq), which is why two concurrent sales can never receive the same number — verified by TestConcurrentSalesOrderNumbers.",
        "Idempotency records are written to Redis and replayed by the middleware; the response body and status are stored, so a replay is byte-identical.",
    ])

    b.h2("7.7 Database access architecture")
    b.figure(Path("../diagrams/rendered/fig-db-access.png"),
             "Database access paths: sqlc-generated queries, the sequence allocator, and the "
             "Redis-backed idempotency store.")


def ch8_erp(b: Builder) -> None:
    b.h1("8. ERP documentation")
    b.h2("8.1 Identity and entry points")
    b.table(["Property", "Value"],
            [["Purpose", "Back-office operations for every internal role except the cashier's selling flow."],
             ["Framework", "Vue 3 SPA, Vite 8, vue-router 5 (history mode), Pinia 4."],
             ["Entry", "src/main.js → src/App.vue → src/router/index.js"],
             ["Layouts", "AuthLayout.vue (login), DefaultLayout.vue (sidebar + topbar + router-view)"],
             ["API client", "src/services/http.js — axios with baseURL VITE_API_URL || '/api/v1', 20 s timeout, bearer from session storage, Idempotency-Key fallback"],
             ["Auth store", "src/stores/auth.js — token, user, effective permissions, isERP, can(perm), checkSetup()"],
             ["Navigation", "src/config/navigation.js — MODULES metadata and NAV_ITEMS (perm-gated)"],
             ["Build", "vite build → dist/, served by nginx with SPA fallback"],
             ["Tests", "39 files, 344 tests, Vitest + @vue/test-utils + jsdom"]],
            caption="ERP identity", widths=[1.5, 5.0])

    b.h2("8.2 Route inventory")
    b.p("The ERP router contains 95 path records: 78 page routes (each resolving to a "
        "page component), 15 redirect-only records kept for legacy bookmarks, and 2 layout "
        "wrappers. Seventy distinct page components are reachable; 72 .vue files exist under "
        "src/pages, of which finance/CashflowPage.vue and finance/InvoicesPage.vue are no "
        "longer referenced by any route. Every page route except login, the launcher, "
        "changelog and contact carries meta.perm — the exact backend permission key for its "
        "surface — and the guard rejects when auth.can(perm) is false.")
    b.table(["Path", "Name", "Permission", "Module"],
            [
                ["/login", "login-page", "(public)", "—"],
                ["/", "launcher", "(authenticated)", "dashboard"],
                ["/dashboard", "dashboard", "dashboard.view", "dashboard"],
                ["/master-data/products", "products", "masterdata.products.read", "masterData"],
                ["/master-data/categories", "categories", "masterdata.categories.read", "masterData"],
                ["/master-data/units", "units", "masterdata.units.read", "masterData"],
                ["/master-data/suppliers", "suppliers", "masterdata.suppliers.view", "masterData"],
                ["/master-data/customers", "customers", "masterdata.customers.read", "masterData"],
                ["/memberships", "memberships", "masterdata.memberships.lookup", "membership"],
                ["/memberships/tiers", "membership-tiers", "masterdata.memberships.tiers.manage", "membership"],
                ["/inventory/stock", "stock", "inventory.stock.view", "inventory"],
                ["/inventory/movements", "stock-movements", "inventory.movements.view", "inventory"],
                ["/inventory/transfers", "transfers", "inventory.transfers.view", "inventory"],
                ["/inventory/opnames", "opnames", "inventory.opnames.view", "inventory"],
                ["/purchasing/po", "po", "purchasing.purchase-orders.view", "purchasing"],
                ["/purchasing/po/:id/print", "po-print", "purchasing.purchase-orders.view", "purchasing"],
                ["/purchasing/goods-receipts", "goods-receipts", "purchasing.goods-receipts.view", "purchasing"],
                ["/purchasing/vouchers", "vouchers", "purchasing.vouchers.view", "purchasing"],
                ["/purchasing/supplier-debts", "supplier-debts", "purchasing.purchase-orders.view", "purchasing"],
                ["/sales/pos", "pos", "sales.pos.create", "sales"],
                ["/sales/orders", "sales-orders", "sales.sales-orders.view", "sales"],
                ["/sales/orders/:id", "sales-order-detail", "sales.sales-orders.view", "sales"],
                ["/sales/lhpp", "lhpp", "sales.lhpp.view", "sales"],
                ["/sales/targets", "sales-targets", "sales.targets.view", "sales"],
                ["/sales/vouchers", "sales-vouchers", "sales.sales-vouchers.view", "sales"],
                ["/distribution/customer-orders", "customer-orders", "sales.sales-orders.view", "distribution"],
                ["/distribution/delivery-orders", "delivery-orders", "distribution.delivery-orders.view", "distribution"],
                ["/distribution/drivers", "drivers", "distribution.dispatches.create", "distribution"],
                ["/finance/cashier-cash", "cashier-cash", "finance.cashflows.view", "finance"],
                ["/finance/cashflow", "cashflow", "finance.cashflows.view", "finance"],
                ["/finance/invoices", "invoices", "fakturisasi.invoices.view", "finance"],
                ["/finance/payments", "payments", "fakturisasi.payments.view", "finance"],
                ["/finance/receivables", "finance-receivables", "fakturisasi.invoices.view", "finance"],
                ["/finance/hub", "finance-hub", "finance.cashflows.view", "finance"],
                ["/accounting/journals/general", "accounting-journals-general", "accounting.journals.view", "accounting"],
                ["/accounting/journals/cash-disbursement", "accounting-journals-cash-disbursement", "accounting.journals.view", "accounting"],
                ["/accounting/journals/cash-receipt", "accounting-journals-cash-receipt", "accounting.journals.view", "accounting"],
                ["/accounting/journals/bank-disbursement", "accounting-journals-bank-disbursement", "accounting.journals.view", "accounting"],
                ["/accounting/journals/bank-receipt", "accounting-journals-bank-receipt", "accounting.journals.view", "accounting"],
                ["/accounting/journals/deposit-receipt", "accounting-journals-deposit-receipt", "accounting.journals.view", "accounting"],
                ["/accounting/journals/deposit-payment", "accounting-journals-deposit-payment", "accounting.journals.view", "accounting"],
                ["/accounting/ledger", "accounting-ledger", "accounting.ledger.view", "accounting"],
                ["/accounting/coa", "accounting-coa", "accounting.accounts.view", "accounting"],
                ["/accounting/advances", "accounting-advances", "accounting.advances.view", "accounting"],
                ["/reports/cashflow-statement", "reports-cashflow-statement", "finance.reports.view", "reports"],
                ["/reports/profit-loss", "reports-profit-loss", "finance.reports.view", "reports"],
                ["/reports/balance-sheet", "reports-balance-sheet", "finance.reports.view", "reports"],
                ["/reports/sales", "reports-sales", "sales.reports.view", "reports"],
                ["/reports/sales-breakdown", "reports-sales-breakdown", "sales.reports.view", "reports"],
                ["/reports/purchase-breakdown", "reports-purchase-breakdown", "finance.reports.view", "reports"],
                ["/reports/aging", "reports-aging", "finance.reports.view", "reports"],
                ["/reports/stock", "reports-stock", "inventory.stock.view", "reports"],
                ["/reports/stock-detail", "reports-stock-detail", "inventory.stock.view", "reports"],
                ["/reports/consolidated", "consolidated", "finance.reports.view", "reports"],
                ["/warehouse", "warehouse", "inventory.stock.view", "distribution"],
                ["/tracking/livemap", "livemap", "sales_tracking.locations.view", "tracking"],
                ["/tracking/visits", "visits", "sales_tracking.visits.view", "tracking"],
                ["/tracking/locations", "tracking-locations", "sales_tracking.locations.view", "tracking"],
                ["/tracking/activities", "activities", "sales_tracking.activities.view", "tracking"],
                ["/tracking/routes", "routes", "sales_tracking.routes.view", "tracking"],
                ["/tracking/visit-schedule", "visit-schedule", "sales_tracking.visits.view", "tracking"],
                ["/tracking/my-visits", "my-visits", "sales_tracking.visits.view_own", "tracking"],
                ["/distribution/map", "tracking-map", "distribution.delivery-orders.view", "distribution"],
                ["/warehouse", "warehouse", "inventory.stock.view", "distribution"],
                ["/hr/employees", "employees", "hr.employees.view", "hr"],
                ["/hr/attendance", "attendance", "hr.attendance.view", "hr"],
                ["/hr/payroll", "payroll", "hr.payrolls.view", "hr"],
                ["/hr/leaves", "leaves", "hr.leaves.view", "hr"],
                ["/hr/performances", "performances", "hr.performances.view", "hr"],
                ["/system/users", "users", "system.users.view", "system"],
                ["/system/roles", "roles", "system.roles.view", "system"],
                ["/system/reports", "report-templates", "system.reports.view", "system"],
                ["/system/reports/:id/designer", "report-designer", "system.reports.manage", "system"],
                ["/system/stores", "stores", "system.stores.view", "system"],
                ["/system/stores/:id", "branch-detail", "system.stores.view", "system"],
                ["/system/activity-log", "activity-log", "system.activity-logs.view", "system"],
                ["/settings", "settings", "system.settings.view", "settings"],
                ["/support/tickets", "support-tickets", "support.tickets.view", "system"],
                ["/support/tickets/:id", "support-ticket-detail", "support.tickets.view", "system"],
                ["/changelog", "changelog", "(authenticated)", "dashboard"],
                ["/contact", "contact", "(authenticated)", "dashboard"],
               ],
            caption="ERP page routes (78 records; 15 legacy redirects and 2 layout wrappers are omitted)",
            widths=[2.5, 1.9, 2.0, 0.95], font_size=7.2)
    b.note("Route/permission consistency",
           "Every meta.perm in the router (48 distinct keys) exists in the backend "
           "catalogue; 46 of them also appear in the sidebar config. The two router-only "
           "keys are system.reports.manage (designer page) and system.settings.view "
           "(settings page), which are actions rather than navigation entries.", "verified")

    b.h2("8.3 Navigation model")
    b.p("Sidebar visibility is permission-driven, not role-driven. navigation.js states this "
        "explicitly: the only authority is child.perm evaluated through auth.can(). The "
        "MODULES array still carries role hints, but the comment in the file says it is "
        "metadata used as a fallback, not the visibility rule.")
    b.table(["Section", "Items (permission)"],
            [["—", "Dashboard (dashboard.view)"],
             ["Operasional", "Master Data: Produk, Kategori, Satuan, Supplier, Customer; Keanggotaan: Daftar Member, Pengaturan Diskon Tier; Inventory: Posisi Stok, Pergerakan Stok, Transfer Stok, Opname; Purchasing: Purchase Order, Penerimaan Barang, Voucher, Hutang Supplier; Sales: Katalog Produk, Sales Order, LHPP, Target Sales, Voucher Penjualan; Finance: Laporan Arus Kas, Piutang & Pembayaran, Hub Keuangan, Arus Kas Kasir"],
             ["Jurnal & Buku Besar", "Jurnal Umum, Jurnal Pengeluaran Kas, Jurnal Penerimaan Kas, Jurnal Pengeluaran Bank, Jurnal Penerimaan Bank, Jurnal Terima Deposit, Jurnal Keluar Deposit, Buku Besar, COA & Departemen, Deposit Customer"],
             ["Analitik", "Laporan Arus Kas, Laba Rugi, Neraca, Laporan Penjualan, Penjualan Rinci, Pembelian Rinci, Umur Piutang/Hutang, Laporan Stok, Stok Rinci, Konsolidasi"],
             ["Toko & Cabang", "Toko (system.stores.view)"],
             ["Pelacakan & Lapangan", "Kunjungan Sales, Jadwal Kunjungan, Jadwal Saya, Peta Aktivitas Sales, Lokasi Customer, Aktivitas, Rute, Customer Order, Delivery Order, Driver, Peta Pelacakan, Gudang & Serah Terima, Karyawan, Absensi, Payroll, Cuti, Evaluasi"],
             ["Sistem", "Pengguna, Peran (adminOnly), Template Laporan, Log Aktivitas"],
             ["Dukungan & Bantuan", "Tiket Kendala, Catatan Rilis, Hubungi Developer"]],
            caption="ERP sidebar structure", widths=[1.4, 5.1], font_size=8.0)
    b.note("Navigation gap", "Several journal sub-pages carry an additional roles: [...] list in "
           "navigation.js on top of their permission (for example cash-disbursement is limited to "
           "cashier, store_manager, finance, finance_head, admin, owner). The router does not apply "
           "that role list — only meta.perm — so a user who holds accounting.journals.view but is "
           "not in the role list can reach the page by URL while the menu hides it. The backend "
           "still enforces its own journal-type/role validation (validateRoleForJournalType).",
           "verified")

    b.h2("8.4 Key pages and components")
    b.table(["Component group", "Examples", "Role"],
            [["k/ (design system)", "KButton, KInput, KSelect, KDataTable, KModal, KFormModal, KDatePicker, KCurrencyInput, KStatCard, KToaster", "In-house component layer used by every page; 55 components"],
             ["k/entity", "KCustomerSelect, KProductSelect, KSupplierSelect, KWarehouseSelect", "Async search selects backed by the corresponding list endpoints"],
             ["layout", "Sidebar, Topbar, ShortcutCheatsheetModal", "Persistent shell; shortcut cheatsheet opens with ? or Ctrl+/"],
             ["dashboard", "RevenueTrendChart, CashflowChart, LowStockList, TopProductsComparisonChart, AOPerformance, RecentNOO, PendingInvoices", "Dashboard widgets; each reads a reports or dashboard endpoint"],
             ["inventory", "StockValueModal", "HPP/stock-value breakdown dialog"],
             ["reports", "ReportPrintButton", "Calls /api/v1/reports/render and opens the PDF"],
             ["scanner", "BarcodeScannerModal", "zxing-wasm camera scanning for products"],
             ["map", "BaseMap, MapPicker", "Leaflet + OSM tiles; coordinate precision helpers"],
             ["accounting", "AccountingWorkflowBanner, JournalGuideModal", "Draft → post → reverse guidance"]],
            caption="ERP component inventory (90 .vue files, grouped)",
            widths=[1.35, 3.15, 2.0], font_size=8.0)

    b.h2("8.5 State and interaction behaviour")
    b.bullets([
        "Auth store persists the token and user in session storage; on boot it refreshes /auth/me to obtain the effective permission map before any route renders.",
        "The router guard clears the session and bounces to login when the identity's application scope is not ERP. The code comments state plainly that this is UX, not a security control.",
        "Admin/owner with an empty permission matrix are forced to the Roles page (?setup=1) until at least one permission is assigned — a first-run bootstrap guard.",
        "Table pages share useDataTableQuery (search, page, per-page, sort) so query strings survive a refresh.",
        "Idempotency keys are minted by services/idempotency.js (keyFor(slot) / settle(slot, err)); the axios interceptor adds a random fallback for unmigrated writes and logs a development warning (finding F-14).",
        "Charts use chart.js via vue-chartjs; maps use Leaflet with OSM tiles configured through VITE_MAP_* (attribution mandatory).",
        "Keyboard: the shortcut cheatsheet is reachable from the layout; barcode scanning uses the browser BarcodeDetector with a zxing-wasm fallback.",
    ])


def ch9_pos(b: Builder) -> None:
    b.h1("9. POS documentation")
    b.h2("9.1 Identity")
    b.table(["Property", "Value"],
            [["Purpose", "Cashier register for a single outlet and a single shift."],
             ["Framework", "Vue 3 SPA, Vite 8, Pinia, vue-router 5."],
             ["Entry", "src/main.js → App.vue (RouterView + Toaster + ConfirmDialog) → router/index.js"],
             ["Layout", "DefaultLayout.vue with AppSidebar and AppTopbar."],
             ["API client", "src/services/api/http.js — baseURL VITE_API_URL (origin only; the client appends /api/v1), token from sessionStorage or localStorage depending on 'remember me'."],
             ["Auth store", "src/stores/auth.js — token, user, stores[], permissions, isPOS, can(permission)."],
             ["Tests", "20 files, 111 tests."],
             ["Deliberate exclusions", "No product/category/unit master data, no user management, no analytics. Legacy routes redirect to /pos."]],
            caption="POS identity", widths=[1.5, 5.0])

    b.h2("9.2 Route inventory")
    b.table(["Path", "Name", "Permission", "Page"],
            [["/login", "login", "(public)", "LoginPage"],
             ["/", "—", "redirect → pos", "—"],
             ["/pos", "pos", "sales.pos.create", "POSPage — product grid, cart, checkout"],
             ["/transactions", "transactions", "sales.sales-orders.view", "TransactionsPage — history and receipt reprint"],
             ["/returns", "returns", "sales.returns.view", "ReturnsPage — return a completed sale"],
             ["/shifts", "shifts", "(authenticated)", "ShiftsPage — shift history, open/close, drawer"],
             ["/customers", "customers", "masterdata.customers.read", "CustomersPage — lookup, create, enrol member"],
             ["/settings", "settings", "(authenticated)", "SettingsPage — receipt prefs, store/galon toggles (device-local)"],
             ["/changelog", "changelog", "(authenticated)", "ChangelogPage"],
             ["/contact", "contact", "(authenticated)", "ContactPage"]],
            caption="POS routes", widths=[1.3, 1.2, 1.8, 2.2], font_size=8.0)

    b.h2("9.3 Checkout flow (verified)")
    b.figure(Path("../diagrams/rendered/fig-pos-flow.png"),
             "POS checkout: the order, its lines, stock deduction, invoice, payment and "
             "cashflow are written in one transaction; the deferred-QRIS branch leaves the "
             "invoice open until the provider confirms.")
    b.p("The register posts to POST /api/v1/pos with a mandatory Idempotency-Key. The "
        "service resolves the caller's store binding, maps a walk-in (customer_id 0) to the "
        "'Pelanggan Umum' placeholder without applying membership pricing, resolves the "
        "authoritative unit price from product_units (the client's unit_price is ignored), "
        "computes totals, allocates SO and invoice numbers from the sequence table, deducts "
        "stock with a row lock, writes a stock movement per line, creates the invoice, and "
        "records the payment. A cash tender below the total is rejected.")

    b.code("""POST /api/v1/pos
Idempotency-Key: <client-generated, stable per checkout attempt>
{
  "payment_method": "cash",
  "items": [{ "product_id": 1, "quantity": 2 }],
  "cash_received": 100000
}

201 Created
{
  "data": {
    "sales_order": {
      "so_number": "SO-202610-000001", "status": "invoiced",
      "order_type": "pos", "subtotal": "16000", "tax_amount": "0",
      "discount_amount": "0", "total_amount": "16000",
      "paid_amount": "16000", "remaining_amount": "0", "warehouse_id": 1
    },
    "invoice": {
      "invoice_number": "INV-202610-000001", "status": "paid",
      "total_amount": "16000", "paid_amount": "16000", "remaining_amount": "0"
    }
  },
  "meta": { "request_id": "..." }
}""", caption="POS checkout request and actual response (values from the audit run, one seeded product at 8 000)")

    b.note("Verified runtime behaviour",
           "The replay with the same key and body returned 201 with the identical invoice; "
           "the same key with a different quantity returned 409 CONFLICT 'Idempotency-Key "
           "already used with a different request'. Stock after the sale was 48 (from 50 "
           "stocked in, minus 2 sold). A request without the header returned 422 "
           "'Idempotency-Key header is required'.", "verified")

    b.h2("9.4 Shift and drawer")
    b.p("A shift records opening cash, then accumulates drawer entries (cash_in / cash_out) "
        "and cash sales linked by sales_orders.shift_id. Closing computes "
        "expected = opening + cash_in − cash_out + cash_sales and stores "
        "variance = actual − expected. A shift that is not open cannot be closed; the row "
        "is locked for the whole operation.")
    b.table(["Operation", "Endpoint", "Permission"],
            [["Open shift", "POST /api/v1/cashier/shifts", "cashier.shifts.open"],
             ["Close shift", "POST /api/v1/cashier/shifts/{id}/close", "cashier.shifts.close"],
             ["Cash in", "POST /api/v1/cashier/shifts/{id}/cash-in", "cashier.shifts.drawer"],
             ["Cash out", "POST /api/v1/cashier/shifts/{id}/cash-out", "cashier.shifts.drawer"],
             ["List shifts", "GET /api/v1/cashier/shifts", "cashier.shifts.view"]],
            caption="Shift endpoints", widths=[1.5, 3.1, 1.9])

    b.h2("9.5 Hardware and browser interactions")
    b.table(["Capability", "Implementation", "Status"],
            [["Barcode scanning", "BarcodeScannerModal + BarcodeInput; BarcodeDetector with zxing-wasm fallback; the WASM binary is bundled locally", "Verified in source"],
             ["Keyboard-first entry", "ShortcutCheatsheetModal, focus handling in PaymentInput/ProductSearch", "Verified in source"],
             ["Receipt printing", "utils/print.js → POST /api/v1/reports/render?template=pos-receipt&format=pdf, then window.print() or download; HTML fallback when the render fails", "Verified in source; PDF rendering verified at runtime"],
             ["Cash drawer", "Recorded as a drawer entry; no hardware kick command exists", "Verified: no device API in source"],
             ["Offline sales", "Not implemented — no service worker, no IndexedDB queue (finding F-17)", "Missing"]],
            caption="POS hardware/browser integration", widths=[1.4, 3.4, 1.7], font_size=8.0)


def ch10_b2b(b: Builder) -> None:
    b.h1("10. B2B documentation")
    b.h2("10.1 Identity")
    b.table(["Property", "Value"],
            [["Purpose", "Wholesale storefront with SSR, buyer accounts, tier pricing, and a commerce-admin console."],
             ["Framework", "Nuxt 4 (app/ directory convention), @nuxt/ui 4, Pinia, Nitro server."],
             ["Entry", "app/app.vue; file-based routing under app/pages; server route server/api/v1/[...path].ts."],
             ["API access", "Browser calls same-origin /api/**; the Nitro catch-all proxy forwards to NUXT_API_PROXY_TARGET and converts the km_auth_token cookie into an Authorization header for SSR."],
             ["Auth middleware", "app/middleware/auth.ts, admin.ts, guest.ts — all client-only, all documented as UX rather than security."],
             ["Locales", "app/locales/id (single locale)."],
             ["Tests", "20 files, 170 tests."]],
            caption="B2B identity", widths=[1.5, 5.0])

    b.h2("10.2 Page inventory (42 pages)")
    b.table(["Area", "Pages"],
            [["Public", "/ (home), /catalog, /products/[slug], /cart, /checkout, /checkout/confirmation, /login, /register, /register/status, /forgot-password, /reset-password, /membership, /galon, /changelog"],
             ["Account", "/account, /account/profile, /account/users, /account/addresses, /account/addresses/new, /account/settings, /account/notifications, /account/orders, /account/membership"],
             ["Orders & payment", "/orders, /orders/[id], /orders/[id]/tracking, /payments, /payments/[id], /quick-order, /order-import, /returns"],
             ["Support", "/support, /support/tickets, /support/[id]"],
             ["Admin", "/admin, /admin/customers, /admin/customers/[id], /admin/orders, /admin/orders/[id], /admin/products, /admin/promotions, /admin/analytics"]],
            caption="B2B pages grouped by area", widths=[1.3, 5.2], font_size=8.0)
    b.note("Documented vs implemented", "apps/b2b/docs/ROUTES.md lists /admin/catalog and /reorder, and "
           "notes that /admin/analytics redirects to /admin. The page tree has /admin/products and "
           "/admin/analytics.vue but no catalog or reorder page; the actual analytics page exists as a "
           "file rather than a redirect.", "verified")

    b.h2("10.3 Buyer flows")
    b.figure(Path("../diagrams/rendered/fig-b2b-order.png"),
             "B2B order lifecycle from catalogue to payment confirmation, including the "
             "deferred Midtrans branch and the return path.")
    b.p("A buyer browses the public catalogue, adds to cart, and checks out. Checkout "
        "requires an authenticated buyer with a linked customers row; the order is created "
        "with order_type=ecommerce and store/warehouse resolved from the cart context. "
        "Payment is either an offline transfer with a proof upload "
        "(POST /api/v1/payments/{id}/proof) or Midtrans Snap "
        "(POST /api/v1/sales-orders/{id}/snap-token). Buyer self-service is scoped by "
        "RequireApp(AppECommerce) plus an ownership check against the linked customer.")

    b.h2("10.4 Commerce admin")
    b.p("The admin console is gated by the customer's is_ecommerce_admin flag rather than by "
        "a platform role: GET /auth/me publishes customer.is_ecommerce_admin, and the admin "
        "middleware checks isAdmin. Backend-side, the admin surfaces use ecomGate(...) which "
        "accepts either the e-commerce-admin identity or the corresponding internal "
        "permission.")


def ch11_nextreport(b: Builder) -> None:
    b.h1("11. NextReport documentation")
    b.h2("11.1 Service surface")
    b.table(["Endpoint", "Method", "Behaviour"],
            [["/", "GET", "Health banner: {status:'ok', engine:'nextreport', version:'0.2.1'}"],
             ["/health", "GET", "Same banner"],
             ["/validate", "POST", "Runs the engine's template schema validation; 400 on invalid JSON"],
             ["/render", "POST", "Renders template+data to HTML (default) or PDF; 400 without a template; 500 with the error message and stack"],
             ["/api/render", "POST", "Alias of /render"]],
            caption="NextReport HTTP surface (apps/nextreport/server.mjs)")
    b.p("The service keeps one Chromium instance and reuses it (relaunching when disconnected). "
        "Each PDF request opens a page, sets content with a 15 s load timeout, prints with "
        "printBackground and zero margins, then closes the page. Page size comes from the "
        "template (A4 default; landscape honoured).")

    b.h2("11.2 Template model")
    b.p("Templates are stored in report_templates as JSONB (template_data + sample_data), "
        "versioned per report_code with a single active/default row. 22 rows exist after a "
        "clean migration: 19 report codes, with do-print at v2 and lhpp-print at v3.")
    b.table(["Report code", "Versions", "Consuming surface"],
            [["invoice-print", "1", "ERP finance / buyer invoice"],
             ["po-print", "1", "Purchasing purchase order"],
             ["do-print", "2", "Distribution delivery order"],
             ["receipt-print", "1", "Goods receipt"],
             ["pos-receipt", "1", "POS receipt"],
             ["b2b-order", "1", "B2B order document"],
             ["transaction-list", "1", "Transaction report"],
             ["shift-print / shift-list", "1 / 1", "Cashier shift slip and list"],
             ["lhpp-print", "3", "LHPP settlement"],
             ["voucher-print", "1", "Payment voucher"],
             ["sales-by-product / sales-by-customer", "1 / 1", "Sales analysis"],
             ["stock-summary", "1", "Stock report"],
             ["receivables-aging / payables-aging", "1 / 1", "Aging reports"],
             ["profit-loss / balance-sheet / cashflow-statement", "1 / 1 / 1", "Financial statements"]],
            caption="Seeded report templates (from migration 000064-000088)", widths=[2.3, 0.9, 3.3])
    b.note("Runtime verified", "GET /api/v1/reports/render?template=pos-receipt&format=pdf&sales_order_id=1 "
           "returned HTTP 200, application/pdf, 25 869 bytes, a valid single-page PDF. With the "
           "template rows missing the same call returns 404 'Active template not found for "
           "pos-receipt' (finding F-15).", "verified")

    b.h2("11.3 Access control")
    b.p("NextReport itself has no authentication. Every render goes through the backend's "
        "/api/v1/reports/render, which resolves the template, checks templatePerms (a "
        "per-template permission key), enforces buyer ownership for buyer-reachable "
        "templates, shapes the data from the owning service, and only then calls the "
        "internal service.")


def ch12_packages(b: Builder) -> None:
    b.h1("12. Shared packages and components")
    b.table(["Package", "Exports", "Generated from", "Consumers"],
            [["@keluargamart/contracts", "API_PREFIX, BACKEND_ROUTES (373 strings), routes.txt", "scripts/gen-contracts.py ← openapi.yaml", "erp, pos, b2b, api-client"],
             ["@keluargamart/permissions", "ROLES, ALL_PERMISSIONS (201), hasPermission/hasRole helpers, permissions.txt", "scripts/gen-permissions.py ← internal/auth/catalog.go", "erp, pos"],
             ["@keluargamart/utils", "PLATFORM_INFO, currency/date formatters, newIdempotencyKey, metadata", "hand-written", "erp, pos, b2b, api-client"],
             ["@keluargamart/api-client", "createHttpClient, normalizeApiError, ApiError", "hand-written", "none (declared by no app)"],
             ["@keluargamart/ui", "KAlert, KBadge, KButton, KCard, KEmptyState, KModal, KSkeleton, KSpinner, design tokens", "hand-written", "none (declared by three apps, imported by none)"]],
            caption="Shared package inventory", widths=[1.5, 2.2, 1.5, 1.3], font_size=7.8)

    b.h2("12.1 Drift guard")
    b.p("scripts/check-drift.py compares the generated route list with openapi.yaml and the "
        "generated permission list with catalog.go. It is the first CI job. Running it during "
        "this audit printed:")
    b.code("""PASS: Contracts drift check clean (373 routes).
PASS: Permissions drift check clean (201 keys).
ALL DRIFT CHECKS PASSED.""", caption="pnpm drift:check output at the audited commit")
    b.note("What the guard does not cover", "The guard compares only the two generated "
           "text artefacts. It does not compare the ROLES array (which has drifted, F-08), "
           "the OpenAPI RoleName enum (F-21), or the OpenAPI schema bodies against the Go "
           "structs.", "gap")

    b.h2("12.2 Why the UI package is unused")
    b.p("packages/ui ships eight Vue components and tokens.css. A search for the "
        "'@keluargamart/ui' specifier across apps/erp/src, apps/pos/src and apps/b2b/app "
        "returns no import. Each app has its own K* or Ui* component layer (erp: 55 k/ "
        "components; pos: 13 ui/ + 26 pos/ components). The declared dependency therefore "
        "adds install time and a false sense of shared design without sharing anything.")
