@happyvertical/smrt-expenses 0.53.3 → 0.53.5

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 (36) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +11 -1
  3. package/agents/expense-ui.md +92 -0
  4. package/dist/index.js +1 -1
  5. package/dist/manifest.json +1 -1
  6. package/dist/playground.d.ts +2 -0
  7. package/dist/playground.d.ts.map +1 -0
  8. package/dist/playground.js +21 -0
  9. package/dist/playground.js.map +1 -0
  10. package/dist/smrt-knowledge.json +26 -7
  11. package/dist/svelte/components/ExpenseForm.svelte +111 -0
  12. package/dist/svelte/components/ExpenseForm.svelte.d.ts +8 -0
  13. package/dist/svelte/components/ExpenseForm.svelte.d.ts.map +1 -0
  14. package/dist/svelte/components/ExpenseReviewPanel.svelte +58 -0
  15. package/dist/svelte/components/ExpenseReviewPanel.svelte.d.ts +8 -0
  16. package/dist/svelte/components/ExpenseReviewPanel.svelte.d.ts.map +1 -0
  17. package/dist/svelte/index.d.ts +5 -0
  18. package/dist/svelte/index.d.ts.map +1 -0
  19. package/dist/svelte/index.js +9 -0
  20. package/dist/svelte/messages.d.ts +29 -0
  21. package/dist/svelte/messages.d.ts.map +1 -0
  22. package/dist/svelte/messages.js +29 -0
  23. package/dist/svelte/playground/ExpenseWorkflowPreview.svelte +38 -0
  24. package/dist/svelte/playground/ExpenseWorkflowPreview.svelte.d.ts +4 -0
  25. package/dist/svelte/playground/ExpenseWorkflowPreview.svelte.d.ts.map +1 -0
  26. package/dist/svelte/playground.d.ts +21 -0
  27. package/dist/svelte/playground.d.ts.map +1 -0
  28. package/dist/svelte/playground.js +18 -0
  29. package/dist/svelte/types.d.ts +150 -0
  30. package/dist/svelte/types.d.ts.map +1 -0
  31. package/dist/svelte/types.js +1 -0
  32. package/dist/ui.d.ts +6 -0
  33. package/dist/ui.d.ts.map +1 -0
  34. package/dist/ui.js +33 -0
  35. package/dist/ui.js.map +1 -0
  36. package/package.json +45 -13
package/AGENTS.md CHANGED
@@ -125,3 +125,11 @@ pnpm --filter @happyvertical/smrt-expenses test:postgres
125
125
  SQLite file and the PostgreSQL lane (`*.optional.test.ts`, schema from
126
126
  `migrateSmrtSchemas` plus live parity) both run it. Use UUID tenant and
127
127
  profile ids in tests: PostgreSQL stores them as `uuid`.
128
+
129
+ ## UI
130
+
131
+ `src/svelte/` owns native expense entry and review presentation. See
132
+ [the UI contract](agents/expense-ui.md) for DTOs, native payloads and recovery.
133
+ Draft amounts are lossless currency-unit text; model/review amounts remain integer
134
+ minor units. Receipt upload is composed from Assets, never reimplemented here.
135
+ `test` includes backend and UI SSR; `test:e2e` proves native browser submissions.
package/README.md CHANGED
@@ -169,8 +169,9 @@ Imported rows start `unreviewed`; replay each donor review through
169
169
  ## Testing
170
170
 
171
171
  ```bash
172
- pnpm --filter @happyvertical/smrt-expenses test # SQLite
172
+ pnpm --filter @happyvertical/smrt-expenses test # SQLite + UI SSR
173
173
  pnpm --filter @happyvertical/smrt-expenses test:postgres # PostgreSQL lane
174
+ pnpm --filter @happyvertical/smrt-expenses test:e2e # Native browser forms
174
175
  ```
175
176
 
176
177
  ## See also
@@ -179,3 +180,12 @@ pnpm --filter @happyvertical/smrt-expenses test:postgres # PostgreSQL lane
179
180
  - [smrt-commerce](../commerce/README.md) — vendors and purchase orders
180
181
  - [smrt-assets](../assets/README.md) — receipt files
181
182
  - [smrt-tenancy](../tenancy/README.md) — tenant context and required mode
183
+
184
+ ## Expense and receipt UI
185
+
186
+ `ExpenseForm` and `ExpenseReviewPanel` from `@happyvertical/smrt-expenses/svelte`
187
+ provide native purchase entry, caller-driven duplicate/correction review and
188
+ Assets receipt composition. `ExpenseForm` uses the shared localized currency
189
+ selector while preserving native field names and rejected raw drafts. See
190
+ [the UI contract](agents/expense-ui.md) for public props, selector restrictions,
191
+ native payloads, retained errors and server responsibilities.
@@ -0,0 +1,92 @@
1
+ # Expense entry and receipt review
2
+
3
+ Import `ExpenseForm`, `ExpenseReviewPanel` and their named Props types from
4
+ `@happyvertical/smrt-expenses/svelte`. Registry slots are `expense-form` and
5
+ `expense-review`. `@happyvertical/smrt-expenses/ui` exposes metadata without
6
+ Svelte; `./playground` exposes an interactive retained-draft/review example.
7
+
8
+ `ExpenseDraftValues` is a complete set of native strings: `amount`, `currency`,
9
+ `incurredOn`, `description`, `category`, `vendorId`, `commitmentId`, `paidBy`,
10
+ `paidByProfileId`, and `correctionReason`. Initialize unused fields to `''`.
11
+ Amount is ordinary currency text (`'125.00'` CAD), distinct from the public
12
+ Expense model's integer minor-unit `amount` (`12500`). Server adapters parse
13
+ currency precision, safe integer range and business rules. Calendar dates stay
14
+ lossless text, normally `YYYY-MM-DD`; invalid amounts/dates never disappear due
15
+ to browser number/date coercion. Every validation failure returns original
16
+ strings, not reconstructed saved-model values.
17
+
18
+ ```svelte
19
+ <script lang="ts">
20
+ import { ExpenseForm, ExpenseReviewPanel } from '@happyvertical/smrt-expenses/svelte';
21
+ let { data, form } = $props();
22
+ </script>
23
+ <ExpenseForm
24
+ action="?/save"
25
+ title="Record purchase"
26
+ values={form?.values ?? data.values}
27
+ errors={form?.errors}
28
+ message={form?.message}
29
+ canEdit={data.mayEdit}
30
+ correcting={data.correcting}
31
+ vendors={data.authorizedVendors}
32
+ commitments={data.authorizedCommitments}
33
+ hiddenFields={[
34
+ { name: 'requestId', value: form?.requestId ?? data.requestId },
35
+ { name: 'costObjectId', value: data.costObjectId },
36
+ { name: 'expectedTenantId', value: data.tenantId },
37
+ ]}
38
+ />
39
+ <ExpenseReviewPanel
40
+ expense={data.expenseSummary}
41
+ duplicates={data.authorizedDuplicateCandidates}
42
+ history={data.history}
43
+ receipts={data.receiptPanel}
44
+ />
45
+ ```
46
+
47
+ Vendor and commitment options are authorized Commerce `{id, label}` projections.
48
+ Resolve Profile names and application-specific award lineage in the consumer.
49
+ Unknown retained references remain selectable/postable; selecting a reference
50
+ never establishes tenant ownership or commitment eligibility. `fields` maps
51
+ logical keys to native names; `errors` always uses logical keys. Submitter
52
+ `intentField`/`intent` default to `intent=save`. `hiddenFields` preserves exact
53
+ values and repeated names. Avoid field-name collisions. `children` adds
54
+ application-owned package allocation, quantity, reimbursement and correction
55
+ policy fields. `correcting` shows a retained reason, but does not implement a
56
+ new Expense correction model. The caller applies its own correction/history
57
+ policy and invokes existing guarded methods where appropriate.
58
+
59
+ The base form performs native POST by default, with or without JavaScript.
60
+ `onsubmit` allows optional enhancement. `pending` or a supplied
61
+ `smrt-ui/form-retry` `retryStatus` controls progress presentation without clearing
62
+ values or creating/rotating request keys. `canEdit=false` disables fields and
63
+ hides save; server-side authorization and replay enforcement remain mandatory.
64
+
65
+ Currency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n
66
+ context. It preserves the exact retained draft string and the field name from
67
+ `fields.currency`, including native no-JavaScript correction flows. Pass
68
+ `currencyOptions` only when the caller must restrict the shared currency catalog.
69
+
70
+ `ExpenseReviewSummary` projects public Expense description, integer minor-unit
71
+ amount, currency and incurredOn, with caller-localized `statusLabel` and `note`.
72
+ `duplicates` and `history` are explicit authorized presentation DTOs, not a
73
+ parallel ledger. The server supplies findings from ExpenseReceipt duplicate
74
+ queries and application history. Unknown labels render verbatim. No state or
75
+ action is inferred: `canReview`, `action`, `actions` and hidden fingerprints
76
+ explicitly provide a native review form. `action=""` posts to the current page;
77
+ omitting `action` hides the review form; `children` can supply retained reason
78
+ or duplicate-selection inputs. A server must reauthorize, reload the model and
79
+ invoke `review()`, `reject()`, `markDuplicate()` or `reopen()` under its policy.
80
+ Never assign review fields through UI payload binding. Reviewed money remains
81
+ frozen by the existing model; UI props cannot bypass it.
82
+
83
+ `receipts` is exactly the public Assets `AttachmentPanelProps`, supporting
84
+ private view/download URLs, version notes, native upload and recovery. It is
85
+ rendered outside the review form so upload and review remain separate native
86
+ actions. Use authorized receipt projections; attaching a file does not review
87
+ an expense, mark a duplicate, or change invoice approval. Assets owns file
88
+ selection and reselect-file help; Expenses does not duplicate uploads/storage.
89
+
90
+ Validation: `pnpm --filter @happyvertical/smrt-expenses test`, `test:ui`,
91
+ `test:e2e`, `typecheck`, `build`, and `verify:pack`. PostgreSQL model validation
92
+ remains in the existing opt-in `test:postgres` lane; this UI does not alter it.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var __exportAll = (all, no_symbols) => {
17
17
  };
18
18
  //#endregion
19
19
  //#region src/__smrt-register__.ts
