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

194 lines (142 loc) • 8.27 kB
--- title: Party and platform compose (Wave 13) description: One guide for multi-package pipelines approved in Wave 13 — party normalizers, measures, finance posting, API edge guards, and outbound render/export. --- # Party and platform compose (Wave 13) One guide for **multi-package pipelines** approved in Wave 13 — party normalizers, measures, finance posting, API edge guards, and outbound render/export. Packages **collaborate through shared string contracts**; they do **not** require sibling `@eristack/*` dependencies in core. Load: `@eristack/ai-knowledge#party-and-platform-compose` · Dependency map: [package-relationships](./package-relationships.md) · WIP detail: `roadmap/horizon.md` + `_ai-docs/wip/wave13-party-platform/` **Ship rule:** implement **one npm package per PR** with tests, docs, skill, recipe row, and `pnpm knowledge:sync`. --- ## Compose rules | Rule | Meaning | | --- | --- | | Core stays pure | No Express/React/Drizzle in primitive cores | | No primitive sibling hard deps | `person` does not import `phone`; `contact` does not require `person` at publish time | | Same strings in → same strings out | E.164 phone, normalized email, money/tax as decimal strings at boundaries | | Compose in app or optional `/compose` | Optional peer group on `@eristack/contact/compose` only when all peers installed | | One recipe per pipeline | Discovery via `recommend()` — see recipes at end | | Adapters optional | Drizzle stores, Redis rate limit, Excel/PDF drivers — not cross-primitive normalize | --- ## Party pipeline Independent normalizers; app handler chains them. ```ts import { normalizePerson } from "@eristack/person"; import { normalizeE164 } from "@eristack/phone"; import { normalizeEmail } from "@eristack/email-address"; import { normalizeContactList } from "@eristack/contact"; export function normalizePartyInput(body: PartyInput) { const person = body.person ? normalizePerson(body.person) : undefined; const channels = normalizeContactList({ channels: body.channels.map((ch) => ({ ...ch, phone: ch.phone ? normalizeE164(ch.phone) : undefined, email: ch.email ? normalizeEmail(ch.email) : undefined, })), }); return { person, channels }; } ``` **Address / geo (orthogonal):** - `@eristack/address` + `@eristack/iso-3166` for postal - `@eristack/geo` for lat/lng — not a substitute for postal - `@eristack/dimension` for L×W×H; mass/volume via `@eristack/uom` (no weight/volume packages unless product insists) --- ## Measures pipeline | Step | Package | Role | | --- | --- | --- | | 1 | `dimension` | Normalize L×W×H; unit codes align with `@eristack/uom` catalog | | 2 | `uom` | Convert qty when lines need kg/L | | 3 | `geo` | Optional distance | --- ## Finance and posting pipeline | Step | Package | Role | | --- | --- | --- | | 1 | `entity-id` | Sortable UUID v7 PKs | | 2 | `@eristack/timestamp` | Transaction instant or wall date | | 3 | `@eristack/fiscal-calendar` | Fiscal period open/closed | | 4 | `business-calendar` | Operating days and holidays | | 5 | `currency-pair` | Validate FX pair keys (rates: app or future fx-table) | | 6 | `rounding-policy` | Company policy → `@eristack/money` `Rounding` | | 7 | `tax` | Resolve rate by code + date → money Tax ops | | 8 | `@eristack/qups` | Line extension; app passes resolved rate strings | ```ts import { Money } from "@eristack/money"; import { wallOf } from "@eristack/timestamp"; import { assertPeriodOpen, requirePeriodForDate } from "@eristack/fiscal-calendar"; import { createBusinessCalendar } from "@eristack/business-calendar"; import { createRoundingPolicyRegistry } from "@eristack/rounding-policy"; import { createTaxRegistry } from "@eristack/tax"; // Build once at the compose root (rows come from app-owned tables). const businessCal = createBusinessCalendar({ weekendDays: [0, 6], holidays: holidayRows }); const roundingPolicies = createRoundingPolicyRegistry(roundingPolicyRows); const taxRegistry = createTaxRegistry({ codes: taxCodeRows, rates: taxRateRows }); // Per posting (input.postingWallDate is "YYYY-MM-DD"). const period = requirePeriodForDate(fiscalCal, wallOf(`${input.postingWallDate}T00:00:00`, fiscalCal.timezone)); assertPeriodOpen(period); // single argument: the resolved period if (!businessCal.isBusinessDay(input.postingWallDate)) throw new Error("POSTING_DATE_NOT_BUSINESS_DAY"); const { ratePercent } = taxRegistry.resolveTaxRate({ code: line.taxCode, asOf: input.postingWallDate }); const net = Money.of(line.amount, line.currency); const taxUnrounded = taxRegistry.applyTaxToAmount(net, ratePercent); // tax portion, not gross const tax = taxUnrounded.with( roundingPolicies.roundingFor({ policyId: company.defaultRoundingId, currency: line.currency }), ); ``` `applyTaxToAmount(base: Money, ratePercent: string)` returns the **unrounded tax portion**; apply the rounding policy once at the ledger boundary. For flat app masters without effective dates, use the top-level `buildTaxCatalog` / `resolveTaxRateFromCatalog` instead of the registry. **Do not duplicate:** money rounding math, qups line tax application, or Avalara-style engines in `@eristack/tax` v0. --- ## Platform API edge (middleware order) ```text 1. rate-limit.check(clientIp | apiKeyPrefix) → 429 + Retry-After 2. api-key.verify(Authorization) → 401 3. idempotency.run(Idempotency-Key, handler) → replay / 409 in-flight 4. business handler ``` Shared header constants export from each package (`Idempotency-Key`, `Authorization`, `Retry-After`). No package imports another. --- ## Outbound content | Flow | Packages | | --- | --- | | Email | person/address formatters → `email-template.render` → `@eristack/comms` | | PDF | HTML template → `email-template` → `pdf-render` driver (app or adapter) | | Spreadsheet export | App list → string row DTOs → `spreadsheet-render` → download or `@eristack/file-manager` | | Import (inverse) | `@eristack/import-job` (horizon) — share column keys with export DTOs only | Template vars: `Record<string, string>` — format money/timestamp JSON **before** render. Optional integrity: `@eristack/checksum` `sha256Hex` on export bytes before file-manager `checksumSha256`. --- ## Deploy and DX (Wave G) | Package | Role | | --- | --- | | `health` | Register Drizzle/ping checks; aggregate `/health` and `/ready` | | `drizzle-kit-helpers` | Shared drizzle-kit config for consumer monorepos | | `vercel-adapters` | Serverless handler factory; no Vercel SDK in core | No runtime coupling between G packages and party/tax primitives. --- ## Wave 13 implementation order (macro) ```text E1 entity-id (shipped @eristack/entity-id) → A person, phone, email, contact → B dimension, geo → E2 business-calendar, E3 checksum → F currency-pair, rounding-policy, tax → C email-template, idempotency, api-key, rate-limit, pdf-render, spreadsheet-render → G health, drizzle-kit-helpers, vercel-adapters ``` **Deferred:** `@eristack/weight` / `@eristack/volume` — use `@eristack/uom`. **UI (parallel):** ERP list/doc screens — `@eristack/ai-knowledge#ui-package-stack`; headless fields — `@eristack/money/react/fields`, `@eristack/percent/react`, `@eristack/timestamp/react/fields`. --- ## Recipes (discovery) | Recipe id | Load when | | --- | --- | | `party-and-platform-compose` | Any Wave 13 multi-package ask — **this file** | | `party-contact-normalize` | Partner/contact CRUD normalize | | `platform-api-guard` | B2B API middleware stack | | `posting-date-guard` | GL post date + calendars | | `invoice-line-tax` | Tax code + qups line | | `spreadsheet-export-download` | List → xlsx/csv export | Until each package ships, recipes route to **`@eristack/ai-knowledge#party-and-platform-compose`** only; add package skills to recipes on first publish. --- ## Anti-patterns | Avoid | Do instead | | --- | --- | | Mega `@eristack/party` package | This guide + compose handler | | contact imports phone in core | Document snippet; optional `/compose` | | pdf-render imports email-template | App passes rendered HTML | | spreadsheet-render imports data-grid | App builds `SpreadsheetWorkbook` | | Required peer web across Wave 13 | Optional peers for adapters only |