@happyvertical/smrt-commerce 0.53.4 → 0.53.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/AGENTS.md +19 -3
  2. package/README.md +29 -1
  3. package/agents/commercial-ui.md +78 -0
  4. package/agents/customer-vendor-ui.md +76 -0
  5. package/agents/invoice-calculations.md +17 -0
  6. package/agents/invoice-preparation.md +86 -0
  7. package/agents/invoice-quantity.md +46 -0
  8. package/agents/pricing-versions.md +28 -0
  9. package/agents/purchasing-ui.md +82 -0
  10. package/agents/quote-ui.md +82 -0
  11. package/dist/billing/units.d.ts +1 -7
  12. package/dist/billing/units.d.ts.map +1 -1
  13. package/dist/chunks/calculations-DPuldr1J.js +242 -0
  14. package/dist/chunks/calculations-DPuldr1J.js.map +1 -0
  15. package/dist/chunks/types-CRnln577.js.map +1 -1
  16. package/dist/chunks/{units-CpBepqG1.js → units-CJcAOQm1.js} +3 -39
  17. package/dist/chunks/units-CJcAOQm1.js.map +1 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +164 -44
  21. package/dist/index.js.map +1 -1
  22. package/dist/invoices.d.ts +3 -0
  23. package/dist/invoices.d.ts.map +1 -0
  24. package/dist/invoices.js +2 -0
  25. package/dist/manifest.json +66 -8
  26. package/dist/migrations/invoiceEditorStorage.d.ts +39 -0
  27. package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -0
  28. package/dist/models/ContractLineItem.d.ts +1 -1
  29. package/dist/models/InvoiceLineItem.d.ts +18 -36
  30. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  31. package/dist/playground.js +201 -3
  32. package/dist/playground.js.map +1 -1
  33. package/dist/smrt-knowledge.json +109 -7
  34. package/dist/svelte/components/CustomerDetail.svelte +150 -0
  35. package/dist/svelte/components/CustomerDetail.svelte.d.ts +28 -0
  36. package/dist/svelte/components/CustomerDetail.svelte.d.ts.map +1 -0
  37. package/dist/svelte/components/CustomerDirectory.svelte +177 -0
  38. package/dist/svelte/components/CustomerDirectory.svelte.d.ts +42 -0
  39. package/dist/svelte/components/CustomerDirectory.svelte.d.ts.map +1 -0
  40. package/dist/svelte/components/CustomerForm.svelte +234 -0
  41. package/dist/svelte/components/CustomerForm.svelte.d.ts +44 -0
  42. package/dist/svelte/components/CustomerForm.svelte.d.ts.map +1 -0
  43. package/dist/svelte/components/InvoiceAllocationFields.svelte +72 -0
  44. package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts +8 -0
  45. package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts.map +1 -0
  46. package/dist/svelte/components/InvoiceCard.svelte +2 -2
  47. package/dist/svelte/components/InvoiceEditor.svelte +134 -0
  48. package/dist/svelte/components/InvoiceEditor.svelte.d.ts +8 -0
  49. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/InvoiceLineEditor.svelte +85 -0
  51. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +8 -0
  52. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +1 -0
  53. package/dist/svelte/components/InvoiceReview.svelte +23 -0
  54. package/dist/svelte/components/InvoiceReview.svelte.d.ts +10 -0
  55. package/dist/svelte/components/InvoiceReview.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/PartyAddressFields.svelte +95 -0
  57. package/dist/svelte/components/PartyAddressFields.svelte.d.ts +25 -0
  58. package/dist/svelte/components/PartyAddressFields.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/PartyContactFields.svelte +153 -0
  60. package/dist/svelte/components/PartyContactFields.svelte.d.ts +28 -0
  61. package/dist/svelte/components/PartyContactFields.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/PricingVersionDecision.svelte +88 -0
  63. package/dist/svelte/components/PricingVersionDecision.svelte.d.ts +35 -0
  64. package/dist/svelte/components/PricingVersionDecision.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/PricingVersionSummary.svelte +46 -0
  66. package/dist/svelte/components/PricingVersionSummary.svelte.d.ts +9 -0
  67. package/dist/svelte/components/PricingVersionSummary.svelte.d.ts.map +1 -0
  68. package/dist/svelte/components/PurchaseOrderEditor.svelte +210 -0
  69. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +79 -0
  70. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -0
  71. package/dist/svelte/components/PurchaseSourceSelector.svelte +56 -0
  72. package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts +30 -0
  73. package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts.map +1 -0
  74. package/dist/svelte/components/PurchasingPlayground.svelte +133 -0
  75. package/dist/svelte/components/PurchasingPlayground.svelte.d.ts +9 -0
  76. package/dist/svelte/components/PurchasingPlayground.svelte.d.ts.map +1 -0
  77. package/dist/svelte/components/QuoteEditor.svelte +177 -0
  78. package/dist/svelte/components/QuoteEditor.svelte.d.ts +76 -0
  79. package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -0
  80. package/dist/svelte/components/QuotePlayground.svelte +108 -0
  81. package/dist/svelte/components/QuotePlayground.svelte.d.ts +10 -0
  82. package/dist/svelte/components/QuotePlayground.svelte.d.ts.map +1 -0
  83. package/dist/svelte/components/QuoteRevisionComparison.svelte +44 -0
  84. package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts +14 -0
  85. package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts.map +1 -0
  86. package/dist/svelte/components/QuoteRevisionHistory.svelte +55 -0
  87. package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts +15 -0
  88. package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts.map +1 -0
  89. package/dist/svelte/components/VendorDetail.svelte +134 -0
  90. package/dist/svelte/components/VendorDetail.svelte.d.ts +26 -0
  91. package/dist/svelte/components/VendorDetail.svelte.d.ts.map +1 -0
  92. package/dist/svelte/components/VendorDirectory.svelte +178 -0
  93. package/dist/svelte/components/VendorDirectory.svelte.d.ts +42 -0
  94. package/dist/svelte/components/VendorDirectory.svelte.d.ts.map +1 -0
  95. package/dist/svelte/components/VendorForm.svelte +224 -0
  96. package/dist/svelte/components/VendorForm.svelte.d.ts +42 -0
  97. package/dist/svelte/components/VendorForm.svelte.d.ts.map +1 -0
  98. package/dist/svelte/i18n.d.ts +102 -0
  99. package/dist/svelte/i18n.d.ts.map +1 -1
  100. package/dist/svelte/i18n.js +102 -0
  101. package/dist/svelte/index.d.ts +52 -0
  102. package/dist/svelte/index.d.ts.map +1 -1
  103. package/dist/svelte/index.js +49 -0
  104. package/dist/svelte/invoices/calculations.d.ts +121 -0
  105. package/dist/svelte/invoices/calculations.d.ts.map +1 -0
  106. package/dist/svelte/invoices/calculations.js +259 -0
  107. package/dist/svelte/invoices/currency.d.ts +9 -0
  108. package/dist/svelte/invoices/currency.d.ts.map +1 -0
  109. package/dist/svelte/invoices/currency.js +55 -0
  110. package/dist/svelte/invoices/fields.d.ts +6 -0
  111. package/dist/svelte/invoices/fields.d.ts.map +1 -0
  112. package/dist/svelte/invoices/fields.js +22 -0
  113. package/dist/svelte/invoices/messages.d.ts +53 -0
  114. package/dist/svelte/invoices/messages.d.ts.map +1 -0
  115. package/dist/svelte/invoices/messages.js +53 -0
  116. package/dist/svelte/invoices/types.d.ts +194 -0
  117. package/dist/svelte/invoices/types.d.ts.map +1 -0
  118. package/dist/svelte/invoices/types.js +1 -0
  119. package/dist/svelte/party-types.d.ts +203 -0
  120. package/dist/svelte/party-types.d.ts.map +1 -0
  121. package/dist/svelte/party-types.js +82 -0
  122. package/dist/svelte/playground/InvoicePreparationPreview.svelte +116 -0
  123. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts +4 -0
  124. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -0
  125. package/dist/svelte/playground/PartyFormPreview.svelte +211 -0
  126. package/dist/svelte/playground/PartyFormPreview.svelte.d.ts +8 -0
  127. package/dist/svelte/playground/PartyFormPreview.svelte.d.ts.map +1 -0
  128. package/dist/svelte/playground/PricingVersionPreview.svelte +55 -0
  129. package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts +4 -0
  130. package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts.map +1 -0
  131. package/dist/svelte/playground.d.ts +442 -3
  132. package/dist/svelte/playground.d.ts.map +1 -1
  133. package/dist/svelte/playground.js +199 -3
  134. package/dist/svelte/pricing-i18n.d.ts +10 -0
  135. package/dist/svelte/pricing-i18n.d.ts.map +1 -0
  136. package/dist/svelte/pricing-i18n.js +10 -0
  137. package/dist/svelte/pricing-types.d.ts +38 -0
  138. package/dist/svelte/pricing-types.d.ts.map +1 -0
  139. package/dist/svelte/pricing-types.js +1 -0
  140. package/dist/svelte/purchasing-i18n.d.ts +53 -0
  141. package/dist/svelte/purchasing-i18n.d.ts.map +1 -0
  142. package/dist/svelte/purchasing-i18n.js +53 -0
  143. package/dist/svelte/purchasing-types.d.ts +91 -0
  144. package/dist/svelte/purchasing-types.d.ts.map +1 -0
  145. package/dist/svelte/purchasing-types.js +13 -0
  146. package/dist/svelte/quote-i18n.d.ts +49 -0
  147. package/dist/svelte/quote-i18n.d.ts.map +1 -0
  148. package/dist/svelte/quote-i18n.js +49 -0
  149. package/dist/svelte/quote-types.d.ts +81 -0
  150. package/dist/svelte/quote-types.d.ts.map +1 -0
  151. package/dist/svelte/quote-types.js +51 -0
  152. package/dist/types/index.d.ts +13 -3
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/dist/ui.d.ts.map +1 -1
  155. package/dist/ui.js +150 -6
  156. package/dist/ui.js.map +1 -1
  157. package/package.json +25 -13
  158. package/dist/chunks/units-CpBepqG1.js.map +0 -1