20
- ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-expenses\",\"packageVersion\":\"0.53.3\",\"objects\":{\"@happyvertical/smrt-expenses:ExpenseCollection\":{\"name\":\"expensecollection\",\"className\":\"ExpenseCollection\",\"qualifiedName\":\"@happyvertical/smrt-expenses:ExpenseCollection\",\"collection\":\"expenses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/collections/ExpenseCollection.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{},\"methods\":{\"forCostObject\":{\"name\":\"forCostObject\",\"async\":true,\"parameters\":[{\"name\":\"costObjectType\",\"type\":\"string\",\"optional\":false},{\"name\":\"costObjectId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Expense[]>\",\"isStatic\":false,\"isPublic\":true},\"totalsByCurrency\":{\"name\":\"totalsByCurrency\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"ExpenseTotalsOptions\",\"optional\":true}],\"returnType\":\"Promise<CurrencyTotals>\",\"isStatic\":false,\"isPublic\":true},\"commitmentPosition\":{\"name\":\"commitmentPosition\",\"async\":true,\"parameters\":[{\"name\":\"commitmentId\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"object\",\"optional\":true,\"memberTypes\":[\"string\"]}],\"returnType\":\"Promise<CommitmentPosition>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"tableName\":\"expenses\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"Expense\",\"exportName\":\"ExpenseCollection\",\"collectionExportName\":\"ExpenseCollectionCollection\",\"schema\":{\"tableName\":\"expenses\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expenses\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"cost_object_type\\\" TEXT,\\n \\\"cost_object_id\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"amount\\\" INTEGER,\\n \\\"currency\\\" TEXT,\\n \\\"incurred_on\\\" TEXT,\\n \\\"recorded_at\\\" TIMESTAMP,\\n \\\"recorded_by_profile_id\\\" UUID,\\n \\\"paid_by\\\" TEXT,\\n \\\"paid_by_profile_id\\\" UUID,\\n \\\"vendor_id\\\" UUID,\\n \\\"commitment_id\\\" UUID,\\n \\\"commitment_line_id\\\" UUID,\\n \\\"review_status\\\" TEXT,\\n \\\"reviewed_by_profile_id\\\" UUID,\\n \\\"reviewed_at\\\" TIMESTAMP,\\n \\\"review_note\\\" TEXT,\\n \\\"duplicate_of_id\\\" UUID,\\n \\\"reimbursable\\\" BOOLEAN,\\n \\\"reimbursed_at\\\" TIMESTAMP,\\n \\\"reimbursement_reference\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"cost_object_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"cost_object_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"amount\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"incurred_on\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"recorded_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"recorded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"paid_by\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"paid_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"vendor_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_line_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"review_status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"reviewed_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"reviewed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"review_note\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"duplicate_of_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"SET NULL\",\"onUpdate\":\"CASCADE\"}},\"reimbursable\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"reimbursed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"reimbursement_reference\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"expenses_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expenses_cost_object_idx\",\"columns\":[\"tenant_id\",\"cost_object_type\",\"cost_object_id\",\"incurred_on\"]},{\"name\":\"expenses_commitment_review_idx\",\"columns\":[\"tenant_id\",\"commitment_id\",\"review_status\"]},{\"name\":\"expenses_review_status_idx\",\"columns\":[\"tenant_id\",\"review_status\",\"incurred_on\"]},{\"name\":\"expenses_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expenses_recorded_by_profile_id_idx\",\"columns\":[\"recorded_by_profile_id\"]},{\"name\":\"expenses_paid_by_profile_id_idx\",\"columns\":[\"paid_by_profile_id\"]},{\"name\":\"expenses_vendor_id_idx\",\"columns\":[\"vendor_id\"]},{\"name\":\"expenses_commitment_id_idx\",\"columns\":[\"commitment_id\"]},{\"name\":\"expenses_commitment_line_id_idx\",\"columns\":[\"commitment_line_id\"]},{\"name\":\"expenses_reviewed_by_profile_id_idx\",\"columns\":[\"reviewed_by_profile_id\"]},{\"name\":\"expenses_duplicate_of_id_idx\",\"columns\":[\"duplicate_of_id\"]}],\"version\":\"af101807\"},\"importPath\":\"@happyvertical/smrt-expenses\"},\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\":{\"name\":\"expensereceiptcollection\",\"className\":\"ExpenseReceiptCollection\",\"qualifiedName\":\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\",\"collection\":\"expensereceipts\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/collections/ExpenseReceiptCollection.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{},\"methods\":{\"byLeft\":{\"name\":\"byLeft\",\"async\":true,\"parameters\":[{\"name\":\"leftId\",\"type\":\"string\",\"optional\":false},{\"name\":\"opts\",\"type\":\"JunctionFilterOptions\",\"optional\":true}],\"returnType\":\"Promise<TItem[]>\",\"isStatic\":false,\"isPublic\":true},\"byRight\":{\"name\":\"byRight\",\"async\":true,\"parameters\":[{\"name\":\"rightId\",\"type\":\"string\",\"optional\":false},{\"name\":\"opts\",\"type\":\"JunctionFilterOptions\",\"optional\":true}],\"returnType\":\"Promise<TItem[]>\",\"isStatic\":false,\"isPublic\":true},\"attach\":{\"name\":\"attach\",\"async\":true,\"parameters\":[{\"name\":\"_expenseId\",\"type\":\"string\",\"optional\":false},{\"name\":\"_assetId\",\"type\":\"string\",\"optional\":false},{\"name\":\"_opts\",\"type\":\"JunctionAttachOptions\",\"optional\":true}],\"returnType\":\"Promise<ExpenseReceipt>\",\"isStatic\":false,\"isPublic\":true},\"detach\":{\"name\":\"detach\",\"async\":true,\"parameters\":[{\"name\":\"leftId\",\"type\":\"string\",\"optional\":false},{\"name\":\"rightId\",\"type\":\"string\",\"optional\":false},{\"name\":\"opts\",\"type\":\"JunctionFilterOptions\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"setLinks\":{\"name\":\"setLinks\",\"async\":true,\"parameters\":[{\"name\":\"_expenseId\",\"type\":\"string\",\"optional\":false},{\"name\":\"_assetIds\",\"type\":\"string[]\",\"optional\":false},{\"name\":\"_opts\",\"type\":\"JunctionAttachOptions\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"attachReceipt\":{\"name\":\"attachReceipt\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"AttachReceiptInput\",\"optional\":false}],\"returnType\":\"Promise<ExpenseReceipt>\",\"isStatic\":false,\"isPublic\":true},\"receiptsFor\":{\"name\":\"receiptsFor\",\"async\":true,\"parameters\":[{\"name\":\"expenseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ExpenseReceipt[]>\",\"isStatic\":false,\"isPublic\":true},\"findDuplicateReceipts\":{\"name\":\"findDuplicateReceipts\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"FindDuplicateReceiptsOptions\",\"optional\":true}],\"returnType\":\"Promise<DuplicateReceiptGroup[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"tableName\":\"expense_receipts\"},\"extends\":\"SmrtJunction\",\"extendsTypeArg\":\"ExpenseReceipt\",\"exportName\":\"ExpenseReceiptCollection\",\"collectionExportName\":\"ExpenseReceiptCollectionCollection\",\"schema\":{\"tableName\":\"expense_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expense_receipts\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"expense_id\\\" UUID NOT NULL,\\n \\\"asset_id\\\" UUID NOT NULL,\\n \\\"uploaded_by_profile_id\\\" UUID,\\n \\\"filename\\\" TEXT,\\n \\\"mime_type\\\" TEXT,\\n \\\"byte_count\\\" INTEGER,\\n \\\"content_sha256\\\" TEXT NOT NULL\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"expense_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"asset_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":true,\"unique\":false},\"uploaded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"filename\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"mime_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"byte_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"content_sha256\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false}},\"indexes\":[{\"name\":\"expense_receipts_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expense_receipts_expense_sha256_key\",\"columns\":[\"expense_id\",\"content_sha256\"],\"unique\":true},{\"name\":\"expense_receipts_tenant_sha256_idx\",\"columns\":[\"tenant_id\",\"content_sha256\"]},{\"name\":\"expense_receipts_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expense_receipts_asset_id_idx\",\"columns\":[\"asset_id\"]},{\"name\":\"expense_receipts_uploaded_by_profile_id_idx\",\"columns\":[\"uploaded_by_profile_id\"]}],\"version\":\"5aa77571\"},\"importPath\":\"@happyvertical/smrt-expenses\"},\"@happyvertical/smrt-expenses:Expense\":{\"name\":\"expense\",\"className\":\"Expense\",\"qualifiedName\":\"@happyvertical/smrt-expenses:Expense\",\"collection\":\"expenses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/models/Expense.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"costObjectType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"costObjectId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"category\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"description\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"amount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"currency\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"incurredOn\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"recordedAt\":{\"type\":\"datetime\",\"required\":false},\"recordedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"PROFILE\"},\"paidBy\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"paidByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"PROFILE\"},\"vendorId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:Vendor\"},\"commitmentId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:Contract\"},\"commitmentLineId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:ContractLineItem\"},\"reviewStatus\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"readonly\":true},\"readonly\":true},\"reviewedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"PROFILE\"},\"reviewedAt\":{\"type\":\"datetime\",\"required\":false,\"_meta\":{\"readonly\":true},\"readonly\":true},\"reviewNote\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"readonly\":true},\"readonly\":true},\"duplicateOfId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"Expense\",\"_meta\":{\"onDelete\":\"SET NULL\"}},\"reimbursable\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"reimbursedAt\":{\"type\":\"datetime\",\"required\":false},\"reimbursementReference\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"isReimbursed\":{\"name\":\"isReimbursed\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"isCountable\":{\"name\":\"isCountable\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"review\":{\"name\":\"review\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"ReviewExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"reject\":{\"name\":\"reject\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"RejectExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"markDuplicate\":{\"name\":\"markDuplicate\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"MarkDuplicateExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"reopen\":{\"name\":\"reopen\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"ReopenExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"markReimbursed\":{\"name\":\"markReimbursed\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"MarkReimbursedInput\",\"optional\":true}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"getCostObject\":{\"name\":\"getCostObject\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<T | null>\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"expenses\",\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"indexes\":[{\"name\":\"expenses_cost_object_idx\",\"columns\":[\"tenantId\",\"costObjectType\",\"costObjectId\",\"incurredOn\"]},{\"name\":\"expenses_commitment_review_idx\",\"columns\":[\"tenantId\",\"commitmentId\",\"reviewStatus\"]},{\"name\":\"expenses_review_status_idx\",\"columns\":[\"tenantId\",\"reviewStatus\",\"incurredOn\"]}],\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"Expense\",\"collectionExportName\":\"ExpenseCollection\",\"schema\":{\"tableName\":\"expenses\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expenses\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"cost_object_type\\\" TEXT,\\n \\\"cost_object_id\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"amount\\\" INTEGER,\\n \\\"currency\\\" TEXT,\\n \\\"incurred_on\\\" TEXT,\\n \\\"recorded_at\\\" TIMESTAMP,\\n \\\"recorded_by_profile_id\\\" UUID,\\n \\\"paid_by\\\" TEXT,\\n \\\"paid_by_profile_id\\\" UUID,\\n \\\"vendor_id\\\" UUID,\\n \\\"commitment_id\\\" UUID,\\n \\\"commitment_line_id\\\" UUID,\\n \\\"review_status\\\" TEXT,\\n \\\"reviewed_by_profile_id\\\" UUID,\\n \\\"reviewed_at\\\" TIMESTAMP,\\n \\\"review_note\\\" TEXT,\\n \\\"duplicate_of_id\\\" UUID,\\n \\\"reimbursable\\\" BOOLEAN,\\n \\\"reimbursed_at\\\" TIMESTAMP,\\n \\\"reimbursement_reference\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"cost_object_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"cost_object_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"amount\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"incurred_on\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"recorded_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"recorded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"paid_by\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"paid_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"vendor_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_line_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"review_status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"reviewed_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"reviewed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"review_note\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"duplicate_of_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"SET NULL\",\"onUpdate\":\"CASCADE\"}},\"reimbursable\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"reimbursed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"reimbursement_reference\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"expenses_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expenses_cost_object_idx\",\"columns\":[\"tenant_id\",\"cost_object_type\",\"cost_object_id\",\"incurred_on\"]},{\"name\":\"expenses_commitment_review_idx\",\"columns\":[\"tenant_id\",\"commitment_id\",\"review_status\"]},{\"name\":\"expenses_review_status_idx\",\"columns\":[\"tenant_id\",\"review_status\",\"incurred_on\"]},{\"name\":\"expenses_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expenses_recorded_by_profile_id_idx\",\"columns\":[\"recorded_by_profile_id\"]},{\"name\":\"expenses_paid_by_profile_id_idx\",\"columns\":[\"paid_by_profile_id\"]},{\"name\":\"expenses_vendor_id_idx\",\"columns\":[\"vendor_id\"]},{\"name\":\"expenses_commitment_id_idx\",\"columns\":[\"commitment_id\"]},{\"name\":\"expenses_commitment_line_id_idx\",\"columns\":[\"commitment_line_id\"]},{\"name\":\"expenses_reviewed_by_profile_id_idx\",\"columns\":[\"reviewed_by_profile_id\"]},{\"name\":\"expenses_duplicate_of_id_idx\",\"columns\":[\"duplicate_of_id\"]}],\"version\":\"af101807\"},\"importPath\":\"@happyvertical/smrt-expenses\"},\"@happyvertical/smrt-expenses:ExpenseReceipt\":{\"name\":\"expensereceipt\",\"className\":\"ExpenseReceipt\",\"qualifiedName\":\"@happyvertical/smrt-expenses:ExpenseReceipt\",\"collection\":\"expensereceipts\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/models/ExpenseReceipt.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"expenseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Expense\",\"_meta\":{\"required\":true,\"onDelete\":\"CASCADE\"}},\"assetId\":{\"type\":\"crossPackageRef\",\"required\":true,\"related\":\"@happyvertical/smrt-assets:Asset\"},\"uploadedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\"},\"filename\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"mimeType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"byteCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"contentSha256\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}}},\"methods\":{\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"expense_receipts\",\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"indexes\":[{\"name\":\"expense_receipts_expense_sha256_key\",\"columns\":[\"expenseId\",\"contentSha256\"],\"unique\":true},{\"name\":\"expense_receipts_tenant_sha256_idx\",\"columns\":[\"tenantId\",\"contentSha256\"]}],\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"ExpenseReceipt\",\"collectionExportName\":\"ExpenseReceiptCollection\",\"validationRules\":[{\"field\":\"expenseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"assetId\",\"rule\":\"required\",\"fieldType\":\"crossPackageRef\"},{\"field\":\"contentSha256\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"expense_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expense_receipts\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"expense_id\\\" UUID NOT NULL,\\n \\\"asset_id\\\" UUID NOT NULL,\\n \\\"uploaded_by_profile_id\\\" UUID,\\n \\\"filename\\\" TEXT,\\n \\\"mime_type\\\" TEXT,\\n \\\"byte_count\\\" INTEGER,\\n \\\"content_sha256\\\" TEXT NOT NULL\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"expense_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"asset_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":true,\"unique\":false},\"uploaded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"filename\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"mime_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"byte_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"content_sha256\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false}},\"indexes\":[{\"name\":\"expense_receipts_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expense_receipts_expense_sha256_key\",\"columns\":[\"expense_id\",\"content_sha256\"],\"unique\":true},{\"name\":\"expense_receipts_tenant_sha256_idx\",\"columns\":[\"tenant_id\",\"content_sha256\"]},{\"name\":\"expense_receipts_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expense_receipts_asset_id_idx\",\"columns\":[\"asset_id\"]},{\"name\":\"expense_receipts_uploaded_by_profile_id_idx\",\"columns\":[\"uploaded_by_profile_id\"]}],\"version\":\"5aa77571\"},\"importPath\":\"@happyvertical/smrt-expenses\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-assets\",\"@happyvertical/smrt-commerce\",\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\"]}"));
20
+ ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-expenses\",\"packageVersion\":\"0.53.5\",\"objects\":{\"@happyvertical/smrt-expenses:ExpenseCollection\":{\"name\":\"expensecollection\",\"className\":\"ExpenseCollection\",\"qualifiedName\":\"@happyvertical/smrt-expenses:ExpenseCollection\",\"collection\":\"expenses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/collections/ExpenseCollection.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{},\"methods\":{\"forCostObject\":{\"name\":\"forCostObject\",\"async\":true,\"parameters\":[{\"name\":\"costObjectType\",\"type\":\"string\",\"optional\":false},{\"name\":\"costObjectId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<Expense[]>\",\"isStatic\":false,\"isPublic\":true},\"totalsByCurrency\":{\"name\":\"totalsByCurrency\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"ExpenseTotalsOptions\",\"optional\":true}],\"returnType\":\"Promise<CurrencyTotals>\",\"isStatic\":false,\"isPublic\":true},\"commitmentPosition\":{\"name\":\"commitmentPosition\",\"async\":true,\"parameters\":[{\"name\":\"commitmentId\",\"type\":\"string\",\"optional\":false},{\"name\":\"options\",\"type\":\"object\",\"optional\":true,\"memberTypes\":[\"string\"]}],\"returnType\":\"Promise<CommitmentPosition>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"tableName\":\"expenses\"},\"extends\":\"SmrtCollection\",\"extendsTypeArg\":\"Expense\",\"exportName\":\"ExpenseCollection\",\"collectionExportName\":\"ExpenseCollectionCollection\",\"schema\":{\"tableName\":\"expenses\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expenses\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"cost_object_type\\\" TEXT,\\n \\\"cost_object_id\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"amount\\\" INTEGER,\\n \\\"currency\\\" TEXT,\\n \\\"incurred_on\\\" TEXT,\\n \\\"recorded_at\\\" TIMESTAMP,\\n \\\"recorded_by_profile_id\\\" UUID,\\n \\\"paid_by\\\" TEXT,\\n \\\"paid_by_profile_id\\\" UUID,\\n \\\"vendor_id\\\" UUID,\\n \\\"commitment_id\\\" UUID,\\n \\\"commitment_line_id\\\" UUID,\\n \\\"review_status\\\" TEXT,\\n \\\"reviewed_by_profile_id\\\" UUID,\\n \\\"reviewed_at\\\" TIMESTAMP,\\n \\\"review_note\\\" TEXT,\\n \\\"duplicate_of_id\\\" UUID,\\n \\\"reimbursable\\\" BOOLEAN,\\n \\\"reimbursed_at\\\" TIMESTAMP,\\n \\\"reimbursement_reference\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"cost_object_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"cost_object_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"amount\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"incurred_on\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"recorded_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"recorded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"paid_by\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"paid_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"vendor_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_line_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"review_status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"reviewed_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"reviewed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"review_note\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"duplicate_of_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"SET NULL\",\"onUpdate\":\"CASCADE\"}},\"reimbursable\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"reimbursed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"reimbursement_reference\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"expenses_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expenses_cost_object_idx\",\"columns\":[\"tenant_id\",\"cost_object_type\",\"cost_object_id\",\"incurred_on\"]},{\"name\":\"expenses_commitment_review_idx\",\"columns\":[\"tenant_id\",\"commitment_id\",\"review_status\"]},{\"name\":\"expenses_review_status_idx\",\"columns\":[\"tenant_id\",\"review_status\",\"incurred_on\"]},{\"name\":\"expenses_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expenses_recorded_by_profile_id_idx\",\"columns\":[\"recorded_by_profile_id\"]},{\"name\":\"expenses_paid_by_profile_id_idx\",\"columns\":[\"paid_by_profile_id\"]},{\"name\":\"expenses_vendor_id_idx\",\"columns\":[\"vendor_id\"]},{\"name\":\"expenses_commitment_id_idx\",\"columns\":[\"commitment_id\"]},{\"name\":\"expenses_commitment_line_id_idx\",\"columns\":[\"commitment_line_id\"]},{\"name\":\"expenses_reviewed_by_profile_id_idx\",\"columns\":[\"reviewed_by_profile_id\"]},{\"name\":\"expenses_duplicate_of_id_idx\",\"columns\":[\"duplicate_of_id\"]}],\"version\":\"af101807\"},\"importPath\":\"@happyvertical/smrt-expenses\"},\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\":{\"name\":\"expensereceiptcollection\",\"className\":\"ExpenseReceiptCollection\",\"qualifiedName\":\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\",\"collection\":\"expensereceipts\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/collections/ExpenseReceiptCollection.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{},\"methods\":{\"byLeft\":{\"name\":\"byLeft\",\"async\":true,\"parameters\":[{\"name\":\"leftId\",\"type\":\"string\",\"optional\":false},{\"name\":\"opts\",\"type\":\"JunctionFilterOptions\",\"optional\":true}],\"returnType\":\"Promise<TItem[]>\",\"isStatic\":false,\"isPublic\":true},\"byRight\":{\"name\":\"byRight\",\"async\":true,\"parameters\":[{\"name\":\"rightId\",\"type\":\"string\",\"optional\":false},{\"name\":\"opts\",\"type\":\"JunctionFilterOptions\",\"optional\":true}],\"returnType\":\"Promise<TItem[]>\",\"isStatic\":false,\"isPublic\":true},\"attach\":{\"name\":\"attach\",\"async\":true,\"parameters\":[{\"name\":\"_expenseId\",\"type\":\"string\",\"optional\":false},{\"name\":\"_assetId\",\"type\":\"string\",\"optional\":false},{\"name\":\"_opts\",\"type\":\"JunctionAttachOptions\",\"optional\":true}],\"returnType\":\"Promise<ExpenseReceipt>\",\"isStatic\":false,\"isPublic\":true},\"detach\":{\"name\":\"detach\",\"async\":true,\"parameters\":[{\"name\":\"leftId\",\"type\":\"string\",\"optional\":false},{\"name\":\"rightId\",\"type\":\"string\",\"optional\":false},{\"name\":\"opts\",\"type\":\"JunctionFilterOptions\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"setLinks\":{\"name\":\"setLinks\",\"async\":true,\"parameters\":[{\"name\":\"_expenseId\",\"type\":\"string\",\"optional\":false},{\"name\":\"_assetIds\",\"type\":\"string[]\",\"optional\":false},{\"name\":\"_opts\",\"type\":\"JunctionAttachOptions\",\"optional\":true}],\"returnType\":\"Promise<void>\",\"isStatic\":false,\"isPublic\":true},\"attachReceipt\":{\"name\":\"attachReceipt\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"AttachReceiptInput\",\"optional\":false}],\"returnType\":\"Promise<ExpenseReceipt>\",\"isStatic\":false,\"isPublic\":true},\"receiptsFor\":{\"name\":\"receiptsFor\",\"async\":true,\"parameters\":[{\"name\":\"expenseId\",\"type\":\"string\",\"optional\":false}],\"returnType\":\"Promise<ExpenseReceipt[]>\",\"isStatic\":false,\"isPublic\":true},\"findDuplicateReceipts\":{\"name\":\"findDuplicateReceipts\",\"async\":true,\"parameters\":[{\"name\":\"options\",\"type\":\"FindDuplicateReceiptsOptions\",\"optional\":true}],\"returnType\":\"Promise<DuplicateReceiptGroup[]>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"tableName\":\"expense_receipts\"},\"extends\":\"SmrtJunction\",\"extendsTypeArg\":\"ExpenseReceipt\",\"exportName\":\"ExpenseReceiptCollection\",\"collectionExportName\":\"ExpenseReceiptCollectionCollection\",\"schema\":{\"tableName\":\"expense_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expense_receipts\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"expense_id\\\" UUID NOT NULL,\\n \\\"asset_id\\\" UUID NOT NULL,\\n \\\"uploaded_by_profile_id\\\" UUID,\\n \\\"filename\\\" TEXT,\\n \\\"mime_type\\\" TEXT,\\n \\\"byte_count\\\" INTEGER,\\n \\\"content_sha256\\\" TEXT NOT NULL\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"expense_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"asset_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":true,\"unique\":false},\"uploaded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"filename\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"mime_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"byte_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"content_sha256\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false}},\"indexes\":[{\"name\":\"expense_receipts_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expense_receipts_expense_sha256_key\",\"columns\":[\"expense_id\",\"content_sha256\"],\"unique\":true},{\"name\":\"expense_receipts_tenant_sha256_idx\",\"columns\":[\"tenant_id\",\"content_sha256\"]},{\"name\":\"expense_receipts_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expense_receipts_asset_id_idx\",\"columns\":[\"asset_id\"]},{\"name\":\"expense_receipts_uploaded_by_profile_id_idx\",\"columns\":[\"uploaded_by_profile_id\"]}],\"version\":\"5aa77571\"},\"importPath\":\"@happyvertical/smrt-expenses\"},\"@happyvertical/smrt-expenses:Expense\":{\"name\":\"expense\",\"className\":\"Expense\",\"qualifiedName\":\"@happyvertical/smrt-expenses:Expense\",\"collection\":\"expenses\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/models/Expense.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"costObjectType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"costObjectId\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"category\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"description\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"amount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"currency\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"incurredOn\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"recordedAt\":{\"type\":\"datetime\",\"required\":false},\"recordedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"PROFILE\"},\"paidBy\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"paidByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"PROFILE\"},\"vendorId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:Vendor\"},\"commitmentId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:Contract\"},\"commitmentLineId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-commerce:ContractLineItem\"},\"reviewStatus\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"readonly\":true},\"readonly\":true},\"reviewedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"PROFILE\"},\"reviewedAt\":{\"type\":\"datetime\",\"required\":false,\"_meta\":{\"readonly\":true},\"readonly\":true},\"reviewNote\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"readonly\":true},\"readonly\":true},\"duplicateOfId\":{\"type\":\"foreignKey\",\"required\":false,\"related\":\"Expense\",\"_meta\":{\"onDelete\":\"SET NULL\"}},\"reimbursable\":{\"type\":\"boolean\",\"required\":false,\"_meta\":{}},\"reimbursedAt\":{\"type\":\"datetime\",\"required\":false},\"reimbursementReference\":{\"type\":\"text\",\"required\":false,\"_meta\":{}}},\"methods\":{\"isReimbursed\":{\"name\":\"isReimbursed\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"isCountable\":{\"name\":\"isCountable\",\"async\":false,\"parameters\":[],\"returnType\":\"boolean\",\"isStatic\":false,\"isPublic\":true},\"review\":{\"name\":\"review\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"ReviewExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"reject\":{\"name\":\"reject\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"RejectExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"markDuplicate\":{\"name\":\"markDuplicate\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"MarkDuplicateExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"reopen\":{\"name\":\"reopen\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"ReopenExpenseInput\",\"optional\":false}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"markReimbursed\":{\"name\":\"markReimbursed\",\"async\":true,\"parameters\":[{\"name\":\"input\",\"type\":\"MarkReimbursedInput\",\"optional\":true}],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true},\"getCostObject\":{\"name\":\"getCostObject\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<T | null>\",\"isStatic\":false,\"isPublic\":true},\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"expenses\",\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"indexes\":[{\"name\":\"expenses_cost_object_idx\",\"columns\":[\"tenantId\",\"costObjectType\",\"costObjectId\",\"incurredOn\"]},{\"name\":\"expenses_commitment_review_idx\",\"columns\":[\"tenantId\",\"commitmentId\",\"reviewStatus\"]},{\"name\":\"expenses_review_status_idx\",\"columns\":[\"tenantId\",\"reviewStatus\",\"incurredOn\"]}],\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"Expense\",\"collectionExportName\":\"ExpenseCollection\",\"schema\":{\"tableName\":\"expenses\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expenses\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"cost_object_type\\\" TEXT,\\n \\\"cost_object_id\\\" TEXT,\\n \\\"category\\\" TEXT,\\n \\\"description\\\" TEXT,\\n \\\"amount\\\" INTEGER,\\n \\\"currency\\\" TEXT,\\n \\\"incurred_on\\\" TEXT,\\n \\\"recorded_at\\\" TIMESTAMP,\\n \\\"recorded_by_profile_id\\\" UUID,\\n \\\"paid_by\\\" TEXT,\\n \\\"paid_by_profile_id\\\" UUID,\\n \\\"vendor_id\\\" UUID,\\n \\\"commitment_id\\\" UUID,\\n \\\"commitment_line_id\\\" UUID,\\n \\\"review_status\\\" TEXT,\\n \\\"reviewed_by_profile_id\\\" UUID,\\n \\\"reviewed_at\\\" TIMESTAMP,\\n \\\"review_note\\\" TEXT,\\n \\\"duplicate_of_id\\\" UUID,\\n \\\"reimbursable\\\" BOOLEAN,\\n \\\"reimbursed_at\\\" TIMESTAMP,\\n \\\"reimbursement_reference\\\" TEXT\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"cost_object_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"cost_object_id\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"category\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"description\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"amount\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"currency\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"incurred_on\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"recorded_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"recorded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"paid_by\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"paid_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"vendor_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"commitment_line_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"review_status\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"reviewed_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"reviewed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"review_note\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"duplicate_of_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":false,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"SET NULL\",\"onUpdate\":\"CASCADE\"}},\"reimbursable\":{\"type\":\"BOOLEAN\",\"notNull\":false,\"unique\":false},\"reimbursed_at\":{\"type\":\"TIMESTAMP\",\"notNull\":false,\"unique\":false},\"reimbursement_reference\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false}},\"indexes\":[{\"name\":\"expenses_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expenses_cost_object_idx\",\"columns\":[\"tenant_id\",\"cost_object_type\",\"cost_object_id\",\"incurred_on\"]},{\"name\":\"expenses_commitment_review_idx\",\"columns\":[\"tenant_id\",\"commitment_id\",\"review_status\"]},{\"name\":\"expenses_review_status_idx\",\"columns\":[\"tenant_id\",\"review_status\",\"incurred_on\"]},{\"name\":\"expenses_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expenses_recorded_by_profile_id_idx\",\"columns\":[\"recorded_by_profile_id\"]},{\"name\":\"expenses_paid_by_profile_id_idx\",\"columns\":[\"paid_by_profile_id\"]},{\"name\":\"expenses_vendor_id_idx\",\"columns\":[\"vendor_id\"]},{\"name\":\"expenses_commitment_id_idx\",\"columns\":[\"commitment_id\"]},{\"name\":\"expenses_commitment_line_id_idx\",\"columns\":[\"commitment_line_id\"]},{\"name\":\"expenses_reviewed_by_profile_id_idx\",\"columns\":[\"reviewed_by_profile_id\"]},{\"name\":\"expenses_duplicate_of_id_idx\",\"columns\":[\"duplicate_of_id\"]}],\"version\":\"af101807\"},\"importPath\":\"@happyvertical/smrt-expenses\"},\"@happyvertical/smrt-expenses:ExpenseReceipt\":{\"name\":\"expensereceipt\",\"className\":\"ExpenseReceipt\",\"qualifiedName\":\"@happyvertical/smrt-expenses:ExpenseReceipt\",\"collection\":\"expensereceipts\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/expenses/src/models/ExpenseReceipt.ts\",\"packageName\":\"@happyvertical/smrt-expenses\",\"fields\":{\"tenantId\":{\"type\":\"text\",\"required\":false,\"_meta\":{\"sqlType\":\"UUID\",\"nullable\":true,\"__tenancy\":{\"isTenantIdField\":true,\"autoFilter\":true,\"required\":false,\"autoPopulate\":true,\"nullable\":true,\"mode\":\"optional\",\"field\":\"tenantId\",\"allowSuperAdminBypass\":false}}},\"expenseId\":{\"type\":\"foreignKey\",\"required\":true,\"related\":\"Expense\",\"_meta\":{\"required\":true,\"onDelete\":\"CASCADE\"}},\"assetId\":{\"type\":\"crossPackageRef\",\"required\":true,\"related\":\"@happyvertical/smrt-assets:Asset\"},\"uploadedByProfileId\":{\"type\":\"crossPackageRef\",\"required\":false,\"related\":\"@happyvertical/smrt-profiles:Profile\"},\"filename\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"mimeType\":{\"type\":\"text\",\"required\":false,\"_meta\":{}},\"byteCount\":{\"type\":\"integer\",\"required\":false,\"_meta\":{}},\"contentSha256\":{\"type\":\"text\",\"required\":true,\"_meta\":{\"required\":true}}},\"methods\":{\"save\":{\"name\":\"save\",\"async\":true,\"parameters\":[],\"returnType\":\"Promise<this>\",\"isStatic\":false,\"isPublic\":true}},\"decoratorConfig\":{\"tableName\":\"expense_receipts\",\"api\":{\"include\":[]},\"mcp\":{\"include\":[]},\"cli\":false,\"indexes\":[{\"name\":\"expense_receipts_expense_sha256_key\",\"columns\":[\"expenseId\",\"contentSha256\"],\"unique\":true},{\"name\":\"expense_receipts_tenant_sha256_idx\",\"columns\":[\"tenantId\",\"contentSha256\"]}],\"tenantScoped\":{\"mode\":\"optional\"},\"conflictColumns\":[\"tenant_id\",\"slug\",\"context\"]},\"extends\":\"SmrtObject\",\"exportName\":\"ExpenseReceipt\",\"collectionExportName\":\"ExpenseReceiptCollection\",\"validationRules\":[{\"field\":\"expenseId\",\"rule\":\"required\",\"fieldType\":\"foreignKey\"},{\"field\":\"assetId\",\"rule\":\"required\",\"fieldType\":\"crossPackageRef\"},{\"field\":\"contentSha256\",\"rule\":\"required\",\"fieldType\":\"text\"}],\"schema\":{\"tableName\":\"expense_receipts\",\"ddl\":\"CREATE TABLE IF NOT EXISTS \\\"expense_receipts\\\" (\\n \\\"id\\\" UUID PRIMARY KEY NOT NULL,\\n \\\"slug\\\" TEXT NOT NULL,\\n \\\"context\\\" TEXT NOT NULL DEFAULT '',\\n \\\"created_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"updated_at\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\\"tenant_id\\\" UUID,\\n \\\"expense_id\\\" UUID NOT NULL,\\n \\\"asset_id\\\" UUID NOT NULL,\\n \\\"uploaded_by_profile_id\\\" UUID,\\n \\\"filename\\\" TEXT,\\n \\\"mime_type\\\" TEXT,\\n \\\"byte_count\\\" INTEGER,\\n \\\"content_sha256\\\" TEXT NOT NULL\\n);\",\"columns\":{\"id\":{\"type\":\"UUID\",\"primaryKey\":true,\"referenceKind\":\"id\",\"notNull\":true},\"slug\":{\"type\":\"TEXT\",\"notNull\":true},\"context\":{\"type\":\"TEXT\",\"notNull\":true,\"default\":\"\"},\"created_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"updated_at\":{\"type\":\"TIMESTAMP\",\"notNull\":true,\"default\":\"current_timestamp\"},\"tenant_id\":{\"type\":\"UUID\",\"referenceKind\":\"tenantId\",\"notNull\":false,\"unique\":false},\"expense_id\":{\"type\":\"UUID\",\"referenceKind\":\"foreignKey\",\"notNull\":true,\"unique\":false,\"foreignKey\":{\"table\":\"expenses\",\"column\":\"id\",\"onDelete\":\"CASCADE\",\"onUpdate\":\"CASCADE\"}},\"asset_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":true,\"unique\":false},\"uploaded_by_profile_id\":{\"type\":\"UUID\",\"referenceKind\":\"crossPackageRef\",\"notNull\":false,\"unique\":false},\"filename\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"mime_type\":{\"type\":\"TEXT\",\"notNull\":false,\"unique\":false},\"byte_count\":{\"type\":\"INTEGER\",\"notNull\":false,\"unique\":false},\"content_sha256\":{\"type\":\"TEXT\",\"notNull\":true,\"unique\":false}},\"indexes\":[{\"name\":\"expense_receipts_slug_context_idx\",\"columns\":[\"tenant_id\",\"slug\",\"context\"],\"unique\":true,\"nullsNotDistinct\":true},{\"name\":\"expense_receipts_expense_sha256_key\",\"columns\":[\"expense_id\",\"content_sha256\"],\"unique\":true},{\"name\":\"expense_receipts_tenant_sha256_idx\",\"columns\":[\"tenant_id\",\"content_sha256\"]},{\"name\":\"expense_receipts_tenant_id_created_at_idx\",\"columns\":[\"tenant_id\",\"created_at\"]},{\"name\":\"expense_receipts_asset_id_idx\",\"columns\":[\"asset_id\"]},{\"name\":\"expense_receipts_uploaded_by_profile_id_idx\",\"columns\":[\"uploaded_by_profile_id\"]}],\"version\":\"5aa77571\"},\"importPath\":\"@happyvertical/smrt-expenses\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-assets\",\"@happyvertical/smrt-commerce\",\"@happyvertical/smrt-core\",\"@happyvertical/smrt-tenancy\"]}"));
21
21
  //#endregion
