@happyvertical/smrt-expenses 0.55.23 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/collections/ExpenseCollection.ts","../src/models/ExpenseReceipt.ts","../src/collections/ExpenseReceiptCollection.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-expenses\\\",\\\"packageVersion\\\":\\\"0.55.23\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-expenses:ExpenseCollection\\\":{\\\"name\\\":\\\"expensecollection\\\",\\\"className\\\":\\\"ExpenseCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-expenses:ExpenseCollection\\\",\\\"collection\\\":\\\"expenses\\\",\\\"description\\\":\\\"Collection for Expense. Reads go through the normal tenant interceptors, so inside withTenant() every method here sees only that tenant's 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 \\\\\\\"financial_breakdown\\\\\\\" TEXT,\\\\n \\\\\\\"paid_by_name\\\\\\\" TEXT,\\\\n \\\\\\\"refund_of_id\\\\\\\" UUID,\\\\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},\\\"financial_breakdown\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"paid_by_name\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"refund_of_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"foreignKey\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"foreignKey\\\":{\\\"table\\\":\\\"expenses\\\",\\\"column\\\":\\\"id\\\",\\\"onDelete\\\":\\\"RESTRICT\\\",\\\"onUpdate\\\":\\\"CASCADE\\\"}},\\\"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_refund_of_id_idx\\\",\\\"columns\\\":[\\\"refund_of_id\\\"]},{\\\"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\\\":\\\"287668fa\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-expenses\\\"},\\\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\\\":{\\\"name\\\":\\\"expensereceiptcollection\\\",\\\"className\\\":\\\"ExpenseReceiptCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\\\",\\\"collection\\\":\\\"expensereceipts\\\",\\\"description\\\":\\\"Junction collection over expense_receipts (left: expense, right: asset). Reads go through the normal tenant interceptors.\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/expenses/src/collections/ExpenseReceiptCollection.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-expenses\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"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},\\\"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},\\\"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\\\",\\\"description\\\":\\\"A cost someone incurred.\\\",\\\"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\\\":{}},\\\"financialBreakdown\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{}},\\\"paidByName\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{}},\\\"refundOfId\\\":{\\\"type\\\":\\\"foreignKey\\\",\\\"required\\\":false,\\\"related\\\":\\\"Expense\\\",\\\"_meta\\\":{\\\"onDelete\\\":\\\"RESTRICT\\\"}},\\\"incurredOn\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{}},\\\"recordedAt\\\":{\\\"type\\\":\\\"datetime\\\",\\\"required\\\":false},\\\"recordedByProfileId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":false,\\\"related\\\":\\\"PROFILE\\\"},\\\"paidBy\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{},\\\"enum\\\":[\\\"company\\\",\\\"person\\\",\\\"third-party\\\"]},\\\"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},\\\"enum\\\":[\\\"unreviewed\\\",\\\"reviewed\\\",\\\"rejected\\\"],\\\"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\\\":{\\\"getFinancialBreakdown\\\":{\\\"name\\\":\\\"getFinancialBreakdown\\\",\\\"async\\\":false,\\\"parameters\\\":[],\\\"returnType\\\":\\\"ExpenseFinancialBreakdown | null\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"setFinancialBreakdown\\\":{\\\"name\\\":\\\"setFinancialBreakdown\\\",\\\"async\\\":false,\\\"parameters\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"ExpenseFinancialBreakdown | null\\\",\\\"optional\\\":false,\\\"unionBranches\\\":[{\\\"type\\\":\\\"ExpenseFinancialBreakdown\\\"},{\\\"type\\\":\\\"null\\\"}]}],\\\"returnType\\\":\\\"void\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"getCostBasis\\\":{\\\"name\\\":\\\"getCostBasis\\\",\\\"async\\\":false,\\\"parameters\\\":[],\\\"returnType\\\":\\\"ExpenseCostBasis | null\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"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 \\\\\\\"financial_breakdown\\\\\\\" TEXT,\\\\n \\\\\\\"paid_by_name\\\\\\\" TEXT,\\\\n \\\\\\\"refund_of_id\\\\\\\" UUID,\\\\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},\\\"financial_breakdown\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"paid_by_name\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"refund_of_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"foreignKey\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"foreignKey\\\":{\\\"table\\\":\\\"expenses\\\",\\\"column\\\":\\\"id\\\",\\\"onDelete\\\":\\\"RESTRICT\\\",\\\"onUpdate\\\":\\\"CASCADE\\\"}},\\\"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_refund_of_id_idx\\\",\\\"columns\\\":[\\\"refund_of_id\\\"]},{\\\"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\\\":\\\"287668fa\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-expenses\\\"},\\\"@happyvertical/smrt-expenses:ExpenseReceipt\\\":{\\\"name\\\":\\\"expensereceipt\\\",\\\"className\\\":\\\"ExpenseReceipt\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-expenses:ExpenseReceipt\\\",\\\"collection\\\":\\\"expensereceipts\\\",\\\"description\\\":\\\"A receipt file attached to an expense.\\\",\\\"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\\\"],\\\"browser\\\":{\\\"status\\\":\\\"server-only\\\",\\\"issues\\\":[\\\"#3615\\\",\\\"#3624\\\",\\\"#3626\\\",\\\"#3627\\\"],\\\"reason\\\":\\\"node:crypto\\\",\\\"via\\\":[\\\"@happyvertical/smrt-commerce\\\",\\\"@happyvertical/smrt-jobs\\\",\\\"@happyvertical/smrt-ledgers\\\",\\\"@happyvertical/smrt-subscriptions\\\",\\\"@happyvertical/smrt-tenancy\\\"]},\\\"stampsConstructors\\\":true}\"));\n","/**\n * ExpenseCollection — queries, per-currency totals, and commitment drawdown.\n *\n * @packageDocumentation\n */\n\nimport {\n ContractCollection,\n ContractLineItemCollection,\n} from '@happyvertical/smrt-commerce';\nimport { SmrtCollection, smrt } from '@happyvertical/smrt-core';\nimport { Expense } from '../models/Expense.js';\nimport {\n type CommitmentPosition,\n type CurrencyTotals,\n ExpenseError,\n type ExpenseReviewStatus,\n type ExpenseTotalsOptions,\n} from '../types.js';\n\n/** Plain projection read for sums: never hydrates objects. */\ninterface AmountRow {\n id: string;\n amount: number | string;\n currency: string;\n reviewStatus: ExpenseReviewStatus;\n duplicateOfId: string | null;\n}\n\nconst AMOUNT_FIELDS = [\n 'id',\n 'amount',\n 'currency',\n 'reviewStatus',\n 'duplicateOfId',\n] as const;\n\nfunction toMinorUnits(value: number | string): number {\n const amount = typeof value === 'number' ? value : Number(value);\n if (!Number.isSafeInteger(amount)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Stored expense amount ${String(value)} is not a safe integer.`,\n );\n }\n return amount;\n}\n\nfunction addTo(totals: CurrencyTotals, currency: string, amount: number) {\n const entry = totals[currency] ?? { amount: 0, count: 0 };\n const sum = entry.amount + amount;\n if (!Number.isSafeInteger(sum)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Total for ${currency} exceeds the safe integer range.`,\n );\n }\n totals[currency] = { amount: sum, count: entry.count + 1 };\n}\n\n/**\n * Collection for {@link Expense}. Reads go through the normal tenant\n * interceptors, so inside `withTenant()` every method here sees only that\n * tenant's expenses.\n *\n * Collection classes are registered too, so this one carries its own closed\n * surface; without it every public method here would become an MCP tool.\n */\n@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })\nexport class ExpenseCollection extends SmrtCollection<Expense> { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n static readonly _itemClass = Expense;\n\n /**\n * Expenses charged to one cost object, oldest incurred first.\n *\n * @param costObjectType - Qualified class name of the cost object.\n * @param costObjectId - Id of the cost object.\n */\n async forCostObject(\n costObjectType: string,\n costObjectId: string,\n ): Promise<Expense[]> {\n return this.list({\n where: { costObjectType, costObjectId },\n orderBy: ['incurred_on ASC', 'created_at ASC'],\n });\n }\n\n /**\n * Sum expenses per currency. Amounts in different currencies are never\n * added together: the result is keyed by ISO 4217 code.\n *\n * By default only `reviewed` expenses count; a duplicate never counts.\n *\n * @example\n * ```typescript\n * const totals = await expenses.totalsByCurrency({\n * where: { costObjectType: JOB, costObjectId: job.id },\n * });\n * // { USD: { amount: 125000, count: 3 }, CAD: { amount: 4000, count: 1 } }\n * ```\n */\n async totalsByCurrency(\n options: ExpenseTotalsOptions = {},\n ): Promise<CurrencyTotals> {\n const statuses = options.reviewStatuses ?? ['reviewed'];\n if (statuses.length === 0) return {};\n const rows = (await this.list({\n where: {\n ...(options.where ?? {}),\n reviewStatus: [...statuses],\n duplicateOfId: null,\n },\n select: [...AMOUNT_FIELDS],\n })) as unknown as AmountRow[];\n\n const totals: CurrencyTotals = {};\n const seen = new Set<string>();\n for (const row of rows) {\n if (seen.has(row.id)) continue;\n seen.add(row.id);\n addTo(totals, row.currency, toMinorUnits(row.amount));\n }\n return totals;\n }\n\n /**\n * What remains open on a commitment — a commerce `Contract`, normally a\n * `PurchaseOrder` — or on one of its lines.\n *\n * `committed` is the contract's `totalAmount` (or the line's `amount`);\n * `drawn` sums the matched actuals that are `reviewed` and not marked\n * duplicate, in the commitment's currency, each expense once. Unreviewed,\n * rejected and duplicate matches are counted in `excluded` and never\n * drawn. A 4,600 commitment with 1,000 reviewed leaves 3,600 open.\n *\n * @param commitmentId - The commitment's contract id.\n * @param options.lineId - Restrict to one line of the commitment.\n * @throws {ExpenseError} `EXPENSE_COMMITMENT_MISMATCH` when the commitment\n * (or line) is not visible in the current tenant.\n */\n async commitmentPosition(\n commitmentId: string,\n options: { lineId?: string } = {},\n ): Promise<CommitmentPosition> {\n const contracts = await ContractCollection.create({ db: this.db });\n const commitment = await contracts.get({ id: commitmentId });\n if (!commitment) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Commitment ${commitmentId} was not found.`,\n );\n }\n let committed = toMinorUnits(commitment.totalAmount);\n if (options.lineId) {\n const lines = await ContractLineItemCollection.create({ db: this.db });\n const line = await lines.get({ id: options.lineId });\n if (!line || line.contractId !== commitmentId) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Line ${options.lineId} is not a line of commitment ${commitmentId}.`,\n );\n }\n committed = toMinorUnits(line.amount);\n }\n const currency = String(commitment.currency).toUpperCase();\n\n const rows = (await this.list({\n where: {\n commitmentId,\n ...(options.lineId ? { commitmentLineId: options.lineId } : {}),\n },\n select: [...AMOUNT_FIELDS],\n })) as unknown as AmountRow[];\n\n const position: CommitmentPosition = {\n commitmentId,\n commitmentLineId: options.lineId ?? null,\n currency,\n committed,\n drawn: 0,\n open: 0,\n overrun: 0,\n countedExpenseIds: [],\n excluded: { unreviewed: 0, rejected: 0, duplicate: 0 },\n otherCurrencies: {},\n };\n const seen = new Set<string>();\n for (const row of rows) {\n if (seen.has(row.id)) continue;\n seen.add(row.id);\n if (row.duplicateOfId) {\n position.excluded.duplicate += 1;\n } else if (row.reviewStatus === 'unreviewed') {\n position.excluded.unreviewed += 1;\n } else if (row.reviewStatus === 'rejected') {\n position.excluded.rejected += 1;\n } else if (row.currency !== currency) {\n addTo(position.otherCurrencies, row.currency, toMinorUnits(row.amount));\n } else {\n const drawn = position.drawn + toMinorUnits(row.amount);\n if (!Number.isSafeInteger(drawn)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Drawdown on commitment ${commitmentId} exceeds the safe range.`,\n );\n }\n position.drawn = drawn;\n position.countedExpenseIds.push(row.id);\n }\n }\n position.countedExpenseIds.sort();\n position.open = Math.max(committed - position.drawn, 0);\n position.overrun = Math.max(position.drawn - committed, 0);\n return position;\n }\n}\n","/**\n * ExpenseReceipt — the expense-owned join between an {@link Expense} and the\n * smrt-assets `Asset` that proves it.\n *\n * @packageDocumentation\n */\n\nimport { AssetCollection } from '@happyvertical/smrt-assets';\nimport {\n classifyDatabaseError,\n crossPackageRef,\n field,\n foreignKey,\n isUniqueViolationError,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n DuplicateReceiptError,\n ExpenseError,\n type ExpenseReceiptOptions,\n} from '../types.js';\nimport {\n assertMinorUnits,\n effectiveTenant,\n identityConflict,\n normalizeSha256,\n normalizeTenantId,\n pinNaturalKey,\n} from '../validation.js';\nimport { Expense } from './Expense.js';\n\n/**\n * The unique index that refuses a second copy of a file on one expense. Kept\n * in step with the literal in the `@smrt()` indexes below (the scanner reads\n * that literal); the PostgreSQL schema test asserts the name.\n */\nconst RECEIPT_FILE_KEY = 'expense_receipts_expense_sha256_key';\n\n/** The columns of {@link RECEIPT_FILE_KEY}; no other unique key uses them. */\nconst RECEIPT_FILE_COLUMNS = new Set(['expense_id', 'content_sha256']);\n\n/**\n * The columns a unique violation reports. Core's typed write error carries\n * them in `details.fieldName` (recovered from PostgreSQL's\n * `Key (expense_id, content_sha256)=…` detail, or SQLite's column list) but\n * not the driver error itself, so this is where the violated key is named.\n */\nfunction violatedColumns(error: unknown): string[] {\n const details = (error as { details?: { fieldName?: unknown } } | null)\n ?.details;\n const fieldName =\n typeof details?.fieldName === 'string' ? details.fieldName : '';\n return fieldName\n .split(',')\n .map((column) => column.trim())\n .filter(Boolean);\n}\n\n/**\n * Columns a receipt keeps for life: it is evidence, not a draft. The tenant\n * is among them because it is derived from the expense on insert; letting a\n * later save (e.g. one without tenant context) change it would split the\n * receipt from its expense's tenant.\n */\nconst IMMUTABLE = [\n ['tenantId', 'tenant_id'],\n ['expenseId', 'expense_id'],\n ['assetId', 'asset_id'],\n ['contentSha256', 'content_sha256'],\n] as const;\n\n/**\n * A receipt file attached to an expense.\n *\n * Asset ownership uses a noun-specific join (`expense_receipts`), not the\n * generic `AssetAssociation`. The file's SHA-256 is stored so:\n *\n * - the same file attached twice to one expense is refused — a unique index\n * on `(expense_id, content_sha256)` plus a pre-insert check that raises\n * {@link DuplicateReceiptError}. The index leaves `tenant_id` out on\n * purpose: an expense belongs to exactly one tenant, so the key is already\n * tenant-scoped, and a NULL tenant would otherwise make every global row\n * distinct and the index unenforced;\n * - the same file on two different expenses is allowed, and surfaced for a\n * person to look at by `ExpenseReceiptCollection.findDuplicateReceipts()`.\n *\n * Attaching a receipt never changes the expense's review state. The receipt\n * takes its tenant from the expense, and its asset must be visible in that\n * tenant. The generated surface is closed, like {@link Expense}'s.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'expense_receipts',\n api: { include: [] },\n mcp: { include: [] },\n cli: false,\n indexes: [\n {\n name: 'expense_receipts_expense_sha256_key',\n columns: ['expenseId', 'contentSha256'],\n unique: true,\n },\n {\n name: 'expense_receipts_tenant_sha256_idx',\n columns: ['tenantId', 'contentSha256'],\n },\n ],\n})\nexport class ExpenseReceipt extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n /** Owning tenant; always the expense's tenant. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The expense this receipt proves. Deleting the expense removes the link. */\n @foreignKey(Expense, { required: true, onDelete: 'CASCADE' })\n expenseId: string = '';\n\n /** The smrt-assets `Asset` holding the file. */\n @crossPackageRef('@happyvertical/smrt-assets:Asset', { required: true })\n assetId: string = '';\n\n /** Profile that uploaded the file. */\n @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n uploadedByProfileId: string | null = null;\n\n /** Original filename as uploaded. */\n @field({ type: 'text' })\n filename: string = '';\n\n /** MIME type of the file. */\n @field({ type: 'text' })\n mimeType: string = '';\n\n /** Size of the file in bytes. */\n @field({ type: 'integer' })\n byteCount: number = 0;\n\n /** Lowercase hex SHA-256 of the file's bytes. */\n @field({ type: 'text', required: true })\n contentSha256: string = '';\n\n constructor(options: ExpenseReceiptOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.expenseId !== undefined) this.expenseId = options.expenseId;\n if (options.assetId !== undefined) this.assetId = options.assetId;\n if (options.uploadedByProfileId !== undefined)\n this.uploadedByProfileId = options.uploadedByProfileId;\n if (options.filename !== undefined) this.filename = options.filename;\n if (options.mimeType !== undefined) this.mimeType = options.mimeType;\n if (options.byteCount !== undefined) this.byteCount = options.byteCount;\n if (options.contentSha256 !== undefined)\n this.contentSha256 = options.contentSha256;\n }\n\n /**\n * Validate the link, refuse a second copy of the same file on the same\n * expense, then persist. Never touches the expense.\n *\n * @throws {DuplicateReceiptError} when the expense already has this file.\n * @throws {ExpenseError} `EXPENSE_RECEIPT_INVALID` for a bad hash, a\n * missing or foreign-tenant expense or asset, or a change to an\n * immutable column.\n */\n override async save(): Promise<this> {\n const label = this.id || '<new>';\n this.tenantId = normalizeTenantId(this.tenantId);\n this.contentSha256 = normalizeSha256(\n 'ExpenseReceipt',\n label,\n this.contentSha256,\n );\n assertMinorUnits(\n 'ExpenseReceipt',\n label,\n 'byteCount',\n this.byteCount,\n 'EXPENSE_RECEIPT_INVALID',\n );\n if (!this.expenseId || !this.assetId) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: expenseId and assetId are required.`,\n );\n }\n\n const persisted = this.id\n ? (((await this.db.get(this.tableName, { id: this.id })) as\n | Record<string, unknown>\n | undefined\n | null) ?? null)\n : null;\n // The checks below are against `persisted`; pin the write to that row.\n const inserting = await pinNaturalKey('ExpenseReceipt', this, persisted);\n if (persisted) {\n const self = this as unknown as Record<string, unknown>;\n for (const [fieldName, column] of IMMUTABLE) {\n // The tenant is compared as it will be written: an empty tenantId\n // saved inside a tenant context is filled in by the interceptor.\n const value =\n fieldName === 'tenantId'\n ? effectiveTenant(this.tenantId)\n : self[fieldName];\n if (String(persisted[column] ?? '') !== String(value ?? '')) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${this.id}: ${fieldName} cannot change; attach ` +\n 'a new receipt instead.',\n );\n }\n }\n } else {\n await this.assertLinkTargets(label);\n await this.assertNotDuplicate();\n }\n\n try {\n return await super.save();\n } catch (error) {\n if (inserting && isUniqueViolationError(error)) {\n throw await this.classifyInsertConflict(error);\n }\n throw error;\n }\n }\n\n /**\n * A refused INSERT is either the same file racing onto the same expense,\n * or a natural-key collision with another receipt.\n *\n * Classified from the error first, by the violated key's name or columns\n * (no other unique key on this table uses `expense_id` or\n * `content_sha256`). That matters inside a caller's transaction,\n * which the violation has already aborted on PostgreSQL, so nothing could\n * be re-read there. Only an adapter that reports neither falls back to a\n * re-read; if that re-read fails too, the original error is kept rather\n * than guessed at.\n */\n private async classifyInsertConflict(error: unknown): Promise<unknown> {\n const info = classifyDatabaseError(error);\n const columns = violatedColumns(error);\n if (\n info.constraint === RECEIPT_FILE_KEY ||\n info.driverMessages.some((text) => text.includes(RECEIPT_FILE_KEY)) ||\n columns.some((column) => RECEIPT_FILE_COLUMNS.has(column))\n ) {\n return new DuplicateReceiptError(this.expenseId, this.contentSha256, {\n cause: error,\n });\n }\n if (info.constraint || columns.length > 0) {\n // Another named key: the natural key or the primary key.\n return identityConflict('ExpenseReceipt', this, error);\n }\n try {\n await this.assertNotDuplicate();\n } catch (duplicate) {\n if (duplicate instanceof DuplicateReceiptError) {\n return new DuplicateReceiptError(this.expenseId, this.contentSha256, {\n cause: error,\n });\n }\n return error;\n }\n return identityConflict('ExpenseReceipt', this, error);\n }\n\n private async assertLinkTargets(label: string): Promise<void> {\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const expense = await expenses.get({ id: this.expenseId });\n if (!expense) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: expense ${this.expenseId} was not found.`,\n );\n }\n if (this.tenantId === null || this.tenantId === undefined) {\n this.tenantId = expense.tenantId ?? null;\n }\n if ((this.tenantId ?? null) !== (expense.tenantId ?? null)) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: a receipt belongs to its expense's tenant.`,\n );\n }\n\n const assets = await AssetCollection.create({ db: this.db });\n const asset = await assets.get({ id: this.assetId });\n const assetTenant = asset?.tenantId ?? null;\n if (!asset || (assetTenant !== null && assetTenant !== this.tenantId)) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: asset ${this.assetId} was not found in ` +\n \"the expense's tenant.\",\n );\n }\n if (!this.filename) this.filename = asset.name ?? '';\n if (!this.mimeType) this.mimeType = asset.mimeType ?? '';\n }\n\n private async assertNotDuplicate(): Promise<void> {\n const { ExpenseReceiptCollection } = await import(\n '../collections/ExpenseReceiptCollection.js'\n );\n const receipts = await ExpenseReceiptCollection.create({ db: this.db });\n const existing = await receipts.list({\n where: {\n expenseId: this.expenseId,\n contentSha256: this.contentSha256,\n },\n limit: 1,\n });\n if (existing.length > 0) {\n throw new DuplicateReceiptError(this.expenseId, this.contentSha256);\n }\n }\n}\n","/**\n * ExpenseReceiptCollection — attach receipts and find files shared between\n * expenses.\n *\n * @packageDocumentation\n */\n\nimport {\n type JunctionAttachOptions,\n SmrtJunction,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { ExpenseReceipt } from '../models/ExpenseReceipt.js';\nimport {\n type AttachReceiptInput,\n type DuplicateReceiptGroup,\n ExpenseError,\n type FindDuplicateReceiptsOptions,\n} from '../types.js';\n\nconst DEFAULT_DUPLICATE_LIMIT = 100;\nconst MAX_DUPLICATE_LIMIT = 1000;\n\n/**\n * Junction collection over `expense_receipts` (left: expense, right: asset).\n * Reads go through the normal tenant interceptors.\n *\n * Writes: {@link attachReceipt} adds a receipt; the inherited `detach()`\n * removes one (an explicit act; the asset itself is kept). The inherited\n * `attach()` and `setLinks()` are refused because they cannot carry the\n * content hash and `setLinks()` deletes before it writes.\n *\n * Collection classes are registered too, so this one carries its own closed\n * surface; without it every public method here would become an MCP tool.\n */\n@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })\nexport class ExpenseReceiptCollection extends SmrtJunction<ExpenseReceipt> { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n static readonly _itemClass = ExpenseReceipt;\n protected leftField = 'expenseId';\n protected rightField = 'assetId';\n protected override sortField: string | null = 'createdAt';\n protected override positionField: string | null = null;\n\n /**\n * Attach a receipt file to an expense.\n *\n * Does not change the expense in any way — in particular not its review\n * state.\n *\n * @throws {DuplicateReceiptError} when this expense already carries a file\n * with the same SHA-256.\n */\n async attachReceipt(input: AttachReceiptInput): Promise<ExpenseReceipt> {\n const receipt = await this.create({\n expenseId: input.expenseId,\n assetId: input.assetId,\n contentSha256: input.contentSha256,\n uploadedByProfileId: input.uploadedByProfileId ?? null,\n filename: input.filename ?? '',\n mimeType: input.mimeType ?? '',\n byteCount: input.byteCount ?? 0,\n });\n await receipt.save();\n return receipt;\n }\n\n /**\n * Refused: a receipt is evidence and needs its content hash, which a\n * generic junction attach does not carry. Use {@link attachReceipt}.\n *\n * @throws {ExpenseError} `EXPENSE_RECEIPT_INVALID`, always.\n */\n override async attach(\n _expenseId: string,\n _assetId: string,\n _opts: JunctionAttachOptions = {},\n ): Promise<ExpenseReceipt> {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n 'ExpenseReceiptCollection.attach() is not supported: use ' +\n 'attachReceipt(), which records and checks the file hash.',\n );\n }\n\n /**\n * Refused before anything is deleted. The inherited replace-all deletes\n * every receipt of the expense and re-attaches without hashes, which would\n * lose the evidence. Attach with {@link attachReceipt}; remove a link with\n * `detach()`.\n *\n * @throws {ExpenseError} `EXPENSE_RECEIPT_INVALID`, always.\n */\n override async setLinks(\n _expenseId: string,\n _assetIds: string[],\n _opts: JunctionAttachOptions = {},\n ): Promise<void> {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n 'ExpenseReceiptCollection.setLinks() is not supported: it would delete ' +\n 'receipt evidence. Use attachReceipt() to add and detach() to remove.',\n );\n }\n\n /** Receipts of one expense, oldest first. */\n async receiptsFor(expenseId: string): Promise<ExpenseReceipt[]> {\n return this.byLeft(expenseId);\n }\n\n /**\n * Files (by SHA-256) attached to more than one expense, for a person to\n * review. Nothing is changed or rejected: the same receipt on two expenses\n * may be a split cost or a double claim, and only a reviewer can tell.\n *\n * With `expenseId`, reports only files that expense shares with others;\n * without it, scans the visible tenant (largest groups first).\n */\n async findDuplicateReceipts(\n options: FindDuplicateReceiptsOptions = {},\n ): Promise<DuplicateReceiptGroup[]> {\n const limit = Math.min(\n Math.max(Math.trunc(options.limit ?? DEFAULT_DUPLICATE_LIMIT), 1),\n MAX_DUPLICATE_LIMIT,\n );\n\n let hashes: string[];\n if (options.expenseId) {\n const own = await this.byLeft(options.expenseId);\n hashes = [...new Set(own.map((receipt) => receipt.contentSha256))];\n } else {\n const [facet] = await this.facets({\n fields: [{ field: 'contentSha256', limit: MAX_DUPLICATE_LIMIT }],\n });\n // One row per (expense, hash) is enforced, so count > 1 means the\n // hash sits on more than one expense.\n // Already only shared hashes (largest groups first), so limiting the\n // hashes here limits the groups.\n hashes = (facet?.values ?? [])\n .filter((value) => value.count > 1)\n .map((value) => String(value.value))\n .slice(0, limit);\n }\n // The expense's own hashes include its unique receipts, so the limit is\n // applied to the groups below, after unshared hashes are dropped.\n if (hashes.length === 0) return [];\n\n const rows = await this.list({\n where: { contentSha256: hashes },\n orderBy: ['content_sha256 ASC', 'created_at ASC'],\n });\n const byHash = new Map<string, ExpenseReceipt[]>();\n for (const receipt of rows) {\n const group = byHash.get(receipt.contentSha256) ?? [];\n group.push(receipt);\n byHash.set(receipt.contentSha256, group);\n }\n\n const groups: DuplicateReceiptGroup[] = [];\n for (const [contentSha256, receipts] of byHash) {\n const expenseIds = [\n ...new Set(receipts.map((receipt) => receipt.expenseId)),\n ].sort();\n if (expenseIds.length < 2) continue;\n groups.push({ contentSha256, expenseIds, receipts });\n }\n groups.sort(\n (a, b) =>\n b.expenseIds.length - a.expenseIds.length ||\n a.contentSha256.localeCompare(b.contentSha256),\n );\n return groups.slice(0, limit);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/collections/ExpenseCollection.ts","../src/models/ExpenseReceipt.ts","../src/collections/ExpenseReceiptCollection.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-expenses\\\",\\\"packageVersion\\\":\\\"0.56.0\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-expenses:ExpenseCollection\\\":{\\\"name\\\":\\\"expensecollection\\\",\\\"className\\\":\\\"ExpenseCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-expenses:ExpenseCollection\\\",\\\"collection\\\":\\\"expenses\\\",\\\"description\\\":\\\"Collection for Expense. Reads go through the normal tenant interceptors, so inside withTenant() every method here sees only that tenant's 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 \\\\\\\"financial_breakdown\\\\\\\" TEXT,\\\\n \\\\\\\"paid_by_name\\\\\\\" TEXT,\\\\n \\\\\\\"refund_of_id\\\\\\\" UUID,\\\\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},\\\"financial_breakdown\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"paid_by_name\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"refund_of_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"foreignKey\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"foreignKey\\\":{\\\"table\\\":\\\"expenses\\\",\\\"column\\\":\\\"id\\\",\\\"onDelete\\\":\\\"RESTRICT\\\",\\\"onUpdate\\\":\\\"CASCADE\\\"}},\\\"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_refund_of_id_idx\\\",\\\"columns\\\":[\\\"refund_of_id\\\"]},{\\\"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\\\":\\\"287668fa\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-expenses\\\"},\\\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\\\":{\\\"name\\\":\\\"expensereceiptcollection\\\",\\\"className\\\":\\\"ExpenseReceiptCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-expenses:ExpenseReceiptCollection\\\",\\\"collection\\\":\\\"expensereceipts\\\",\\\"description\\\":\\\"Junction collection over expense_receipts (left: expense, right: asset). Reads go through the normal tenant interceptors.\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/expenses/src/collections/ExpenseReceiptCollection.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-expenses\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"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},\\\"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},\\\"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\\\",\\\"description\\\":\\\"A cost someone incurred.\\\",\\\"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\\\":{}},\\\"financialBreakdown\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{}},\\\"paidByName\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{}},\\\"refundOfId\\\":{\\\"type\\\":\\\"foreignKey\\\",\\\"required\\\":false,\\\"related\\\":\\\"Expense\\\",\\\"_meta\\\":{\\\"onDelete\\\":\\\"RESTRICT\\\"}},\\\"incurredOn\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{}},\\\"recordedAt\\\":{\\\"type\\\":\\\"datetime\\\",\\\"required\\\":false},\\\"recordedByProfileId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":false,\\\"related\\\":\\\"PROFILE\\\"},\\\"paidBy\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{},\\\"enum\\\":[\\\"company\\\",\\\"person\\\",\\\"third-party\\\"]},\\\"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:PurchaseContract\\\",\\\"_meta\\\":{\\\"validateSubtype\\\":true}},\\\"commitmentLineId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":false,\\\"related\\\":\\\"@happyvertical/smrt-commerce:ContractLineItem\\\"},\\\"reviewStatus\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"readonly\\\":true},\\\"enum\\\":[\\\"unreviewed\\\",\\\"reviewed\\\",\\\"rejected\\\"],\\\"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\\\":{\\\"getFinancialBreakdown\\\":{\\\"name\\\":\\\"getFinancialBreakdown\\\",\\\"async\\\":false,\\\"parameters\\\":[],\\\"returnType\\\":\\\"ExpenseFinancialBreakdown | null\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"setFinancialBreakdown\\\":{\\\"name\\\":\\\"setFinancialBreakdown\\\",\\\"async\\\":false,\\\"parameters\\\":[{\\\"name\\\":\\\"value\\\",\\\"type\\\":\\\"ExpenseFinancialBreakdown | null\\\",\\\"optional\\\":false,\\\"unionBranches\\\":[{\\\"type\\\":\\\"ExpenseFinancialBreakdown\\\"},{\\\"type\\\":\\\"null\\\"}]}],\\\"returnType\\\":\\\"void\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"getCostBasis\\\":{\\\"name\\\":\\\"getCostBasis\\\",\\\"async\\\":false,\\\"parameters\\\":[],\\\"returnType\\\":\\\"ExpenseCostBasis | null\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"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 \\\\\\\"financial_breakdown\\\\\\\" TEXT,\\\\n \\\\\\\"paid_by_name\\\\\\\" TEXT,\\\\n \\\\\\\"refund_of_id\\\\\\\" UUID,\\\\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},\\\"financial_breakdown\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"paid_by_name\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"refund_of_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"foreignKey\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"foreignKey\\\":{\\\"table\\\":\\\"expenses\\\",\\\"column\\\":\\\"id\\\",\\\"onDelete\\\":\\\"RESTRICT\\\",\\\"onUpdate\\\":\\\"CASCADE\\\"}},\\\"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_refund_of_id_idx\\\",\\\"columns\\\":[\\\"refund_of_id\\\"]},{\\\"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\\\":\\\"287668fa\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-expenses\\\"},\\\"@happyvertical/smrt-expenses:ExpenseReceipt\\\":{\\\"name\\\":\\\"expensereceipt\\\",\\\"className\\\":\\\"ExpenseReceipt\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-expenses:ExpenseReceipt\\\",\\\"collection\\\":\\\"expensereceipts\\\",\\\"description\\\":\\\"A receipt file attached to an expense.\\\",\\\"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\\\"],\\\"browser\\\":{\\\"status\\\":\\\"server-only\\\",\\\"issues\\\":[\\\"#3615\\\",\\\"#3624\\\",\\\"#3626\\\",\\\"#3627\\\"],\\\"reason\\\":\\\"node:crypto\\\",\\\"via\\\":[\\\"@happyvertical/smrt-commerce\\\",\\\"@happyvertical/smrt-jobs\\\",\\\"@happyvertical/smrt-ledgers\\\",\\\"@happyvertical/smrt-subscriptions\\\",\\\"@happyvertical/smrt-tenancy\\\"]},\\\"stampsConstructors\\\":true}\"));\n","/**\n * ExpenseCollection — queries, per-currency totals, and commitment drawdown.\n *\n * @packageDocumentation\n */\n\nimport {\n ContractLineItemCollection,\n PurchaseContractCollection,\n} from '@happyvertical/smrt-commerce';\nimport { SmrtCollection, smrt } from '@happyvertical/smrt-core';\nimport { Expense } from '../models/Expense.js';\nimport {\n type CommitmentPosition,\n type CurrencyTotals,\n ExpenseError,\n type ExpenseReviewStatus,\n type ExpenseTotalsOptions,\n} from '../types.js';\n\n/** Plain projection read for sums: never hydrates objects. */\ninterface AmountRow {\n id: string;\n amount: number | string;\n currency: string;\n reviewStatus: ExpenseReviewStatus;\n duplicateOfId: string | null;\n}\n\nconst AMOUNT_FIELDS = [\n 'id',\n 'amount',\n 'currency',\n 'reviewStatus',\n 'duplicateOfId',\n] as const;\n\nfunction toMinorUnits(value: number | string): number {\n const amount = typeof value === 'number' ? value : Number(value);\n if (!Number.isSafeInteger(amount)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Stored expense amount ${String(value)} is not a safe integer.`,\n );\n }\n return amount;\n}\n\nfunction addTo(totals: CurrencyTotals, currency: string, amount: number) {\n const entry = totals[currency] ?? { amount: 0, count: 0 };\n const sum = entry.amount + amount;\n if (!Number.isSafeInteger(sum)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Total for ${currency} exceeds the safe integer range.`,\n );\n }\n totals[currency] = { amount: sum, count: entry.count + 1 };\n}\n\n/**\n * Collection for {@link Expense}. Reads go through the normal tenant\n * interceptors, so inside `withTenant()` every method here sees only that\n * tenant's expenses.\n *\n * Collection classes are registered too, so this one carries its own closed\n * surface; without it every public method here would become an MCP tool.\n */\n@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })\nexport class ExpenseCollection extends SmrtCollection<Expense> { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n static readonly _itemClass = Expense;\n\n /**\n * Expenses charged to one cost object, oldest incurred first.\n *\n * @param costObjectType - Qualified class name of the cost object.\n * @param costObjectId - Id of the cost object.\n */\n async forCostObject(\n costObjectType: string,\n costObjectId: string,\n ): Promise<Expense[]> {\n return this.list({\n where: { costObjectType, costObjectId },\n orderBy: ['incurred_on ASC', 'created_at ASC'],\n });\n }\n\n /**\n * Sum expenses per currency. Amounts in different currencies are never\n * added together: the result is keyed by ISO 4217 code.\n *\n * By default only `reviewed` expenses count; a duplicate never counts.\n *\n * @example\n * ```typescript\n * const totals = await expenses.totalsByCurrency({\n * where: { costObjectType: JOB, costObjectId: job.id },\n * });\n * // { USD: { amount: 125000, count: 3 }, CAD: { amount: 4000, count: 1 } }\n * ```\n */\n async totalsByCurrency(\n options: ExpenseTotalsOptions = {},\n ): Promise<CurrencyTotals> {\n const statuses = options.reviewStatuses ?? ['reviewed'];\n if (statuses.length === 0) return {};\n const rows = (await this.list({\n where: {\n ...(options.where ?? {}),\n reviewStatus: [...statuses],\n duplicateOfId: null,\n },\n select: [...AMOUNT_FIELDS],\n })) as unknown as AmountRow[];\n\n const totals: CurrencyTotals = {};\n const seen = new Set<string>();\n for (const row of rows) {\n if (seen.has(row.id)) continue;\n seen.add(row.id);\n addTo(totals, row.currency, toMinorUnits(row.amount));\n }\n return totals;\n }\n\n /**\n * What remains open on a commitment — a commerce `Contract`, normally a\n * `PurchaseOrder` — or on one of its lines.\n *\n * `committed` is the contract's `totalAmount` (or the line's `amount`);\n * `drawn` sums the matched actuals that are `reviewed` and not marked\n * duplicate, in the commitment's currency, each expense once. Unreviewed,\n * rejected and duplicate matches are counted in `excluded` and never\n * drawn. A 4,600 commitment with 1,000 reviewed leaves 3,600 open.\n *\n * @param commitmentId - The commitment's contract id.\n * @param options.lineId - Restrict to one line of the commitment.\n * @throws {ExpenseError} `EXPENSE_COMMITMENT_MISMATCH` when the commitment\n * (or line) is not visible in the current tenant.\n */\n async commitmentPosition(\n commitmentId: string,\n options: { lineId?: string } = {},\n ): Promise<CommitmentPosition> {\n const contracts = await PurchaseContractCollection.create({ db: this.db });\n const commitment = await contracts.get({ id: commitmentId });\n if (!commitment) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Commitment ${commitmentId} was not found.`,\n );\n }\n let committed = toMinorUnits(commitment.totalAmount);\n if (options.lineId) {\n const lines = await ContractLineItemCollection.create({ db: this.db });\n const line = await lines.get({ id: options.lineId });\n if (!line || line.contractId !== commitmentId) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Line ${options.lineId} is not a line of commitment ${commitmentId}.`,\n );\n }\n committed = toMinorUnits(line.amount);\n }\n const currency = String(commitment.currency).toUpperCase();\n\n const rows = (await this.list({\n where: {\n commitmentId,\n ...(options.lineId ? { commitmentLineId: options.lineId } : {}),\n },\n select: [...AMOUNT_FIELDS],\n })) as unknown as AmountRow[];\n\n const position: CommitmentPosition = {\n commitmentId,\n commitmentLineId: options.lineId ?? null,\n currency,\n committed,\n drawn: 0,\n open: 0,\n overrun: 0,\n countedExpenseIds: [],\n excluded: { unreviewed: 0, rejected: 0, duplicate: 0 },\n otherCurrencies: {},\n };\n const seen = new Set<string>();\n for (const row of rows) {\n if (seen.has(row.id)) continue;\n seen.add(row.id);\n if (row.duplicateOfId) {\n position.excluded.duplicate += 1;\n } else if (row.reviewStatus === 'unreviewed') {\n position.excluded.unreviewed += 1;\n } else if (row.reviewStatus === 'rejected') {\n position.excluded.rejected += 1;\n } else if (row.currency !== currency) {\n addTo(position.otherCurrencies, row.currency, toMinorUnits(row.amount));\n } else {\n const drawn = position.drawn + toMinorUnits(row.amount);\n if (!Number.isSafeInteger(drawn)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Drawdown on commitment ${commitmentId} exceeds the safe range.`,\n );\n }\n position.drawn = drawn;\n position.countedExpenseIds.push(row.id);\n }\n }\n position.countedExpenseIds.sort();\n position.open = Math.max(committed - position.drawn, 0);\n position.overrun = Math.max(position.drawn - committed, 0);\n return position;\n }\n}\n","/**\n * ExpenseReceipt — the expense-owned join between an {@link Expense} and the\n * smrt-assets `Asset` that proves it.\n *\n * @packageDocumentation\n */\n\nimport { AssetCollection } from '@happyvertical/smrt-assets';\nimport {\n classifyDatabaseError,\n crossPackageRef,\n field,\n foreignKey,\n isUniqueViolationError,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n DuplicateReceiptError,\n ExpenseError,\n type ExpenseReceiptOptions,\n} from '../types.js';\nimport {\n assertMinorUnits,\n effectiveTenant,\n identityConflict,\n normalizeSha256,\n normalizeTenantId,\n pinNaturalKey,\n} from '../validation.js';\nimport { Expense } from './Expense.js';\n\n/**\n * The unique index that refuses a second copy of a file on one expense. Kept\n * in step with the literal in the `@smrt()` indexes below (the scanner reads\n * that literal); the PostgreSQL schema test asserts the name.\n */\nconst RECEIPT_FILE_KEY = 'expense_receipts_expense_sha256_key';\n\n/** The columns of {@link RECEIPT_FILE_KEY}; no other unique key uses them. */\nconst RECEIPT_FILE_COLUMNS = new Set(['expense_id', 'content_sha256']);\n\n/**\n * The columns a unique violation reports. Core's typed write error carries\n * them in `details.fieldName` (recovered from PostgreSQL's\n * `Key (expense_id, content_sha256)=…` detail, or SQLite's column list) but\n * not the driver error itself, so this is where the violated key is named.\n */\nfunction violatedColumns(error: unknown): string[] {\n const details = (error as { details?: { fieldName?: unknown } } | null)\n ?.details;\n const fieldName =\n typeof details?.fieldName === 'string' ? details.fieldName : '';\n return fieldName\n .split(',')\n .map((column) => column.trim())\n .filter(Boolean);\n}\n\n/**\n * Columns a receipt keeps for life: it is evidence, not a draft. The tenant\n * is among them because it is derived from the expense on insert; letting a\n * later save (e.g. one without tenant context) change it would split the\n * receipt from its expense's tenant.\n */\nconst IMMUTABLE = [\n ['tenantId', 'tenant_id'],\n ['expenseId', 'expense_id'],\n ['assetId', 'asset_id'],\n ['contentSha256', 'content_sha256'],\n] as const;\n\n/**\n * A receipt file attached to an expense.\n *\n * Asset ownership uses a noun-specific join (`expense_receipts`), not the\n * generic `AssetAssociation`. The file's SHA-256 is stored so:\n *\n * - the same file attached twice to one expense is refused — a unique index\n * on `(expense_id, content_sha256)` plus a pre-insert check that raises\n * {@link DuplicateReceiptError}. The index leaves `tenant_id` out on\n * purpose: an expense belongs to exactly one tenant, so the key is already\n * tenant-scoped, and a NULL tenant would otherwise make every global row\n * distinct and the index unenforced;\n * - the same file on two different expenses is allowed, and surfaced for a\n * person to look at by `ExpenseReceiptCollection.findDuplicateReceipts()`.\n *\n * Attaching a receipt never changes the expense's review state. The receipt\n * takes its tenant from the expense, and its asset must be visible in that\n * tenant. The generated surface is closed, like {@link Expense}'s.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'expense_receipts',\n api: { include: [] },\n mcp: { include: [] },\n cli: false,\n indexes: [\n {\n name: 'expense_receipts_expense_sha256_key',\n columns: ['expenseId', 'contentSha256'],\n unique: true,\n },\n {\n name: 'expense_receipts_tenant_sha256_idx',\n columns: ['tenantId', 'contentSha256'],\n },\n ],\n})\nexport class ExpenseReceipt extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n /** Owning tenant; always the expense's tenant. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The expense this receipt proves. Deleting the expense removes the link. */\n @foreignKey(Expense, { required: true, onDelete: 'CASCADE' })\n expenseId: string = '';\n\n /** The smrt-assets `Asset` holding the file. */\n @crossPackageRef('@happyvertical/smrt-assets:Asset', { required: true })\n assetId: string = '';\n\n /** Profile that uploaded the file. */\n @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n uploadedByProfileId: string | null = null;\n\n /** Original filename as uploaded. */\n @field({ type: 'text' })\n filename: string = '';\n\n /** MIME type of the file. */\n @field({ type: 'text' })\n mimeType: string = '';\n\n /** Size of the file in bytes. */\n @field({ type: 'integer' })\n byteCount: number = 0;\n\n /** Lowercase hex SHA-256 of the file's bytes. */\n @field({ type: 'text', required: true })\n contentSha256: string = '';\n\n constructor(options: ExpenseReceiptOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.expenseId !== undefined) this.expenseId = options.expenseId;\n if (options.assetId !== undefined) this.assetId = options.assetId;\n if (options.uploadedByProfileId !== undefined)\n this.uploadedByProfileId = options.uploadedByProfileId;\n if (options.filename !== undefined) this.filename = options.filename;\n if (options.mimeType !== undefined) this.mimeType = options.mimeType;\n if (options.byteCount !== undefined) this.byteCount = options.byteCount;\n if (options.contentSha256 !== undefined)\n this.contentSha256 = options.contentSha256;\n }\n\n /**\n * Validate the link, refuse a second copy of the same file on the same\n * expense, then persist. Never touches the expense.\n *\n * @throws {DuplicateReceiptError} when the expense already has this file.\n * @throws {ExpenseError} `EXPENSE_RECEIPT_INVALID` for a bad hash, a\n * missing or foreign-tenant expense or asset, or a change to an\n * immutable column.\n */\n override async save(): Promise<this> {\n const label = this.id || '<new>';\n this.tenantId = normalizeTenantId(this.tenantId);\n this.contentSha256 = normalizeSha256(\n 'ExpenseReceipt',\n label,\n this.contentSha256,\n );\n assertMinorUnits(\n 'ExpenseReceipt',\n label,\n 'byteCount',\n this.byteCount,\n 'EXPENSE_RECEIPT_INVALID',\n );\n if (!this.expenseId || !this.assetId) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: expenseId and assetId are required.`,\n );\n }\n\n const persisted = this.id\n ? (((await this.db.get(this.tableName, { id: this.id })) as\n | Record<string, unknown>\n | undefined\n | null) ?? null)\n : null;\n // The checks below are against `persisted`; pin the write to that row.\n const inserting = await pinNaturalKey('ExpenseReceipt', this, persisted);\n if (persisted) {\n const self = this as unknown as Record<string, unknown>;\n for (const [fieldName, column] of IMMUTABLE) {\n // The tenant is compared as it will be written: an empty tenantId\n // saved inside a tenant context is filled in by the interceptor.\n const value =\n fieldName === 'tenantId'\n ? effectiveTenant(this.tenantId)\n : self[fieldName];\n if (String(persisted[column] ?? '') !== String(value ?? '')) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${this.id}: ${fieldName} cannot change; attach ` +\n 'a new receipt instead.',\n );\n }\n }\n } else {\n await this.assertLinkTargets(label);\n await this.assertNotDuplicate();\n }\n\n try {\n return await super.save();\n } catch (error) {\n if (inserting && isUniqueViolationError(error)) {\n throw await this.classifyInsertConflict(error);\n }\n throw error;\n }\n }\n\n /**\n * A refused INSERT is either the same file racing onto the same expense,\n * or a natural-key collision with another receipt.\n *\n * Classified from the error first, by the violated key's name or columns\n * (no other unique key on this table uses `expense_id` or\n * `content_sha256`). That matters inside a caller's transaction,\n * which the violation has already aborted on PostgreSQL, so nothing could\n * be re-read there. Only an adapter that reports neither falls back to a\n * re-read; if that re-read fails too, the original error is kept rather\n * than guessed at.\n */\n private async classifyInsertConflict(error: unknown): Promise<unknown> {\n const info = classifyDatabaseError(error);\n const columns = violatedColumns(error);\n if (\n info.constraint === RECEIPT_FILE_KEY ||\n info.driverMessages.some((text) => text.includes(RECEIPT_FILE_KEY)) ||\n columns.some((column) => RECEIPT_FILE_COLUMNS.has(column))\n ) {\n return new DuplicateReceiptError(this.expenseId, this.contentSha256, {\n cause: error,\n });\n }\n if (info.constraint || columns.length > 0) {\n // Another named key: the natural key or the primary key.\n return identityConflict('ExpenseReceipt', this, error);\n }\n try {\n await this.assertNotDuplicate();\n } catch (duplicate) {\n if (duplicate instanceof DuplicateReceiptError) {\n return new DuplicateReceiptError(this.expenseId, this.contentSha256, {\n cause: error,\n });\n }\n return error;\n }\n return identityConflict('ExpenseReceipt', this, error);\n }\n\n private async assertLinkTargets(label: string): Promise<void> {\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const expense = await expenses.get({ id: this.expenseId });\n if (!expense) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: expense ${this.expenseId} was not found.`,\n );\n }\n if (this.tenantId === null || this.tenantId === undefined) {\n this.tenantId = expense.tenantId ?? null;\n }\n if ((this.tenantId ?? null) !== (expense.tenantId ?? null)) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: a receipt belongs to its expense's tenant.`,\n );\n }\n\n const assets = await AssetCollection.create({ db: this.db });\n const asset = await assets.get({ id: this.assetId });\n const assetTenant = asset?.tenantId ?? null;\n if (!asset || (assetTenant !== null && assetTenant !== this.tenantId)) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `ExpenseReceipt ${label}: asset ${this.assetId} was not found in ` +\n \"the expense's tenant.\",\n );\n }\n if (!this.filename) this.filename = asset.name ?? '';\n if (!this.mimeType) this.mimeType = asset.mimeType ?? '';\n }\n\n private async assertNotDuplicate(): Promise<void> {\n const { ExpenseReceiptCollection } = await import(\n '../collections/ExpenseReceiptCollection.js'\n );\n const receipts = await ExpenseReceiptCollection.create({ db: this.db });\n const existing = await receipts.list({\n where: {\n expenseId: this.expenseId,\n contentSha256: this.contentSha256,\n },\n limit: 1,\n });\n if (existing.length > 0) {\n throw new DuplicateReceiptError(this.expenseId, this.contentSha256);\n }\n }\n}\n","/**\n * ExpenseReceiptCollection — attach receipts and find files shared between\n * expenses.\n *\n * @packageDocumentation\n */\n\nimport {\n type JunctionAttachOptions,\n SmrtJunction,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { ExpenseReceipt } from '../models/ExpenseReceipt.js';\nimport {\n type AttachReceiptInput,\n type DuplicateReceiptGroup,\n ExpenseError,\n type FindDuplicateReceiptsOptions,\n} from '../types.js';\n\nconst DEFAULT_DUPLICATE_LIMIT = 100;\nconst MAX_DUPLICATE_LIMIT = 1000;\n\n/**\n * Junction collection over `expense_receipts` (left: expense, right: asset).\n * Reads go through the normal tenant interceptors.\n *\n * Writes: {@link attachReceipt} adds a receipt; the inherited `detach()`\n * removes one (an explicit act; the asset itself is kept). The inherited\n * `attach()` and `setLinks()` are refused because they cannot carry the\n * content hash and `setLinks()` deletes before it writes.\n *\n * Collection classes are registered too, so this one carries its own closed\n * surface; without it every public method here would become an MCP tool.\n */\n@smrt({ api: { include: [] }, mcp: { include: [] }, cli: false })\nexport class ExpenseReceiptCollection extends SmrtJunction<ExpenseReceipt> { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n static readonly _itemClass = ExpenseReceipt;\n protected leftField = 'expenseId';\n protected rightField = 'assetId';\n protected override sortField: string | null = 'createdAt';\n protected override positionField: string | null = null;\n\n /**\n * Attach a receipt file to an expense.\n *\n * Does not change the expense in any way — in particular not its review\n * state.\n *\n * @throws {DuplicateReceiptError} when this expense already carries a file\n * with the same SHA-256.\n */\n async attachReceipt(input: AttachReceiptInput): Promise<ExpenseReceipt> {\n const receipt = await this.create({\n expenseId: input.expenseId,\n assetId: input.assetId,\n contentSha256: input.contentSha256,\n uploadedByProfileId: input.uploadedByProfileId ?? null,\n filename: input.filename ?? '',\n mimeType: input.mimeType ?? '',\n byteCount: input.byteCount ?? 0,\n });\n await receipt.save();\n return receipt;\n }\n\n /**\n * Refused: a receipt is evidence and needs its content hash, which a\n * generic junction attach does not carry. Use {@link attachReceipt}.\n *\n * @throws {ExpenseError} `EXPENSE_RECEIPT_INVALID`, always.\n */\n override async attach(\n _expenseId: string,\n _assetId: string,\n _opts: JunctionAttachOptions = {},\n ): Promise<ExpenseReceipt> {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n 'ExpenseReceiptCollection.attach() is not supported: use ' +\n 'attachReceipt(), which records and checks the file hash.',\n );\n }\n\n /**\n * Refused before anything is deleted. The inherited replace-all deletes\n * every receipt of the expense and re-attaches without hashes, which would\n * lose the evidence. Attach with {@link attachReceipt}; remove a link with\n * `detach()`.\n *\n * @throws {ExpenseError} `EXPENSE_RECEIPT_INVALID`, always.\n */\n override async setLinks(\n _expenseId: string,\n _assetIds: string[],\n _opts: JunctionAttachOptions = {},\n ): Promise<void> {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n 'ExpenseReceiptCollection.setLinks() is not supported: it would delete ' +\n 'receipt evidence. Use attachReceipt() to add and detach() to remove.',\n );\n }\n\n /** Receipts of one expense, oldest first. */\n async receiptsFor(expenseId: string): Promise<ExpenseReceipt[]> {\n return this.byLeft(expenseId);\n }\n\n /**\n * Files (by SHA-256) attached to more than one expense, for a person to\n * review. Nothing is changed or rejected: the same receipt on two expenses\n * may be a split cost or a double claim, and only a reviewer can tell.\n *\n * With `expenseId`, reports only files that expense shares with others;\n * without it, scans the visible tenant (largest groups first).\n */\n async findDuplicateReceipts(\n options: FindDuplicateReceiptsOptions = {},\n ): Promise<DuplicateReceiptGroup[]> {\n const limit = Math.min(\n Math.max(Math.trunc(options.limit ?? DEFAULT_DUPLICATE_LIMIT), 1),\n MAX_DUPLICATE_LIMIT,\n );\n\n let hashes: string[];\n if (options.expenseId) {\n const own = await this.byLeft(options.expenseId);\n hashes = [...new Set(own.map((receipt) => receipt.contentSha256))];\n } else {\n const [facet] = await this.facets({\n fields: [{ field: 'contentSha256', limit: MAX_DUPLICATE_LIMIT }],\n });\n // One row per (expense, hash) is enforced, so count > 1 means the\n // hash sits on more than one expense.\n // Already only shared hashes (largest groups first), so limiting the\n // hashes here limits the groups.\n hashes = (facet?.values ?? [])\n .filter((value) => value.count > 1)\n .map((value) => String(value.value))\n .slice(0, limit);\n }\n // The expense's own hashes include its unique receipts, so the limit is\n // applied to the groups below, after unshared hashes are dropped.\n if (hashes.length === 0) return [];\n\n const rows = await this.list({\n where: { contentSha256: hashes },\n orderBy: ['content_sha256 ASC', 'created_at ASC'],\n });\n const byHash = new Map<string, ExpenseReceipt[]>();\n for (const receipt of rows) {\n const group = byHash.get(receipt.contentSha256) ?? [];\n group.push(receipt);\n byHash.set(receipt.contentSha256, group);\n }\n\n const groups: DuplicateReceiptGroup[] = [];\n for (const [contentSha256, receipts] of byHash) {\n const expenseIds = [\n ...new Set(receipts.map((receipt) => receipt.expenseId)),\n ].sort();\n if (expenseIds.length < 2) continue;\n groups.push({ contentSha256, expenseIds, receipts });\n }\n groups.sort(\n (a, b) =>\n b.expenseIds.length - a.expenseIds.length ||\n a.contentSha256.localeCompare(b.contentSha256),\n );\n return groups.slice(0, limit);\n }\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-expenses",
5
- "packageVersion": "0.55.23",
5
+ "packageVersion": "0.56.0",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-expenses:ExpenseCollection": {
8
8
  "name": "expensecollection",