package/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ E-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, pay
12
12
  - **LicenseSale**: industry-neutral licensing primitive. Carries an *immutable* rights snapshot (`rightsMedium`, `rightsDistributionScope`, `rightsExclusivity`, `rightsDuration`, `rightsTerritory`, `rightsSublicensing`, `rightsDerivatives` — typed `Meta<T>` fields), licensee identity (`licenseeEmail`, optional `licenseeLegalEntity` / `licenseeJurisdiction`), and a signed-PDF reference (`pdfUrl`, `pdfHash`, optional `onChainHashRegistryRef`). Once saved at `ContractStatus.ACCEPTED`, the rights snapshot is frozen — mutating any of the seven rights fields and re-saving throws. The only legal transition out of ACCEPTED is `revoke()` (moves to CANCELLED without touching rights). Useful for stock media, music licensing, code-asset marketplaces, license keys, anywhere rights are sold for a fee.
13
13
  - **ContractLineItem**: items on contracts.
14
14
  - **Invoice**: status machine `DRAFT → SENT → VIEWED → PARTIAL → PAID` (also OVERDUE, CANCELLED, WRITTEN_OFF). `recognizeRevenue()` creates balanced AR journal entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable).
15
- - **InvoiceLineItem**: line items on invoices.
15
+ - **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).
16
16
  - **Payment** / **PaymentAllocation**: tracks payments against invoices. Status controlled by `Invoice.updatePaymentStatus()`, not Payment model. Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.
17
17
  - **PaymentInstrument**: reusable saved payment method ("card on file"). Stores only provider references and non-sensitive display metadata (`providerCustomerId`, `providerPaymentMethodId`, brand / last4 / expiry), never raw card data. Use `PaymentInstrumentCollection.setDefaultForCustomer()` to change the default so the single-default-per-customer invariant is enforced; generated create/update routes cannot write `isDefault` directly.
18
18
  - **PaymentIntent**: short-lived pre-payment commitment with multi-option semantics. Locks a USD price for a fixed window (default 15 minutes) and lists one or more `PaymentOption`s describing different rails (`backendId`, `currency`, `payTo`, `nativeAmount`, optional `chain` / `memo` / `x402Capable` / `expiresAt`). First option to receive payment wins; the others are implicitly retired. State machine `awaiting_payment → paid → (issued | retired)` plus `expired` and `cancelled`. Mutate via dedicated `markPaid` / `markIssued` / `expire` / `cancel` / `retire` helpers — direct status assignment bypasses the invariant checks. Idempotency via natural key `(tenant_id, offering_ref, licensee_email, idempotency_key)` and `PaymentIntentCollection.getOrCreateByIdempotencyKey()`.
@@ -146,8 +146,8 @@ for usage.
146
146
 
147
147
  - **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId
148
148
  - **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).
149
- - **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is an integer count.
150
- - **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem.getTaxAmount()` and `ContractLineItem.calculateAmount()` `Math.round()` the fractional product to whole minor units. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)
149
+ - **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is also decimal (six fractional places); existing integer columns require the explicit [quantity migration](agents/invoice-quantity.md).
150
+ - **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem` uses exact shared invoice arithmetic to round fractional quantity × price and then tax to minor units; `ContractLineItem.calculateAmount()` uses `Math.round()` for tax. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)
151
151
  - **Migrating an existing database to minor units**: `preflightCommerceMoneyMinorUnits(db)` reports, per column, whether it still holds major units and which rows would be rounded or exceed JavaScript's safe-integer range; `migrateCommerceMoneyToMinorUnits(db)` converts them. Both are exported from the package root. The rescale is idempotent via a `_smrt_backfills` marker, changes the column type in place on PostgreSQL/DuckDB (`ALTER … TYPE BIGINT USING round(col * 100)`), and on SQLite rescales values only — SQLite cannot alter a declared type in place, so those columns come back in `declaredTypeChangePending` and need the table-rebuild path (#2370).
