@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.
- package/AGENTS.md +19 -3
- package/README.md +29 -1
- package/agents/commercial-ui.md +78 -0
- package/agents/customer-vendor-ui.md +76 -0
- package/agents/invoice-calculations.md +17 -0
- package/agents/invoice-preparation.md +86 -0
- package/agents/invoice-quantity.md +46 -0
- package/agents/pricing-versions.md +28 -0
- package/agents/purchasing-ui.md +82 -0
- package/agents/quote-ui.md +82 -0
- package/dist/billing/units.d.ts +1 -7
- package/dist/billing/units.d.ts.map +1 -1
- package/dist/chunks/calculations-DPuldr1J.js +242 -0
- package/dist/chunks/calculations-DPuldr1J.js.map +1 -0
- package/dist/chunks/types-CRnln577.js.map +1 -1
- package/dist/chunks/{units-CpBepqG1.js → units-CJcAOQm1.js} +3 -39
- package/dist/chunks/units-CJcAOQm1.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -44
- package/dist/index.js.map +1 -1
- package/dist/invoices.d.ts +3 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +2 -0
- package/dist/manifest.json +66 -8
- package/dist/migrations/invoiceEditorStorage.d.ts +39 -0
- package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -0
- package/dist/models/ContractLineItem.d.ts +1 -1
- package/dist/models/InvoiceLineItem.d.ts +18 -36
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/playground.js +201 -3
- package/dist/playground.js.map +1 -1
- package/dist/smrt-knowledge.json +109 -7
- package/dist/svelte/components/CustomerDetail.svelte +150 -0
- package/dist/svelte/components/CustomerDetail.svelte.d.ts +28 -0
- package/dist/svelte/components/CustomerDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CustomerDirectory.svelte +177 -0
- package/dist/svelte/components/CustomerDirectory.svelte.d.ts +42 -0
- package/dist/svelte/components/CustomerDirectory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/CustomerForm.svelte +234 -0
- package/dist/svelte/components/CustomerForm.svelte.d.ts +44 -0
- package/dist/svelte/components/CustomerForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceAllocationFields.svelte +72 -0
- package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts +8 -0
- package/dist/svelte/components/InvoiceAllocationFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceCard.svelte +2 -2
- package/dist/svelte/components/InvoiceEditor.svelte +134 -0
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts +8 -0
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceLineEditor.svelte +85 -0
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +8 -0
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceReview.svelte +23 -0
- package/dist/svelte/components/InvoiceReview.svelte.d.ts +10 -0
- package/dist/svelte/components/InvoiceReview.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PartyAddressFields.svelte +95 -0
- package/dist/svelte/components/PartyAddressFields.svelte.d.ts +25 -0
- package/dist/svelte/components/PartyAddressFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PartyContactFields.svelte +153 -0
- package/dist/svelte/components/PartyContactFields.svelte.d.ts +28 -0
- package/dist/svelte/components/PartyContactFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PricingVersionDecision.svelte +88 -0
- package/dist/svelte/components/PricingVersionDecision.svelte.d.ts +35 -0
- package/dist/svelte/components/PricingVersionDecision.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PricingVersionSummary.svelte +46 -0
- package/dist/svelte/components/PricingVersionSummary.svelte.d.ts +9 -0
- package/dist/svelte/components/PricingVersionSummary.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte +210 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +79 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseSourceSelector.svelte +56 -0
- package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts +30 -0
- package/dist/svelte/components/PurchaseSourceSelector.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchasingPlayground.svelte +133 -0
- package/dist/svelte/components/PurchasingPlayground.svelte.d.ts +9 -0
- package/dist/svelte/components/PurchasingPlayground.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuoteEditor.svelte +177 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts +76 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuotePlayground.svelte +108 -0
- package/dist/svelte/components/QuotePlayground.svelte.d.ts +10 -0
- package/dist/svelte/components/QuotePlayground.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuoteRevisionComparison.svelte +44 -0
- package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts +14 -0
- package/dist/svelte/components/QuoteRevisionComparison.svelte.d.ts.map +1 -0
- package/dist/svelte/components/QuoteRevisionHistory.svelte +55 -0
- package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts +15 -0
- package/dist/svelte/components/QuoteRevisionHistory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/VendorDetail.svelte +134 -0
- package/dist/svelte/components/VendorDetail.svelte.d.ts +26 -0
- package/dist/svelte/components/VendorDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/components/VendorDirectory.svelte +178 -0
- package/dist/svelte/components/VendorDirectory.svelte.d.ts +42 -0
- package/dist/svelte/components/VendorDirectory.svelte.d.ts.map +1 -0
- package/dist/svelte/components/VendorForm.svelte +224 -0
- package/dist/svelte/components/VendorForm.svelte.d.ts +42 -0
- package/dist/svelte/components/VendorForm.svelte.d.ts.map +1 -0
- package/dist/svelte/i18n.d.ts +102 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +102 -0
- package/dist/svelte/index.d.ts +52 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +49 -0
- package/dist/svelte/invoices/calculations.d.ts +121 -0
- package/dist/svelte/invoices/calculations.d.ts.map +1 -0
- package/dist/svelte/invoices/calculations.js +259 -0
- package/dist/svelte/invoices/currency.d.ts +9 -0
- package/dist/svelte/invoices/currency.d.ts.map +1 -0
- package/dist/svelte/invoices/currency.js +55 -0
- package/dist/svelte/invoices/fields.d.ts +6 -0
- package/dist/svelte/invoices/fields.d.ts.map +1 -0
- package/dist/svelte/invoices/fields.js +22 -0
- package/dist/svelte/invoices/messages.d.ts +53 -0
- package/dist/svelte/invoices/messages.d.ts.map +1 -0
- package/dist/svelte/invoices/messages.js +53 -0
- package/dist/svelte/invoices/types.d.ts +194 -0
- package/dist/svelte/invoices/types.d.ts.map +1 -0
- package/dist/svelte/invoices/types.js +1 -0
- package/dist/svelte/party-types.d.ts +203 -0
- package/dist/svelte/party-types.d.ts.map +1 -0
- package/dist/svelte/party-types.js +82 -0
- package/dist/svelte/playground/InvoicePreparationPreview.svelte +116 -0
- package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/PartyFormPreview.svelte +211 -0
- package/dist/svelte/playground/PartyFormPreview.svelte.d.ts +8 -0
- package/dist/svelte/playground/PartyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/PricingVersionPreview.svelte +55 -0
- package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/PricingVersionPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +442 -3
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +199 -3
- package/dist/svelte/pricing-i18n.d.ts +10 -0
- package/dist/svelte/pricing-i18n.d.ts.map +1 -0
- package/dist/svelte/pricing-i18n.js +10 -0
- package/dist/svelte/pricing-types.d.ts +38 -0
- package/dist/svelte/pricing-types.d.ts.map +1 -0
- package/dist/svelte/pricing-types.js +1 -0
- package/dist/svelte/purchasing-i18n.d.ts +53 -0
- package/dist/svelte/purchasing-i18n.d.ts.map +1 -0
- package/dist/svelte/purchasing-i18n.js +53 -0
- package/dist/svelte/purchasing-types.d.ts +91 -0
- package/dist/svelte/purchasing-types.d.ts.map +1 -0
- package/dist/svelte/purchasing-types.js +13 -0
- package/dist/svelte/quote-i18n.d.ts +49 -0
- package/dist/svelte/quote-i18n.d.ts.map +1 -0
- package/dist/svelte/quote-i18n.js +49 -0
- package/dist/svelte/quote-types.d.ts +81 -0
- package/dist/svelte/quote-types.d.ts.map +1 -0
- package/dist/svelte/quote-types.js +51 -0
- package/dist/types/index.d.ts +13 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +150 -6
- package/dist/ui.js.map +1 -1
- package/package.json +25 -13
- 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
|
|
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
|
|
150
|
-
- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem
|
|
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 (
|
|
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.
|
package/dist/billing/units.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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"}
|