@@ -780,7 +780,10 @@
780
780
  "commitmentId": {
781
781
  "type": "crossPackageRef",
782
782
  "required": false,
783
- "related": "@happyvertical/smrt-commerce:Contract"
783
+ "related": "@happyvertical/smrt-commerce:PurchaseContract",
784
+ "_meta": {
785
+ "validateSubtype": true
786
+ }
784
787
  },
785
788
  "commitmentLineId": {
786
789
  "type": "crossPackageRef",
@@ -69,8 +69,8 @@ export declare class Expense extends SmrtObject {
69
69
  /** Optional commerce vendor the cost was paid to. */
70
70
  vendorId: string | null;
71
71
  /**
72
- * Optional commitment this actual draws down: a commerce `Contract`
73
- * (normally a `PurchaseOrder`). Must be in the same currency and tenant.
72
+ * Optional purchase commitment this actual draws down (normally a
73
+ * `PurchaseOrder`). Must be in the same currency and tenant.
74
74
  */
75
75
  commitmentId: string | null;
76
76
  /** Optional line of {@link Expense.commitmentId} this actual draws down. */
@@ -1 +1 @@
1
- {"version":3,"file":"Expense.d.ts","sourceRoot":"","sources":["../../src/models/Expense.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Expense.d.ts","sourceRoot":"","sources":["../../src/models/Expense.ts"],"names":[],"mappings":"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"}
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-expenses",
6
- "packageVersion": "0.55.23",
6
+ "packageVersion": "0.56.0",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "227ef8bfe86af4eda1c76999e64b14f968c55c4e4041a720fa7e8116aa69a825",
11
- "packageJson": "c7b433677add48651fa9b56685c142766bb6702fc71f5795c760f6cf66d3b54e",
12
- "agents": "5eceb196cc83aae058b9289b23bc05a8a313b7f2f5975852cdf2924e73dcd772",
10
+ "manifest": "cad8026615b08f5713d99bb862297156ce151f77bd01f74bc1c2bebfebb2c0ff",
11
+ "packageJson": "5e010fd0e5ac5de36e6f2c2a1c43ddd3e10a97bfd48aa5a532580444b03cee75",
12
+ "agents": "334dd53162f1e5623a6cff59d97cc2c356a5b30ed532fff610fa6e0d5cc4c73e",
13
13
  "moduleDoc:agents/financial-facts-test-matrix.md": "798c9f6c7c5694057ec438cf17252832a38164dd80a47f77c9cd4642b3e5cbd6",
14
14
  "moduleDoc:agents/expense-ui.md": "0bdea9e6c627f8cbab2a1f21ef4da3ab7f00409764a1ef48572dd40694fb6785"
15
15
  },
@@ -407,7 +407,7 @@
407
407
  "name": "commitmentId",
408
408
  "type": "crossPackageRef",
409
409
  "required": false,
410
- "related": "@happyvertical/smrt-commerce:Contract",
410
+ "related": "@happyvertical/smrt-commerce:PurchaseContract",
411
411
  "columnType": "UUID"
412
412
  },
413
413
  {
@@ -504,7 +504,7 @@
504
504
  "name": "commitmentId",
505
505
  "type": "crossPackageRef",
506
506
  "required": false,
507
- "related": "@happyvertical/smrt-commerce:Contract",
507
+ "related": "@happyvertical/smrt-commerce:PurchaseContract",
508
508
  "columnType": "UUID"
509
509
  },
510
510
  {
@@ -856,7 +856,7 @@
856
856
  "polymorphicAssociations": 0,
857
857
  "uuidColumns": 30
858
858
  },
859
- "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` | `third-party`) + `paidByProfileId` / `paidByName`, `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## Optional financial facts\n\n`financialBreakdown` is guarded JSON text (`get/setFinancialBreakdown`), absent\nfor legacy rows. Exact `getCostBasis()` returns null without facts. Complete facts\nreconcile subtotal + signed tax components to amount/currency; recoverable amounts\nare explicit. No tax law or customer pricing is inferred. `third-party` payer\nrequires `paidByName` and is not reimbursable. Negative amounts require `refundOfId`\n(same-owner/currency positive source, checked through the bound executor).\nSource edits also check inbound same-owner refunds: a referenced source stays\npositive, non-refund, and in their currency. `save()` encloses guards and write\nin the public `withTransaction()` boundary; PostgreSQL locks its row and proposed\nsource in id order before either refund check, while SQLite serializes writes.\nThe inbound projected lookup uses the existing `refund_of_id` index and an\nexplicit owner filter. Keep caller transaction affinity and per-row revision\nchecks; neither an unlocked lookup nor revision CAS alone protects this pair.\nAll three fields participate in the reviewed-money freeze. See the README and\n[financial facts test matrix](agents/financial-facts-test-matrix.md).\n\nLinking a new refund (or changing its source) also claims the source revision in\nthat same transaction through `claimRevision(source.updated_at)`. This makes a\nsource edit under an older PostgreSQL `REPEATABLE READ` snapshot fail with a\nserialization error instead of missing the new refund. Reload a source after\nlinking a refund before editing/reviewing it; on a serialization failure, retry\nthe entire caller transaction with fresh instances. Failed outer transactions\nroll back both the refund and the source revision claim. Existing-link saves do\nnot advance the source revision.\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",
859
+ "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` | `third-party`) + `paidByProfileId` / `paidByName`, `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## Optional financial facts\n\n`financialBreakdown` is guarded JSON text (`get/setFinancialBreakdown`), absent\nfor legacy rows. Exact `getCostBasis()` returns null without facts. Complete facts\nreconcile subtotal + signed tax components to amount/currency; recoverable amounts\nare explicit. No tax law or customer pricing is inferred. `third-party` payer\nrequires `paidByName` and is not reimbursable. Negative amounts require `refundOfId`\n(same-owner/currency positive source, checked through the bound executor).\nSource edits also check inbound same-owner refunds: a referenced source stays\npositive, non-refund, and in their currency. `save()` encloses guards and write\nin the public `withTransaction()` boundary; PostgreSQL locks its row and proposed\nsource in id order before either refund check, while SQLite serializes writes.\nThe inbound projected lookup uses the existing `refund_of_id` index and an\nexplicit owner filter. Keep caller transaction affinity and per-row revision\nchecks; neither an unlocked lookup nor revision CAS alone protects this pair.\nAll three fields participate in the reviewed-money freeze. See the README and\n[financial facts test matrix](agents/financial-facts-test-matrix.md).\n\nLinking a new refund (or changing its source) also claims the source revision in\nthat same transaction through `claimRevision(source.updated_at)`. This makes a\nsource edit under an older PostgreSQL `REPEATABLE READ` snapshot fail with a\nserialization error instead of missing the new refund. Reload a source after\nlinking a refund before editing/reviewing it; on a serialization failure, retry\nthe entire caller transaction with fresh instances. Failed outer transactions\nroll back both the refund and the source revision claim. Existing-link saves do\nnot advance the source revision.\n\n## Cross-package references\n\n- `vendorId` → `@crossPackageRef('@happyvertical/smrt-commerce:Vendor')`\n- `commitmentId` → `…smrt-commerce:PurchaseContract` (`PurchaseOrder` or `ProductionOrder`)\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",
860
860
  "moduleDocs": [
861
861
  {
862
862
  "path": "agents/financial-facts-test-matrix.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-expenses",
3
- "version": "0.55.23",
3
+ "version": "0.56.0",
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",
@@ -33,15 +33,15 @@
33
33
  }
34
34
  },
35
35
  "dependencies": {
36
- "@happyvertical/smrt-assets": "0.55.23",
37
- "@happyvertical/smrt-commerce": "0.55.23",
38
- "@happyvertical/smrt-core": "0.55.23",
39
- "@happyvertical/smrt-tenancy": "0.55.23",
40
- "@happyvertical/smrt-types": "0.55.23",
41
- "@happyvertical/smrt-ui": "0.55.23"
36
+ "@happyvertical/smrt-assets": "0.56.0",
37
+ "@happyvertical/smrt-commerce": "0.56.0",
38
+ "@happyvertical/smrt-core": "0.56.0",
39
+ "@happyvertical/smrt-tenancy": "0.56.0",
40
+ "@happyvertical/smrt-types": "0.56.0",
41
+ "@happyvertical/smrt-ui": "0.56.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@happyvertical/smrt-vitest": "0.55.23",
44
+ "@happyvertical/smrt-vitest": "0.56.0",
45
45
  "@types/node": "24.13.6",
46
46
  "typescript": "6.0.3",
47
47
  "vite": "8.1.4",