152
152
  - **`Payment.nativeAmount` is NOT in that migration, and must not be.** Its minor unit is a property of the *row's asset*: satoshis on a BTC rail (×1e8), cents on a fiat or stablecoin rail (×1e2), in one column discriminated only by `nativeCurrency`. A blanket ×100 turns 0.00713 BTC into `1` instead of `713000` sats, and a round 0.01 BTC even passes the preflight's integrality check while being wrong by six orders of magnitude. Convert it in two deliberate steps — normalise each row to its own asset's minor units while the column is still floating-point, then `rescaleMoneyColumnsToMinorUnits(db, COMMERCE_NATIVE_UNIT_COLUMNS, { scale: 1, … })` for the type change. `COMMERCE_NATIVE_UNIT_COLUMNS` is exported with a worked example; step one is deliberately the deployment's to write, because only it knows which `nativeCurrency` values it has used.
153
153
  - **`PaymentIntent.paymentOptions[].nativeAmount` has the same per-asset problem inside a JSON column and is likewise not migrated.** An intent's price lock defaults to 15 minutes, so the supported answer is to let open intents expire and be re-quoted: an unconverted option fails the exact `!==` reconciliation against the payment that arrives, which is a loud failure rather than a silently mis-scaled quote.
@@ -161,3 +161,19 @@ for usage.
161
161
  - **PaymentInstrument defaults**: `isDefault` is domain-managed, not API-writable. Call `setDefaultForCustomer(customerId, instrumentId)` so the target is validated and the customer's other instruments are cleared.
162
162
  - **LicenseSale immutability**: rights snapshot freezes on save-with-status-ACCEPTED. The captured snapshot lives in a module-scoped `WeakMap<LicenseSale, string>` so it doesn't interact with the schema or round-trip through `_meta_data`. Drafts (status != ACCEPTED) remain mutable. To "change" an issued license: `revoke()` it, then issue a new `LicenseSale` row.
163
163
  - **Vendor.payoutAddresses normalization**: the constructor accepts either a `Record<string, string>` or a pre-serialized JSON string. `initialize()` re-normalizes after the framework's option-override pass; `save()` re-normalizes defensively against direct field assignment. Non-string values inside the input map are silently dropped to preserve the typed invariant downstream.