22
22
  //#region src/collections/ExpenseCollection.ts
23
23
  var ExpenseCollection_exports = /* @__PURE__ */ __exportAll({ ExpenseCollection: () => ExpenseCollection });
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-expenses",
5
- "packageVersion": "0.53.3",
5
+ "packageVersion": "0.53.5",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-expenses:ExpenseCollection": {
8
8
  "name": "expensecollection",
@@ -0,0 +1,2 @@
1
+ export { default } from './svelte/playground.js';
2
+ //# sourceMappingURL=playground.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../src/playground.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { EXPENSES_MODULE_META } from "./ui.js";
2
+ //#region src/svelte/playground.ts
3
+ var playground_default = {
4
+ packageName: "@happyvertical/smrt-expenses",
5
+ displayName: "Expenses",
6
+ description: "Purchase entry, receipts and duplicate review",
7
+ moduleMeta: EXPENSES_MODULE_META,
8
+ entries: [{
9
+ id: "expense-workflow",
10
+ title: "Expense Workflow",
11
+ description: "Native purchase entry with rejected-value retention and receipt review.",
12
+ loadComponent: () => import("./svelte/playground/ExpenseWorkflowPreview.svelte"),
13
+ order: 1,
14
+ props: {},
15
+ modes: { mock: { label: "Mock" } }
16
+ }]
17
+ };
18
+ //#endregion
19
+ export { playground_default as default };
20
+
21
+ //# sourceMappingURL=playground.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.js","names":[],"sources":["../src/svelte/playground.ts"],"sourcesContent":["import { EXPENSES_MODULE_META } from '../ui.js';\nexport default {\n packageName: '@happyvertical/smrt-expenses',\n displayName: 'Expenses',\n description: 'Purchase entry, receipts and duplicate review',\n moduleMeta: EXPENSES_MODULE_META,\n entries: [\n {\n id: 'expense-workflow',\n title: 'Expense Workflow',\n description:\n 'Native purchase entry with rejected-value retention and receipt review.',\n loadComponent: () => import('./playground/ExpenseWorkflowPreview.svelte'),\n order: 1,\n props: {},\n modes: { mock: { label: 'Mock' } },\n },\n ],\n};\n"],"mappings":";;AACA,IAAA,qBAAe;CACb,aAAa;CACb,aAAa;CACb,aAAa;CACb,YAAY;CACZ,SAAS,CACP;EACE,IAAI;EACJ,OAAO;EACP,aACE;EACF,qBAAqB,OAAO;EAC5B,OAAO;EACP,OAAO,CAAC;EACR,OAAO,EAAE,MAAM,EAAE,OAAO,OAAO,EAAE;CACnC,CACF;AACF"}
@@ -3,35 +3,47 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-expenses",
6
- "packageVersion": "0.53.3",
6
+ "packageVersion": "0.53.5",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "463b0646cf69017d7d75cb6b0bdb84a10c3a061e2a3666283b63faa62ab5970c",
11
- "packageJson": "55395bdc07e8b8d4b8dc9c5c2a12b8432035ecd2457355ec35374c2e4f2b1b3d",
12
- "agents": "3af9d39ed7cbb390749e3828cb667dab39a6fd9104ad6c37abcf1db023f8dfcb"
10
+ "manifest": "af0cf74b9fb3072e42770f8c29965f923f05a305e7636163d752f68cb5c7b8ad",
11
+ "packageJson": "a3f7d29a4d20a163d53504c938f36ad4e879c688242a228aa1f227c39951126b",
12
+ "agents": "874f50fb4947edfa4547fa4355767f94693481c548f6b087174b7ed4286c47f6",
13
+ "moduleDoc:agents/expense-ui.md": "b2a0e24c48243eb2de351c299a9153b2e1020658760d68a032c21592aedf83f1"
13
14
  },
