UNPKG

@eristack/ai-knowledge

Version:

Eristack knowledge pack for AI agents: recommend packages first, load the right Intent skills, and keep catalog facts in sync

285 lines (207 loc) • 10.5 kB
--- title: Document-with-lines ERP (header + QUPS lines) description: Canonical guide for job orders, cost sheets, invoices, forwarding — header document + priced lines. --- # Document-with-lines ERP (header + QUPS lines) **Canonical guide** for job orders, cost sheets, invoices, forwarding — header document + priced lines. Not warehouse GL. Partner/product masters stay **app-owned** — Eristack does not ship `@eristack/feature-*` vertical modules. Load: `@eristack/ai-knowledge#document-lines-erp` · Package map: [package-relationships](./package-relationships.md) · Horizon A: `@eristack/ai-knowledge#backseat-then-backend` (`registerHorizonDocumentSpine` + `examples/horizon-a`) · Version conflicts: [optimistic-document-version](./optimistic-document-version.md) · 409 canon: [http-errors](./http-errors.md). --- ## Spine (generic layers) | Concern | Package | | --- | --- | | Lines / GP | `@eristack/qups` — `calculateLine`, `patchLine`, `applyCellPatch`, `withQupsFields` | | Grid keyboard | `@eristack/spreadsheet-operator` — active cell / Enter / Tab; not xlsx (`spreadsheet-render`) | | Money / FX | `@eristack/money` — strings only; `convertAtQuotePerBase` for quote-per-base | | Document numbers | `@eristack/doc-number` — `{YYYY}` + optional `scope` per branch | | Lists | `@eristack/data-grid` — `type: wall`, `executeBackseatList` / `executeDrizzleList` | | Status rules | `@eristack/pbac` — `documents.transitions()` | | Access | `@eristack/rbac` + `@eristack/abac` (`assignmentPairMatch`) | | Mock API | `@eristack/backseat` — `atomic()`, `listRoutes()`, `jsonError()` | | Dates | `@eristack/timestamp` — wall mode for ETD/due; instant for posted_at | | Cache | `@eristack/epoch` — `bumpMany` after writes | | Version | App `version` + `expectedVersion` on PATCH | **Do not default:** `@eristack/stock-movement`, `@eristack/valuations`, `@eristack/financial-ledger` unless inventory/accounting is in scope. --- ## App-owned masters | Master | App schema | | --- | --- | | Partner | `isCustomer`, `isVendor`, codes, addresses — app tables | | Product / charge codes | App tables or enums | | Job / cost sheet / invoice | App aggregates + Drizzle migrations | Libraries supply **line math, lists, auth, numbering** — not vertical tables. --- ## Typical aggregates ```text Job (header: branch, trade, ETD wall, customerId, status, version) └── CostSheet 1:1 (lines: QUPS buy/sell, status, version) Invoice (header + commercial lines, due_at wall, version) Partner (app-owned) ``` --- ## End-to-end PATCH sequences ### A. Edit job header only ```text GET /jobs/:id → { …header, version: 3 } PATCH /jobs/:id → { expectedVersion: 3, etd: { kind:"wall", local:"2026-09-01", timezone:"Asia/Jakarta" } } → 200 { version: 4 } → epoch.bumpMany(["jobs"]) ``` 409 if another tab saved first → `CONFLICT_VERSION` ([http-errors](./http-errors.md)). ### B. Post cost sheet (status + PBAC) ```text PATCH /cost-sheets/:id → { expectedVersion: 2, action: "post" } → pbac: cost-sheet.can-post (status draft, lines valid) → 200 { status: "posted", version: 3 } → epoch.bumpMany(["cost-sheets", "jobs"]) ``` 409 `BUSINESS_POLICY_DENIED` if lines empty or status wrong — show `error.reason`. ### C. Line cell edit (form recalc, then save document) Client-side on blur (no HTTP yet): ```ts const next = applyCellPatch(line, "unitPrice", edited); const calculated = calculateLine(next, { truthMode: "unitPrice" }); ``` Keyboard / active cell: `@eristack/spreadsheet-operator` emits commit; then `applyCellPatch` as above. Do not copy arrow-key logic into the app. On document save (header + lines array): ```text PATCH /cost-sheets/:id → { expectedVersion, lines: [...], action?: "save" } → validate qups invariants server-side (mirror calculateLine) → atomic update header version + replace lines OR line-level upsert → epoch.bumpMany(["cost-sheets"]) ``` Use `store.atomic()` in Backseat when job header and cost sheet must move together. ### D. Create job + cost sheet (multi-collection) ```text POST /jobs → { header fields } OR Backseat atomic: ``` ```ts import { generateEntityId } from "@eristack/entity-id"; await api.store.atomic(async (tx) => { const jobId = generateEntityId(); await tx.set("jobs", { id: jobId, version: 1, status: "draft", ...header }); await tx.set("costSheets", { id: generateEntityId(), jobId, version: 1, status: "draft", lines: [], }); }); await epoch.bumpMany(["jobs", "cost-sheets"]); ``` ### E. Issue invoice number + insert (server transaction) ```ts return db.transaction(async (tx) => { const number = await docNumberFor(tx).next({ entityKey: "invoice", scope: branchId }); const [invoice] = await tx.insert(invoices).values({ ...input, number: number.value, version: 1, }).returning(); return invoice; }); await epoch.bumpMany(["invoices"]); ``` `next()` never over HTTP — see [doc-number wiring-production](https://github.com/eristack/business-libs/blob/main/packages/capability/doc-number/docs/wiring-production.md). ### Print layout (company default vs document) Nullable `print_shipment_fields` (or equivalent) on the invoice: **`null` follows the live company print settings**; a saved object freezes that invoice’s field mask. Resolve at print/PDF time — see [print-layout-default-vs-document](./print-layout-default-vs-document.md). PATCH print-settings uses `expectedVersion` like any other document slice. --- ## Multi-collection create (Backseat) ```ts await api.store.atomic(async (tx) => { await tx.set("jobs", { id, version: 1, ...header }); await tx.set("costSheets", { id: csId, jobId: id, version: 1, lines: [] }); }); await epoch.bumpMany(["jobs", "cost-sheets"]); ``` Epoch bumps **after** atomic — separate store. --- ## List with wall ETD + ABAC scope ```ts import { executeBackseatList } from "@eristack/data-grid/backseat"; import { matchesAssignmentPair } from "@eristack/abac"; const result = await executeBackseatList({ store: api.store, collection: "jobs", schema: jobGridSchema, // etd field: type "wall", timezone "Asia/Jakarta" query, prefilter: (doc) => matchesAssignmentPair(user.assignments, doc.branchId, doc.trade), toRow: async (doc) => ({ customerName: (await api.store.get("partners", doc.customerId))?.name ?? "", etd: doc.etd, gpIdr: doc.gpIdr, }), }); ``` After any write affecting list membership, `epoch.bump("jobs")` so TanStack Query refetches. --- ## 409 handling matrix | Code | Trigger | UX | | --- | --- | --- | | `CONFLICT_VERSION` | Stale `expectedVersion` on PATCH | Merge dialog / refetch document | | `BUSINESS_POLICY_DENIED` | Illegal `action` or status gate | Toast `reason`, keep editor open | | `STALE_EPOCH` | List query older than server epoch | Background refetch grid | | `POLICY_DENIED` | ABAC scope (wrong branch) | "No access" — not a version issue | See [http-errors](./http-errors.md) for Express mapper copy-paste. --- ## Seed pack notes (Horizon A) Minimum demo seed entities for document-lines ERP: | Collection | Fields agents forget | Packages | | --- | --- | --- | | `users` + credentials | `subject` links to user id | jwt-auth backseat | | `partners` | `branchId` for ABAC demos | app-owned | | `jobs` | `version: 1`, wall `etd`, `branchId`, `trade` | timestamp, epoch | | `costSheets` | `lines[]` with QUPS fields via `withQupsFields` | qups | | `docFormats` | active invoice format per branch | doc-number | | `epoch` scopes | `jobs`, `cost-sheets`, `invoices` at `0` | epoch | No checked-in `seed-v1.json` yet — use `@eristack/backseat/seeds` → `loadHorizonASeedV1()` in `examples/horizon-a/` (see [backseat-then-backend](./backseat-then-backend.md)). PBAC presets to register in seed bootstrap: ```ts pbac.registerPolicy({ id: "cost-sheet.can-post", evaluate: documents.transitions("status", { draft: ["post"], posted: [], }), }); ``` --- ## Line patch on commit ```ts const next = applyCellPatch(line, "unitPrice", edited); const calculated = calculateLine(next, { truthMode: "unitPrice" }); ``` Persist `withQupsFields(calculated)` in Backseat; `withQupsColumns` in Drizzle line tables for Horizon B. --- ## recommend() goals Product language that should hit this recipe (not FIFO/stock): - job order, cost sheet, forwarding, freight, shipment, bill of lading - document with lines, commercial invoice, service ERP --- ## Production wiring (Horizon B) | Horizon A | Horizon B | | --- | --- | | `executeBackseatList` | `executeDrizzleList` + joins | | `withQupsFields` | `withQupsColumns` + migrations | | Backseat PATCH handlers | Express routes + Drizzle transactions | | IndexedDB seed | Postgres seed script | Package-specific production guides: - [jwt-auth wiring-production](https://github.com/eristack/business-libs/blob/main/packages/service/jwt-auth/docs/wiring-production.md) - [doc-number wiring-production](https://github.com/eristack/business-libs/blob/main/packages/capability/doc-number/docs/wiring-production.md) - [money wiring-production](https://github.com/eristack/business-libs/blob/main/packages/primitive/money/docs/wiring-production.md) - [data-grid wiring-production](https://github.com/eristack/business-libs/blob/main/packages/service/data-grid/docs/wiring-production.md) - [file-manager wiring-production](https://github.com/eristack/business-libs/blob/main/packages/service/file-manager/docs/wiring-production.md) ### Document attachments (optional) Store **metadata in Postgres**, bytes in **S3** — not BLOB columns on line tables. - Add nullable `attachmentFileId` (uuid) or `attachmentFileRef` (jsonb `FileRef` v1) on your header table. - Upload via `@eristack/file-manager` presign flow; persist returned `FileRef` on save. - **Preview:** `resolveDownloadUrl(fileId)` (no options) — safe for `<img src>`. **Download:** `{ downloadFilename }` or `GET /files/:id/download-url?downloadFilename=` — do not default attachment on every URL. - Horizon A: `registerFileManagerBackseat` + same `dbName` as jwt-auth/doc-number IndexedDB factories. --- ## Related - [backseat-then-backend](./backseat-then-backend.md) — Horizon A → B - [optimistic-document-version](./optimistic-document-version.md) — `expectedVersion` / 409 - [http-errors](./http-errors.md) — unified error envelope - `@eristack/qups#qups-line`, `@eristack/pbac#pbac-core`