164
+
165
+ ## Commercial UI
166
+
167
+ See [commercial UI composition](agents/commercial-ui.md) for native form recovery, amount compatibility and backend ownership. Package-owned guides under `agents/` ship with the package.
168
+
169
+ ## Quote and estimate UI
170
+
171
+ `src/svelte/quote-types.ts` separates unparsed draft strings from explicit
172
+ minor-unit revision DTOs. Native row submitters, caller-owned retry identity and
173
+ extension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md).
174
+
175
+ ## Purchasing UI
176
+
177
+ `PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including
178
+ retained reduction rows; the server validates review fingerprints before recording.
179
+ See [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @happyvertical/smrt-commerce
2
2
 
3
- Commerce models for the s-m-r-t framework. Covers customers, vendors, contracts (5 STI types), invoices with ledger integration, payments, and fulfillment tracking.
3
+ Commerce models for the s-m-r-t framework. Covers customers, vendors, contracts (9 STI types), invoices with ledger integration, payments, and fulfillment tracking.
4
4
 
5
5
  ## Installation
6
6
 
@@ -514,3 +514,31 @@ known limits.
514
514
 
515
515
  See [`AGENTS.md`](./AGENTS.md) for package architecture, invariants, validation,
516
516
  and contributor guidance.
517
+
518
+ ## Pricing version UI
519
+
520
+ `PricingVersionSummary` and `PricingVersionDecision` compose the quote revision
521
+ contract without adding a pricing ledger. Compare versions using
522
+ `QuoteRevisionComparison`; the application owns every decision and native form
523
+ action. See [pricing version presentation](agents/pricing-versions.md).
524
+
525
+ ## Commercial UI composition
526
+
527
+ Customer addresses and vendor/quote currencies use the shared localized code
528
+ selectors while preserving native field names and rejected raw drafts. See
529
+ [commercial UI contracts and backend boundaries](agents/commercial-ui.md) for
530
+ native forms, recovery, amount units and consumer adoption, plus the focused
531
+ [customer/vendor](agents/customer-vendor-ui.md) and
532
+ [quotation](agents/quote-ui.md) contracts for selector restrictions.
533
+
534
+ ## Invoice preparation UI
535
+
536
+ `InvoiceLineEditor` and `InvoiceEditor` from `@happyvertical/smrt-commerce/svelte`
537
+ provide editable descriptions/SKUs, fractional quantities, unit prices, flat or
538
+ percentage discounts, explicit or inherited tax, customer, dates, currency and
539
+ terms. Calculated previews use the same exact minor-unit arithmetic exported
540
+ for server handlers from `@happyvertical/smrt-commerce/invoices`.
541
+ `InvoiceAllocationFields` and `InvoiceReview` extend this foundation with
542
+ caller-owned sources and approvals. See [the editor contract](agents/invoice-preparation.md),
543
+ [calculation rules](agents/invoice-calculations.md), and the required
544
+ [existing-database storage migration](agents/invoice-quantity.md).
@@ -0,0 +1,78 @@
1
+ # Commercial UI composition
2
+
3
+ The commercial UI release is tracked in [#3423](https://github.com/happyvertical/smrt/issues/3423).
4
+ Components compose the provider-free controls in `@happyvertical/smrt-ui/forms`
5
+ and visual controls in `@happyvertical/smrt-ui`. They do not require SvelteKit,
6
+ a database, or the top-level `smrt-svelte` integration package.
7
+
8
+ ## Native forms and recovery
9
+
10
+ The application owns action URLs, HTTP methods, submitter names/values, field
11
+ names, request identity, tenant context and returned validation errors. Native
12
+ forms use the base `Form` with `preventDefault={false}`. Enhancement is optional;
13
+ a handler that takes over submission is responsible for preventing navigation.
14
+
15
+ A failed action returns the submitted strings, including invalid amounts and
16
+ repeated fields, alongside errors. Components must not reconstruct these values
17
+ from saved models or generate a replacement request token. Unknown commit outcomes
18
+ retain the same identity and payload until the application resolves the result.
19
+ File inputs cannot be repopulated after navigation: explain that the file must be
20
+ selected again and retain the caller's recovery identity.
21
+
22
+ The existing [form retry contract](../../../docs/content/form-retry.md) provides
23
+ optional browser enhancement. It does not replace server-side tenant/actor checks
24
+ or idempotency. Permission props control presentation only; applications must
25
+ supply already-authorized data and reauthorize every submitted action.
26
+
27
+ ## Amounts and public compatibility
28
+
29
+ New commercial editors use explicitly documented integer minor-unit amounts.
30
+ Editable values remain strings until the owning server validates them; blank,
31
+ invalid and zero are distinct. Currency and its display precision are explicit.
32
+ Do not mix currencies in totals or infer an unknown allocation as zero.
33
+
34
+ The original invoice display DTOs in `src/svelte/types.ts` and their components
35
+ accept decimal major-unit amounts. Preserve that published behavior. Applications
36
+ adapting model amounts into those legacy components must explicitly convert units;
37
+ do not pass a model's integer amount directly or silently change a legacy prop's
38
+ meaning. New editor DTOs are separate contracts.
39
+
40
+ ## Domain boundaries
41
+
42
+ Customer and Vendor reference Profiles identity data. A consumer's Client record
43
+ is not automatically a Commerce Customer; UI adoption performs no identity merge
44
+ or persistence migration. Domain-specific contact fields, trades, project/package
45
+ allocations and approval policies enter through typed adapters and extension slots.
46
+
47
+ Quote/estimate preparation, customer acceptance, vendor award, expense review and
48
+ invoice approval are separate decisions. A UI status or selection must never
49
+ implicitly perform another transition. The server supplies current decision and
50
+ source availability, including restricted-reader presentation.
51
+
52
+ ## Backend capability assessment
53
+
54
+ Assessment baseline: SMRT `98c872be4` and reviewed DomaCraft epic-138
55
+ `b03b322f75fbfdcb9193b18e73d3ec818ef18811`.
56
+
57
+ | Responsibility | Existing capability | Release decision |
58
+ | --- | --- | --- |
59
+ | Commitment versus actual | `ExpenseCollection.commitmentPosition()` sums reviewed, non-duplicate expenses in the commitment currency, optionally per line. | Reuse Expenses; keep DomaCraft award lineage and package-cap constraints in its adapter. |
60
+ | Receipt identity and duplicate review | `ExpenseReceiptCollection` owns receipt joins and duplicate detection; `Expense` owns guarded review transitions. | Reuse these contracts. Private upload authorization, draft capacity and storage compensation stay in application composition. |
61
+ | Idempotent submission and uncertain outcomes | Core `runOnce()` and UI `createFormRetry()` provide shared server/browser primitives. | Preserve the caller's request identity. Do not replace DomaCraft's transaction-bound financial submission logic without proving equivalent transaction and recovery semantics. |
62
+ | Invoice source reservations | DomaCraft `finance.ts` reserves its own cost allocations under project transaction locks; Commerce Invoice has no equivalent source-allocation contract. | Keep the reservation service local: extracting it changes a published financial contract and requires an agreed cross-domain source model. UI accepts supplied source availability. |
63
+ | Source-change approval invalidation | DomaCraft `withdrawInvoiceApprovalForSource()` and invoice fingerprint checks bind project policy, actor decisions and retained sources. | Keep this policy local. Shared presentation renders server-supplied approval state and cannot authorize execution. |
64
+
65
+ No generic backend redesign is required by these UI slices. A future extraction
66
+ needs evidence that a contract works beyond the application-specific project and
67
+ policy model; UI reuse alone does not establish that requirement.
68
+
69
+ ## Validation and consumer adoption
70
+
71
+ Each slice covers its public exports, SSR/native payload, retained errors,
72
+ read-only and empty states, keyboard operation and a 390px viewport. The release
73
+ also validates the complete touched packages and integrated workbench. A browser
74
+ viewport test does not establish physical-device camera behavior.
75
+
76
+ DomaCraft adopts a published release through normal dependency admission. Its
77
+ native financial, authorization, tenant and uncertain-outcome tests remain required;
78
+ shared component tests do not establish those server-side properties.
@@ -0,0 +1,76 @@
1
+ # Customer and vendor UI
2
+
3
+ Import the provider-free surfaces from `@happyvertical/smrt-commerce/svelte`:
4
+
5
+ ```svelte
6
+ <script lang="ts">
7
+ import {
8
+ CustomerDirectory,
9
+ CustomerDetail,
10
+ CustomerForm,
11
+ VendorDirectory,
12
+ VendorDetail,
13
+ VendorForm,
14
+ type CustomerDisplayData,
15
+ } from '@happyvertical/smrt-commerce/svelte';
16
+
17
+ let { data, form } = $props();
18
+ </script>
19
+
20
+ <CustomerForm
21
+ mode="edit"
22
+ values={form?.values ?? data.values}
23
+ errors={form?.errors}
24
+ transport={{
25
+ action: '?/update',
26
+ method: 'post',
27
+ hiddenFields: {
28
+ requestId: form?.values.requestId ?? data.requestId,
29
+ expectedTenantId: data.tenantId,
30
+ },
31
+ }}
32
+ labels={{ singular: 'Client', plural: 'Clients' }}
33
+ cancelHref={`/clients/${data.customer.id}`}
34
+ />
35
+ ```
36
+
37
+ The caller owns action URLs, server authorization, tenancy checks, request tokens, persistence, and retry behavior. Capability props only decide whether an affordance is rendered. The components never create a Profile, Customer, Vendor, or contact relationship and never generate request tokens.
38
+
39
+ `CustomerDisplayData.creditLimitMinor` and `VendorDisplayData.minimumOrderMinor` are integer currency minor units. Detail components convert them only for display and accept an explicit `minorUnitExponent`. Form values deliberately use retained strings such as `1250.00`; the server validates and converts those major-unit strings to the model's integer minor units.
40
+
41
+ `VendorFormValues.leadTimeDays` is also retained text. The control supplies a numeric keyboard hint without browser number-input sanitization, and the server must parse and validate the submitted non-negative integer.
42
+
43
+ Forms use the provider-free `Form`, `Input`, `Select`, `Textarea`, and `Checkbox` controls from `@happyvertical/smrt-ui/forms`. They default to native `POST` submission through `preventDefault={false}`. `transport.hiddenFields`, `transport.intentName`, and the field-name maps let an application preserve its existing payload contract. Repeatable contact rows submit repeated field names in DOM order. Add and remove controls are native submitters, so they work without JavaScript; the server returns the revised rows and retained values.
44
+
45
+ Customer shipping and billing addresses use the shared `CountrySelect` and
46
+ `ProvinceSelect`. The native names remain `shippingCountry`, `shippingState`,
47
+ `billingCountry`, and `billingState` unless `fieldNames` overrides them. Changing
48
+ country changes the region control without clearing its current string; CA and US
49
+ render localized subdivision choices, while unsupported countries render an
50
+ editable text field. Unknown values remain selectable/postable after a failed
51
+ submission. Use `countryOptions` or `provinceOptions` only when the caller must
52
+ restrict the choices. `VendorForm` has no structured address fields in its public
53
+ DTO and does not invent them.
54
+
55
+ Vendor currency uses the shared `CurrencySelect`. The raw retained string remains
56
+ the native `currency` payload, including unknown values returned after validation.
57
+ Use `currencyOptions` for caller-authorized restrictions; omitted options use the
58
+ shared currency catalog. Selector labels follow the active smrt-ui i18n locale.
59
+
60
+ The forms place a non-focusable default save submitter before contact row actions so Enter in a text field cannot accidentally add or remove a contact. This remains in place when the `actions` snippet customizes the visible action bar; set `transport.saveIntent` to the custom save intent.
61
+
62
+ Use the `extension` snippets for application fields such as construction roles and trades. Keep those fields and their policy in the application:
63
+
64
+ ```svelte
65
+ <VendorForm values={data.values} transport={{ action: '?/save' }}>
66
+ {#snippet extension(values)}
67
+ <ConstructionVendorFields roles={values.roles} trades={values.trades} />
68
+ {/snippet}
69
+ </VendorForm>
70
+ ```
71
+
72
+ Directory entries carry caller-owned detail links and pagination links. Detail edit links and form submit buttons appear only when their capability props allow them; the server must still authorize every request.
73
+
74
+ The registry keys are `customer-directory`, `customer-detail`, `customer-form`, `vendor-directory`, `vendor-detail`, `vendor-form`, and `party-contact-fields`. Interactive examples are published by `@happyvertical/smrt-commerce/playground` and ship in the next generated Commerce package release. DomaCraft can replace its local routes incrementally by adapting its existing server DTOs and payload names; no identity or database migration is required.
75
+
76
+ The form playground previews handle save, add-contact, and remove-contact intents in memory. They deliberately return a simulated validation error on save so retained values and retry behavior can be inspected without a backend.
@@ -0,0 +1,17 @@
1
+ # General invoice calculations
2
+
3
+ `@happyvertical/smrt-commerce/invoices` is a Svelte-free entry usable by server handlers and browsers. `calculateInvoiceLine`, `calculateInvoiceDraft`, and `resolveInvoiceLineDraft` share exact decimal arithmetic. The UI consumes these helpers for previews; preview results confer no authority.
4
+
5
+ Drafts retain unparsed text. Quantity accepts nonnegative decimals with at most six fractional digits and must round-trip through the model numeric representation. Prices and flat discounts accept only whole currency minor units expressed in major-unit text. Currency uses Commerce billing's explicit ISO exponent table (0/3/4 exceptions; other uppercase three-letter codes use 2); servers must authorize their supported currency set. No exchange conversion occurs. Rates are percentages from 0 through 100 with at most six fractional digits. `discountType` is `flat` or `percent`; `taxMode` is `inherit` or `override`. An override of `0` is zero tax; an empty override is invalid. Inherited lines ignore their dormant override string.
6
+
7
+ Calculation order is quantity × price rounded half up to minor units, discount rounded half up and subtracted, then tax on the discounted subtotal rounded half up. Flat discounts cannot exceed gross. Numeric model quantities and rates use their canonical decimal representation (including supported exponent notation), never a `toFixed` expansion of binary approximation. No floating-point multiplication or epsilon comparison determines money. Unsafe individual or aggregate results are rejected; invalid lines produce errors without monetary totals. Header validation requires customer, real issue/due dates with due date not before issue date, valid currency shape/default rate, at least one line, and unique nonblank row keys.
8
+
9
+ `resolveInvoiceLineDraft` returns the existing model's resolved integer-minor price, flat discount, fractional quantity, effective fractional tax rate, and calculated amount. It verifies exact agreement with model arithmetic. `InvoiceLineItem.applyEditorDraft()` retains the editing modes alongside resolved fields; `getEditorDraft(authorizedContext)` restores them only while current authority and fields agree. See [invoice quantity storage](invoice-quantity.md) for the required existing-database migration. `calculateInvoiceMinorLine` is the currency-independent model helper: it permits existing negative-price credit lines, retains their historical ties toward positive infinity, and rejects invalid quantities/rates or unsafe/fractional money.
10
+
11
+ Trailing zeroes do not make an exact amount invalid: `100.00` JPY is 100 minor units, while `100.01` JPY is rejected. Parsing never rewrites the draft text.
12
+
13
+ ## Server responsibilities
14
+
15
+ Parse native form values into the typed draft (never pass unvalidated arbitrary JSON as the typed object), enforce request/row limits, authorize customer and source references in the active tenant, and validate edits against invoice lifecycle state. Replace inherited rate/currency policy with server-authorized context, call the shared validator/resolver, and save resolved rows and invoice within the application's transaction/retry contract. Ignore any client-supplied calculated amount. Default tax remains externally determined; this module is not a tax determination service. Caller-owned extension allocations, budget availability and approval evidence require their own server validation and are not part of line pricing.
16
+
17
+ Changing currency changes the precision and denomination of the entered values, not their numeric text. The UI must not silently convert entered amounts or rewrite invalid drafts. Reopening or changing authorized tax context requires fresh server validation.
@@ -0,0 +1,86 @@
1
+ # General invoice editor and allocation extensions
2
+
3
+ Import `InvoiceEditor`, `InvoiceLineEditor`, `InvoiceAllocationFields`, `InvoiceReview`
4
+ and their Props types from `@happyvertical/smrt-commerce/svelte`. `InvoiceEditor`
5
+ is the general invoice form; `InvoiceLineEditor` and `InvoiceAllocationFields`
6
+ render fields only and never nest a form. The interactive Commerce invoice preview
7
+ shows general line items with an optional allocation extension.
8
+
9
+ `InvoiceDraftValues` contains customerId, issuedOn, dueOn, currency, paymentTerms,
10
+ taxRate and ordered lines. Each `InvoiceLineDraft` contains caller key, description,
11
+ sku, fractional quantity, unitPrice, discountType, discountValue, taxMode and
12
+ taxRate. Editable values are lossless strings, including invalid submitted input.
13
+ Unit price and flat discount are currency-unit text; percentage values use `5`
14
+ for five percent. `discountType` is `flat` or `percent`; `taxMode` is `inherit` or
15
+ `override`. Explicit override `0` means zero tax; it never means inherit. Unknown
16
+ modes remain representable for rejected submissions. Dates remain text so invalid
17
+ values survive action failures. Customer options are caller-authorized projections;
18
+ unknown retained IDs remain visible. Currency uses the shared CurrencySelect.
19
+
20
+ See [server calculation and persistence semantics](invoice-calculations.md).
21
+
22
+ `InvoiceLineEditor` also recalculates when used independently: it retains a local
23
+ draft copy and calls optional `onchange(line)` with each edit. It never mutates
24
+ the supplied line. Replacing the line prop applies caller/server response values.
25
+
26
+ The UI invokes the exact, Svelte-free `calculateInvoiceDraft` and
27
+ `calculateInvoiceLine` helpers for previews. The shared public calculation entry
28
+ is `@happyvertical/smrt-commerce/invoices`. Valid totals use safe integer minor
29
+ units, with exact decimal arithmetic and explicit currency precision. Invalid
30
+ drafts show an unavailable-total message rather than plausible numbers. The
31
+ calculator uses Commerce's explicit exponent table and its existing two-digit
32
+ fallback for other uppercase three-letter codes; the server must authorize its
33
+ supported currencies. Calculated values are never submitted as authority.
34
+ The server revalidates customer/SKU, prices, quantity, discounts, tax policy,
35
+ permissions and calculated totals. Header taxRate is a draft preview default,
36
+ not a new persisted Invoice tax field; replace it with authorized tax context
37
+ on the server. Client edits never establish approval or allocate inventory.
38
+
39
+ ```svelte
40
+ <InvoiceEditor action="?/save" values={form?.values ?? data.values}
41
+ customers={data.authorizedCustomers} errors={form?.errors}
42
+ message={form?.message} canEdit={data.mayEdit}
43
+ hiddenFields={[{ name: 'requestId', value: form?.requestId ?? data.requestId }]}>
44
+ {#snippet children(current)}
45
+ <InvoiceAllocationFields currency={current.currency}
46
+ allocations={form?.allocations ?? data.allocations}
47
+ sources={data.authorizedSources} mayReadSources={data.mayReadSources}
48
+ canEdit={data.mayEdit} />
49
+ {/snippet}
50
+ </InvoiceEditor>
51
+ ```
52
+
53
+ Native header names default to customerId, issuedOn, dueOn, currency,
54
+ paymentTerms, invoiceTaxRate. Repeated line names are lineKey, lineDescription,
55
+ lineSku, lineQuantity, lineUnitPrice, lineDiscountType, lineDiscountValue,
56
+ lineTaxMode and lineTaxRate, in displayed order. `fields` and `lineFields` adapt
57
+ names. Caller `errors` use header names or `lineKey.fieldName` keys. Repeated
58
+ hidden fields retain exact values. Avoid collisions with the component's names.
59
+
60
+ Default submitter `intent` values are `save`, `addLine` and `removeLine:<key>`;
61
+ `saveIntent`, `addIntent`, `removePrefix` and `fields.intent` adapt the contract.
62
+ Save is first so implicit Enter cannot remove/add a row. Structural actions
63
+ use formnovalidate, preserving every submitted draft string and request token.
64
+ The caller returns updated rows for add/remove; it must not persist or rotate
65
+ request identity simply because the user added a line. The default transport
66
+ is native POST, including without JavaScript. With JavaScript, input changes
67
+ update exact previews without mutating the supplied values object. `onsubmit`
68
+ may enhance transport; caller-owned retryStatus/pending controls submission.
69
+
70
+ Allocation fields remain optional application-specific composition. Each row
71
+ has key, sourceId, currency-unit amount, optional remove and field errors.
72
+ `mayReadSources=false` hides supplied labels/balances and renders references.
73
+ Retained unavailable sources remain postable for server reconciliation. Native
74
+ names remain sourceId/allocationAmount and checked zero-based removeRow;
75
+ `fields` maps these, and addIntent defaults to addAllocation. Availability,
76
+ reservations, budget policy and approval are caller/server concerns. The
77
+ extension must receive the same canEdit/pending capability as its owner.
78
+
79
+ `review` is a caller-supplied snapshot; edits do not infer status changes.
80
+ Read-only disables fields and hides mutation buttons. On rejected, denied or
81
+ uncertain actions, return every original string, stable row key, errors and
82
+ caller request/tenant/fingerprint identity. No component generates request keys.
83
+
84
+ Focused validation: Commerce `vitest.invoices.config.ts`,
85
+ `e2e/invoices/playwright.config.ts`, `typecheck`, and `build`; consolidated
86
+ release gates and calculator/model checks remain separate owning validations.
@@ -0,0 +1,46 @@
1
+ # Invoice quantity persistence and editor recovery
2
+
3
+ InvoiceLineItem accepts nonnegative decimal quantities with at most six decimal
4
+ places. The `1.0` default declares a floating-point quantity column. Unit price,
5
+ flat discount, and calculated amount remain safe integer minor units. Save always
6
+ validates resolved fields and recalculates amount; a supplied amount is never
7
+ trusted. Gross quantity × price and then tax on discounted gross round to minor
8
+ units, with ties toward positive infinity (including existing negative-price
9
+ credit lines). Tax is a resolved fraction in [0, 1], with eight decimal places.
10
+ Accounting export retains an explicit zero rate instead of omitting it.
11
+
12
+ `applyEditorDraft(draft, authorizedContext)` resolves the shared invoice draft
13
+ helper into description/SKU/quantity/price/flat discount/tax and stores cloned
14
+ `invoiceEditorStateJson` TEXT, accessed through a guarded typed `invoiceEditorState` getter/setter. It does not save.
15
+ `getEditorDraft(authorizedContext)` returns a clone only when currency, inherited
16
+ tax authority, and all source fields still agree. Otherwise it returns null so the
17
+ caller can reconstruct a flat-discount/explicit-tax editor from resolved fields.
18
+ Normal save clears stale editor state and never derives money from metadata.
19
+ All other object metadata, authorization, and transaction ownership are unchanged.
20
+
21
+ ## Existing integer columns
22
+
23
+ Run a backup and pause invoice writers for this explicit migration. With a root
24
+ database handle and its known engine (`sqlite`, `postgres`, or `duckdb`), call
25
+ `preflightInvoiceEditorStorage(db, engine)`, resolve every reported invalid row,
26
+ then `migrateInvoiceEditorStorage(db, engine)`. The preflight rejects negative,
27
+ nonfinite, unsafe, excess-precision, and numerically non-roundtripping quantities.
28
+ The migration uses a transaction (and a PostgreSQL writer lock) and never rescales
29
+ or rounds stored values. PostgreSQL widens INTEGER/BIGINT to DOUBLE PRECISION;
30
+ DuckDB widens to DOUBLE. Repeating it leaves an already widened declaration alone.
31
+ An absent table is reported as absent and is never marked migrated.
32
+
33
+ SQLite uses the public core table-rebuild planner to change the declaration to
34
+ REAL while preserving existing columns, constraints, indexes, and triggers. If
35
+ incoming foreign keys make that rebuild unsafe, the migration throws with the
36
+ planner reason and rolls back; it does not disable constraints or lose children.
37
+ Resolve that maintenance condition before deploying fractional writers. The same
38
+ transaction adds the nullable TEXT `invoice_editor_state_json` column with an empty
39
+ string default on every engine. This column is required for updated model saves.
40
+ An existing floating quantity column still receives the missing state column.
41
+ No schema mutation runs implicitly during runtime initialization or model save.
42
+
43
+ The actual DuckDB quantity/storage migration is tested. Complete Commerce model
44
+ persistence on DuckDB is currently unsupported because its hard foreign keys use
45
+ ON UPDATE CASCADE, which DuckDB cannot preserve; the schema builder refuses that
46
+ contract. SQLite and PostgreSQL exercise complete save/reload model behavior.
@@ -0,0 +1,28 @@
1
+ # Pricing version presentation
2
+
3
+ Import `PricingVersionSummary`, `PricingVersionDecision` and
4
+ `QuoteRevisionComparison` from `@happyvertical/smrt-commerce/svelte`.
5
+ `PricingVersionData` extends `QuoteRevision`: use the same explicit document kind,
6
+ counterparty, currency, minor-unit scale and integer total. Optional `sources`
7
+ retain authorized source labels, amounts and explanations in that currency.
8
+ Compare versions with `QuoteRevisionComparison`; no second comparison algorithm
9
+ or pricing ledger is introduced.
10
+
11
+ `PricingVersionDecision` accepts an immutable `version`, native POST `action`,
12
+ explicit `actions` (label/name/value and optional formAction/disabled), ordered
13
+ `hiddenFields`, and retained `reason`/`error`. Pass every server-required version,
14
+ fingerprint, request and tenant field through `hiddenFields`; displaying a version
15
+ never creates or replaces those identities. Repeated hidden names remain repeated
16
+ form entries. `reasonName` adapts to existing endpoints and `children` composes
17
+ additional native fields. Native submission is the default; `onsubmit` can opt
18
+ into enhancement. `readOnly` removes mutation controls; `busy` disables actions.
19
+ Neither prop replaces authorization or server-side fingerprint checks.
20
+
21
+ Applications must return current version/action availability with failed-action
22
+ values and preserve the same request identity after an uncertain outcome. A
23
+ source-change conflict requires fresh server review; this component never
24
+ approves, awards work, accepts a budget, or changes a saved status on its own.
25
+
26
+ The Commerce workbench pricing example composes comparison and decision controls
27
+ and demonstrates value retention on a rejected preview action. Its actions are
28
+ explicitly demonstrations and perform no financial writes.
@@ -0,0 +1,82 @@
1
+ # Purchasing and award presentation
2
+
3
+ Public `@happyvertical/smrt-commerce/svelte` exports `PurchaseSourceSelector`,
4
+ `PurchaseOrderEditor`, their Props and `Purchase*` DTOs. Registry slots are
5
+ `purchase-source-selector` and `purchase-order-editor`. Interactive playground
6
+ entries cover ordinary amendment and explicit retained-allocation reduction.
7
+
8
+ These components use public Commerce PurchaseOrder/Contract as their owning
9
+ financial domain without adding a backend command, ledger or fixed approval
10
+ policy. The caller decides instrument options, source availability, allocation
11
+ labels, prior commitments, reviewed actuals, floors/ceilings and exact source
12
+ identity. Construction work packages, accepted budgets, contractor entitlement
13
+ and vendor award policy stay in the consumer. Capability/disabled props only
14
+ control presentation. Every POST needs authoritative server authorization.
15
+
16
+ `PurchaseSourceSelector` uses a native GET to the supplied action with `name`
17
+ (default `source`) and optional hidden search/context fields. It never writes an
18
+ award. Disabled sources remain visible with caller notices; unknown saved IDs
19
+ remain visible as unavailable. Keep source selection outside a reduction form;
20
+ the server must reject a downward amendment under an ordinary amendment mode
21
+ and enforce the retained source/instrument during reductions.
22
+
23
+ `PurchaseOrderEditor` receives `operation` (`create`, `amend`, `reduce`), a
24
+ retained `source`, `currency`, explicit `minorUnitDigits`, raw `values`,
25
+ `instruments`, native POST `action`, and caller-owned `hiddenFields`. Supply
26
+ source ID, currency, predecessor, tenant and retry IDs in those hidden fields
27
+ under the exact names your endpoint expects. `names` adapts all built-in fields.
28
+ `labels` adapts approval language; `extensions` and `attachments` compose domain
29
+ fields and authorized Assets evidence without fetching bytes in this component.
30
+ `formAttributes` supports native encoding/ID and caller-owned form-retry Svelte
31
+ attachments; `onsubmit` supports caller transport enhancement. Tokens are never
32
+ minted or rotated here. See the shared [form/action contract](commercial-ui.md)
33
+ and repository `docs/content/form-retry.md`.
34
+
35
+ All editable money is **raw major-unit text**, including malformed values and
36
+ empty unknown amounts. The caller parses against the exact currency contract;
37
+ there is no UI floating-point calculation or implicit blank-to-zero conversion.
38
+ Saved/source/review numbers are safe integer minor units formatted by the
39
+ shared quotation helper. `sourceMinor: null` renders Unknown. Displayed bounds
40
+ are guidance; the server validates floors, ceilings and source eligibility.
41
+
42
+ Every allocation posts its stable `id` and amount, even when zero. Mark every
43
+ predecessor allocation `retained: true`. A `reduce` form requires those amounts
44
+ explicitly and never offers their removal. Supplying fewer predecessor rows is
45
+ not safe: the server must compare the complete ID set and reject omissions.
46
+ Optional `rowActions` uses native formnovalidate add/remove POSTs carrying all
47
+ current fields; the caller re-renders unchanged rejected strings and IDs.
48
+ New-row IDs belong to the caller. Row editing never records a commitment.
49
+
50
+ Pressing Enter in an input activates review or record, preserving draft rows.
51
+ Explicit add/remove actions remain available without JavaScript.
52
+ Default submitter `intent` values are `review`, `record`, `edit`, all configurable.
53
+ Only a caller-produced `review` snapshot or a retained `retryFingerprint` exposes
54
+ explicit confirmation. `review.fingerprint` posts under `reviewFingerprint` by
55
+ default. After an uncertain outcome, re-render the exact submitted values,
56
+ request identity and fingerprint; never fabricate a new intent/token. Confirmation
57
+ is an unchecked required checkbox on native renders and when a new review
58
+ fingerprint arrives in a hydrated form. Its value defaults to
59
+ `yes`. The server must bind fingerprint to all submitted values and authority,
60
+ reject changed/stale review data, and replay identical committed retries.
61
+ Rendering a review snapshot or selecting a source never accepts/records anything.
62
+
63
+ Validation: `pnpm --filter @happyvertical/smrt-commerce exec vitest run --config
64
+ vitest.purchasing.config.ts`, owning build/typecheck/pack checks, and the shared
65
+ quote native/hydrated browser harness with `QUOTE_BROWSER_EXTENSION` pointing at
66
+ `packages/commerce/test-support/purchasing-browser-extension.mjs`. Full repository
67
+ CI/E2E is consolidated at the #3423 release boundary by owner instruction.
68
+
69
+ Invalid or unsafe display amounts/scales render localized unavailable text.
70
+ Strict money helpers continue to reject malformed data; presentation recovery
71
+ does not validate or authorize submitted money.
72
+
73
+ The `attachments` snippet renders outside the editor form so an Assets
74
+ `AttachmentPanel`/`AttachmentUpload` keeps its own multipart action and request
75
+ identity. The attachment region remains disabled while the editor is read-only
76
+ or busy. Use `extensions` only for controls belonging to the editor submission;
77
+ never place another form there.
78
+
79
+ Composition regression: run the quote SSR lane, then the maintained quote browser
80
+ host with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/quote-attachments-browser.mjs`.
81
+ It submits actual Assets upload bytes and the separate editor action with and
82
+ without JavaScript at 390px.
@@ -0,0 +1,82 @@
1
+ # Quotation and estimate presentation
2
+
3
+ Import `QuoteEditor`, `QuoteRevisionHistory`, `QuoteRevisionComparison` and their
4
+ Props/DTOs from `@happyvertical/smrt-commerce/svelte`. Registry slots are
5
+ `quote-editor`, `quote-revision-history`, and `quote-revision-comparison`.
6
+ Commerce playground has interactive vendor quotation and customer estimate demos.
7
+
8
+ `kind: 'vendor-quotation'` means received supplier evidence;
9
+ `kind: 'customer-estimate'` means pricing proposed to a customer. Neither widget
10
+ selects pricing, accepts budgets, awards work, sends messages or mutates a model.
11
+ Adapt public Commerce Estimate/Contract fields (`currency`, `totalAmount`,
12
+ `status`, `customerId`/`vendorId`) to these presentation DTOs;
13
+ construction package allocation and approval policy remain caller extensions.
14
+ Client records need not become Commerce Customers.
15
+
16
+ `QuoteRevision.total.amountMinor` is a safe integer or null (unknown), with an
17
+ explicit `minorUnitDigits` from the owning currency/asset contract. No locale
18
+ inference, monetary rounding or unknown-as-zero comparison occurs. Different
19
+ kinds/currencies/scales cannot produce a delta. Existing invoice `LineItem`
20
+ major-unit props retain their established contract. Form money values are raw
21
+ **major-unit strings**: the caller validates and converts before saving; invalid
22
+ strings remain visible after failed actions. Dates likewise remain text to avoid
23
+ native date inputs silently clearing rejected server values.
24
+
25
+ Supply `values`, authorized `counterparties`, `action`, optional `method`,
26
+ `hiddenFields` (name/value pairs), `errors` and `message`. `names` maps payload
27
+ names to an existing endpoint. `labels.counterparty = 'Client'` supports local
28
+ terminology. `extensions` composes pricing-source/allocation fields;
29
+ `attachments` composes private Assets controls without loading bytes here.
30
+ `QuoteRevisionHistory.evidence(revision)` composes authorized document links.
31
+ All fixed component text uses the shared i18n catalog; data/status/date labels
32
+ come from the caller.
33
+
34
+ Currency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n
35
+ context. Its native name still comes from `names.currency`, and rejected or
36
+ unknown raw strings remain selected for a corrected submission. Pass
37
+ `currencyOptions` only when the caller must restrict the catalog.
38
+
39
+ Rows submit repeated `lineId`, `lineDescription`, `lineQuantity`, `lineUnitRate`.
40
+ Default submitter `intent` is `save`, `addLine`, or `removeLine:<stable-id>`;
41
+ override via `intents`. Add/remove are ordinary `formnovalidate` POSTs, including
42
+ all current strings and hidden tokens even without JavaScript. The action must
43
+ read all repeated fields, preserve row IDs, and re-render them; add/remove are
44
+ form-edit operations, not financial writes. Never treat an add-row POST as a save.
45
+
46
+ The editor does not generate or rotate submission tokens. Caller-owned
47
+ `@happyvertical/smrt-ui/form-retry` can be attached via `formAttributes` using
48
+ Svelte's `createAttachmentKey()` and `retry.attach`; caller transport enhancement
49
+ may use `onsubmit`. Use the same server-owned hidden identity when retrying an
50
+ uncertain commit. Never blindly reset after a response. The caller must preserve
51
+ hidden tenant/request/predecessor values, validate authorization on every action,
52
+ and enforce idempotency transactionally. See `docs/content/form-retry.md`.
53
+ `readonly`/`busy` only disable the presented fields; these props are not security.
54
+
55
+ Validation: `pnpm --filter @happyvertical/smrt-commerce exec vitest run --config vitest.quotes.config.ts`,
56
+ `node packages/commerce/test-support/quote-browser.mjs`, package typecheck/build.
57
+ The browser harness executes actual SSR POST recovery with JavaScript disabled
58
+ and hydrated demo interactions at 390px. Full release CI/E2E runs at #3423's
59
+ consolidated release boundary by explicit owner instruction.
60
+
61
+ On NixOS, the browser lane accepts the repository-standard
62
+ `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/run/current-system/sw/bin/chromium`.
63
+ Set `QUOTE_EVIDENCE_DIR` and `CI_TEST_TMPDIR` to an owned evidence directory outside
64
+ `/tmp`. Other commercial tests may reuse its host via
65
+ `QUOTE_BROWSER_EXTENSION=/absolute/fixture.mjs`: export `handleRequest(req, res,
66
+ {vite, render, css, requests})` returning true when handled, and
67
+ `run({browser, baseUrl, evidence, requests})` for additional assertions.
68
+
69
+ Invalid or unsafe display amounts/scales render localized unavailable text.
70
+ Strict money helpers continue to reject malformed data; presentation recovery
71
+ does not validate or authorize submitted money.
72
+
73
+ The `attachments` snippet renders outside the editor form so an Assets
74
+ `AttachmentPanel`/`AttachmentUpload` keeps its own multipart action and request
75
+ identity. The attachment region remains disabled while the editor is read-only
76
+ or busy. Use `extensions` only for controls belonging to the editor submission;
77
+ never place another form there.
78
+
79
+ Composition regression: run the quote SSR lane, then the maintained quote browser
80
+ host with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/quote-attachments-browser.mjs`.
81
+ It submits actual Assets upload bytes and the separate editor action with and
82
+ without JavaScript at 390px.
@@ -6,13 +6,7 @@
6
6
  * representation itself, so the only conversion this package performs is the
7
7
  * ISO 4217 one: minor units ÷ 10^exponent.
8
8
  */
9
- /** Upper-case, validate, and return an ISO 4217-shaped currency code. */
10
- export declare function normalizeCurrency(currency: string): string;
11
- /**
12
- * The ISO 4217 minor-unit exponent of a currency (2 for USD and RSD, 0 for
13
- * JPY, 3 for IQD). Any code not listed as 0, 3, or 4 is 2.
14
- */
15
- export declare function currencyMinorUnitExponent(currency: string): number;
9
+ export { currencyMinorUnitExponent, normalizeCurrency, } from '../svelte/invoices/currency.js';
16
10
  /** Integer minor units → major units (`1999` USD → `19.99`). */
17
11
  export declare function minorToMajorUnits(amount: number, currency: string): number;
18
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"units.d.ts","sourceRoot":"","sources":["../../src/billing/units.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ1D;AAuCD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAetE;AAKD,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,SAAc,GAAG,MAAM,CAKzE;AAED,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE1C;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,4DAA4D;AAC5D,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE/D"}
1
+ {"version":3,"file":"units.d.ts","sourceRoot":"","sources":["../../src/billing/units.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,EACL,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,gCAAgC,CAAC;AAExC,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY1E;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAetE;AAKD,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,SAAc,GAAG,MAAM,CAKzE;AAED,sEAAsE;AACtE,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE1C;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE9C;AAED,4DAA4D;AAC5D,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAE/D"}