14
15
  "exports": [
15
16
  ".",
16
17
  "./manifest",
17
- "./manifest.json"
18
+ "./manifest.json",
19
+ "./playground",
20
+ "./svelte",
21
+ "./ui"
18
22
  ],
19
23
  "dependencies": {
20
24
  "@happyvertical/smrt-assets": "workspace:*",
21
25
  "@happyvertical/smrt-commerce": "workspace:*",
22
26
  "@happyvertical/smrt-core": "workspace:*",
23
27
  "@happyvertical/smrt-tenancy": "workspace:*",
28
+ "@happyvertical/smrt-types": "workspace:*",
29
+ "@happyvertical/smrt-ui": "workspace:*",
24
30
  "@happyvertical/smrt-vitest": "workspace:*",
25
31
  "@types/node": "24.13.6",
26
32
  "typescript": "6.0.3",
27
33
  "vite": "8.1.4",
28
- "vitest": "4.1.10"
34
+ "vitest": "4.1.10",
35
+ "@sveltejs/package": "^2.5.8",
36
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
37
+ "svelte": "^5.57.0",
38
+ "svelte-check": "^4.7.6"
29
39
  },
30
40
  "smrtDependencies": [
31
41
  "@happyvertical/smrt-assets",
32
42
  "@happyvertical/smrt-commerce",
33
43
  "@happyvertical/smrt-core",
34
44
  "@happyvertical/smrt-tenancy",
45
+ "@happyvertical/smrt-types",
46
+ "@happyvertical/smrt-ui",
35
47
  "@happyvertical/smrt-vitest"
36
48
  ],
37
49
  "sdkDependencies": [],
@@ -774,5 +786,12 @@
774
786
  "polymorphicAssociations": 0,
775
787
  "uuidColumns": 28
776
788
  },
777
- "agentDoc": "# @happyvertical/smrt-expenses\n\nIncurred costs against a cost object, with deduplicated receipts, an explicit\nreview, reimbursement recording, and smrt-commerce commitment drawdown (#3289).\n\n## Models\n\n- **Expense** (`expenses`): `costObjectType` (qualified class name) +\n `costObjectId` (bare string), `category` (lowercase kebab-case; suggestions\n in `SUGGESTED_EXPENSE_CATEGORIES`), `amount` integer minor units + ISO 4217\n `currency`, `incurredOn` (`YYYY-MM-DD` text: a calendar date, not an\n instant), `recordedAt`, `recordedByProfileId`, `paidBy` (`company` |\n `person`) + `paidByProfileId`, `vendorId`, `commitmentId` /\n `commitmentLineId`, review fields, `reimbursable` / `reimbursedAt` /\n `reimbursementReference`.\n- **ExpenseReceipt** (`expense_receipts`): noun-specific asset join\n (`expenseId` → Expense, `assetId` → smrt-assets Asset) with uploader,\n `filename`, `mimeType`, `byteCount`, `contentSha256`. Collection extends\n `SmrtJunction` (left expense, right asset, ordered by `created_at`).\n\n## Invariants\n\n- **Review is method-only.** `review()`, `reject()`, `markDuplicate()`,\n `reopen()` read the persisted status, check the move, and authorize exactly\n one save through a module `WeakMap`. Every transition assigns every review\n field, and the token carries that complete resulting state, so a field set\n directly beside `review()`/`reject()` (e.g. an injected `duplicateOfId`)\n is refused. A `reviewed` expense never carries `duplicateOfId`. `save()` compares every review field\n (status, reviewer, time, note, `duplicateOfId`) with the stored row and\n refuses an unauthorized change (`EXPENSE_REVIEW_FIELDS_LOCKED`), including\n on a brand-new row. Review fields are also `readonly` for generated writes.\n- **Writes are pinned to the guarded row.** The guards read the row stored\n under `id`, but core's natural-key save adopts any same-owner row on\n `(tenant_id, slug, context)` and upserts it with no revision predicate;\n these models' slugs come from the id. So `pinNaturalKey()` lets a fresh\n (not `isPersisted`) instance only INSERT (`requireInsertOnSave()`): naming\n an existing row's id is `EXPENSE_IDENTITY_CONFLICT`, and a row appearing\n mid-save fails the INSERT instead of being adopted. An existing row changes\n only through a loaded instance (revision compare-and-swap) that keeps its\n slug/context. Never `create({ id: <existing> })` or `getOrUpsert()` these\n models; load and save instead.\n- **Reviewed money is frozen.** While the stored status is `reviewed`,\n `LOCKED_WHEN_REVIEWED` fields (money, currency, both dates, cost object,\n vendor, commitment, payer) cannot change; `reopen()` first. `recordedAt` is\n compared as an instant, since drivers return it as a `Date` or a string.\n- **Drawdown counts once.** `ExpenseCollection.commitmentPosition()` sums only\n `reviewed` rows with no `duplicateOfId`, in the commitment's currency, deduped\n by id. Committed is the contract's `totalAmount` (or the line's `amount`).\n- **Currencies never mix.** A commitment match must share the currency and\n tenant (`EXPENSE_COMMITMENT_MISMATCH`), checked when the match, line or\n currency is set or changed — not on every save, so a later commitment edit\n never blocks e.g. `markReimbursed()`. `totalsByCurrency()` keys by currency.\n Rows left in another currency by a later commitment change land in\n `otherCurrencies`, never in `drawn`.\n- **Duplicate receipts.** Same `(expense, sha256)` is refused by a pre-insert\n read plus the unique index `expense_receipts_expense_sha256_key`\n `(expense_id, content_sha256)`; a unique violation is rethrown as\n `DuplicateReceiptError`, classified from the error itself (violated key\n name, or `details.fieldName` columns on core's typed error) because on\n PostgreSQL the violation aborts a caller's transaction and nothing can be\n re-read; only an error naming no key falls back to a re-read. The index omits `tenant_id` deliberately: the\n expense already fixes the tenant, and a nullable leading `tenant_id` would\n leave NULL-tenant (global) receipts unenforced. Same hash on different\n expenses is allowed; `findDuplicateReceipts()` reports it (facet on\n `contentSha256`, count > 1). Receipt `tenantId` (derived from the\n expense on insert), `expenseId`, `assetId` and `contentSha256` are\n immutable. The inherited `SmrtJunction.attach()` and\n `setLinks()` are overridden to throw (`setLinks()` would delete every\n receipt, then fail to re-attach without hashes); `attachReceipt()` adds and\n `detach()` removes a link, keeping the asset. Because `ExpenseReceipt`\n overrides `save()`, `hasBaseJunctionLifecycle()` is false and no batch\n junction path can write a hashless row around it.\n- **Vendor ownership.** `vendorId` is a `crossPackageRef` (no FK, no\n validation), so `assertVendor()` checks the vendor is visible in and owned\n by the expense's tenant whenever it is set or changed\n (`EXPENSE_VENDOR_MISMATCH`).\n- **Tenant never moves.** Expense and receipt tenants are fixed once\n stored, compared as `effectiveTenant()` (the field, or the active context\n the `beforeSave` interceptor would fill an unset one from; unset is falsy,\n so `''` counts). `normalizeTenantId()` stores `''` as NULL (global), never\n as a pseudo-tenant, matching core/tenancy's falsy \"unset\". Otherwise a\n global (NULL-tenant) row saved inside `withTenant()` would be silently\n populated and split from its global expense or receipts.\n- **Tenancy.** Optional (`@TenantScoped({ mode: 'optional' })`, nullable\n `tenantId`), like smrt-commerce. A receipt takes its expense's tenant; its\n expense and asset must be visible in that tenant. A commitment must belong\n to the expense's tenant; before `super.save()` that is\n `tenantId ?? getTenantId()`, because the interceptor has not populated it.\n Consumers make it required with a qualified\n `registerTenantScopedClass(…, { mode: 'required' })`.\n- **Closed generated surface.** Models AND collections declare\n `api: { include: [] }`, `mcp: { include: [] }`, `cli: false`. Collection\n classes are registered too: an undecorated collection publishes its CRUD and\n public methods as MCP tools. Core has no consumer-side switch to reopen a\n package model's surface; consumers write their own routes.\n- **Cost object is not dereferenced on save.** `getCostObject()` resolves it\n through the registry and returns `null` for a foreign-tenant row. One cost\n object per expense (columns, not a `SmrtPolymorphicAssociation` join) so a\n cost can never roll up into two jobs.\n\n## Cross-package references\n\n- `vendorId` → `@crossPackageRef('@happyvertical/smrt-commerce:Vendor')`\n- `commitmentId` → `…smrt-commerce:Contract` (usually a `PurchaseOrder`)\n- `commitmentLineId` → `…smrt-commerce:ContractLineItem`\n- `assetId` → `…smrt-assets:Asset`\n- profile ids → `…smrt-profiles:Profile` (no dependency; references only)\n- `expenseId`, `duplicateOfId` → `@foreignKey` within the package\n\n## Scope\n\nAllowance (a planned amount) is not an expense kind; it belongs with\nbudget/commitment records. Labour belongs to `@happyvertical/smrt-timesheets`\n(#3288). No OCR, no accounting-provider posting.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-expenses test\npnpm --filter @happyvertical/smrt-expenses typecheck\npnpm --filter @happyvertical/smrt-expenses test:postgres\n```\n\n`src/__tests__/helpers/expense-suite.ts` is the shared behaviour suite: the\nSQLite file and the PostgreSQL lane (`*.optional.test.ts`, schema from\n`migrateSmrtSchemas` plus live parity) both run it. Use UUID tenant and\nprofile ids in tests: PostgreSQL stores them as `uuid`.\n"
789
+ "agentDoc": "# @happyvertical/smrt-expenses\n\nIncurred costs against a cost object, with deduplicated receipts, an explicit\nreview, reimbursement recording, and smrt-commerce commitment drawdown (#3289).\n\n## Models\n\n- **Expense** (`expenses`): `costObjectType` (qualified class name) +\n `costObjectId` (bare string), `category` (lowercase kebab-case; suggestions\n in `SUGGESTED_EXPENSE_CATEGORIES`), `amount` integer minor units + ISO 4217\n `currency`, `incurredOn` (`YYYY-MM-DD` text: a calendar date, not an\n instant), `recordedAt`, `recordedByProfileId`, `paidBy` (`company` |\n `person`) + `paidByProfileId`, `vendorId`, `commitmentId` /\n `commitmentLineId`, review fields, `reimbursable` / `reimbursedAt` /\n `reimbursementReference`.\n- **ExpenseReceipt** (`expense_receipts`): noun-specific asset join\n (`expenseId` → Expense, `assetId` → smrt-assets Asset) with uploader,\n `filename`, `mimeType`, `byteCount`, `contentSha256`. Collection extends\n `SmrtJunction` (left expense, right asset, ordered by `created_at`).\n\n## Invariants\n\n- **Review is method-only.** `review()`, `reject()`, `markDuplicate()`,\n `reopen()` read the persisted status, check the move, and authorize exactly\n one save through a module `WeakMap`. Every transition assigns every review\n field, and the token carries that complete resulting state, so a field set\n directly beside `review()`/`reject()` (e.g. an injected `duplicateOfId`)\n is refused. A `reviewed` expense never carries `duplicateOfId`. `save()` compares every review field\n (status, reviewer, time, note, `duplicateOfId`) with the stored row and\n refuses an unauthorized change (`EXPENSE_REVIEW_FIELDS_LOCKED`), including\n on a brand-new row. Review fields are also `readonly` for generated writes.\n- **Writes are pinned to the guarded row.** The guards read the row stored\n under `id`, but core's natural-key save adopts any same-owner row on\n `(tenant_id, slug, context)` and upserts it with no revision predicate;\n these models' slugs come from the id. So `pinNaturalKey()` lets a fresh\n (not `isPersisted`) instance only INSERT (`requireInsertOnSave()`): naming\n an existing row's id is `EXPENSE_IDENTITY_CONFLICT`, and a row appearing\n mid-save fails the INSERT instead of being adopted. An existing row changes\n only through a loaded instance (revision compare-and-swap) that keeps its\n slug/context. Never `create({ id: <existing> })` or `getOrUpsert()` these\n models; load and save instead.\n- **Reviewed money is frozen.** While the stored status is `reviewed`,\n `LOCKED_WHEN_REVIEWED` fields (money, currency, both dates, cost object,\n vendor, commitment, payer) cannot change; `reopen()` first. `recordedAt` is\n compared as an instant, since drivers return it as a `Date` or a string.\n- **Drawdown counts once.** `ExpenseCollection.commitmentPosition()` sums only\n `reviewed` rows with no `duplicateOfId`, in the commitment's currency, deduped\n by id. Committed is the contract's `totalAmount` (or the line's `amount`).\n- **Currencies never mix.** A commitment match must share the currency and\n tenant (`EXPENSE_COMMITMENT_MISMATCH`), checked when the match, line or\n currency is set or changed — not on every save, so a later commitment edit\n never blocks e.g. `markReimbursed()`. `totalsByCurrency()` keys by currency.\n Rows left in another currency by a later commitment change land in\n `otherCurrencies`, never in `drawn`.\n- **Duplicate receipts.** Same `(expense, sha256)` is refused by a pre-insert\n read plus the unique index `expense_receipts_expense_sha256_key`\n `(expense_id, content_sha256)`; a unique violation is rethrown as\n `DuplicateReceiptError`, classified from the error itself (violated key\n name, or `details.fieldName` columns on core's typed error) because on\n PostgreSQL the violation aborts a caller's transaction and nothing can be\n re-read; only an error naming no key falls back to a re-read. The index omits `tenant_id` deliberately: the\n expense already fixes the tenant, and a nullable leading `tenant_id` would\n leave NULL-tenant (global) receipts unenforced. Same hash on different\n expenses is allowed; `findDuplicateReceipts()` reports it (facet on\n `contentSha256`, count > 1). Receipt `tenantId` (derived from the\n expense on insert), `expenseId`, `assetId` and `contentSha256` are\n immutable. The inherited `SmrtJunction.attach()` and\n `setLinks()` are overridden to throw (`setLinks()` would delete every\n receipt, then fail to re-attach without hashes); `attachReceipt()` adds and\n `detach()` removes a link, keeping the asset. Because `ExpenseReceipt`\n overrides `save()`, `hasBaseJunctionLifecycle()` is false and no batch\n junction path can write a hashless row around it.\n- **Vendor ownership.** `vendorId` is a `crossPackageRef` (no FK, no\n validation), so `assertVendor()` checks the vendor is visible in and owned\n by the expense's tenant whenever it is set or changed\n (`EXPENSE_VENDOR_MISMATCH`).\n- **Tenant never moves.** Expense and receipt tenants are fixed once\n stored, compared as `effectiveTenant()` (the field, or the active context\n the `beforeSave` interceptor would fill an unset one from; unset is falsy,\n so `''` counts). `normalizeTenantId()` stores `''` as NULL (global), never\n as a pseudo-tenant, matching core/tenancy's falsy \"unset\". Otherwise a\n global (NULL-tenant) row saved inside `withTenant()` would be silently\n populated and split from its global expense or receipts.\n- **Tenancy.** Optional (`@TenantScoped({ mode: 'optional' })`, nullable\n `tenantId`), like smrt-commerce. A receipt takes its expense's tenant; its\n expense and asset must be visible in that tenant. A commitment must belong\n to the expense's tenant; before `super.save()` that is\n `tenantId ?? getTenantId()`, because the interceptor has not populated it.\n Consumers make it required with a qualified\n `registerTenantScopedClass(…, { mode: 'required' })`.\n- **Closed generated surface.** Models AND collections declare\n `api: { include: [] }`, `mcp: { include: [] }`, `cli: false`. Collection\n classes are registered too: an undecorated collection publishes its CRUD and\n public methods as MCP tools. Core has no consumer-side switch to reopen a\n package model's surface; consumers write their own routes.\n- **Cost object is not dereferenced on save.** `getCostObject()` resolves it\n through the registry and returns `null` for a foreign-tenant row. One cost\n object per expense (columns, not a `SmrtPolymorphicAssociation` join) so a\n cost can never roll up into two jobs.\n\n## Cross-package references\n\n- `vendorId` → `@crossPackageRef('@happyvertical/smrt-commerce:Vendor')`\n- `commitmentId` → `…smrt-commerce:Contract` (usually a `PurchaseOrder`)\n- `commitmentLineId` → `…smrt-commerce:ContractLineItem`\n- `assetId` → `…smrt-assets:Asset`\n- profile ids → `…smrt-profiles:Profile` (no dependency; references only)\n- `expenseId`, `duplicateOfId` → `@foreignKey` within the package\n\n## Scope\n\nAllowance (a planned amount) is not an expense kind; it belongs with\nbudget/commitment records. Labour belongs to `@happyvertical/smrt-timesheets`\n(#3288). No OCR, no accounting-provider posting.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-expenses test\npnpm --filter @happyvertical/smrt-expenses typecheck\npnpm --filter @happyvertical/smrt-expenses test:postgres\n```\n\n`src/__tests__/helpers/expense-suite.ts` is the shared behaviour suite: the\nSQLite file and the PostgreSQL lane (`*.optional.test.ts`, schema from\n`migrateSmrtSchemas` plus live parity) both run it. Use UUID tenant and\nprofile ids in tests: PostgreSQL stores them as `uuid`.\n\n## UI\n\n`src/svelte/` owns native expense entry and review presentation. See\n[the UI contract](agents/expense-ui.md) for DTOs, native payloads and recovery.\nDraft amounts are lossless currency-unit text; model/review amounts remain integer\nminor units. Receipt upload is composed from Assets, never reimplemented here.\n`test` includes backend and UI SSR; `test:e2e` proves native browser submissions.\n",
790
+ "moduleDocs": [
791
+ {
792
+ "path": "agents/expense-ui.md",
793
+ "module": "expense-ui",
794
+ "content": "# Expense entry and receipt review\n\nImport `ExpenseForm`, `ExpenseReviewPanel` and their named Props types from\n`@happyvertical/smrt-expenses/svelte`. Registry slots are `expense-form` and\n`expense-review`. `@happyvertical/smrt-expenses/ui` exposes metadata without\nSvelte; `./playground` exposes an interactive retained-draft/review example.\n\n`ExpenseDraftValues` is a complete set of native strings: `amount`, `currency`,\n`incurredOn`, `description`, `category`, `vendorId`, `commitmentId`, `paidBy`,\n`paidByProfileId`, and `correctionReason`. Initialize unused fields to `''`.\nAmount is ordinary currency text (`'125.00'` CAD), distinct from the public\nExpense model's integer minor-unit `amount` (`12500`). Server adapters parse\ncurrency precision, safe integer range and business rules. Calendar dates stay\nlossless text, normally `YYYY-MM-DD`; invalid amounts/dates never disappear due\nto browser number/date coercion. Every validation failure returns original\nstrings, not reconstructed saved-model values.\n\n```svelte\n<script lang=\"ts\">\n import { ExpenseForm, ExpenseReviewPanel } from '@happyvertical/smrt-expenses/svelte';\n let { data, form } = $props();\n</script>\n<ExpenseForm\n action=\"?/save\"\n title=\"Record purchase\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n message={form?.message}\n canEdit={data.mayEdit}\n correcting={data.correcting}\n vendors={data.authorizedVendors}\n commitments={data.authorizedCommitments}\n hiddenFields={[\n { name: 'requestId', value: form?.requestId ?? data.requestId },\n { name: 'costObjectId', value: data.costObjectId },\n { name: 'expectedTenantId', value: data.tenantId },\n ]}\n/>\n<ExpenseReviewPanel\n expense={data.expenseSummary}\n duplicates={data.authorizedDuplicateCandidates}\n history={data.history}\n receipts={data.receiptPanel}\n/>\n```\n\nVendor and commitment options are authorized Commerce `{id, label}` projections.\nResolve Profile names and application-specific award lineage in the consumer.\nUnknown retained references remain selectable/postable; selecting a reference\nnever establishes tenant ownership or commitment eligibility. `fields` maps\nlogical keys to native names; `errors` always uses logical keys. Submitter\n`intentField`/`intent` default to `intent=save`. `hiddenFields` preserves exact\nvalues and repeated names. Avoid field-name collisions. `children` adds\napplication-owned package allocation, quantity, reimbursement and correction\npolicy fields. `correcting` shows a retained reason, but does not implement a\nnew Expense correction model. The caller applies its own correction/history\npolicy and invokes existing guarded methods where appropriate.\n\nThe base form performs native POST by default, with or without JavaScript.\n`onsubmit` allows optional enhancement. `pending` or a supplied\n`smrt-ui/form-retry` `retryStatus` controls progress presentation without clearing\nvalues or creating/rotating request keys. `canEdit=false` disables fields and\nhides save; server-side authorization and replay enforcement remain mandatory.\n\nCurrency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n\ncontext. It preserves the exact retained draft string and the field name from\n`fields.currency`, including native no-JavaScript correction flows. Pass\n`currencyOptions` only when the caller must restrict the shared currency catalog.\n\n`ExpenseReviewSummary` projects public Expense description, integer minor-unit\namount, currency and incurredOn, with caller-localized `statusLabel` and `note`.\n`duplicates` and `history` are explicit authorized presentation DTOs, not a\nparallel ledger. The server supplies findings from ExpenseReceipt duplicate\nqueries and application history. Unknown labels render verbatim. No state or\naction is inferred: `canReview`, `action`, `actions` and hidden fingerprints\nexplicitly provide a native review form. `action=\"\"` posts to the current page;\nomitting `action` hides the review form; `children` can supply retained reason\nor duplicate-selection inputs. A server must reauthorize, reload the model and\ninvoke `review()`, `reject()`, `markDuplicate()` or `reopen()` under its policy.\nNever assign review fields through UI payload binding. Reviewed money remains\nfrozen by the existing model; UI props cannot bypass it.\n\n`receipts` is exactly the public Assets `AttachmentPanelProps`, supporting\nprivate view/download URLs, version notes, native upload and recovery. It is\nrendered outside the review form so upload and review remain separate native\nactions. Use authorized receipt projections; attaching a file does not review\nan expense, mark a duplicate, or change invoice approval. Assets owns file\nselection and reselect-file help; Expenses does not duplicate uploads/storage.\n\nValidation: `pnpm --filter @happyvertical/smrt-expenses test`, `test:ui`,\n`test:e2e`, `typecheck`, `build`, and `verify:pack`. PostgreSQL model validation\nremains in the existing opt-in `test:postgres` lane; this UI does not alter it.\n"
795
+ }
796
+ ]
778
797
  }
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import {
3
+ CurrencySelect,
4
+ Form,
5
+ FormGroup,
6
+ Input,
7
+ Select,
8
+ Textarea,
9
+ } from '@happyvertical/smrt-ui/forms';
10
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
11
+ import { Button } from '@happyvertical/smrt-ui/ui';
12
+ import { expenseMessages as M } from '../messages.js';
13
+ import type { ExpenseDraftValues, ExpenseFormProps } from '../types.js';
14
+ /** Native expense entry preserving every caller-owned submitted value. */
15
+ export interface Props extends ExpenseFormProps {}
16
+ let {
17
+ action,
18
+ values,
19
+ id,
20
+ title,
21
+ canEdit = true,
22
+ correcting = false,
23
+ vendors = [],
24
+ commitments = [],
25
+ currencyOptions,
26
+ fields = {},
27
+ errors = {},
28
+ message,
29
+ hiddenFields = [],
30
+ intentField = 'intent',
31
+ intent = 'save',
32
+ submitLabel,
33
+ pending = false,
34
+ retryStatus = 'idle',
35
+ children,
36
+ onsubmit,
37
+ }: Props = $props();
38
+ const i18n = useI18n();
39
+ const { t } = i18n;
40
+ const instanceId = $props.id();
41
+ const formId = $derived(id ?? `expense-form-${instanceId}`);
42
+ const busy = $derived(
43
+ pending || retryStatus === 'submitting' || retryStatus === 'busy',
44
+ );
45
+ const textFields: (keyof ExpenseDraftValues)[] = [
46
+ 'amount',
47
+ 'currency',
48
+ 'incurredOn',
49
+ 'category',
50
+ 'paidByProfileId',
51
+ ];
52
+ function name(field: keyof ExpenseDraftValues) {
53
+ return fields[field] ?? field;
54
+ }
55
+ </script>
56
+ <section class="expense-form" aria-labelledby={`${formId}-title`}>
57
+ <h2 id={`${formId}-title`}>{title ?? t(M['expenses.form.title'])}</h2>
58
+ {#if !canEdit}<p>{t(M['expenses.form.readonly'])}</p>{/if}
59
+ <Form id={formId} {action} method="post" preventDefault={false} {onsubmit} aria-busy={busy}>
60
+ <div class="fields">
61
+ {#each hiddenFields as field}<Input type="hidden" name={field.name} value={field.value} interaction={false} />{/each}
62
+ {#if message}<p role="alert">{message}</p>{/if}
63
+ {#if retryStatus === 'transport-error'}<p role="status">{t(M['expenses.form.uncertain'])}</p>{/if}
64
+ <fieldset disabled={!canEdit}>
65
+ {#each textFields as field}
66
+ <FormGroup id={`${formId}-${field}`} label={t(M[`expenses.form.${field}`])} error={errors[field]} hint={field === 'amount' ? t(M['expenses.form.amount_help']) : field === 'incurredOn' ? t(M['expenses.form.date_help']) : undefined}>
67
+ {#if field === 'currency'}
68
+ <CurrencySelect name={name(field)} value={values[field]} options={currencyOptions} locale={i18n.locale} readOnly={!canEdit} />
69
+ {:else}
70
+ <Input name={name(field)} value={values[field]} inputmode={field === 'amount' ? 'decimal' : undefined} />
71
+ {/if}
72
+ </FormGroup>
73
+ {/each}
74
+ <FormGroup id={`${formId}-description`} label={t(M['expenses.form.description'])} error={errors.description}>
75
+ <Textarea name={name('description')} value={values.description} />
76
+ </FormGroup>
77
+ {#each ['vendorId', 'commitmentId'] as reference}
78
+ {@const field = reference as 'vendorId' | 'commitmentId'}
79
+ {@const options = field === 'vendorId' ? vendors : commitments}
80
+ <FormGroup id={`${formId}-${field}`} label={t(M[`expenses.form.${field}`])} error={errors[field]}>
81
+ <Select name={name(field)} value={values[field]}>
82
+ <option value="">{t(M['expenses.form.choose'])}</option>
83
+ {#if values[field] && !options.some(option => option.id === values[field])}<option value={values[field]}>{t(M['expenses.form.retained'], { reference: values[field] })}</option>{/if}
84
+ {#each options as option}<option value={option.id}>{option.label}</option>{/each}
85
+ </Select>
86
+ </FormGroup>
87
+ {/each}
88
+ <FormGroup id={`${formId}-paidBy`} label={t(M['expenses.form.paidBy'])} error={errors.paidBy}>
89
+ <Select name={name('paidBy')} value={values.paidBy}>
90
+ {#if !['company', 'person'].includes(values.paidBy)}<option value={values.paidBy}>{values.paidBy}</option>{/if}
91
+ <option value="company">{t(M['expenses.form.company'])}</option>
92
+ <option value="person">{t(M['expenses.form.person'])}</option>
93
+ </Select>
94
+ </FormGroup>
95
+ {#if correcting}
96
+ <FormGroup id={`${formId}-correctionReason`} label={t(M['expenses.form.correctionReason'])} error={errors.correctionReason}>
97
+ <Textarea name={name('correctionReason')} value={values.correctionReason} />
98
+ </FormGroup>
99
+ {:else}<Input type="hidden" name={name('correctionReason')} value={values.correctionReason} interaction={false} />{/if}
100
+ {@render children?.()}
101
+ </fieldset>
102
+ {#if canEdit}<div><Button type="submit" name={intentField} value={intent} disabled={busy}>{submitLabel ?? t(M[correcting ? 'expenses.form.correct' : 'expenses.form.save'])}</Button></div>{/if}
103
+ </div>
104
+ </Form>
105
+ </section>
106
+ <style>
107
+ .expense-form, .fields { display: grid; gap: var(--smrt-spacing-4); min-width: 0; }
108
+ .expense-form { max-width: 48rem; overflow-wrap: anywhere; }
109
+ fieldset { min-width: 0; border: 0; padding: 0; margin: 0; }
110
+ h2, p { margin: 0; }
111
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { ExpenseFormProps } from '../types.js';
2
+ /** Native expense entry preserving every caller-owned submitted value. */
3
+ export interface Props extends ExpenseFormProps {
4
+ }
5
+ declare const ExpenseForm: import("svelte").Component<Props, {}, "">;
6
+ type ExpenseForm = ReturnType<typeof ExpenseForm>;
7
+ export default ExpenseForm;
8
+ //# sourceMappingURL=ExpenseForm.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpenseForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ExpenseForm.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAExE,0EAA0E;AAC1E,MAAM,WAAW,KAAM,SAAQ,gBAAgB;CAAG;AAsGlD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { AttachmentPanel } from '@happyvertical/smrt-assets/svelte';
3
+ import { CurrencyDisplay } from '@happyvertical/smrt-ui';
4
+ import { Form, Input } from '@happyvertical/smrt-ui/forms';
5
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
6
+ import { Button } from '@happyvertical/smrt-ui/ui';
7
+ import { expenseMessages as M } from '../messages.js';
8
+ import type { ExpenseReviewPanelProps } from '../types.js';
9
+ /** Server-authoritative expense review, duplicates/history and receipt composition. */
10
+ export interface Props extends ExpenseReviewPanelProps {}
11
+ let {
12
+ expense,
13
+ duplicates = [],
14
+ history = [],
15
+ receipts,
16
+ canReview = false,
17
+ action,
18
+ actions = [],
19
+ intentField = 'intent',
20
+ hiddenFields = [],
21
+ message,
22
+ pending = false,
23
+ children,
24
+ onsubmit,
25
+ }: Props = $props();
26
+ const { t } = useI18n();
27
+ </script>
28
+ <section class="expense-review" aria-label={t(M['expenses.review.title'])}>
29
+ <h3>{expense.description}</h3>
30
+ <p><CurrencyDisplay amount={expense.amount} currency={expense.currency} unit="cents" /> · {expense.incurredOn}</p>
31
+ <p aria-live="polite">{expense.statusLabel}</p>
32
+ {#if expense.note}<p>{expense.note}</p>{/if}
33
+ {#if message}<p role="alert">{message}</p>{/if}
34
+ <section aria-label={t(M['expenses.review.duplicates'])}>
35
+ <h4>{t(M['expenses.review.duplicates'])}</h4>
36
+ {#if duplicates.length === 0}<p>{t(M['expenses.review.no_duplicates'])}</p>{/if}
37
+ <ul>{#each duplicates as duplicate (duplicate.id)}<li><strong>{duplicate.label}</strong>{#if duplicate.reason}<p>{duplicate.reason}</p>{/if}</li>{/each}</ul>
38
+ </section>
39
+ <section aria-label={t(M['expenses.review.history'])}>
40
+ <h4>{t(M['expenses.review.history'])}</h4>
41
+ {#if history.length === 0}<p>{t(M['expenses.review.no_history'])}</p>{/if}
42
+ <ol>{#each history as entry (entry.id)}<li><strong>{entry.label}</strong>{#if entry.detail}<p>{entry.detail}</p>{/if}</li>{/each}</ol>
43
+ </section>
44
+ {#if canReview && action !== undefined && actions.length}
45
+ <Form {action} method="post" preventDefault={false} {onsubmit} aria-busy={pending}>
46
+ {#each hiddenFields as field}<Input type="hidden" name={field.name} value={field.value} interaction={false} />{/each}
47
+ {@render children?.()}
48
+ <div class="actions">{#each actions as choice}<Button type="submit" name={intentField} value={choice.intent} disabled={pending || choice.disabled}>{choice.label}</Button>{/each}</div>
49
+ </Form>
50
+ {/if}
51
+ {#if receipts}<AttachmentPanel {...receipts} />{/if}
52
+ </section>
53
+ <style>
54
+ .expense-review { display: grid; gap: var(--smrt-spacing-4); min-width: 0; overflow-wrap: anywhere; }
55
+ .actions { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-3); }
56
+ h3, h4, p { margin: 0; }
57
+ ul, ol { display: grid; gap: var(--smrt-spacing-2); padding-inline-start: var(--smrt-spacing-5); }
58
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { ExpenseReviewPanelProps } from '../types.js';
2
+ /** Server-authoritative expense review, duplicates/history and receipt composition. */
3
+ export interface Props extends ExpenseReviewPanelProps {
4
+ }
5
+ declare const ExpenseReviewPanel: import("svelte").Component<Props, {}, "">;
6
+ type ExpenseReviewPanel = ReturnType<typeof ExpenseReviewPanel>;
7
+ export default ExpenseReviewPanel;
8
+ //# sourceMappingURL=ExpenseReviewPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpenseReviewPanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ExpenseReviewPanel.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAE3D,uFAAuF;AACvF,MAAM,WAAW,KAAM,SAAQ,uBAAuB;CAAG;AAwDzD,QAAA,MAAM,kBAAkB,2CAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import ExpenseForm from './components/ExpenseForm.svelte';
2
+ import ExpenseReviewPanel from './components/ExpenseReviewPanel.svelte';
3
+ export type { ExpenseDraftValues, ExpenseDuplicateCandidate, ExpenseFormProps, ExpenseHistoryEntry, ExpenseReferenceOption, ExpenseRequestField, ExpenseReviewAction, ExpenseReviewPanelProps, ExpenseReviewSummary, } from './types.js';
4
+ export { ExpenseForm, ExpenseReviewPanel };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAGA,OAAO,WAAW,MAAM,iCAAiC,CAAC;AAC1D,OAAO,kBAAkB,MAAM,wCAAwC,CAAC;AAExE,YAAY,EACV,kBAAkB,EAClB,yBAAyB,EACzB,gBAAgB,EAChB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,9 @@
1
+ /** Package-owned Expenses components and native presentation contracts. */
2
+ import { ModuleUIRegistry } from '@happyvertical/smrt-ui/registry';
3
+ import { EXPENSES_MODULE_META } from '../ui.js';
4
+ import ExpenseForm from './components/ExpenseForm.svelte';
5
+ import ExpenseReviewPanel from './components/ExpenseReviewPanel.svelte';
6
+ export { ExpenseForm, ExpenseReviewPanel };
7
+ ModuleUIRegistry.registerModule(EXPENSES_MODULE_META);
8
+ ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-form', ExpenseForm);
9
+ ModuleUIRegistry.register('@happyvertical/smrt-expenses', 'expense-review', ExpenseReviewPanel);
@@ -0,0 +1,29 @@
1
+ export declare const expenseMessages: {
2
+ readonly 'expenses.form.title': "expenses.form.title";
3
+ readonly 'expenses.form.amount': "expenses.form.amount";
4
+ readonly 'expenses.form.currency': "expenses.form.currency";
5
+ readonly 'expenses.form.incurredOn': "expenses.form.incurredOn";
6
+ readonly 'expenses.form.description': "expenses.form.description";
7
+ readonly 'expenses.form.category': "expenses.form.category";
8
+ readonly 'expenses.form.vendorId': "expenses.form.vendorId";
9
+ readonly 'expenses.form.commitmentId': "expenses.form.commitmentId";
10
+ readonly 'expenses.form.paidBy': "expenses.form.paidBy";
11
+ readonly 'expenses.form.paidByProfileId': "expenses.form.paidByProfileId";
12
+ readonly 'expenses.form.correctionReason': "expenses.form.correctionReason";
13
+ readonly 'expenses.form.company': "expenses.form.company";
14
+ readonly 'expenses.form.person': "expenses.form.person";
15
+ readonly 'expenses.form.choose': "expenses.form.choose";
16
+ readonly 'expenses.form.retained': "expenses.form.retained";
17
+ readonly 'expenses.form.date_help': "expenses.form.date_help";
18
+ readonly 'expenses.form.amount_help': "expenses.form.amount_help";
19
+ readonly 'expenses.form.save': "expenses.form.save";
20
+ readonly 'expenses.form.correct': "expenses.form.correct";
21
+ readonly 'expenses.form.readonly': "expenses.form.readonly";
22
+ readonly 'expenses.form.uncertain': "expenses.form.uncertain";
23
+ readonly 'expenses.review.title': "expenses.review.title";
24
+ readonly 'expenses.review.duplicates': "expenses.review.duplicates";
25
+ readonly 'expenses.review.no_duplicates': "expenses.review.no_duplicates";
26
+ readonly 'expenses.review.history': "expenses.review.history";
27
+ readonly 'expenses.review.no_history': "expenses.review.no_history";
28
+ };
29
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/svelte/messages.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6B1B,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { defineMessages } from '@happyvertical/smrt-ui/i18n';
2
+ export const expenseMessages = defineMessages({
3
+ 'expenses.form.title': 'Record expense',
4
+ 'expenses.form.amount': 'Amount',
5
+ 'expenses.form.currency': 'Currency',
6
+ 'expenses.form.incurredOn': 'Incurred date',
7
+ 'expenses.form.description': 'Description',
8
+ 'expenses.form.category': 'Category',
9
+ 'expenses.form.vendorId': 'Vendor',
10
+ 'expenses.form.commitmentId': 'Commitment',
11
+ 'expenses.form.paidBy': 'Paid by',
12
+ 'expenses.form.paidByProfileId': 'Payer profile reference',
13
+ 'expenses.form.correctionReason': 'Correction reason',
14
+ 'expenses.form.company': 'Company',
15
+ 'expenses.form.person': 'Person',
16
+ 'expenses.form.choose': 'No reference selected',
17
+ 'expenses.form.retained': 'Retained reference: {reference}',
18
+ 'expenses.form.date_help': 'Calendar date (YYYY-MM-DD).',
19
+ 'expenses.form.amount_help': 'Enter ordinary currency amounts, for example 125.00 CAD. The server validates currency precision.',
20
+ 'expenses.form.save': 'Save expense',
21
+ 'expenses.form.correct': 'Save correction',
22
+ 'expenses.form.readonly': 'This expense is read-only.',
23
+ 'expenses.form.uncertain': 'The outcome is uncertain. Your entries and request identity are retained; follow the application’s retry guidance.',
24
+ 'expenses.review.title': 'Expense review',
25
+ 'expenses.review.duplicates': 'Possible duplicates',
26
+ 'expenses.review.no_duplicates': 'No possible duplicates supplied.',
27
+ 'expenses.review.history': 'Correction and review history',
28
+ 'expenses.review.no_history': 'No history supplied.',
29
+ });
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { Checkbox } from '@happyvertical/smrt-ui/forms';
3
+ import ExpenseForm from '../components/ExpenseForm.svelte';
4
+ import ExpenseReviewPanel from '../components/ExpenseReviewPanel.svelte';
5
+ import type { ExpenseDraftValues } from '../types.js';
6
+
7
+ let canEdit = $state(true);
8
+ let correcting = $state(false);
9
+ let message = $state('');
10
+ let values = $state<ExpenseDraftValues>({
11
+ amount: '125.00',
12
+ currency: 'CAD',
13
+ incurredOn: '2026-10-03',
14
+ description: 'Supplier materials',
15
+ category: 'materials',
16
+ vendorId: 'vendor-a',
17
+ commitmentId: '',
18
+ paidBy: 'company',
19
+ paidByProfileId: '',
20
+ correctionReason: '',
21
+ });
22
+ function submit(event: SubmitEvent & { currentTarget: HTMLFormElement }) {
23
+ event.preventDefault();
24
+ const data = new FormData(event.currentTarget, event.submitter);
25
+ values = Object.fromEntries(
26
+ Object.keys(values).map((key) => [key, String(data.get(key) ?? '')]),
27
+ ) as unknown as ExpenseDraftValues;
28
+ message =
29
+ 'Example server rejection: a receipt is required by this application. Your entries and request token are retained.';
30
+ }
31
+ </script>
32
+ <div class="preview">
33
+ <Checkbox label="May edit expense" bind:checked={canEdit} />
34
+ <Checkbox label="Correction workflow" bind:checked={correcting} />
35
+ <ExpenseForm action="" {values} {canEdit} {correcting} {message} vendors={[{ id: 'vendor-a', label: 'Supplier A' }]} hiddenFields={[{ name: 'requestId', value: 'demo-expense-request' }]} onsubmit={submit} />
36
+ <ExpenseReviewPanel expense={{ description: 'Supplier materials', amount: 12500, currency: 'CAD', incurredOn: '2026-10-03', statusLabel: 'Unreviewed' }} duplicates={[{ id: 'expense-other', label: 'Possible matching expense', reason: 'The server found matching receipt evidence. A reviewer must decide.' }]} history={[{ id: 'correction', label: 'Description corrected', detail: 'Caller-provided correction history' }]} receipts={{ attachments: [{ id: 'receipt', name: 'Receipt.pdf', mimeType: 'application/pdf' }] }} />
37
+ </div>
38
+ <style>.preview { display: grid; gap: var(--smrt-spacing-4); }</style>
@@ -0,0 +1,4 @@
1
+ declare const ExpenseWorkflowPreview: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ExpenseWorkflowPreview = ReturnType<typeof ExpenseWorkflowPreview>;
3
+ export default ExpenseWorkflowPreview;
4
+ //# sourceMappingURL=ExpenseWorkflowPreview.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpenseWorkflowPreview.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/playground/ExpenseWorkflowPreview.svelte.ts"],"names":[],"mappings":"AAiDA,QAAA,MAAM,sBAAsB,2DAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,21 @@
1
+ declare const _default: {
2
+ packageName: string;
3
+ displayName: string;
4
+ description: string;
5
+ moduleMeta: import("@happyvertical/smrt-types").SmrtModuleMeta;
6
+ entries: {
7
+ id: string;
8
+ title: string;
9
+ description: string;
10
+ loadComponent: () => Promise<typeof import("./playground/ExpenseWorkflowPreview.svelte")>;
11
+ order: number;
12
+ props: {};
13
+ modes: {
14
+ mock: {
15
+ label: string;
16
+ };
17
+ };
18
+ }[];
19
+ };
20
+ export default _default;
21
+ //# sourceMappingURL=playground.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../src/svelte/playground.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AACA,wBAiBE"}
@@ -0,0 +1,18 @@
1
+ import { EXPENSES_MODULE_META } from '../ui.js';
2
+ export default {
3
+ packageName: '@happyvertical/smrt-expenses',
4
+ displayName: 'Expenses',
5
+ description: 'Purchase entry, receipts and duplicate review',
6
+ moduleMeta: EXPENSES_MODULE_META,
7
+ entries: [
8
+ {
9
+ id: 'expense-workflow',
10
+ title: 'Expense Workflow',
11
+ description: 'Native purchase entry with rejected-value retention and receipt review.',
12
+ loadComponent: () => import('./playground/ExpenseWorkflowPreview.svelte'),
13
+ order: 1,
14
+ props: {},
15
+ modes: { mock: { label: 'Mock' } },
16
+ },
17
+ ],
18
+ };
@@ -0,0 +1,150 @@
1
+ import type { AttachmentPanelProps } from '@happyvertical/smrt-assets/svelte';
2
+ import type { FormRetryStatus } from '@happyvertical/smrt-ui/form-retry';
3
+ import type { CodeSelectOption } from '@happyvertical/smrt-ui/forms';
4
+ import type { Snippet } from 'svelte';
5
+ import type { HTMLFormAttributes } from 'svelte/elements';
6
+ import type { Expense } from '../models/Expense.js';
7
+ /** Lossless native form strings, separate from Expense's integer model amount. */
8
+ export interface ExpenseDraftValues {
9
+ /** Ordinary currency-unit text, e.g. '125.00' CAD; parsed server-side. */
10
+ amount: string;
11
+ /** Currency as submitted, even if invalid; server validates. */
12
+ currency: string;
13
+ /** Calendar-date text, normally YYYY-MM-DD; invalid strings remain visible. */
14
+ incurredOn: string;
15
+ /** Caller-entered cost description. */
16
+ description: string;
17
+ /** Caller-entered category; server validates its vocabulary. */
18
+ category: string;
19
+ /** Commerce Vendor reference, not an identity merge. */
20
+ vendorId: string;
21
+ /** Commerce Contract commitment reference. */
22
+ commitmentId: string;
23
+ /** Payer text, normally company or person; unknown submitted values retained. */
24
+ paidBy: string;
25
+ /** Profile reference for a person payer. */
26
+ paidByProfileId: string;
27
+ /** Caller-owned correction reason; not a new Expense persistence property. */
28
+ correctionReason: string;
29
+ }
30
+ /** Authorized Commerce choice projection. Labels remain caller-owned. */
31
+ export interface ExpenseReferenceOption {
32
+ /** Opaque Vendor/Contract reference. */
33
+ id: string;
34
+ /** Caller-localized label. */
35
+ label: string;
36
+ }
37
+ /** Request identity/context forwarded as native hidden fields. */
38
+ export interface ExpenseRequestField {
39
+ /** Native field name; repetitions are preserved. */
40
+ name: string;
41
+ /** Exact caller value; never generated or rotated here. */
42
+ value: string;
43
+ }
44
+ /** Native purchase/expense entry. Caller endpoints enforce financial policy. */
45
+ export interface ExpenseFormProps {
46
+ /** Authorized native POST destination. */
47
+ action: string;
48
+ /** Retained draft strings from load or the failed action response. */
49
+ values: ExpenseDraftValues;
50
+ /** Optional stable form identity for enhancement. */
51
+ id?: string;
52
+ /** Caller-localized heading, including Purchase terminology. */
53
+ title?: string;
54
+ /** Capability controls presentation; server must authorize every mutation. */
55
+ canEdit?: boolean;
56
+ /** Whether caller correction workflow requires the reason field. */
57
+ correcting?: boolean;
58
+ /** Authorized Commerce Vendor options. */
59
+ vendors?: readonly ExpenseReferenceOption[];
60
+ /** Authorized Commerce Contract options. */
61
+ commitments?: readonly ExpenseReferenceOption[];
62
+ /** Caller-restricted currency choices; omitted uses the shared ISO defaults. */
63
+ currencyOptions?: readonly CodeSelectOption[];
64
+ /** Optional native field-name adapter; defaults to draft property names. */
65
+ fields?: Partial<Record<keyof ExpenseDraftValues, string>>;
66
+ /** Server errors keyed by logical draft property. */
67
+ errors?: Partial<Record<keyof ExpenseDraftValues, string>>;
68
+ /** Caller error/denial/uncertain-outcome message. */
69
+ message?: string;
70
+ /** Caller-owned request identity, cost object, predecessor and context. */
71
+ hiddenFields?: readonly ExpenseRequestField[];
72
+ /** Native submitter name, default intent. */
73
+ intentField?: string;
74
+ /** Native submitter value, default save. */
75
+ intent?: string;
76
+ /** Caller-localized submit label. */
77
+ submitLabel?: string;
78
+ /** Request in flight. Values stay editable, another submit is disabled. */
79
+ pending?: boolean;
80
+ /** Optional status from caller-owned smrt-ui/form-retry. */
81
+ retryStatus?: FormRetryStatus;
82
+ /** Domain-specific fields (allocation, quantity, reimbursement, etc.). */
83
+ children?: Snippet;
84
+ /** Optional enhancement; native POST remains the default. */
85
+ onsubmit?: HTMLFormAttributes['onsubmit'];
86
+ }
87
+ /** Display projection of the public Expense; amount remains integer minor units. */
88
+ export interface ExpenseReviewSummary extends Pick<Expense, 'description' | 'amount' | 'currency' | 'incurredOn'> {
89
+ /** Caller-localized review status, including unknown future states. */
90
+ statusLabel: string;
91
+ /** Caller policy/review explanation. */
92
+ note?: string;
93
+ }
94
+ /** Possible duplicate supplied by an authorized server query. */
95
+ export interface ExpenseDuplicateCandidate {
96
+ /** Opaque expense reference; no linking or merging happens automatically. */
97
+ id: string;
98
+ /** Caller-localized duplicate description. */
99
+ label: string;
100
+ /** Caller-localized reason, such as a matching receipt. */
101
+ reason?: string;
102
+ }
103
+ /** An authorized correction or review history entry supplied in display order. */
104
+ export interface ExpenseHistoryEntry {
105
+ /** Unique history identity. */
106
+ id: string;
107
+ /** Caller-localized decision/correction label. */
108
+ label: string;
109
+ /** Caller-localized time/actor metadata. */
110
+ detail?: string;
111
+ }
112
+ /** Caller-defined review action; visibility never replaces server authorization. */
113
+ export interface ExpenseReviewAction {
114
+ /** Native clicked submitter value. */
115
+ intent: string;
116
+ /** Caller-localized action label. */
117
+ label: string;
118
+ /** Presentation disable flag for this action. */
119
+ disabled?: boolean;
120
+ }
121
+ /** Reviewed-state and duplicate/correction presentation with Assets composition. */
122
+ export interface ExpenseReviewPanelProps {
123
+ /** Authorized model projection; UI does not infer status or mutate it. */
124
+ expense: ExpenseReviewSummary;
125
+ /** Supplied duplicate candidates; no deduplication policy runs here. */
126
+ duplicates?: readonly ExpenseDuplicateCandidate[];
127
+ /** Supplied correction/review history. */
128
+ history?: readonly ExpenseHistoryEntry[];
129
+ /** Assets public attachment presentation/upload contract. */
130
+ receipts?: AttachmentPanelProps;
131
+ /** Explicitly enable caller-supplied review actions. */
132
+ canReview?: boolean;
133
+ /** Authorized review endpoint; empty posts to this page, undefined hides actions. */
134
+ action?: string;
135
+ /** Caller-defined review actions; no status-derived actions. */
136
+ actions?: readonly ExpenseReviewAction[];
137
+ /** Native submitter name, default intent. */
138
+ intentField?: string;
139
+ /** Caller request/tenant/fingerprint context. */
140
+ hiddenFields?: readonly ExpenseRequestField[];
141
+ /** Caller error or denied/uncertain outcome. */
142
+ message?: string;
143
+ /** Caller request pending state. */
144
+ pending?: boolean;
145
+ /** Retained review reason/duplicate selector and other caller-specific fields. */
146
+ children?: Snippet;
147
+ /** Optional review-form enhancement. */
148
+ onsubmit?: HTMLFormAttributes['onsubmit'];
149
+ }
150
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/svelte/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD,kFAAkF;AAClF,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,eAAe,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,kEAAkE;AAClE,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,EAAE,kBAAkB,CAAC;IAC3B,qDAAqD;IACrD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC5C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAChD,gFAAgF;IAChF,eAAe,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC9C,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,qDAAqD;IACrD,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,kBAAkB,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC9C,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;CAC3C;AAED,oFAAoF;AACpF,MAAM,WAAW,oBACf,SAAQ,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC;IAC3E,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACxC,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,kFAAkF;AAClF,MAAM,WAAW,mBAAmB;IAClC,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,oFAAoF;AACpF,MAAM,WAAW,mBAAmB;IAClC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,oFAAoF;AACpF,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,OAAO,EAAE,oBAAoB,CAAC;IAC9B,wEAAwE;IACxE,UAAU,CAAC,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAClD,0CAA0C;IAC1C,OAAO,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,OAAO,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACzC,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,YAAY,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC9C,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;CAC3C"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/ui.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { ModuleUISlot, SmrtModuleMeta } from '@happyvertical/smrt-types';
2
+ /** Discoverable presentation slots owned by Expenses. */
3
+ export declare const EXPENSES_UI_SLOTS: Record<string, ModuleUISlot>;
4
+ /** Module metadata consumed without importing Svelte components. */
5
+ export declare const EXPENSES_MODULE_META: SmrtModuleMeta;
6
+ //# sourceMappingURL=ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAE9E,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAmB1D,CAAC;AACF,oEAAoE;AACpE,eAAO,MAAM,oBAAoB,EAAE,cAOlC,CAAC"}
package/dist/ui.js ADDED
@@ -0,0 +1,33 @@
1
+ //#region src/ui.ts
2
+ var EXPENSES_UI_SLOTS = {
3
+ "expense-form": {
4
+ id: "expense-form",
5
+ label: "Expense Entry",
6
+ description: "Native retained-value purchase and expense entry",
7
+ icon: "file-text",
8
+ category: "form",
9
+ order: 1,
10
+ propsInterface: "ExpenseFormProps"
11
+ },
12
+ "expense-review": {
13
+ id: "expense-review",
14
+ label: "Expense Review",
15
+ description: "Duplicate/correction review and private receipt composition",
16
+ icon: "file-text",
17
+ category: "detail",
18
+ order: 2,
19
+ propsInterface: "ExpenseReviewPanelProps"
20
+ }
21
+ };
22
+ var EXPENSES_MODULE_META = {
23
+ name: "@happyvertical/smrt-expenses",
24
+ displayName: "Expenses",
25
+ description: "Expense entry, receipt and duplicate review",
26
+ uiSlots: EXPENSES_UI_SLOTS,
27
+ models: ["Expense", "ExpenseReceipt"],
28
+ collections: ["ExpenseCollection", "ExpenseReceiptCollection"]
29
+ };
30
+ //#endregion
31
+ export { EXPENSES_MODULE_META, EXPENSES_UI_SLOTS };
32
+
33
+ //# sourceMappingURL=ui.js.map
package/dist/ui.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui.js","names":[],"sources":["../src/ui.ts"],"sourcesContent":["import type { ModuleUISlot, SmrtModuleMeta } from '@happyvertical/smrt-types';\n\n/** Discoverable presentation slots owned by Expenses. */\nexport const EXPENSES_UI_SLOTS: Record<string, ModuleUISlot> = {\n 'expense-form': {\n id: 'expense-form',\n label: 'Expense Entry',\n description: 'Native retained-value purchase and expense entry',\n icon: 'file-text',\n category: 'form',\n order: 1,\n propsInterface: 'ExpenseFormProps',\n },\n 'expense-review': {\n id: 'expense-review',\n label: 'Expense Review',\n description: 'Duplicate/correction review and private receipt composition',\n icon: 'file-text',\n category: 'detail',\n order: 2,\n propsInterface: 'ExpenseReviewPanelProps',\n },\n};\n/** Module metadata consumed without importing Svelte components. */\nexport const EXPENSES_MODULE_META: SmrtModuleMeta = {\n name: '@happyvertical/smrt-expenses',\n displayName: 'Expenses',\n description: 'Expense entry, receipt and duplicate review',\n uiSlots: EXPENSES_UI_SLOTS,\n models: ['Expense', 'ExpenseReceipt'],\n collections: ['ExpenseCollection', 'ExpenseReceiptCollection'],\n};\n"],"mappings":";AAGO,IAAM,oBAAkD;CAC7D,gBAAgB;EACd,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;CACA,kBAAkB;EAChB,IAAI;EACJ,OAAO;EACP,aAAa;EACb,MAAM;EACN,UAAU;EACV,OAAO;EACP,gBAAgB;CAClB;AACF;AAEO,IAAM,uBAAuC;CAClD,MAAM;CACN,aAAa;CACb,aAAa;CACb,SAAS;CACT,QAAQ,CAAC,WAAW,gBAAgB;CACpC,aAAa,CAAC,qBAAqB,0BAA0B;AAC/D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-expenses",
3
- "version": "0.53.3",
3
+ "version": "0.53.5",
4
4
  "description": "Expense records for the SMRT framework - reviewed costs against a cost object, deduplicated receipts, and commitment drawdown",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,7 +8,8 @@
8
8
  "files": [
9
9
  "dist",
10
10
  "CLAUDE.md",
11
- "AGENTS.md"
11
+ "AGENTS.md",
12
+ "agents"
12
13
  ],
13
14
  "exports": {
14
15
  ".": {
@@ -16,20 +17,39 @@
16
17
  "import": "./dist/index.js"
17
18
  },
18
19
  "./manifest": "./dist/manifest.json",
19
- "./manifest.json": "./dist/manifest.json"
20
+ "./manifest.json": "./dist/manifest.json",
21
+ "./ui": {
22
+ "types": "./dist/ui.d.ts",
23
+ "import": "./dist/ui.js"
24
+ },
25
+ "./svelte": {
26
+ "types": "./dist/svelte/index.d.ts",
27
+ "svelte": "./dist/svelte/index.js",
28
+ "import": "./dist/svelte/index.js"
29
+ },
30
+ "./playground": {
31
+ "types": "./dist/playground.d.ts",
32
+ "import": "./dist/playground.js"
33
+ }
20
34
  },
21
35
  "dependencies": {
22
- "@happyvertical/smrt-assets": "0.53.3",
23
- "@happyvertical/smrt-commerce": "0.53.3",
24
- "@happyvertical/smrt-core": "0.53.3",
25
- "@happyvertical/smrt-tenancy": "0.53.3"
36
+ "@happyvertical/smrt-assets": "0.53.5",
37
+ "@happyvertical/smrt-commerce": "0.53.5",
38
+ "@happyvertical/smrt-core": "0.53.5",
39
+ "@happyvertical/smrt-tenancy": "0.53.5",
40
+ "@happyvertical/smrt-types": "0.53.5",
41
+ "@happyvertical/smrt-ui": "0.53.5"
26
42
  },
27
43
  "devDependencies": {
28
- "@happyvertical/smrt-vitest": "0.53.3",
44
+ "@happyvertical/smrt-vitest": "0.53.5",
29
45
  "@types/node": "24.13.6",
30
46
  "typescript": "6.0.3",
31
47
  "vite": "8.1.4",
32
- "vitest": "4.1.10"
48
+ "vitest": "4.1.10",
49
+ "@sveltejs/package": "^2.5.8",
50
+ "@sveltejs/vite-plugin-svelte": "^7.3.0",
51
+ "svelte": "^5.57.1",
52
+ "svelte-check": "^4.7.6"
33
53
  },
34
54
  "keywords": [
35
55
  "smrt",
@@ -49,15 +69,27 @@
49
69
  "url": "https://github.com/happyvertical/smrt.git",
50
70
  "directory": "packages/expenses"
51
71
  },
72
+ "smrtJsdoc": "strict",
73
+ "smrtRawPrimitives": "strict",
74
+ "peerDependencies": {
75
+ "svelte": "^5.57.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "svelte": {
79
+ "optional": true
80
+ }
81
+ },
52
82
  "scripts": {
53
- "build": "vite build --mode library",
83
+ "build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json && node ../../scripts/prune-svelte-package-artifacts.js dist",
54
84
  "build:watch": "vite build --mode library --watch",
55
85
  "clean": "rm -rf dist",
56
86
  "dev": "vite build --mode library --watch",
57
- "test": "vitest run",
87
+ "test": "vitest run && pnpm run test:ui",
58
88
  "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run optional.test.ts",
59
89
  "test:watch": "vitest",
60
- "typecheck": "tsc --noEmit -p tsconfig.json",
61
- "verify:pack": "node ../../scripts/verify-package-types-exports.js ."
90
+ "typecheck": "tsc --noEmit -p tsconfig.json && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",
91
+ "verify:pack": "node ../../scripts/verify-package-types-exports.js .",
92
+ "test:ui": "vitest run --config vitest.ui.config.ts",
93
+ "test:e2e": "playwright test -c e2e/playwright.config.ts"
62
94
  }
63
95
  }