@happyvertical/smrt-expenses 0.57.3 → 0.57.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +42 -3
- package/README.md +129 -9
- package/agents/financial-facts-test-matrix.md +57 -0
- package/agents/supplier-payable-test-matrix.md +24 -0
- package/dist/actions/expense-actions.d.ts +2 -0
- package/dist/actions/expense-actions.d.ts.map +1 -1
- package/dist/chunks/Expense-C_WC5VLF.js +1364 -0
- package/dist/chunks/Expense-C_WC5VLF.js.map +1 -0
- package/dist/chunks/{ExpenseReceiptCollection-y1SlmnBS.js → ExpenseReceiptCollection-BO5yvJLm.js} +2 -2
- package/dist/chunks/{ExpenseReceiptCollection-y1SlmnBS.js.map → ExpenseReceiptCollection-BO5yvJLm.js.map} +1 -1
- package/dist/chunks/{expense-actions-Ce5CBaBE.js → expense-actions-Bxzeb4s2.js} +8 -3
- package/dist/chunks/expense-actions-Bxzeb4s2.js.map +1 -0
- package/dist/collections/ExpenseCollection.d.ts +17 -1
- package/dist/collections/ExpenseCollection.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -7
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +304 -7
- package/dist/models/Expense.d.ts +35 -1
- package/dist/models/Expense.d.ts.map +1 -1
- package/dist/recipes.d.ts +9 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/refund-position.d.ts +11 -0
- package/dist/refund-position.d.ts.map +1 -0
- package/dist/reimbursement-position.d.ts +20 -0
- package/dist/reimbursement-position.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +267 -10
- package/dist/supplier-payable.d.ts +128 -0
- package/dist/supplier-payable.d.ts.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -9
- package/dist/chunks/Expense-7afLcUTn.js +0 -681
- package/dist/chunks/Expense-7afLcUTn.js.map +0 -1
- package/dist/chunks/expense-actions-Ce5CBaBE.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Expense-C_WC5VLF.js","names":["retained","paid","field"],"sources":["../../src/validation.ts","../../src/financial-facts.ts","../../src/refund-position.ts","../../src/reimbursement-position.ts","../../src/supplier-payable.ts","../../src/models/Expense.ts"],"sourcesContent":["/**\n * Small, pure validators shared by the expense models.\n *\n * @packageDocumentation\n */\n\nimport { createHash } from 'node:crypto';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { ExpenseError, type ExpenseErrorCode } from './types.js';\n\n/** Lowercase kebab-case: `material`, `outside-service`. */\nexport const CATEGORY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\nconst ISO_CURRENCY = /^[A-Z]{3}$/;\nconst ISO_DATE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst QUALIFIED_CLASS = /^[^\\s:]+:[A-Za-z_$][\\w$]*$/;\nconst SHA256_HEX = /^[0-9a-f]{64}$/;\n\n/**\n * Throw unless `value` is a non-negative, JavaScript-safe integer number of\n * minor units. Money is exact: `$19.99` is `1999`, never `19.99`.\n */\nexport function assertMinorUnits(\n model: string,\n label: string,\n fieldName: string,\n value: number,\n code: ExpenseErrorCode = 'EXPENSE_INVALID',\n): void {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new ExpenseError(\n code,\n `${model} ${label}: ${fieldName} must be a non-negative integer ` +\n `number of minor units (cents) — got ${value}. $19.99 is 1999.`,\n );\n }\n}\n\n/** Throw unless `value` is an upper-case ISO 4217 code such as `USD`. */\nexport function assertIsoCurrency(\n model: string,\n label: string,\n value: string,\n): void {\n if (!ISO_CURRENCY.test(value)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `${model} ${label}: currency must be a three-letter ISO 4217 code ` +\n `(for example 'USD'), got '${value}'.`,\n );\n }\n}\n\n/** Throw unless `value` is a real calendar date written `YYYY-MM-DD`. */\nexport function assertIsoDate(\n model: string,\n label: string,\n fieldName: string,\n value: string,\n): void {\n const match = ISO_DATE.exec(value);\n const valid =\n match !== null &&\n (() => {\n const [year, month, day] = match.slice(1).map(Number);\n const date = new Date(Date.UTC(year, month - 1, day));\n return (\n date.getUTCFullYear() === year &&\n date.getUTCMonth() === month - 1 &&\n date.getUTCDate() === day\n );\n })();\n if (!valid) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `${model} ${label}: ${fieldName} must be a calendar date written ` +\n `YYYY-MM-DD, got '${value}'.`,\n );\n }\n}\n\n/**\n * Throw unless `value` is a package-qualified class name such as\n * `@happyvertical/smrt-projects:Project`. A bare class name is ambiguous\n * (packages can share names) and is refused.\n */\nexport function assertQualifiedClassName(\n model: string,\n label: string,\n value: string,\n): void {\n if (!QUALIFIED_CLASS.test(value)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `${model} ${label}: costObjectType must be a qualified class name ` +\n `such as '@happyvertical/smrt-projects:Project', got '${value}'.`,\n );\n }\n}\n\n/** Normalize and check a SHA-256 hex digest; returns it in lower case. */\nexport function normalizeSha256(model: string, label: string, value: string) {\n const digest = String(value ?? '')\n .trim()\n .toLowerCase();\n if (!SHA256_HEX.test(digest)) {\n throw new ExpenseError(\n 'EXPENSE_RECEIPT_INVALID',\n `${model} ${label}: contentSha256 must be a 64-character hex SHA-256 ` +\n 'digest of the file bytes.',\n );\n }\n return digest;\n}\n\n/**\n * Lowercase hex SHA-256 of a file's bytes — the value\n * `ExpenseReceipt.contentSha256` expects.\n *\n * @example\n * ```typescript\n * const digest = computeContentSha256(await readFile('receipt.pdf'));\n * ```\n */\nexport function computeContentSha256(data: Uint8Array | string): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Milliseconds since the epoch of a date-ish value; `null` when empty. */\nexport function instantMs(value: unknown): number | null {\n if (value === null || value === undefined || value === '') return null;\n const ms =\n value instanceof Date ? value.getTime() : new Date(String(value)).getTime();\n return Number.isNaN(ms) ? null : ms;\n}\n\n/** The slice of `SmrtObject` the natural-key identity guard needs. */\ninterface NaturalKeyed {\n id?: string | null;\n slug?: string | null;\n context?: string | null;\n readonly isPersisted: boolean;\n getSlug(): Promise<unknown>;\n requireInsertOnSave(): void;\n}\n\n/**\n * Make a save write only the row its guards checked.\n *\n * Core's natural-key save adopts any same-owner row on\n * `(tenant_id, slug, context)` and upserts it without a revision predicate,\n * and these models have no human slug. So:\n *\n * - a fresh (never loaded, never saved) instance may only INSERT\n * (`requireInsertOnSave`): naming an existing row's id is refused here, and\n * a row that appears between this check and the write makes the INSERT\n * fail rather than be adopted;\n * - an existing row is changed only through a loaded instance, whose save\n * carries core's revision compare-and-swap, and it must keep the slug and\n * context it is stored under, so the write targets that row and no other.\n *\n * @returns `true` when this save is an INSERT.\n * @throws {ExpenseError} `EXPENSE_IDENTITY_CONFLICT` when a fresh instance\n * names an existing row, or a loaded one changes its natural key.\n */\nexport async function pinNaturalKey(\n model: string,\n object: NaturalKeyed,\n persisted: Record<string, unknown> | null,\n): Promise<boolean> {\n await object.getSlug();\n if (!object.isPersisted) {\n if (persisted) {\n throw new ExpenseError(\n 'EXPENSE_IDENTITY_CONFLICT',\n `${model} ${object.id} already exists; load it and save the loaded ` +\n 'instance instead of creating one with its id.',\n );\n }\n object.requireInsertOnSave();\n return true;\n }\n if (\n persisted &&\n (String(persisted.slug ?? '') !== String(object.slug ?? '') ||\n String(persisted.context ?? '') !== String(object.context ?? ''))\n ) {\n throw new ExpenseError(\n 'EXPENSE_IDENTITY_CONFLICT',\n `${model} ${object.id}: slug and context cannot change; they would ` +\n 'point the save at a different row.',\n );\n }\n return false;\n}\n\n/** The error a refused natural-key INSERT is reported as. */\nexport function identityConflict(\n model: string,\n object: { id?: string | null; slug?: string | null },\n cause: unknown,\n): ExpenseError {\n return new ExpenseError(\n 'EXPENSE_IDENTITY_CONFLICT',\n `${model} ${object.id}: another row already uses slug ` +\n `'${object.slug}' in this tenant; a new ${model} cannot take it over.`,\n { cause },\n );\n}\n\n/**\n * The tenant a save will actually write: tenancy's `beforeSave` interceptor\n * fills an unset `tenantId` from the active tenant context, after the\n * model's own checks have run, and \"unset\" is falsy there — `''` included.\n * Compare against this, not the raw field.\n */\nexport function effectiveTenant(\n tenantId: string | null | undefined,\n): string | null {\n return tenantId || getTenantId() || null;\n}\n\n/**\n * An empty tenant id is unset, never a tenant of its own: core and tenancy\n * treat it as falsy (and real tenant ids are UUIDs), so it is stored as\n * NULL — a global row — rather than as a `''` pseudo-tenant.\n */\nexport function normalizeTenantId(\n tenantId: string | null | undefined,\n): string | null {\n return tenantId || null;\n}\n","/** Exact validation of optional supplier facts. No tax policy is inferred. */\nimport {\n type ExpenseCostBasis,\n ExpenseError,\n type ExpenseFinancialBreakdown,\n} from './types.js';\nimport { assertIsoCurrency } from './validation.js';\n\nfunction invalid(message: string): never {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense financial breakdown: ${message}`,\n );\n}\n\nfunction record(value: unknown, keys: string[]): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return invalid('expected an object.');\n }\n const object = value as Record<string, unknown>;\n if (\n Object.keys(object).length !== keys.length ||\n keys.some((key) => !Object.hasOwn(object, key))\n ) {\n return invalid('missing or unknown fields.');\n }\n return object;\n}\n\nfunction minor(value: unknown): bigint {\n if (typeof value !== 'number' || !Number.isSafeInteger(value)) {\n return invalid('amounts must be safe integer minor units.');\n }\n return BigInt(value);\n}\n\nfunction safe(value: bigint): number {\n const result = Number(value);\n if (!Number.isSafeInteger(result))\n return invalid('unsafe calculated amount.');\n return result;\n}\n\n/** Validate facts and derive an exact signed basis, without rounding or defaults. */\nexport function expenseCostBasis(\n value: unknown,\n amount: number,\n currency: string,\n): ExpenseCostBasis {\n const facts = record(value, ['currency', 'subtotal', 'taxes', 'total']);\n if (typeof facts.currency !== 'string' || facts.currency !== currency) {\n return invalid('currency must match the expense.');\n }\n assertIsoCurrency('Expense', '<financial-breakdown>', facts.currency);\n const total = minor(facts.total);\n const subtotal = minor(facts.subtotal);\n if (total !== minor(amount))\n return invalid('total must match the expense amount.');\n const consistent = (part: bigint) =>\n part === 0n || (total > 0n ? part > 0n : total < 0n && part < 0n);\n if (!consistent(subtotal)) return invalid('subtotal sign must match total.');\n if (!Array.isArray(facts.taxes))\n return invalid('taxes must be an explicit array.');\n let tax = 0n;\n let recoverable = 0n;\n const codes = new Set<string>();\n for (const item of facts.taxes) {\n const component = record(item, ['code', 'amount', 'recoverableAmount']);\n if (\n typeof component.code !== 'string' ||\n !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(component.code) ||\n component.code.length > 64 ||\n codes.has(component.code)\n ) {\n return invalid(\n 'tax codes must be unique lower-case kebab-case identifiers.',\n );\n }\n codes.add(component.code);\n const charged = minor(component.amount);\n const recovered = minor(component.recoverableAmount);\n const chargedMagnitude = charged < 0n ? -charged : charged;\n const recoveredMagnitude = recovered < 0n ? -recovered : recovered;\n if (\n !consistent(charged) ||\n !consistent(recovered) ||\n recoveredMagnitude > chargedMagnitude\n ) {\n return invalid(\n 'tax and recovery must share the total sign, with recovery bounded by tax.',\n );\n }\n tax += charged;\n recoverable += recovered;\n }\n if (subtotal + tax !== total)\n return invalid('subtotal plus taxes must equal total.');\n return {\n currency,\n subtotal: safe(subtotal),\n taxTotal: safe(tax),\n recoverableTax: safe(recoverable),\n nonRecoverableTax: safe(tax - recoverable),\n total: safe(total),\n costBasis: safe(total - recoverable),\n };\n}\n\n/** Decode stored facts, failing closed for corrupt or unknown persisted values. */\nexport function parseExpenseFinancialBreakdown(\n stored: string | null,\n amount: number,\n currency: string,\n): ExpenseFinancialBreakdown | null {\n if (stored === '' || stored === null || stored === undefined) return null;\n if (typeof stored !== 'string') return invalid('storage must be JSON text.');\n let parsed: unknown;\n try {\n parsed = JSON.parse(stored);\n } catch {\n return invalid('invalid JSON.');\n }\n expenseCostBasis(parsed, amount, currency);\n return parsed as ExpenseFinancialBreakdown;\n}\n","/** Exact source-preserving reversal bounds; no financial policy is inferred. */\nimport { expenseCostBasis } from './financial-facts.js';\nimport {\n ExpenseError,\n type ExpenseFinancialBreakdown,\n type ExpenseRefundPosition,\n} from './types.js';\n\ninterface RefundFact {\n id: string;\n amount: number;\n currency: string;\n getFinancialBreakdown(): ExpenseFinancialBreakdown | null;\n}\n\nfunction invalid(message: string): never {\n throw new ExpenseError('EXPENSE_INVALID', `Expense refund: ${message}`);\n}\n\nfunction safe(value: bigint): number {\n const result = Number(value);\n if (!Number.isSafeInteger(result)) invalid('unsafe calculated amount.');\n return result;\n}\n\n/** Validate known reversals even when other financial facts remain unknown. */\nexport function calculateRefundPosition(\n source: RefundFact,\n refunds: RefundFact[],\n): ExpenseRefundPosition {\n if (!Number.isSafeInteger(source.amount) || source.amount <= 0) {\n invalid('source must remain positive.');\n }\n const sourceFacts = source.getFinancialBreakdown();\n let refunded = 0n;\n let subtotal = 0n;\n let complete = sourceFacts !== null;\n const components = new Map(\n sourceFacts?.taxes.map((tax) => [tax.code, { charged: 0n, recovered: 0n }]),\n );\n const ids = new Set<string>();\n for (const refund of refunds) {\n if (ids.has(refund.id)) invalid('duplicate refund identity.');\n ids.add(refund.id);\n if (\n !Number.isSafeInteger(refund.amount) ||\n refund.amount >= 0 ||\n refund.currency !== source.currency\n )\n invalid('linked refunds must be negative and share source currency.');\n refunded -= BigInt(refund.amount);\n const facts = refund.getFinancialBreakdown();\n if (!facts) {\n complete = false;\n continue;\n }\n // Model getters validate facts; keep the pure boundary safe for all callers.\n expenseCostBasis(facts, refund.amount, source.currency);\n if (!sourceFacts) continue;\n subtotal -= BigInt(facts.subtotal);\n for (const tax of facts.taxes) {\n const sum = components.get(tax.code);\n if (!sum)\n invalid(\n `tax component ${tax.code} does not identify a source component.`,\n );\n sum.charged -= BigInt(tax.amount);\n sum.recovered -= BigInt(tax.recoverableAmount);\n }\n }\n if (refunded > BigInt(source.amount))\n invalid('cumulative cash exceeds source gross.');\n if (sourceFacts) {\n expenseCostBasis(sourceFacts, source.amount, source.currency);\n if (subtotal > BigInt(sourceFacts.subtotal))\n invalid('subtotal reversal exceeds source subtotal.');\n for (const tax of sourceFacts.taxes) {\n const sum = components.get(tax.code);\n if (!sum) invalid('missing source component.');\n if (\n sum.charged > BigInt(tax.amount) ||\n sum.recovered > BigInt(tax.recoverableAmount) ||\n sum.charged - sum.recovered > BigInt(tax.amount - tax.recoverableAmount)\n )\n invalid(\n `tax component ${tax.code} reversal exceeds its source component.`,\n );\n }\n }\n const position: ExpenseRefundPosition = {\n sourceId: source.id,\n currency: source.currency,\n sourceAmount: source.amount,\n refundedAmount: safe(refunded),\n remainingAmount: safe(BigInt(source.amount) - refunded),\n refundIds: [...ids].sort(),\n financialPosition: null,\n };\n if (complete && sourceFacts) {\n const reversal: ExpenseFinancialBreakdown = {\n currency: source.currency,\n subtotal: safe(-subtotal),\n taxes: sourceFacts.taxes.map((tax) => {\n const sum = components.get(tax.code);\n if (!sum) invalid('missing source component.');\n return {\n code: tax.code,\n amount: safe(-sum.charged),\n recoverableAmount: safe(-sum.recovered),\n };\n }),\n total: safe(-refunded),\n };\n const remaining: ExpenseFinancialBreakdown = {\n currency: source.currency,\n subtotal: safe(BigInt(sourceFacts.subtotal) - subtotal),\n taxes: sourceFacts.taxes.map((tax, index) => ({\n code: tax.code,\n amount: tax.amount + reversal.taxes[index].amount,\n recoverableAmount:\n tax.recoverableAmount + reversal.taxes[index].recoverableAmount,\n })),\n total: position.remainingAmount,\n };\n expenseCostBasis(remaining, remaining.total, source.currency);\n position.financialPosition = {\n source: sourceFacts,\n refunded: reversal,\n remaining,\n };\n }\n return position;\n}\n","/** Explicit cash direction, without inferring tax basis or historical payments. */\nimport {\n ExpenseError,\n type ExpenseRefundRecipient,\n type ExpenseReimbursementPosition,\n} from './types.js';\n\nfunction invalid(message: string): never {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense reimbursement: ${message}`,\n );\n}\n\nexport function parseRefundRecipient(\n stored: string | null,\n): ExpenseRefundRecipient | null {\n if (stored === null || stored === '') return null;\n let value: unknown;\n try {\n value = JSON.parse(stored);\n } catch {\n return invalid('invalid recipient JSON.');\n }\n if (!value || typeof value !== 'object' || Array.isArray(value))\n invalid('recipient must be an object.');\n const object = value as Record<string, unknown>;\n if (object.kind === 'company' && Object.keys(object).length === 1)\n return { kind: 'company' };\n if (\n object.kind === 'person' &&\n Object.keys(object).length === 2 &&\n typeof object.profileId === 'string' &&\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(\n object.profileId,\n )\n )\n return { kind: 'person', profileId: object.profileId };\n // Exact retained evidence only: no trimming, lookup or identity inference.\n if (\n object.kind === 'third-party' &&\n Object.keys(object).length === 2 &&\n typeof object.name === 'string' &&\n object.name.trim().length > 0 &&\n object.name.length <= 200\n )\n return { kind: 'third-party', name: object.name };\n return invalid(\n 'recipient must explicitly identify company, person profile or a third-party name (1–200 characters).',\n );\n}\n\ninterface Source {\n id: string;\n currency: string;\n amount: number;\n paidBy: string;\n paidByProfileId: string | null;\n reimbursable: boolean;\n reimbursedAt: Date | null;\n reimbursedAmount: number | null;\n}\ninterface Refund {\n amount: number;\n getRefundRecipient(): ExpenseRefundRecipient | null;\n}\n\nexport function calculateReimbursementPosition(\n source: Source,\n refunds: Refund[],\n): ExpenseReimbursementPosition {\n if (\n !Number.isSafeInteger(source.amount) ||\n source.amount <= 0 ||\n source.paidBy !== 'person' ||\n !source.paidByProfileId ||\n !source.reimbursable\n )\n invalid('requires a positive person-paid reimbursable source.');\n let person = 0n;\n let company = 0n;\n let reason: ExpenseReimbursementPosition['unknownReason'] = null;\n for (const refund of refunds) {\n const recipient = refund.getRefundRecipient();\n if (!recipient) reason ??= 'unknown-refund-recipient';\n else if (recipient.kind === 'company') company -= BigInt(refund.amount);\n else if (recipient.kind === 'third-party')\n reason ??= 'unknown-third-party-recipient';\n else if (recipient.profileId === source.paidByProfileId)\n person -= BigInt(refund.amount);\n else reason ??= 'recipient-is-another-person';\n }\n if (\n source.reimbursedAmount !== null &&\n (!source.reimbursedAt ||\n !Number.isSafeInteger(source.reimbursedAmount) ||\n source.reimbursedAmount <= 0 ||\n source.reimbursedAmount > source.amount)\n ) {\n invalid('retained paid amount is inconsistent with original gross claim.');\n }\n const paid = source.reimbursedAt ? source.reimbursedAmount : 0;\n if (paid === null) reason = 'legacy-reimbursement-amount';\n const net =\n reason || paid === null\n ? null\n : BigInt(source.amount) - person - BigInt(paid);\n const safe = (amount: bigint): number => {\n const value = Number(amount);\n if (!Number.isSafeInteger(value)) invalid('unsafe cash position.');\n return value;\n };\n return {\n sourceId: source.id,\n currency: source.currency,\n sourceAmount: source.amount,\n personRefundAmount: reason ? null : safe(person),\n companyRefundAmount: reason ? null : safe(company),\n reimbursedAmount: paid,\n remainingAmount: net === null ? null : safe(net > 0n ? net : 0n),\n owedToCompany: net === null ? null : safe(net < 0n ? -net : 0n),\n unknownReason: reason,\n };\n}\n","/** Expense-owned supplier obligation evidence. No bank, provider or journal execution. */\nimport { createHash, randomUUID } from 'node:crypto';\nimport { ExpenseError } from './types.js';\nimport { assertIsoDate } from './validation.js';\n\n/** Exact source identity and original author; author need not be a current user. */\nexport interface SupplierPayableSource {\n namespace: string;\n key: string;\n version: string;\n author: string | null;\n}\n/** Caller-reviewed evidence, independently attributed from original source authors. */\nexport interface SupplierPayableReview {\n recordedByProfileId: string;\n evidenceRef: string;\n}\n/** Dated ordering is explicit, including multiple events on one calendar day. */\nexport type SupplierPayableEventInput = {\n effectiveOn: string;\n sequence: number;\n evidenceRef: string;\n source?: SupplierPayableSource;\n} & (\n | { kind: 'hold' | 'release' | 'settlement'; amountMinor: number }\n | {\n kind: 'refund';\n refundExpenseId: string;\n payableReductionMinor: number;\n retainedReductionMinor: number;\n cashReturnedMinor: number;\n }\n);\n/** Immutable original supplier payable snapshot; null basis is explicitly unknown. */\nexport interface RecordSupplierPayableInput extends SupplierPayableReview {\n requestId: string;\n currency: string;\n source: SupplierPayableSource;\n payableBasisMinor: number | null;\n events: SupplierPayableEventInput[];\n}\n/** Append an evidenced current event. This records facts and never executes a payment. */\nexport interface RecordSupplierPayableEventInput extends SupplierPayableReview {\n requestId: string;\n currency: string;\n event: SupplierPayableEventInput;\n}\n/** Complete unknown original basis once, preserving the original snapshot. */\nexport interface CompleteSupplierPayableBasisInput\n extends SupplierPayableReview {\n requestId: string;\n currency: string;\n payableBasisMinor: number;\n}\n/** Concrete immutable child identity retained by the owning Expense command. */\nexport type SupplierPayableEvent = SupplierPayableEventInput & {\n id: string;\n authority: 'original' | 'current';\n requestId: string;\n requestFingerprint: string;\n recordedByProfileId: string;\n recordedAt: string;\n refundFingerprint?: string;\n};\n/** Original and subsequent authority remain separate in public readback. */\nexport interface SupplierPayableState {\n version: 1;\n expenseId: string;\n currency: string;\n expenseFingerprint: string;\n original: SupplierPayableReview & {\n id: string;\n requestId: string;\n requestFingerprint: string;\n source: SupplierPayableSource;\n payableBasisMinor: number | null;\n recordedAt: string;\n };\n completion:\n | (CompleteSupplierPayableBasisInput & {\n id: string;\n requestFingerprint: string;\n recordedAt: string;\n })\n | null;\n events: SupplierPayableEvent[];\n}\n/** Position is separate from incurred expense, tax recovery and employee reimbursement. */\nexport interface SupplierPayablePosition {\n expenseId: string;\n currency: string;\n asOfDate: string | null;\n payableBasisMinor: number | null;\n heldMinor: number;\n releasedMinor: number;\n retainedReductionMinor: number;\n /** Recorded holds less recorded releases/reductions, before unresolved interpretation. */\n recordedRetainedMinor: number;\n retainedMinor: number | null;\n settledMinor: number;\n cashReturnedMinor: number;\n netPaidMinor: number | null;\n payableReductionMinor: number;\n remainingPayableMinor: number | null;\n unknownReasons: Array<\n 'payable-basis-unknown' | 'refund-interpretation-required'\n >;\n unresolvedRefundExpenseIds: string[];\n authority: SupplierPayableState;\n}\n/** Owning model projection, not an alternative persistence API. */\nexport interface PayableExpense {\n id?: string | null;\n tenantId: string | null;\n amount: number;\n currency: string;\n incurredOn: string;\n refundOfId: string | null;\n financialBreakdown: string | null;\n refundRecipient: string | null;\n vendorId: string | null;\n costObjectType: string;\n costObjectId: string;\n paidBy: string;\n paidByProfileId: string | null;\n paidByName: string | null;\n}\nexport function payableInvalid(message: string): never {\n throw new ExpenseError('EXPENSE_INVALID', `Supplier payable: ${message}`);\n}\nfunction text(value: unknown, label: string): asserts value is string {\n if (typeof value !== 'string' || !value.trim() || value.length > 2000)\n payableInvalid(`${label} must be explicit bounded evidence.`);\n}\nfunction minor(value: unknown): asserts value is number {\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)\n payableInvalid('amounts must be nonnegative safe minor units.');\n}\nfunction source(value: SupplierPayableSource) {\n if (!value || typeof value !== 'object')\n payableInvalid('original source is required.');\n for (const key of ['namespace', 'key', 'version'] as const)\n text(value[key], key);\n if (value.author !== null) text(value.author, 'original author');\n}\nfunction review(\n value: SupplierPayableReview & { requestId: string; currency: string },\n expense: PayableExpense,\n) {\n text(value.requestId, 'requestId');\n text(value.evidenceRef, 'review evidence');\n if (\n !/^[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}$/i.test(\n value.recordedByProfileId,\n )\n )\n payableInvalid('current recorder must be an explicit profile UUID.');\n if (value.currency !== expense.currency)\n payableInvalid('currency must equal the Expense currency.');\n}\n/** Stable canonical request identity; rejects non-JSON evidence rather than dropping it. */\nexport function payableDigest(value: unknown): string {\n const canonical = (item: unknown): string => {\n if (item === null || typeof item === 'string' || typeof item === 'boolean')\n return JSON.stringify(item);\n if (typeof item === 'number' && Number.isSafeInteger(item))\n return JSON.stringify(item);\n if (Array.isArray(item)) return `[${item.map(canonical).join(',')}]`;\n if (\n item &&\n typeof item === 'object' &&\n Object.getPrototypeOf(item) === Object.prototype\n )\n return `{${Object.keys(item)\n .sort()\n .map(\n (key) =>\n `${JSON.stringify(key)}:${canonical((item as Record<string, unknown>)[key])}`,\n )\n .join(',')}}`;\n return payableInvalid('evidence must be exact JSON values.');\n };\n return createHash('sha256').update(canonical(value)).digest('hex');\n}\nexport function payableExpenseFingerprint(expense: PayableExpense): string {\n return payableDigest({\n id: expense.id,\n tenantId: expense.tenantId,\n amount: expense.amount,\n currency: expense.currency,\n incurredOn: expense.incurredOn,\n refundOfId: expense.refundOfId,\n financialBreakdown: expense.financialBreakdown,\n refundRecipient: expense.refundRecipient,\n vendorId: expense.vendorId,\n costObjectType: expense.costObjectType,\n costObjectId: expense.costObjectId,\n paidBy: expense.paidBy,\n paidByProfileId: expense.paidByProfileId,\n paidByName: expense.paidByName,\n });\n}\nexport function parseSupplierPayable(\n stored: string | null,\n): SupplierPayableState | null {\n if (stored === null || stored === '') return null;\n let value: SupplierPayableState;\n try {\n value = JSON.parse(stored);\n } catch {\n return payableInvalid('invalid retained JSON.');\n }\n if (\n !value ||\n value.version !== 1 ||\n !value.original ||\n !Array.isArray(value.events) ||\n !value.expenseId ||\n !value.expenseFingerprint\n )\n payableInvalid('invalid retained authority.');\n return value;\n}\nfunction validateEvent(\n event: SupplierPayableEventInput,\n expense: PayableExpense,\n original: boolean,\n) {\n if (!event || typeof event !== 'object') payableInvalid('event is required.');\n assertIsoDate(\n 'Expense',\n String(expense.id),\n 'supplier payable event date',\n event.effectiveOn,\n );\n if (event.effectiveOn < expense.incurredOn)\n payableInvalid('event precedes the original expense date.');\n if (!Number.isSafeInteger(event.sequence) || event.sequence < 0)\n payableInvalid('explicit event sequence is required.');\n text(event.evidenceRef, 'event evidence');\n if (original || event.source) source(event.source!);\n if (['hold', 'release', 'settlement'].includes(event.kind)) {\n minor((event as { amountMinor: number }).amountMinor);\n if (!(event as { amountMinor: number }).amountMinor)\n payableInvalid('event amount must be positive.');\n } else if (event.kind === 'refund') {\n text(event.refundExpenseId, 'refund Expense identity');\n minor(event.payableReductionMinor);\n minor(event.retainedReductionMinor);\n minor(event.cashReturnedMinor);\n if (event.retainedReductionMinor > event.payableReductionMinor)\n payableInvalid(\n 'retained reduction exceeds the explicit payable reduction.',\n );\n } else payableInvalid('unsupported event kind.');\n}\nfunction bindEvent(\n event: SupplierPayableEventInput,\n expense: PayableExpense,\n refunds: PayableExpense[],\n original: boolean,\n input: SupplierPayableReview & { requestId: string },\n fingerprint: string,\n): SupplierPayableEvent {\n validateEvent(event, expense, original);\n let refundFingerprint: string | undefined;\n if (event.kind === 'refund') {\n const refund = refunds.find((row) => row.id === event.refundExpenseId);\n if (\n !refund ||\n refund.refundOfId !== expense.id ||\n refund.tenantId !== expense.tenantId ||\n refund.currency !== expense.currency ||\n refund.amount >= 0\n )\n payableInvalid(\n 'refund must be an actual same-owner linked negative Expense.',\n );\n if (event.effectiveOn !== refund.incurredOn)\n payableInvalid(\n 'refund interpretation must preserve its actual effective date.',\n );\n if (\n event.payableReductionMinor > -refund.amount ||\n event.cashReturnedMinor > -refund.amount\n )\n payableInvalid(\n 'refund interpretation exceeds its actual gross magnitude.',\n );\n refundFingerprint = payableExpenseFingerprint(refund);\n }\n return {\n ...event,\n id: randomUUID(),\n authority: original ? 'original' : 'current',\n requestId: input.requestId,\n requestFingerprint: fingerprint,\n recordedByProfileId: input.recordedByProfileId,\n recordedAt: new Date().toISOString(),\n ...(refundFingerprint ? { refundFingerprint } : {}),\n };\n}\nexport function createSupplierPayable(\n expense: PayableExpense,\n input: RecordSupplierPayableInput,\n refunds: PayableExpense[],\n): SupplierPayableState {\n if (!expense.id || expense.amount <= 0 || expense.refundOfId)\n payableInvalid('requires a persisted positive non-refund Expense.');\n review(input, expense);\n source(input.source);\n if (input.payableBasisMinor !== null) {\n minor(input.payableBasisMinor);\n if (input.payableBasisMinor > expense.amount)\n payableInvalid('basis exceeds original gross.');\n }\n if (!Array.isArray(input.events))\n payableInvalid('original events must be explicit.');\n const fingerprint = payableDigest(input);\n const state: SupplierPayableState = {\n version: 1,\n expenseId: expense.id,\n currency: expense.currency,\n expenseFingerprint: payableExpenseFingerprint(expense),\n original: {\n id: randomUUID(),\n requestId: input.requestId,\n requestFingerprint: fingerprint,\n source: input.source,\n payableBasisMinor: input.payableBasisMinor,\n recordedByProfileId: input.recordedByProfileId,\n evidenceRef: input.evidenceRef,\n recordedAt: new Date().toISOString(),\n },\n completion: null,\n events: input.events.map((event) =>\n bindEvent(event, expense, refunds, true, input, fingerprint),\n ),\n };\n supplierPayablePosition(expense, state, refunds);\n return state;\n}\nexport function appendSupplierPayableEvent(\n expense: PayableExpense,\n state: SupplierPayableState,\n input: RecordSupplierPayableEventInput,\n refunds: PayableExpense[],\n): SupplierPayableState {\n review(input, expense);\n const fingerprint = payableDigest(input);\n const existing = state.events.find(\n (event) => event.requestId === input.requestId,\n );\n if (\n existing ||\n state.original.requestId === input.requestId ||\n state.completion?.requestId === input.requestId\n ) {\n if (\n existing?.authority === 'current' &&\n existing.requestFingerprint === fingerprint\n )\n return state;\n return payableInvalid(\n 'request identity conflicts with retained authority.',\n );\n }\n const before = supplierPayablePosition(expense, state, refunds);\n if (\n input.event?.kind === 'release' &&\n before.unresolvedRefundExpenseIds.length\n )\n payableInvalid('release requires every linked refund interpretation.');\n if (input.event?.kind === 'settlement' && before.unknownReasons.length)\n payableInvalid(\n 'settlement requires known basis and every linked refund interpretation.',\n );\n const next = {\n ...state,\n events: [\n ...state.events,\n bindEvent(input.event, expense, refunds, false, input, fingerprint),\n ],\n };\n supplierPayablePosition(expense, next, refunds);\n return next;\n}\nexport function completeSupplierPayable(\n expense: PayableExpense,\n state: SupplierPayableState,\n input: CompleteSupplierPayableBasisInput,\n refunds: PayableExpense[],\n): SupplierPayableState {\n review(input, expense);\n minor(input.payableBasisMinor);\n const fingerprint = payableDigest(input);\n if (\n state.completion?.requestId === input.requestId &&\n state.completion.requestFingerprint === fingerprint\n )\n return state;\n if (\n state.original.payableBasisMinor !== null ||\n state.completion ||\n state.original.requestId === input.requestId ||\n state.events.some((event) => event.requestId === input.requestId)\n )\n payableInvalid('original basis or its explicit completion is immutable.');\n if (input.payableBasisMinor > expense.amount)\n payableInvalid('basis exceeds original gross.');\n const next = {\n ...state,\n completion: {\n ...input,\n id: randomUUID(),\n requestFingerprint: fingerprint,\n recordedAt: new Date().toISOString(),\n },\n };\n supplierPayablePosition(expense, next, refunds);\n return next;\n}\n/** Revalidates every historical prefix, even when projecting an earlier date. */\nexport function supplierPayablePosition(\n expense: PayableExpense,\n state: SupplierPayableState,\n refunds: PayableExpense[],\n asOfDate?: string,\n): SupplierPayablePosition {\n if (asOfDate)\n assertIsoDate(\n 'Expense',\n String(expense.id),\n 'supplier payable as-of date',\n asOfDate,\n );\n if (\n state.expenseId !== expense.id ||\n state.currency !== expense.currency ||\n state.expenseFingerprint !== payableExpenseFingerprint(expense)\n )\n payableInvalid('canonical Expense authority changed.');\n const basis =\n state.completion?.payableBasisMinor ?? state.original.payableBasisMinor;\n if (basis !== null) {\n minor(basis);\n if (basis > expense.amount) payableInvalid('basis exceeds original gross.');\n }\n const tuples = new Set<string>(),\n originalKeys = new Set<string>(),\n refundIds = new Set<string>();\n let held = 0n,\n released = 0n,\n retainedReduction = 0n,\n settled = 0n,\n cashReturned = 0n,\n reduction = 0n;\n let projected = {\n held,\n released,\n retainedReduction,\n settled,\n cashReturned,\n reduction,\n };\n for (const event of [...state.events].sort(\n (a, b) =>\n a.effectiveOn.localeCompare(b.effectiveOn) || a.sequence - b.sequence,\n )) {\n validateEvent(event, expense, event.authority === 'original');\n const tuple = `${event.effectiveOn}:${event.sequence}`;\n if (tuples.has(tuple))\n payableInvalid('same-day chronology requires unique explicit sequence.');\n tuples.add(tuple);\n if (event.source) {\n const key = payableDigest([event.source.namespace, event.source.key]);\n if (originalKeys.has(key))\n payableInvalid('duplicate original event identity.');\n originalKeys.add(key);\n }\n if (event.kind === 'hold') held += BigInt(event.amountMinor);\n else if (event.kind === 'release') released += BigInt(event.amountMinor);\n else if (event.kind === 'settlement') settled += BigInt(event.amountMinor);\n else if (event.kind === 'refund') {\n if (refundIds.has(event.refundExpenseId))\n payableInvalid('refund already has a retained interpretation.');\n refundIds.add(event.refundExpenseId);\n const refund = refunds.find((row) => row.id === event.refundExpenseId);\n if (\n !refund ||\n event.refundFingerprint !== payableExpenseFingerprint(refund)\n )\n payableInvalid('interpreted refund authority changed or disappeared.');\n if (\n event.payableReductionMinor > -refund.amount ||\n event.cashReturnedMinor > -refund.amount\n )\n payableInvalid('refund interpretation exceeds source magnitude.');\n reduction += BigInt(event.payableReductionMinor);\n retainedReduction += BigInt(event.retainedReductionMinor);\n cashReturned += BigInt(event.cashReturnedMinor);\n }\n const retained = held - released - retainedReduction,\n paid = settled - cashReturned,\n obligation = BigInt(basis ?? expense.amount) - reduction;\n if (\n retained < 0n ||\n paid < 0n ||\n obligation < 0n ||\n obligation - paid - retained < 0n\n )\n payableInvalid('dated event exceeds retained, paid or payable capacity.');\n for (const value of [\n held,\n released,\n retainedReduction,\n settled,\n cashReturned,\n reduction,\n ])\n if (value > BigInt(Number.MAX_SAFE_INTEGER))\n payableInvalid('unsafe aggregate money.');\n if (!asOfDate || event.effectiveOn <= asOfDate)\n projected = {\n held,\n released,\n retainedReduction,\n settled,\n cashReturned,\n reduction,\n };\n }\n const unresolved = refunds\n .filter(\n (row) =>\n (!asOfDate || row.incurredOn <= asOfDate) &&\n !refundIds.has(String(row.id)),\n )\n .map((row) => String(row.id));\n const unknownReasons: SupplierPayablePosition['unknownReasons'] = [];\n if (basis === null) unknownReasons.push('payable-basis-unknown');\n if (unresolved.length) unknownReasons.push('refund-interpretation-required');\n const p = projected,\n retained = p.held - p.released - p.retainedReduction,\n paid = p.settled - p.cashReturned;\n return {\n expenseId: state.expenseId,\n currency: state.currency,\n asOfDate: asOfDate ?? null,\n payableBasisMinor: basis,\n heldMinor: Number(p.held),\n releasedMinor: Number(p.released),\n retainedReductionMinor: Number(p.retainedReduction),\n recordedRetainedMinor: Number(retained),\n retainedMinor: unresolved.length ? null : Number(retained),\n settledMinor: Number(p.settled),\n cashReturnedMinor: Number(p.cashReturned),\n netPaidMinor: unresolved.length ? null : Number(paid),\n payableReductionMinor: Number(p.reduction),\n remainingPayableMinor: unknownReasons.length\n ? null\n : Number(BigInt(basis!) - p.reduction - paid - retained),\n unknownReasons,\n unresolvedRefundExpenseIds: unresolved,\n authority: state,\n };\n}\n","/**\n * Expense — a cost someone incurred, against a cost object, with a review\n * state and optional reimbursement and commitment links.\n *\n * @packageDocumentation\n */\n\nimport {\n ContractLineItemCollection,\n PurchaseContractCollection,\n VendorCollection,\n} from '@happyvertical/smrt-commerce';\nimport {\n crossPackageRef,\n field,\n foreignKey,\n isPostgresDatabase,\n isUniqueViolationError,\n ObjectRegistry,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport {\n expenseCostBasis,\n parseExpenseFinancialBreakdown,\n} from '../financial-facts.js';\nimport { calculateRefundPosition } from '../refund-position.js';\nimport {\n calculateReimbursementPosition,\n parseRefundRecipient,\n} from '../reimbursement-position.js';\nimport {\n appendSupplierPayableEvent,\n type CompleteSupplierPayableBasisInput,\n completeSupplierPayable,\n createSupplierPayable,\n parseSupplierPayable,\n payableDigest,\n payableExpenseFingerprint,\n payableInvalid,\n type RecordSupplierPayableEventInput,\n type RecordSupplierPayableInput,\n type SupplierPayablePosition,\n type SupplierPayableState,\n supplierPayablePosition,\n} from '../supplier-payable.js';\nimport {\n type ExpenseCostBasis,\n ExpenseError,\n type ExpenseFinancialBreakdown,\n type ExpenseOptions,\n type ExpensePayer,\n type ExpenseRefundPosition,\n type ExpenseRefundRecipient,\n type ExpenseReimbursementPosition,\n type ExpenseReviewStatus,\n type MarkDuplicateExpenseInput,\n type MarkReimbursedInput,\n type RejectExpenseInput,\n type ReopenExpenseInput,\n type ReviewExpenseInput,\n} from '../types.js';\nimport {\n assertIsoCurrency,\n assertIsoDate,\n assertMinorUnits,\n assertQualifiedClassName,\n CATEGORY_PATTERN,\n effectiveTenant,\n identityConflict,\n instantMs,\n normalizeTenantId,\n pinNaturalKey,\n} from '../validation.js';\n\nconst authorizedPayable = new WeakMap<Expense, string>();\n\nconst PROFILE = '@happyvertical/smrt-profiles:Profile';\n\n/**\n * Legal review moves, keyed by the persisted status. Only the dedicated\n * methods perform them; `save()` refuses a review-field change they did not\n * authorize.\n */\nconst REVIEW_TRANSITIONS: Record<\n 'review' | 'reject' | 'markDuplicate' | 'reopen',\n { from: readonly ExpenseReviewStatus[]; to: ExpenseReviewStatus }\n> = {\n review: { from: ['unreviewed'], to: 'reviewed' },\n reject: { from: ['unreviewed', 'reviewed'], to: 'rejected' },\n markDuplicate: { from: ['unreviewed', 'reviewed'], to: 'rejected' },\n reopen: { from: ['reviewed', 'rejected'], to: 'unreviewed' },\n};\n\n/**\n * Fields a reviewed expense freezes. Changing one needs `reopen()` first, so\n * a reviewed amount can never move without a fresh review.\n */\nconst LOCKED_WHEN_REVIEWED = [\n ['amount', 'amount'],\n ['financialBreakdown', 'financial_breakdown'],\n ['paidByName', 'paid_by_name'],\n ['refundOfId', 'refund_of_id'],\n ['refundRecipient', 'refund_recipient'],\n ['currency', 'currency'],\n ['incurredOn', 'incurred_on'],\n ['recordedAt', 'recorded_at'],\n ['category', 'category'],\n ['costObjectType', 'cost_object_type'],\n ['costObjectId', 'cost_object_id'],\n ['vendorId', 'vendor_id'],\n ['commitmentId', 'commitment_id'],\n ['commitmentLineId', 'commitment_line_id'],\n ['paidBy', 'paid_by'],\n ['paidByProfileId', 'paid_by_profile_id'],\n] as const;\n\n/** The review transition a dedicated method has authorized for one save. */\nconst authorizedTransition = new WeakMap<\n Expense,\n { from: ExpenseReviewStatus; expected: ReviewFieldState }\n>();\n\nconst authorizedReimbursement = new WeakMap<\n Expense,\n { at: number; amount: number; reference: string }\n>();\n\n/** Every review field, normalized for comparison. */\ninterface ReviewFieldState {\n status: ExpenseReviewStatus;\n reviewer: string | null;\n reviewedAtMs: number | null;\n note: string;\n duplicateOf: string | null;\n}\n\nfunction sameReviewState(a: ReviewFieldState, b: ReviewFieldState): boolean {\n return (\n a.status === b.status &&\n a.reviewer === b.reviewer &&\n a.reviewedAtMs === b.reviewedAtMs &&\n a.note === b.note &&\n a.duplicateOf === b.duplicateOf\n );\n}\n\ntype PersistedRow = Record<string, unknown>;\n\nfunction asNullableString(value: unknown): string | null {\n return value === null || value === undefined || value === ''\n ? null\n : String(value);\n}\n\n/**\n * A cost someone incurred.\n *\n * - **Cost object** — polymorphic `(costObjectType, costObjectId)`: a job, a\n * work package, a project, any registered SMRT object. Exactly one per\n * expense, so a cost is never rolled up into two jobs.\n * - **Money** — `amount` in integer minor units with an ISO 4217 `currency`.\n * - **Dates** — `incurredOn` (calendar date, `YYYY-MM-DD`) is when the cost\n * happened; `recordedAt` is when it was entered.\n * - **Review** — `unreviewed → reviewed | rejected`, only through\n * {@link Expense.review}, {@link Expense.reject},\n * {@link Expense.markDuplicate} and {@link Expense.reopen}. A plain field\n * write to the review state is refused on save.\n * - **Reimbursement** — recording only: whether a person who paid is owed\n * back and when they were paid. Never changes review state.\n * - **Commitment** — optional commerce `Contract` (usually a `PurchaseOrder`)\n * or one of its lines that this actual draws down.\n *\n * The generated CRUD surface is closed: cost visibility is permission-gated\n * by every consumer, and generated reads check no read permission, so hosts\n * list expenses through their own routes. The only generated writes are the\n * service-backed actions on {@link ExpenseCollection} (#4001); the runtime\n * REST transport gates a collection-hosted action on this item allowlist.\n * Their names differ from this class's own methods, so allowlisting them never\n * opens `review()` or `markReimbursed()`, which take the actor as input.\n */\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'expenses',\n api: {\n include: ['record', 'attachReceipt', 'recordReview', 'recordReimbursement'],\n },\n mcp: { include: [] },\n cli: false,\n indexes: [\n {\n name: 'expenses_cost_object_idx',\n columns: ['tenantId', 'costObjectType', 'costObjectId', 'incurredOn'],\n },\n {\n name: 'expenses_commitment_review_idx',\n columns: ['tenantId', 'commitmentId', 'reviewStatus'],\n },\n {\n name: 'expenses_review_status_idx',\n columns: ['tenantId', 'reviewStatus', 'incurredOn'],\n },\n ],\n})\nexport class Expense extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-expenses\";\n /** Owning tenant. Nullable: optional tenancy, like every commerce model. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Qualified class of the cost object, e.g.\n * `@happyvertical/smrt-projects:Project`. Empty only for an unallocated\n * expense (then `costObjectId` is empty too).\n */\n @field({ type: 'text' })\n costObjectType: string = '';\n\n /** Id of the cost object. Bare string: the target table is polymorphic. */\n @field({ type: 'text' })\n costObjectId: string = '';\n\n /**\n * Lowercase kebab-case category. See `SUGGESTED_EXPENSE_CATEGORIES`;\n * any value matching the pattern is accepted.\n */\n @field({ type: 'text' })\n category: string = 'other';\n\n /** What was bought or paid for. */\n @field({ type: 'text' })\n description: string = '';\n\n /** Integer minor units; negative only for a refund linked by refundOfId. */\n @field({ type: 'integer' })\n amount: number = 0;\n\n /** ISO 4217 currency code, upper case. Required. */\n @field({ type: 'text' })\n currency: string = '';\n\n /** Optional supplier facts as guarded JSON text; null or empty means unknown. */\n @field({ type: 'text' })\n financialBreakdown: string | null = null;\n\n /** Command-owned immutable supplier payable/retention evidence; no cash execution. */\n @field({ type: 'text', nullable: true, readonly: true })\n supplierPayableJson: string | null = null;\n\n /** Name of the third party who paid; no employee reimbursement is implied. */\n @field({ type: 'text' })\n paidByName: string | null = null;\n\n /** Positive source expense reversed by this negative refund; no settlement. */\n @foreignKey('Expense', { onDelete: 'RESTRICT' })\n refundOfId: string | null = null;\n\n /** Explicit cash recipient of a refund; absent legacy direction is unknown. */\n @field({ type: 'text' })\n refundRecipient: string | null = null;\n\n /** Calendar date the cost was incurred, `YYYY-MM-DD`. Required. */\n @field({ type: 'text' })\n incurredOn: string = '';\n\n /** When the expense was recorded; set on first save when empty. */\n @field({ type: 'datetime' })\n recordedAt: Date | null = null;\n\n /** Profile that recorded the expense. */\n @crossPackageRef(PROFILE)\n recordedByProfileId: string | null = null;\n\n /** Company, named person (profile), or named third party (paidByName). */\n @field({ type: 'text' })\n paidBy: ExpensePayer = 'company';\n\n /** Profile that paid out of pocket; required when `paidBy` is `person`. */\n @crossPackageRef(PROFILE)\n paidByProfileId: string | null = null;\n\n /** Optional commerce vendor the cost was paid to. */\n @crossPackageRef('@happyvertical/smrt-commerce:Vendor')\n vendorId: string | null = null;\n\n /**\n * Optional purchase commitment this actual draws down (normally a\n * `PurchaseOrder`). Must be in the same currency and tenant.\n */\n @crossPackageRef('@happyvertical/smrt-commerce:PurchaseContract', {\n validateSubtype: true,\n })\n commitmentId: string | null = null;\n\n /** Optional line of {@link Expense.commitmentId} this actual draws down. */\n @crossPackageRef('@happyvertical/smrt-commerce:ContractLineItem')\n commitmentLineId: string | null = null;\n\n /** Review state. Changed only by the review methods. */\n @field({ type: 'text', readonly: true })\n reviewStatus: ExpenseReviewStatus = 'unreviewed';\n\n /** Profile that made the current review decision. */\n @crossPackageRef(PROFILE, { readonly: true })\n reviewedByProfileId: string | null = null;\n\n /** When the current review decision was made. */\n @field({ type: 'datetime', readonly: true })\n reviewedAt: Date | null = null;\n\n /** Review note or rejection reason. */\n @field({ type: 'text', readonly: true })\n reviewNote: string = '';\n\n /**\n * Set when a reviewer marked this expense a duplicate of another one. A\n * duplicate is `rejected` and never counts toward any total.\n */\n @foreignKey('Expense', { readonly: true, onDelete: 'SET NULL' })\n duplicateOfId: string | null = null;\n\n /** Whether the person who paid is owed the amount back. */\n @field({ type: 'boolean' })\n reimbursable: boolean = false;\n\n /** Exact historical reimbursement cash; null on legacy payment markers. */\n @field({ type: 'integer', nullable: true, readonly: true })\n reimbursedAmount: number | null = null;\n\n /** When the person was paid back; set by {@link Expense.markReimbursed}. */\n @field({ type: 'datetime', readonly: true })\n reimbursedAt: Date | null = null;\n\n /** Payroll line, payables reference, or transfer id of the payback. */\n @field({ type: 'text', readonly: true })\n reimbursementReference: string = '';\n\n constructor(options: ExpenseOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.costObjectType !== undefined)\n this.costObjectType = options.costObjectType;\n if (options.costObjectId !== undefined)\n this.costObjectId = options.costObjectId;\n if (options.category !== undefined) this.category = options.category;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.amount !== undefined) this.amount = options.amount;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.financialBreakdown !== undefined)\n this.financialBreakdown = options.financialBreakdown;\n if (options.paidByName !== undefined) this.paidByName = options.paidByName;\n if (options.supplierPayableJson !== undefined)\n this.supplierPayableJson = options.supplierPayableJson;\n if (options.refundOfId !== undefined) this.refundOfId = options.refundOfId;\n if (options.incurredOn !== undefined) this.incurredOn = options.incurredOn;\n if (options.recordedAt !== undefined) this.recordedAt = options.recordedAt;\n if (options.recordedByProfileId !== undefined)\n this.recordedByProfileId = options.recordedByProfileId;\n if (options.paidBy !== undefined) this.paidBy = options.paidBy;\n if (options.paidByProfileId !== undefined)\n this.paidByProfileId = options.paidByProfileId;\n if (options.vendorId !== undefined) this.vendorId = options.vendorId;\n if (options.commitmentId !== undefined)\n this.commitmentId = options.commitmentId;\n if (options.commitmentLineId !== undefined)\n this.commitmentLineId = options.commitmentLineId;\n if (options.reimbursable !== undefined)\n this.reimbursable = options.reimbursable;\n if (options.refundRecipient !== undefined)\n this.refundRecipient = options.refundRecipient;\n if (options.reimbursedAmount !== undefined)\n this.reimbursedAmount = options.reimbursedAmount;\n if (options.reimbursedAt !== undefined)\n this.reimbursedAt = options.reimbursedAt;\n if (options.reimbursementReference !== undefined)\n this.reimbursementReference = options.reimbursementReference;\n }\n\n /** Read validated supplier facts, or null when none were recorded. */\n getFinancialBreakdown(): ExpenseFinancialBreakdown | null {\n return parseExpenseFinancialBreakdown(\n this.financialBreakdown,\n this.amount,\n this.currency,\n );\n }\n\n /** Set complete supplier facts; null explicitly clears facts on an unreviewed row. */\n setFinancialBreakdown(value: ExpenseFinancialBreakdown | null): void {\n if (value === null) {\n this.financialBreakdown = null;\n return;\n }\n expenseCostBasis(value, this.amount, this.currency);\n this.financialBreakdown = JSON.stringify(value);\n }\n\n /** Explicit recipient, independent of this expense's payer facts. */\n getRefundRecipient(): ExpenseRefundRecipient | null {\n return parseRefundRecipient(this.refundRecipient);\n }\n\n setRefundRecipient(value: ExpenseRefundRecipient | null): void {\n const stored = value === null ? null : JSON.stringify(value);\n parseRefundRecipient(stored);\n this.refundRecipient = stored;\n }\n\n /** Return exact retained purchase cost; legacy unknown tax facts return null. */\n getCostBasis(): ExpenseCostBasis | null {\n const facts = this.getFinancialBreakdown();\n return facts === null\n ? null\n : expenseCostBasis(facts, this.amount, this.currency);\n }\n\n /** True when the person who paid has been paid back. */\n isReimbursed(): boolean {\n return this.reimbursedAt !== null && this.reimbursedAt !== undefined;\n }\n\n /** True when this expense counts toward reviewed totals and drawdown. */\n isCountable(): boolean {\n return this.reviewStatus === 'reviewed' && !this.duplicateOfId;\n }\n\n /**\n * Accept the expense as a real cost: `unreviewed → reviewed`.\n *\n * This is the separately authorizable review action: gate it in your route\n * or service (for example on `expenses.review`). The expense must already\n * be saved and must not be marked a duplicate.\n *\n * @throws {ExpenseError} `EXPENSE_REVIEW_TRANSITION` from any other state.\n */\n async review(input: ReviewExpenseInput): Promise<this> {\n const reviewer = requireReviewer(input?.reviewerProfileId);\n return this.transition('review', () => {\n this.reviewStatus = 'reviewed';\n this.reviewedByProfileId = reviewer;\n this.reviewedAt = input.at ?? new Date();\n this.reviewNote = input.note ?? '';\n this.duplicateOfId = null;\n });\n }\n\n /**\n * Refuse the expense: `unreviewed | reviewed → rejected`. A rejected\n * expense is kept and shown, and never counted.\n *\n * @throws {ExpenseError} `EXPENSE_REVIEW_TRANSITION` from `rejected`, or\n * `EXPENSE_INVALID` without a reason.\n */\n async reject(input: RejectExpenseInput): Promise<this> {\n const reviewer = requireReviewer(input?.reviewerProfileId);\n const reason = (input.reason ?? '').trim();\n if (!reason) {\n throw new ExpenseError('EXPENSE_INVALID', 'A rejection needs a reason.');\n }\n return this.transition('reject', () => {\n this.reviewStatus = 'rejected';\n this.reviewedByProfileId = reviewer;\n this.reviewedAt = input.at ?? new Date();\n this.reviewNote = reason;\n this.duplicateOfId = null;\n });\n }\n\n /**\n * Mark this expense a duplicate of another expense in the same tenant:\n * `unreviewed | reviewed → rejected` with {@link Expense.duplicateOfId}\n * set. A duplicate never counts toward any total.\n *\n * @throws {ExpenseError} `EXPENSE_INVALID` when the target is this expense\n * or is not visible in this expense's tenant.\n */\n async markDuplicate(input: MarkDuplicateExpenseInput): Promise<this> {\n const reviewer = requireReviewer(input?.reviewerProfileId);\n const targetId = String(input.duplicateOfId ?? '').trim();\n if (!targetId || targetId === this.id) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'An expense can only be a duplicate of a different expense.',\n );\n }\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const target = await expenses.get({ id: targetId });\n if (!target || (target.tenantId ?? null) !== (this.tenantId ?? null)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${targetId} was not found in this expense's tenant.`,\n );\n }\n return this.transition('markDuplicate', () => {\n this.reviewStatus = 'rejected';\n this.reviewedByProfileId = reviewer;\n this.reviewedAt = input.at ?? new Date();\n this.reviewNote = `Duplicate of expense ${targetId}`;\n this.duplicateOfId = targetId;\n });\n }\n\n /**\n * Send the expense back for review: `reviewed | rejected → unreviewed`.\n * Clears the reviewer, the review time and any duplicate marker.\n *\n * @throws {ExpenseError} `EXPENSE_REVIEW_TRANSITION` from `unreviewed`.\n */\n async reopen(input: ReopenExpenseInput): Promise<this> {\n requireReviewer(input?.reviewerProfileId);\n return this.transition('reopen', () => {\n this.reviewStatus = 'unreviewed';\n this.reviewedByProfileId = null;\n this.reviewedAt = null;\n this.reviewNote = input.note ?? '';\n this.duplicateOfId = null;\n });\n }\n\n /**\n * Record that the person who paid has been paid back. Recording only:\n * paying happens in payroll or payables, and this never changes the review\n * state.\n *\n * @throws {ExpenseError} `EXPENSE_INVALID` unless the expense is\n * `reimbursable`.\n */\n async markReimbursed(input: MarkReimbursedInput = {}): Promise<this> {\n return this.withTransaction(async () => {\n await this.lockRefundRows();\n const persisted = await this.readPersisted();\n if (!persisted || persisted.reimbursed_at || this.reimbursedAt) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Reimbursement requires a persisted source without an existing payment marker.',\n );\n }\n for (const [field, column] of [\n ['amount', 'amount'],\n ['currency', 'currency'],\n ['paidBy', 'paid_by'],\n ['paidByProfileId', 'paid_by_profile_id'],\n ['reimbursable', 'reimbursable'],\n ['refundOfId', 'refund_of_id'],\n ] as const) {\n if (\n field === 'reimbursable'\n ? Boolean(this[field]) !== Boolean(persisted[column])\n : String(this[field] ?? '') !== String(persisted[column] ?? '')\n )\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Save source corrections separately before recording reimbursement.',\n );\n }\n const position = await this.getReimbursementPosition();\n if (\n position.remainingAmount === null ||\n position.remainingAmount <= 0 ||\n position.owedToCompany !== 0 ||\n (input.amount !== undefined &&\n input.amount !== position.remainingAmount)\n ) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Record only the exact full known remaining reimbursement; unknown, partial, or settled claims require explicit resolution.',\n );\n }\n const at = input.at ?? new Date();\n if (!(at instanceof Date) || !Number.isFinite(at.getTime()))\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Reimbursement date must be valid.',\n );\n const previous = {\n at: this.reimbursedAt,\n amount: this.reimbursedAmount,\n reference: this.reimbursementReference,\n };\n this.reimbursedAt = at;\n this.reimbursedAmount = position.remainingAmount;\n this.reimbursementReference = input.reference ?? '';\n authorizedReimbursement.set(this, {\n at: at.getTime(),\n amount: position.remainingAmount,\n reference: this.reimbursementReference,\n });\n try {\n return await this.save();\n } catch (error) {\n this.reimbursedAt = previous.at;\n this.reimbursedAmount = previous.amount;\n this.reimbursementReference = previous.reference;\n throw error;\n } finally {\n authorizedReimbursement.delete(this);\n }\n });\n }\n\n /**\n * Load the cost object this expense is charged to, through its registered\n * collection (so tenant interceptors apply). Returns `null` when unset,\n * when the type is not registered, when the row is gone, or when the row\n * belongs to a different tenant than this expense.\n */\n async getCostObject<T extends SmrtObject = SmrtObject>(): Promise<T | null> {\n if (!this.costObjectType || !this.costObjectId) return null;\n let registered = ObjectRegistry.getClassByQualifiedName(\n this.costObjectType,\n );\n if (!registered) {\n const loaded = await ObjectRegistry.tryLoadFromExternalPackage(\n this.costObjectType,\n );\n if (!loaded) return null;\n registered = ObjectRegistry.getClassByQualifiedName(this.costObjectType);\n }\n if (!registered) return null;\n const collection = await ObjectRegistry.getCollection<SmrtObject>(\n registered.qualifiedName ?? this.costObjectType,\n { db: this.db },\n );\n const target = await collection.get({ id: this.costObjectId });\n if (!target) return null;\n const targetTenant = (target as unknown as { tenantId?: string | null })\n .tenantId;\n if (\n targetTenant !== undefined &&\n targetTenant !== null &&\n this.tenantId !== null &&\n targetTenant !== this.tenantId\n ) {\n return null;\n }\n return target as T;\n }\n\n /**\n * Validate, enforce the review and reviewed-field locks, check the\n * commitment, then persist.\n */\n override async save(): Promise<this> {\n this.normalize();\n this.validateShape();\n return this.withTransaction(() => this.saveValidated());\n }\n\n /** Keep both directions of refund validation and the write on one executor. */\n private async saveValidated(): Promise<this> {\n await this.lockRefundRows();\n const persisted = await this.readPersisted();\n // The guards below check `persisted`; pin the write to that row.\n const inserting = await pinNaturalKey('Expense', this, persisted);\n if (\n persisted &&\n String(persisted.tenant_id ?? '') !==\n String(effectiveTenant(this.tenantId) ?? '')\n ) {\n // Includes the quiet case: saving a global expense inside a tenant\n // context, where the interceptor would fill in that tenant and split\n // the expense from its global receipts and links.\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${this.id}: its tenant cannot change after it is recorded.`,\n );\n }\n await this.assertSupplierPayable(persisted);\n this.assertReviewFields(persisted);\n this.assertReviewedLock(persisted);\n await this.assertCommitment(persisted);\n await this.assertVendor(persisted);\n this.assertReimbursementFields(persisted);\n await this.assertInboundRefunds(persisted);\n await this.assertRefund(persisted);\n\n if (!this.recordedAt) this.recordedAt = new Date();\n try {\n return await super.save();\n } catch (error) {\n // A new expense is a plain INSERT; its only unique keys are the id and\n // the natural key, so a violation means it named an existing row.\n if (inserting && isUniqueViolationError(error)) {\n throw identityConflict('Expense', this, error);\n }\n throw error;\n }\n }\n\n /** Record original supplier authority without creating approval, cash or journals. */\n async recordSupplierPayable(\n input: RecordSupplierPayableInput,\n ): Promise<SupplierPayablePosition> {\n return this.mutateSupplierPayable((source, state, refunds) => {\n if (state) {\n if (\n state.original.requestId === input.requestId &&\n state.original.requestFingerprint === payableDigest(input)\n )\n return state;\n return payableInvalid('original supplier authority is immutable.');\n }\n return createSupplierPayable(source, input, refunds);\n });\n }\n\n /** Append explicit dated hold, release, supplier settlement or refund interpretation. */\n async recordSupplierPayableEvent(\n input: RecordSupplierPayableEventInput,\n ): Promise<SupplierPayablePosition> {\n return this.mutateSupplierPayable((source, state, refunds) => {\n if (!state)\n return payableInvalid('original supplier authority is required.');\n return appendSupplierPayableEvent(source, state, input, refunds);\n });\n }\n\n /** Complete a previously unknown basis once with independent current review evidence. */\n async completeSupplierPayableBasis(\n input: CompleteSupplierPayableBasisInput,\n ): Promise<SupplierPayablePosition> {\n return this.mutateSupplierPayable((source, state, refunds) => {\n if (!state)\n return payableInvalid('original supplier authority is required.');\n return completeSupplierPayable(source, state, input, refunds);\n });\n }\n\n /** Read exact dated supplier position; null means no authority exists or the source is not yet effective. */\n async getSupplierPayablePosition(\n input: { asOfDate?: string } = {},\n ): Promise<SupplierPayablePosition | null> {\n return this.withTransaction(async () => {\n this.assertSupplierPayableContext();\n await this.lockRefundRows();\n const source = await this.loadSupplierPayableSource();\n const state = parseSupplierPayable(source.supplierPayableJson);\n if (!state) return null;\n const position = supplierPayablePosition(\n source,\n state,\n await source.supplierRefunds(),\n input.asOfDate,\n );\n return input.asOfDate && input.asOfDate < source.incurredOn\n ? null\n : position;\n });\n }\n\n private assertSupplierPayableContext(): void {\n const active = getTenantId();\n if (active && this.tenantId && active !== this.tenantId)\n payableInvalid('active tenant must own the supplier source.');\n }\n\n private async loadSupplierPayableSource(): Promise<Expense> {\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const source = await (await ExpenseCollection.create({ db: this.db })).get(\n { id: this.id, tenantId: effectiveTenant(this.tenantId) },\n { cache: false },\n );\n if (!source || source.amount <= 0 || source.refundOfId)\n return payableInvalid('requires an owned persisted positive source.');\n return source;\n }\n\n private async supplierRefunds(): Promise<Expense[]> {\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const collection = await ExpenseCollection.create({ db: this.db });\n const all: Expense[] = [];\n for (let offset = 0; ; offset += 200) {\n const rows = await collection.list({\n where: {\n refundOfId: this.id,\n tenantId: effectiveTenant(this.tenantId),\n },\n orderBy: ['id ASC'],\n limit: 200,\n offset,\n cache: false,\n });\n all.push(...rows);\n if (rows.length < 200) return all;\n }\n }\n\n private async mutateSupplierPayable(\n change: (\n source: Expense,\n state: SupplierPayableState | null,\n refunds: Expense[],\n ) => SupplierPayableState,\n ): Promise<SupplierPayablePosition> {\n return this.withTransaction(async () => {\n this.assertSupplierPayableContext();\n await this.lockRefundRows();\n const source = await this.loadSupplierPayableSource(),\n refunds = await source.supplierRefunds();\n const state = change(\n source,\n parseSupplierPayable(source.supplierPayableJson),\n refunds,\n );\n const position = supplierPayablePosition(source, state, refunds),\n stored = JSON.stringify(state);\n if (stored !== source.supplierPayableJson) {\n source.supplierPayableJson = stored;\n authorizedPayable.set(source, stored);\n try {\n await source.save();\n } finally {\n authorizedPayable.delete(source);\n }\n }\n return position;\n });\n }\n\n private async assertSupplierPayable(\n persisted: PersistedRow | null,\n ): Promise<void> {\n const prior = asNullableString(persisted?.supplier_payable_json),\n next = asNullableString(this.supplierPayableJson);\n if (prior !== next && (!next || authorizedPayable.get(this) !== next))\n payableInvalid('supplier authority is command-owned.');\n const state = parseSupplierPayable(next);\n if (state)\n supplierPayablePosition(this, state, await this.supplierRefunds());\n const oldParent = asNullableString(persisted?.refund_of_id);\n if (oldParent) {\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const parent = await (\n await ExpenseCollection.create({ db: this.db })\n ).get(\n { id: oldParent, tenantId: effectiveTenant(this.tenantId) },\n { cache: false },\n );\n if (parent?.supplierPayableJson) {\n for (const [field, column] of LOCKED_WHEN_REVIEWED) {\n const old = persisted?.[column],\n current = this[field];\n const equal =\n field === 'recordedAt'\n ? instantMs(old) === instantMs(current)\n : String(old ?? '') === String(current ?? '');\n if (!equal)\n payableInvalid(\n 'linked supplier refund source facts are immutable; reconcile with new evidence.',\n );\n }\n }\n }\n }\n\n /** Managed supplier evidence cannot be erased through ordinary model/collection deletion. */\n override async delete(\n options: { expectedUpdatedAt?: Date | string } = {},\n ): Promise<void> {\n return this.withTransaction(async () => {\n this.assertSupplierPayableContext();\n await this.lockRefundRows();\n const row = await this.readPersisted();\n if (\n row &&\n String(row.tenant_id ?? '') !==\n String(effectiveTenant(this.tenantId) ?? '')\n )\n payableInvalid('owned source is required.');\n if (row?.supplier_payable_json)\n payableInvalid('supplier authority cannot be deleted.');\n const parentId = asNullableString(row?.refund_of_id);\n if (parentId) {\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const parent = await (\n await ExpenseCollection.create({ db: this.db })\n ).get(\n { id: parentId, tenantId: effectiveTenant(this.tenantId) },\n { cache: false },\n );\n if (parent?.supplierPayableJson)\n payableInvalid('supplier refund evidence cannot be deleted.');\n }\n await super.delete(options);\n });\n }\n\n private normalize(): void {\n this.tenantId = normalizeTenantId(this.tenantId);\n this.currency = String(this.currency ?? '')\n .trim()\n .toUpperCase();\n this.category = String(this.category ?? '').trim();\n this.costObjectType = String(this.costObjectType ?? '').trim();\n this.costObjectId = String(this.costObjectId ?? '').trim();\n this.incurredOn = String(this.incurredOn ?? '').trim();\n this.vendorId = asNullableString(this.vendorId);\n this.commitmentId = asNullableString(this.commitmentId);\n this.commitmentLineId = asNullableString(this.commitmentLineId);\n this.paidByProfileId = asNullableString(this.paidByProfileId);\n this.recordedByProfileId = asNullableString(this.recordedByProfileId);\n }\n\n private validateShape(): void {\n const label = this.id || '<new>';\n assertMinorUnits(\n 'Expense',\n label,\n 'amount',\n this.refundOfId && typeof this.amount === 'number'\n ? Math.abs(this.amount)\n : this.amount,\n );\n assertIsoCurrency('Expense', label, this.currency);\n assertIsoDate('Expense', label, 'incurredOn', this.incurredOn);\n if (!CATEGORY_PATTERN.test(this.category) || this.category.length > 64) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: category must be lowercase kebab-case ` +\n `(for example 'outside-service'), got '${this.category}'.`,\n );\n }\n if (Boolean(this.costObjectType) !== Boolean(this.costObjectId)) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: costObjectType and costObjectId are set together ` +\n 'or not at all.',\n );\n }\n if (this.costObjectType) {\n assertQualifiedClassName('Expense', label, this.costObjectType);\n }\n this.getFinancialBreakdown();\n const recipient = this.getRefundRecipient();\n if (recipient && !this.refundOfId)\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Only linked refunds may name a cash recipient.',\n );\n if (\n this.reimbursedAmount !== null &&\n (!Number.isSafeInteger(this.reimbursedAmount) ||\n this.reimbursedAmount <= 0 ||\n !this.reimbursedAt)\n )\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Retained reimbursement needs a positive exact amount and payment date.',\n );\n if (\n this.paidBy === 'third-party'\n ? typeof this.paidByName !== 'string' || !this.paidByName.trim()\n : this.paidByName !== null && this.paidByName !== ''\n ) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Only a third-party payer requires paidByName.',\n );\n }\n if (\n this.refundOfId !== null &&\n (typeof this.refundOfId !== 'string' || !this.refundOfId.trim())\n ) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'refundOfId must name a source expense or be null.',\n );\n }\n if (\n this.paidBy !== 'company' &&\n this.paidBy !== 'person' &&\n this.paidBy !== 'third-party'\n ) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: paidBy must be 'company', 'person' or 'third-party'.`,\n );\n }\n if (this.paidBy === 'person' && !this.paidByProfileId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: an expense paid by a person names that person ` +\n '(paidByProfileId).',\n );\n }\n if (this.paidBy !== 'person' && this.paidByProfileId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: paidByProfileId is only set when paidBy is 'person'.`,\n );\n }\n if (this.reimbursable && this.paidBy !== 'person') {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: only an expense a person paid can be owed back.`,\n );\n }\n if (this.reimbursedAt && instantMs(this.reimbursedAt) === null) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Retained reimbursement date must be valid.',\n );\n }\n if (this.reimbursedAt && !this.reimbursable) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: reimbursedAt needs reimbursable.`,\n );\n }\n if (this.reviewStatus === 'reviewed' && this.duplicateOfId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: a reviewed expense cannot be marked a duplicate.`,\n );\n }\n if (this.commitmentLineId && !this.commitmentId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n `Expense ${label}: a commitment line needs its commitment ` +\n '(commitmentId).',\n );\n }\n }\n\n /**\n * Read the stored row directly. The authoritative prior state for the\n * review and lock guards: the in-memory copy may have been mass-assigned.\n */\n private async readPersisted(): Promise<PersistedRow | null> {\n if (!this.id) return null;\n const row = await this.db.get(this.tableName, { id: this.id });\n return (row as PersistedRow | null | undefined) ?? null;\n }\n\n private reviewState(): ReviewFieldState {\n return {\n status: this.reviewStatus,\n reviewer: asNullableString(this.reviewedByProfileId),\n reviewedAtMs: instantMs(this.reviewedAt),\n note: this.reviewNote ?? '',\n duplicateOf: asNullableString(this.duplicateOfId),\n };\n }\n\n /**\n * Refuse any review-field change a transition did not authorize. The token\n * binds the transition's complete resulting state — status, reviewer,\n * time, note and duplicate marker — so a field set directly beside a\n * legitimate `review()` / `reject()` cannot ride along with it.\n */\n private assertReviewFields(persisted: PersistedRow | null): void {\n const token = authorizedTransition.get(this);\n const prior: ReviewFieldState = {\n status: (persisted?.review_status ?? 'unreviewed') as ExpenseReviewStatus,\n reviewer: asNullableString(persisted?.reviewed_by_profile_id),\n reviewedAtMs: instantMs(persisted?.reviewed_at),\n note: String(persisted?.review_note ?? ''),\n duplicateOf: asNullableString(persisted?.duplicate_of_id),\n };\n const current = this.reviewState();\n if (sameReviewState(current, prior)) return;\n if (\n !token ||\n token.from !== prior.status ||\n !sameReviewState(current, token.expected)\n ) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_FIELDS_LOCKED',\n `Expense ${this.id || '<new>'}: review state changes only through ` +\n 'review(), reject(), markDuplicate() or reopen().',\n );\n }\n }\n\n private assertReviewedLock(persisted: PersistedRow | null): void {\n if (persisted?.review_status !== 'reviewed') return;\n const self = this as unknown as Record<string, unknown>;\n for (const [fieldName, column] of LOCKED_WHEN_REVIEWED) {\n const before = persisted[column];\n const after = self[fieldName];\n const same =\n fieldName === 'amount'\n ? Number(before) === Number(after)\n : fieldName === 'recordedAt'\n ? instantMs(before) === instantMs(after)\n : asNullableString(before) === asNullableString(after);\n if (!same) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_FIELDS_LOCKED',\n `Expense ${this.id}: ${fieldName} of a reviewed expense cannot ` +\n 'change; reopen() it first.',\n );\n }\n }\n }\n\n /**\n * A matched commitment must be visible in this expense's tenant, owned by\n * the same tenant, and in the same currency; a matched line must belong to\n * it. Mixed currencies are refused here rather than summed later.\n *\n * Checked when the match is made or changed, not on every save: a later\n * change to the commitment (its currency, or its deletion) must not stop\n * an unrelated save such as recording a reimbursement.\n */\n private async assertCommitment(\n persisted: PersistedRow | null,\n ): Promise<void> {\n if (!this.commitmentId) return;\n if (\n persisted &&\n asNullableString(persisted.commitment_id) === this.commitmentId &&\n asNullableString(persisted.commitment_line_id) ===\n this.commitmentLineId &&\n String(persisted.currency ?? '') === this.currency\n ) {\n return;\n }\n const contracts = await PurchaseContractCollection.create({ db: this.db });\n const commitment = await contracts.get({ id: this.commitmentId });\n // Before super.save() the tenant interceptor has not populated tenantId\n // yet, so compare against the tenant the row is about to be written in.\n const owner = effectiveTenant(this.tenantId);\n if (!commitment || !sameTenant(commitment.tenantId, owner)) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Expense ${this.id || '<new>'}: commitment ${this.commitmentId} was ` +\n \"not found in this expense's tenant.\",\n );\n }\n if (String(commitment.currency).toUpperCase() !== this.currency) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Expense ${this.id || '<new>'}: currency ${this.currency} does not ` +\n `match commitment ${this.commitmentId} (${commitment.currency}). ` +\n 'Amounts in different currencies are never summed.',\n );\n }\n if (this.commitmentLineId) {\n const lines = await ContractLineItemCollection.create({ db: this.db });\n const line = await lines.get({ id: this.commitmentLineId });\n if (!line || line.contractId !== this.commitmentId) {\n throw new ExpenseError(\n 'EXPENSE_COMMITMENT_MISMATCH',\n `Expense ${this.id || '<new>'}: line ${this.commitmentLineId} is ` +\n `not a line of commitment ${this.commitmentId}.`,\n );\n }\n }\n }\n\n /**\n * A vendor must be visible in, and owned by, the expense's tenant. A\n * `crossPackageRef` neither validates its target nor creates a foreign\n * key, so without this an expense in one tenant could name another\n * tenant's vendor. Checked when the vendor is set or changed.\n */\n private async assertVendor(persisted: PersistedRow | null): Promise<void> {\n if (!this.vendorId) return;\n if (persisted && asNullableString(persisted.vendor_id) === this.vendorId) {\n return;\n }\n const vendors = await VendorCollection.create({ db: this.db });\n const vendor = await vendors.get({ id: this.vendorId });\n const owner = effectiveTenant(this.tenantId);\n if (!vendor || !sameTenant(vendor.tenantId, owner)) {\n throw new ExpenseError(\n 'EXPENSE_VENDOR_MISMATCH',\n `Expense ${this.id || '<new>'}: vendor ${this.vendorId} was not ` +\n \"found in this expense's tenant.\",\n );\n }\n }\n\n /**\n * Source edits and refund writes contend on the same source row. Acquire\n * rows in deterministic order before either relationship check, and hold\n * them through commit. SQLite's write transaction supplies serialization.\n */\n private async lockRefundRows(): Promise<void> {\n if (!isPostgresDatabase(this.db)) return;\n const oldRow = await this.readPersisted();\n const oldParent =\n oldRow &&\n String(oldRow.tenant_id ?? '') ===\n String(effectiveTenant(this.tenantId) ?? '')\n ? asNullableString(oldRow.refund_of_id)\n : null;\n const ids = [\n ...new Set(\n [this.id, this.refundOfId, oldParent].filter((id): id is string =>\n Boolean(id),\n ),\n ),\n ].sort();\n if (ids.length === 0) return;\n const table = `\"${this.tableName.replace(/\"/g, '\"\"')}\"`;\n await this.db.query(\n `SELECT id FROM ${table} WHERE id IN (${ids.map(() => '?').join(', ')}) ` +\n 'AND tenant_id IS NOT DISTINCT FROM ? ORDER BY id FOR UPDATE',\n ...ids,\n effectiveTenant(this.tenantId),\n );\n }\n\n /** Refuse source corrections that invalidate any same-owner refund. */\n private async assertInboundRefunds(\n persisted: PersistedRow | null,\n ): Promise<void> {\n if (\n !persisted ||\n (Number(persisted.amount) === this.amount &&\n String(persisted.currency ?? '') === this.currency &&\n asNullableString(persisted.refund_of_id) === this.refundOfId &&\n asNullableString(persisted.financial_breakdown) ===\n this.financialBreakdown)\n )\n return;\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const refunds = await expenses.list({\n where: { refundOfId: this.id, tenantId: effectiveTenant(this.tenantId) },\n cache: false,\n });\n if (refunds.length === 0) return;\n if (this.amount <= 0 || this.refundOfId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'A referenced refund source must remain positive and non-refund.',\n );\n }\n if (persisted.financial_breakdown && !this.getFinancialBreakdown()) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Financial facts of a linked refund source cannot be cleared.',\n );\n }\n calculateRefundPosition(\n this as Expense & { id: string },\n refunds as Array<Expense & { id: string }>,\n );\n }\n\n private async assertRefund(persisted: PersistedRow | null): Promise<void> {\n if (!this.refundOfId) return;\n if (this.refundOfId === this.id || this.amount >= 0) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'A refund must be negative and name a different positive source expense.',\n );\n }\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const source = await expenses.get(\n { id: this.refundOfId },\n { cache: false },\n );\n if (\n !source ||\n !sameTenant(source.tenantId, effectiveTenant(this.tenantId)) ||\n source.currency !== this.currency ||\n source.amount <= 0 ||\n source.refundOfId\n ) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Refund source must be a positive non-refund expense in the same tenant and currency.',\n );\n }\n if (persisted?.financial_breakdown && !this.getFinancialBreakdown()) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Financial facts of a linked refund cannot be cleared.',\n );\n }\n const refunds = await expenses.list({\n where: {\n refundOfId: source.id,\n tenantId: effectiveTenant(this.tenantId),\n },\n cache: false,\n });\n calculateRefundPosition(\n source as Expense & { id: string },\n [...refunds.filter((refund) => refund.id !== this.id), this] as Array<\n Expense & { id: string }\n >,\n );\n if (\n !persisted ||\n persisted.refund_of_id !== this.refundOfId ||\n Number(persisted.amount) !== this.amount ||\n asNullableString(persisted.financial_breakdown) !==\n this.financialBreakdown ||\n asNullableString(persisted.refund_recipient) !== this.refundRecipient\n ) {\n // A row lock alone cannot refresh an existing REPEATABLE READ snapshot.\n // Advance the source through public CAS so stale source writers abort.\n if (!source.updated_at) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Refund source has no revision.',\n );\n }\n await source.claimRevision(source.updated_at);\n }\n }\n\n private assertReimbursementFields(persisted: PersistedRow | null): void {\n const token = authorizedReimbursement.get(this);\n const at = instantMs(this.reimbursedAt);\n const priorAt = instantMs(persisted?.reimbursed_at);\n const priorAmount =\n persisted?.reimbursed_amount == null\n ? null\n : Number(persisted.reimbursed_amount);\n const changed =\n at !== priorAt ||\n this.reimbursedAmount !== priorAmount ||\n String(this.reimbursementReference ?? '') !==\n String(persisted?.reimbursement_reference ?? '');\n if (\n changed &&\n (!token ||\n priorAt !== null ||\n at !== token.at ||\n this.reimbursedAmount !== token.amount ||\n this.reimbursementReference !== token.reference)\n ) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Retained reimbursement payment fields change only through markReimbursed and cannot be overwritten.',\n );\n }\n if (priorAt !== null && persisted) {\n for (const [field, column] of [\n ['amount', 'amount'],\n ['currency', 'currency'],\n ['paidBy', 'paid_by'],\n ['paidByProfileId', 'paid_by_profile_id'],\n ['reimbursable', 'reimbursable'],\n ['refundOfId', 'refund_of_id'],\n ] as const) {\n if (\n field === 'reimbursable'\n ? Boolean(this[field]) !== Boolean(persisted[column])\n : String(this[field] ?? '') !== String(persisted[column] ?? '')\n )\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'A paid reimbursement preserves its original cash claim and payer.',\n );\n }\n }\n }\n\n /** Remaining person cash claim and separate company receivable; null is unknown. */\n async getReimbursementPosition(): Promise<ExpenseReimbursementPosition> {\n return this.withTransaction(async () => {\n await this.lockRefundRows();\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const source = this.id\n ? await expenses.get({ id: this.id }, { cache: false })\n : null;\n if (!source || source.refundOfId)\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Reimbursement position requires a persisted positive source.',\n );\n const refunds = await expenses.list({\n where: {\n refundOfId: source.id,\n tenantId: effectiveTenant(source.tenantId),\n },\n cache: false,\n });\n calculateRefundPosition(\n source as Expense & { id: string },\n refunds as Array<Expense & { id: string }>,\n );\n return calculateReimbursementPosition(\n source as Expense & { id: string },\n refunds,\n );\n });\n }\n\n /** Exact source cash capacity and optional complete signed financial position. */\n async getRefundPosition(): Promise<ExpenseRefundPosition> {\n return this.withTransaction(async () => {\n await this.lockRefundRows();\n const { ExpenseCollection } = await import(\n '../collections/ExpenseCollection.js'\n );\n const expenses = await ExpenseCollection.create({ db: this.db });\n const source = this.id\n ? await expenses.get({ id: this.id }, { cache: false })\n : null;\n if (!source || source.refundOfId) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'Refund position requires a persisted positive non-refund source.',\n );\n }\n const refunds = await expenses.list({\n where: {\n refundOfId: source.id,\n tenantId: effectiveTenant(source.tenantId),\n },\n cache: false,\n });\n return calculateRefundPosition(\n source as Expense & { id: string },\n refunds as Array<Expense & { id: string }>,\n );\n });\n }\n\n private async transition(\n action: keyof typeof REVIEW_TRANSITIONS,\n apply: () => void,\n ): Promise<this> {\n const rule = REVIEW_TRANSITIONS[action];\n const persisted = await this.readPersisted();\n if (!persisted) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_TRANSITION',\n `Expense ${this.id || '<new>'}: save the expense before ${action}().`,\n );\n }\n const from = String(persisted.review_status) as ExpenseReviewStatus;\n if (!rule.from.includes(from)) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_TRANSITION',\n `Expense ${this.id}: cannot ${action}() an expense that is ${from}.`,\n );\n }\n if (action === 'review' && persisted.duplicate_of_id) {\n throw new ExpenseError(\n 'EXPENSE_REVIEW_TRANSITION',\n `Expense ${this.id}: a duplicate cannot be reviewed; reopen() it first.`,\n );\n }\n const snapshot = {\n reviewStatus: this.reviewStatus,\n reviewedByProfileId: this.reviewedByProfileId,\n reviewedAt: this.reviewedAt,\n reviewNote: this.reviewNote,\n duplicateOfId: this.duplicateOfId,\n };\n // Every transition assigns every review field, so the token below\n // describes the whole resulting review state.\n apply();\n authorizedTransition.set(this, { from, expected: this.reviewState() });\n try {\n return await this.save();\n } catch (error) {\n Object.assign(this, snapshot);\n throw error;\n } finally {\n authorizedTransition.delete(this);\n }\n }\n}\n\nfunction requireReviewer(reviewerProfileId: string | undefined): string {\n const reviewer = String(reviewerProfileId ?? '').trim();\n if (!reviewer) {\n throw new ExpenseError(\n 'EXPENSE_INVALID',\n 'A review decision names its reviewer (reviewerProfileId).',\n );\n }\n return reviewer;\n}\n\nfunction sameTenant(\n a: string | null | undefined,\n b: string | null | undefined,\n): boolean {\n return (a ?? null) === (b ?? null);\n}\n"],"mappings":"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"}
|
package/dist/chunks/{ExpenseReceiptCollection-y1SlmnBS.js → ExpenseReceiptCollection-BO5yvJLm.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DuplicateReceiptError, ExpenseError } from "../types.js";
|
|
2
|
-
import { a as identityConflict, c as pinNaturalKey, i as effectiveTenant, n as assertMinorUnits, o as normalizeSha256, s as normalizeTenantId, t as Expense } from "./Expense-
|
|
2
|
+
import { a as identityConflict, c as pinNaturalKey, i as effectiveTenant, n as assertMinorUnits, o as normalizeSha256, s as normalizeTenantId, t as Expense } from "./Expense-C_WC5VLF.js";
|
|
3
3
|
import { SmrtJunction, SmrtObject, classifyDatabaseError, crossPackageRef, field, foreignKey, isUniqueViolationError, smrt } from "@happyvertical/smrt-core";
|
|
4
4
|
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
5
|
import { AssetCollection } from "@happyvertical/smrt-assets";
|
|
@@ -301,4 +301,4 @@ ExpenseReceiptCollection = __decorateClass([smrt({
|
|
|
301
301
|
//#endregion
|
|
302
302
|
export { ExpenseReceipt as n, __exportAll as r, ExpenseReceiptCollection as t };
|
|
303
303
|
|
|
304
|
-
//# sourceMappingURL=ExpenseReceiptCollection-
|
|
304
|
+
//# sourceMappingURL=ExpenseReceiptCollection-BO5yvJLm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpenseReceiptCollection-y1SlmnBS.js","names":[],"sources":["../../src/models/ExpenseReceipt.ts","../../src/collections/ExpenseReceiptCollection.ts"],"sourcesContent":["/**\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":"ExpenseReceiptCollection-BO5yvJLm.js","names":[],"sources":["../../src/models/ExpenseReceipt.ts","../../src/collections/ExpenseReceiptCollection.ts"],"sourcesContent":["/**\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,4 +1,4 @@
|
|
|
1
|
-
import { t as ExpenseReceiptCollection } from "./ExpenseReceiptCollection-
|
|
1
|
+
import { t as ExpenseReceiptCollection } from "./ExpenseReceiptCollection-BO5yvJLm.js";
|
|
2
2
|
import { i as withActionPrincipal, n as authorizeExpenseAction, r as principalHolds, t as ExpenseActionError } from "./principal-_1CdcvvW.js";
|
|
3
3
|
import { ExpenseError } from "../types.js";
|
|
4
4
|
//#region src/actions/expense-actions.ts
|
|
@@ -123,10 +123,15 @@ async function markExpenseReimbursedAction(expenses, input) {
|
|
|
123
123
|
return withActionPrincipal(principal, async () => {
|
|
124
124
|
const expense = await loadExpense(expenses, principal, body);
|
|
125
125
|
const reference = optionalString(body, "reference");
|
|
126
|
-
|
|
126
|
+
const amount = body.amount;
|
|
127
|
+
if (amount !== void 0 && (typeof amount !== "number" || !Number.isSafeInteger(amount) || amount <= 0)) throw new ExpenseActionError(400, "invalid_request", "amount must be a positive safe integer.");
|
|
128
|
+
return domainCall(() => expense.markReimbursed({
|
|
129
|
+
...reference !== void 0 ? { reference } : {},
|
|
130
|
+
...amount !== void 0 ? { amount } : {}
|
|
131
|
+
}));
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
134
|
//#endregion
|
|
130
135
|
export { attachExpenseReceiptAction, markExpenseReimbursedAction, recordExpenseAction, reviewExpenseAction };
|
|
131
136
|
|
|
132
|
-
//# sourceMappingURL=expense-actions-
|
|
137
|
+
//# sourceMappingURL=expense-actions-Bxzeb4s2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expense-actions-Bxzeb4s2.js","names":[],"sources":["../../src/actions/expense-actions.ts"],"sourcesContent":["/**\n * Service-backed write actions for `Expense` and `ExpenseReceipt` (#4001).\n *\n * The generated CRUD surface stays closed: these are the only generated\n * writes, and each delegates to the existing domain flow — `Expense.save()`\n * with its review lock and refund/commitment/vendor guards, the method-only\n * `review()` / `reject()` / `markReimbursed()`, and\n * `ExpenseReceiptCollection.attachReceipt()` with its hash deduplication.\n * The thin `ExpenseCollection` methods call in here.\n */\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport type { ExpenseCollection } from '../collections/ExpenseCollection.js';\nimport { ExpenseReceiptCollection } from '../collections/ExpenseReceiptCollection.js';\nimport type { Expense } from '../models/Expense.js';\nimport type { ExpenseReceipt } from '../models/ExpenseReceipt.js';\nimport {\n ExpenseError,\n type ExpenseErrorCode,\n type ExpenseFinancialBreakdown,\n type ExpensePayer,\n} from '../types.js';\nimport {\n authorizeExpenseAction,\n ExpenseActionError,\n type ExpenseActionPrincipal,\n principalHolds,\n withActionPrincipal,\n} from './principal.js';\n\n/** Body of the `record` action. Tenant and recorder come from the session. */\nexport interface RecordExpenseActionInput {\n costObjectType?: string;\n costObjectId?: string;\n category?: string;\n description?: string;\n /** Integer minor units of `currency`. */\n amount: number;\n currency: string;\n /** `YYYY-MM-DD`. */\n incurredOn: string;\n paidBy?: ExpensePayer;\n /** Defaults to the signed-in person when `paidBy` is `person`. */\n paidByProfileId?: string | null;\n paidByName?: string | null;\n vendorId?: string | null;\n commitmentId?: string | null;\n commitmentLineId?: string | null;\n refundOfId?: string | null;\n reimbursable?: boolean;\n financialBreakdown?: ExpenseFinancialBreakdown | null;\n}\n\n/** Body of the `attachReceipt` action. */\nexport interface AttachExpenseReceiptActionInput {\n expenseId: string;\n assetId: string;\n contentSha256: string;\n filename?: string;\n mimeType?: string;\n byteCount?: number;\n}\n\n/** Body of the `recordReview` action. */\nexport interface ReviewExpenseActionInput {\n expenseId: string;\n /** `approve` accepts the cost; `reject` refuses it and needs a `reason`. */\n decision: 'approve' | 'reject';\n note?: string;\n reason?: string;\n}\n\n/** Body of the `recordReimbursement` action. */\nexport interface MarkExpenseReimbursedActionInput {\n expenseId: string;\n /** Optional expected full remaining reimbursement in minor units. */\n amount?: number;\n /** Payroll line, payables reference, or transfer id of the payback. */\n reference?: string;\n}\n\n/** `record` fields copied onto the new expense; everything else is ignored. */\nconst RECORD_FIELDS = [\n 'costObjectType',\n 'costObjectId',\n 'category',\n 'description',\n 'amount',\n 'currency',\n 'incurredOn',\n 'paidByName',\n 'vendorId',\n 'commitmentId',\n 'commitmentLineId',\n 'refundOfId',\n 'reimbursable',\n] as const;\n\nconst CONFLICT_CODES = new Set<ExpenseErrorCode>([\n 'EXPENSE_REVIEW_TRANSITION',\n 'EXPENSE_REVIEW_FIELDS_LOCKED',\n 'EXPENSE_IDENTITY_CONFLICT',\n 'EXPENSE_RECEIPT_DUPLICATE',\n]);\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new ExpenseActionError(\n 400,\n 'invalid_request',\n 'The action body must be a JSON object.',\n );\n }\n return value as Record<string, unknown>;\n}\n\nfunction optionalString(\n body: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = body[key];\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'string') {\n throw new ExpenseActionError(\n 400,\n 'invalid_request',\n `${key} must be a string.`,\n );\n }\n return value;\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n const value = optionalString(body, key)?.trim();\n if (!value) {\n throw new ExpenseActionError(400, 'invalid_request', `${key} is required.`);\n }\n return value;\n}\n\n/** Domain refusals become 4xx with their stable code; others pass through. */\nasync function domainCall<T>(fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof ExpenseError) {\n throw new ExpenseActionError(\n CONFLICT_CODES.has(error.code) ? 409 : 400,\n error.code,\n error.message,\n );\n }\n throw error;\n }\n}\n\nfunction itemClass(expenses: ExpenseCollection) {\n return (expenses.constructor as unknown as { _itemClass: unknown })\n ._itemClass as typeof Expense;\n}\n\nfunction options(expenses: ExpenseCollection): SmrtClassOptions {\n return expenses.options;\n}\n\nasync function loadExpense(\n expenses: ExpenseCollection,\n principal: ExpenseActionPrincipal,\n body: Record<string, unknown>,\n): Promise<Expense> {\n const expenseId = requiredString(body, 'expenseId');\n const expense = await expenses.get({ id: expenseId });\n // Optional tenancy also admits global (NULL-tenant) rows in a tenant\n // context; a write must land on the principal's own tenant only.\n if (!expense || (expense.tenantId ?? null) !== principal.tenantId) {\n throw new ExpenseActionError(\n 404,\n 'not_found',\n `Expense ${expenseId} was not found.`,\n );\n }\n return expense;\n}\n\n/**\n * `record`: a new `unreviewed` expense, recorded by the signed-in person in\n * their tenant. Review, reimbursement and duplicate fields are never taken\n * from the body. Paying on someone else's behalf needs `expenses.recordReview`.\n */\nexport async function recordExpenseAction(\n expenses: ExpenseCollection,\n input: unknown,\n): Promise<Expense> {\n const body = asRecord(input);\n const principal = await authorizeExpenseAction(\n itemClass(expenses),\n 'record',\n options(expenses),\n { allowSuperAdminBypass: true },\n );\n const paidBy = (optionalString(body, 'paidBy') ?? 'company') as ExpensePayer;\n let paidByProfileId: string | null = null;\n if (paidBy === 'person') {\n paidByProfileId =\n optionalString(body, 'paidByProfileId')?.trim() || principal.profileId;\n if (\n paidByProfileId !== principal.profileId &&\n !(await principalHolds(\n itemClass(expenses),\n 'recordReview',\n options(expenses),\n ))\n ) {\n throw new ExpenseActionError(\n 403,\n 'permission_denied',\n \"Recording another person's expense needs the recordReview permission.\",\n );\n }\n }\n const fields: Record<string, unknown> = {};\n for (const key of RECORD_FIELDS) {\n if (body[key] !== undefined) fields[key] = body[key];\n }\n const breakdown = body.financialBreakdown;\n if (\n breakdown !== undefined &&\n breakdown !== null &&\n (typeof breakdown !== 'object' || Array.isArray(breakdown))\n ) {\n throw new ExpenseActionError(\n 400,\n 'invalid_request',\n 'financialBreakdown must be an object or null.',\n );\n }\n return withActionPrincipal(principal, () =>\n // One create: `collection.create()` saves, and `Expense.save()` validates\n // the facts (`validateShape()` parses them) before its first INSERT, so an\n // invalid breakdown never leaves a half-recorded row behind.\n domainCall(() =>\n expenses.create({\n ...fields,\n ...(breakdown !== undefined\n ? {\n financialBreakdown:\n breakdown === null ? null : JSON.stringify(breakdown),\n }\n : {}),\n tenantId: principal.tenantId,\n paidBy,\n paidByProfileId,\n recordedByProfileId: principal.profileId,\n recordedAt: null,\n }),\n ),\n );\n}\n\n/**\n * `attachReceipt`: link a stored asset to an expense as its receipt through\n * `ExpenseReceiptCollection.attachReceipt()`. The uploader is the signed-in\n * person, who must have recorded or paid the expense unless they hold\n * `expenses.recordReview`.\n */\nexport async function attachExpenseReceiptAction(\n expenses: ExpenseCollection,\n input: unknown,\n): Promise<ExpenseReceipt> {\n const body = asRecord(input);\n const principal = await authorizeExpenseAction(\n itemClass(expenses),\n 'attachReceipt',\n options(expenses),\n { allowSuperAdminBypass: true },\n );\n return withActionPrincipal(principal, async () => {\n const expense = await loadExpense(expenses, principal, body);\n if (\n expense.recordedByProfileId !== principal.profileId &&\n expense.paidByProfileId !== principal.profileId &&\n !(await principalHolds(\n itemClass(expenses),\n 'recordReview',\n options(expenses),\n ))\n ) {\n throw new ExpenseActionError(\n 403,\n 'permission_denied',\n \"Attaching a receipt to another person's expense needs the recordReview permission.\",\n );\n }\n const byteCount = body.byteCount;\n if (byteCount !== undefined && typeof byteCount !== 'number') {\n throw new ExpenseActionError(\n 400,\n 'invalid_request',\n 'byteCount must be a number.',\n );\n }\n const receipts = await ExpenseReceiptCollection.create(options(expenses));\n return domainCall(() =>\n receipts.attachReceipt({\n expenseId: expense.id as string,\n assetId: requiredString(body, 'assetId'),\n contentSha256: requiredString(body, 'contentSha256'),\n uploadedByProfileId: principal.profileId,\n ...(optionalString(body, 'filename') !== undefined\n ? { filename: optionalString(body, 'filename') }\n : {}),\n ...(optionalString(body, 'mimeType') !== undefined\n ? { mimeType: optionalString(body, 'mimeType') }\n : {}),\n ...(byteCount !== undefined ? { byteCount: byteCount as number } : {}),\n }),\n );\n });\n}\n\n/**\n * `recordReview`: accept (`Expense.review()`) or refuse with a reason\n * (`Expense.reject()`), attributed to the signed-in reviewer. Money: needs an\n * explicit `expenses.recordReview` grant, super-admin bypass does not apply.\n * Named apart from `Expense.review()` so allowlisting the collection action\n * never opens the instance method, which takes the reviewer from its input.\n */\nexport async function reviewExpenseAction(\n expenses: ExpenseCollection,\n input: unknown,\n): Promise<Expense> {\n const body = asRecord(input);\n const principal = await authorizeExpenseAction(\n itemClass(expenses),\n 'recordReview',\n options(expenses),\n { allowSuperAdminBypass: false },\n );\n const decision = optionalString(body, 'decision');\n if (decision !== 'approve' && decision !== 'reject') {\n throw new ExpenseActionError(\n 400,\n 'invalid_request',\n \"decision must be 'approve' or 'reject'.\",\n );\n }\n return withActionPrincipal(principal, async () => {\n const expense = await loadExpense(expenses, principal, body);\n return domainCall(() =>\n decision === 'approve'\n ? expense.review({\n reviewerProfileId: principal.profileId,\n note: optionalString(body, 'note') ?? '',\n })\n : expense.reject({\n reviewerProfileId: principal.profileId,\n reason: optionalString(body, 'reason') ?? '',\n }),\n );\n });\n}\n\n/**\n * `recordReimbursement`: record that the person who paid has been paid back\n * (`Expense.markReimbursed()`); never changes the review state. Money: needs\n * an explicit `expenses.recordReimbursement` grant.\n */\nexport async function markExpenseReimbursedAction(\n expenses: ExpenseCollection,\n input: unknown,\n): Promise<Expense> {\n const body = asRecord(input);\n const principal = await authorizeExpenseAction(\n itemClass(expenses),\n 'recordReimbursement',\n options(expenses),\n { allowSuperAdminBypass: false },\n );\n return withActionPrincipal(principal, async () => {\n const expense = await loadExpense(expenses, principal, body);\n const reference = optionalString(body, 'reference');\n const amount = body.amount;\n if (\n amount !== undefined &&\n (typeof amount !== 'number' ||\n !Number.isSafeInteger(amount) ||\n amount <= 0)\n ) {\n throw new ExpenseActionError(\n 400,\n 'invalid_request',\n 'amount must be a positive safe integer.',\n );\n }\n return domainCall(() =>\n expense.markReimbursed({\n ...(reference !== undefined ? { reference } : {}),\n ...(amount !== undefined ? { amount: amount as number } : {}),\n }),\n );\n });\n}\n"],"mappings":";;;;AAiFA,IAAM,gBAAgB;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAM,iCAAiB,IAAI,IAAsB;CAC/C;CACA;CACA;CACA;AACF,CAAC;AAED,SAAS,SAAS,OAAyC;CACzD,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAC5D,MAAM,IAAI,mBACR,KACA,mBACA,wCACF;CAEF,OAAO;AACT;AAEA,SAAS,eACP,MACA,KACoB;CACpB,MAAM,QAAQ,KAAK;CACnB,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO,KAAA;CAClD,IAAI,OAAO,UAAU,UACnB,MAAM,IAAI,mBACR,KACA,mBACA,GAAG,IAAG,mBACR;CAEF,OAAO;AACT;AAEA,SAAS,eAAe,MAA+B,KAAqB;CAC1E,MAAM,QAAQ,eAAe,MAAM,GAAG,CAAA,EAAG,KAAK;CAC9C,IAAI,CAAC,OACH,MAAM,IAAI,mBAAmB,KAAK,mBAAmB,GAAG,IAAG,cAAe;CAE5E,OAAO;AACT;AAGA,eAAe,WAAc,IAAkC;CAC7D,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,SAAS,OAAO;EACd,IAAI,iBAAiB,cACnB,MAAM,IAAI,mBACR,eAAe,IAAI,MAAM,IAAI,IAAI,MAAM,KACvC,MAAM,MACN,MAAM,OACR;EAEF,MAAM;CACR;AACF;AAEA,SAAS,UAAU,UAA6B;CAC9C,OAAQ,SAAS,YACd;AACL;AAEA,SAAS,QAAQ,UAA+C;CAC9D,OAAO,SAAS;AAClB;AAEA,eAAe,YACb,UACA,WACA,MACkB;CAClB,MAAM,YAAY,eAAe,MAAM,WAAW;CAClD,MAAM,UAAU,MAAM,SAAS,IAAI,EAAE,IAAI,UAAU,CAAC;CAGpD,IAAI,CAAC,YAAY,QAAQ,YAAY,UAAU,UAAU,UACvD,MAAM,IAAI,mBACR,KACA,aACA,WAAW,UAAS,gBACtB;CAEF,OAAO;AACT;AAOA,eAAsB,oBACpB,UACA,OACkB;CAClB,MAAM,OAAO,SAAS,KAAK;CAC3B,MAAM,YAAY,MAAM,uBACtB,UAAU,QAAQ,GAClB,UACA,QAAQ,QAAQ,GAChB,EAAE,uBAAuB,KAAK,CAChC;CACA,MAAM,SAAU,eAAe,MAAM,QAAQ,KAAK;CAClD,IAAI,kBAAiC;CACrC,IAAI,WAAW,UAAU;EACvB,kBACE,eAAe,MAAM,iBAAiB,CAAA,EAAG,KAAK,KAAK,UAAU;EAC/D,IACE,oBAAoB,UAAU,aAC9B,CAAE,MAAM,eACN,UAAU,QAAQ,GAClB,gBACA,QAAQ,QAAQ,CAClB,GAEA,MAAM,IAAI,mBACR,KACA,qBACA,uEACF;CAEJ;CACA,MAAM,SAAkC,CAAC;CACzC,KAAA,MAAW,OAAO,eAChB,IAAI,KAAK,SAAS,KAAA,GAAW,OAAO,OAAO,KAAK;CAElD,MAAM,YAAY,KAAK;CACvB,IACE,cAAc,KAAA,KACd,cAAc,SACb,OAAO,cAAc,YAAY,MAAM,QAAQ,SAAS,IAEzD,MAAM,IAAI,mBACR,KACA,mBACA,+CACF;CAEF,OAAO,oBAAoB,iBAIzB,iBACE,SAAS,OAAO;EACd,GAAG;EACH,GAAI,cAAc,KAAA,IACd,EACE,oBACE,cAAc,OAAO,OAAO,KAAK,UAAU,SAAS,EACxD,IACA,CAAC;EACL,UAAU,UAAU;EACpB;EACA;EACA,qBAAqB,UAAU;EAC/B,YAAY;CACd,CAAC,CACH,CACF;AACF;AAQA,eAAsB,2BACpB,UACA,OACyB;CACzB,MAAM,OAAO,SAAS,KAAK;CAC3B,MAAM,YAAY,MAAM,uBACtB,UAAU,QAAQ,GAClB,iBACA,QAAQ,QAAQ,GAChB,EAAE,uBAAuB,KAAK,CAChC;CACA,OAAO,oBAAoB,WAAW,YAAY;EAChD,MAAM,UAAU,MAAM,YAAY,UAAU,WAAW,IAAI;EAC3D,IACE,QAAQ,wBAAwB,UAAU,aAC1C,QAAQ,oBAAoB,UAAU,aACtC,CAAE,MAAM,eACN,UAAU,QAAQ,GAClB,gBACA,QAAQ,QAAQ,CAClB,GAEA,MAAM,IAAI,mBACR,KACA,qBACA,oFACF;EAEF,MAAM,YAAY,KAAK;EACvB,IAAI,cAAc,KAAA,KAAa,OAAO,cAAc,UAClD,MAAM,IAAI,mBACR,KACA,mBACA,6BACF;EAEF,MAAM,WAAW,MAAM,yBAAyB,OAAO,QAAQ,QAAQ,CAAC;EACxE,OAAO,iBACL,SAAS,cAAc;GACrB,WAAW,QAAQ;GACnB,SAAS,eAAe,MAAM,SAAS;GACvC,eAAe,eAAe,MAAM,eAAe;GACnD,qBAAqB,UAAU;GAC/B,GAAI,eAAe,MAAM,UAAU,MAAM,KAAA,IACrC,EAAE,UAAU,eAAe,MAAM,UAAU,EAAE,IAC7C,CAAC;GACL,GAAI,eAAe,MAAM,UAAU,MAAM,KAAA,IACrC,EAAE,UAAU,eAAe,MAAM,UAAU,EAAE,IAC7C,CAAC;GACL,GAAI,cAAc,KAAA,IAAY,EAAE,UAA+B,IAAI,CAAC;EACtE,CAAC,CACH;CACF,CAAC;AACH;AASA,eAAsB,oBACpB,UACA,OACkB;CAClB,MAAM,OAAO,SAAS,KAAK;CAC3B,MAAM,YAAY,MAAM,uBACtB,UAAU,QAAQ,GAClB,gBACA,QAAQ,QAAQ,GAChB,EAAE,uBAAuB,MAAM,CACjC;CACA,MAAM,WAAW,eAAe,MAAM,UAAU;CAChD,IAAI,aAAa,aAAa,aAAa,UACzC,MAAM,IAAI,mBACR,KACA,mBACA,yCACF;CAEF,OAAO,oBAAoB,WAAW,YAAY;EAChD,MAAM,UAAU,MAAM,YAAY,UAAU,WAAW,IAAI;EAC3D,OAAO,iBACL,aAAa,YACT,QAAQ,OAAO;GACb,mBAAmB,UAAU;GAC7B,MAAM,eAAe,MAAM,MAAM,KAAK;EACxC,CAAC,IACD,QAAQ,OAAO;GACb,mBAAmB,UAAU;GAC7B,QAAQ,eAAe,MAAM,QAAQ,KAAK;EAC5C,CAAC,CACP;CACF,CAAC;AACH;AAOA,eAAsB,4BACpB,UACA,OACkB;CAClB,MAAM,OAAO,SAAS,KAAK;CAC3B,MAAM,YAAY,MAAM,uBACtB,UAAU,QAAQ,GAClB,uBACA,QAAQ,QAAQ,GAChB,EAAE,uBAAuB,MAAM,CACjC;CACA,OAAO,oBAAoB,WAAW,YAAY;EAChD,MAAM,UAAU,MAAM,YAAY,UAAU,WAAW,IAAI;EAC3D,MAAM,YAAY,eAAe,MAAM,WAAW;EAClD,MAAM,SAAS,KAAK;EACpB,IACE,WAAW,KAAA,MACV,OAAO,WAAW,YACjB,CAAC,OAAO,cAAc,MAAM,KAC5B,UAAU,IAEZ,MAAM,IAAI,mBACR,KACA,mBACA,yCACF;EAEF,OAAO,iBACL,QAAQ,eAAe;GACrB,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;GAC/C,GAAI,WAAW,KAAA,IAAY,EAAE,OAAyB,IAAI,CAAC;EAC7D,CAAC,CACH;CACF,CAAC;AACH"}
|
|
@@ -7,7 +7,8 @@ import { SmrtCollection } from '@happyvertical/smrt-core';
|
|
|
7
7
|
import type { AttachExpenseReceiptActionInput, MarkExpenseReimbursedActionInput, RecordExpenseActionInput, ReviewExpenseActionInput } from '../actions/expense-actions.js';
|
|
8
8
|
import { Expense } from '../models/Expense.js';
|
|
9
9
|
import type { ExpenseReceipt } from '../models/ExpenseReceipt.js';
|
|
10
|
-
import {
|
|
10
|
+
import type { CompleteSupplierPayableBasisInput, RecordSupplierPayableEventInput, RecordSupplierPayableInput, SupplierPayablePosition } from '../supplier-payable.js';
|
|
11
|
+
import { type CommitmentPosition, type CurrencyTotals, type ExpenseFinancialBreakdown, type ExpenseRefundPosition, type ExpenseReimbursementPosition, type ExpenseTotalsOptions } from '../types.js';
|
|
11
12
|
/** A reviewed, non-duplicate expense and its optional stored supplier facts. */
|
|
12
13
|
export interface ReviewedExpenseTaxFact {
|
|
13
14
|
/** Legacy owner identity alias retained for existing callers. */
|
|
@@ -41,6 +42,21 @@ export interface ReviewedExpenseTaxFactsOptions {
|
|
|
41
42
|
*/
|
|
42
43
|
export declare class ExpenseCollection extends SmrtCollection<Expense> {
|
|
43
44
|
static readonly _itemClass: typeof Expense;
|
|
45
|
+
/** Record immutable original supplier retention/payable evidence on its Expense. */
|
|
46
|
+
recordSupplierPayable(expenseId: string, input: RecordSupplierPayableInput): Promise<SupplierPayablePosition>;
|
|
47
|
+
/** Append a dated supplier event; no external money movement is executed. */
|
|
48
|
+
recordSupplierPayableEvent(expenseId: string, input: RecordSupplierPayableEventInput): Promise<SupplierPayablePosition>;
|
|
49
|
+
/** Independently review an originally unknown payable basis exactly once. */
|
|
50
|
+
completeSupplierPayableBasis(expenseId: string, input: CompleteSupplierPayableBasisInput): Promise<SupplierPayablePosition>;
|
|
51
|
+
/** Dated supplier position is independent of incurred cost and reimbursement. */
|
|
52
|
+
supplierPayablePosition(expenseId: string, input?: {
|
|
53
|
+
asOfDate?: string;
|
|
54
|
+
}): Promise<SupplierPayablePosition | null>;
|
|
55
|
+
private requireSupplierPayableExpense;
|
|
56
|
+
/** Cash claim after explicit refund receipts and retained historical payments. */
|
|
57
|
+
reimbursementPosition(sourceId: string): Promise<ExpenseReimbursementPosition>;
|
|
58
|
+
/** All persisted linked refunds reserve source capacity, irrespective of review. */
|
|
59
|
+
refundPosition(sourceId: string): Promise<ExpenseRefundPosition>;
|
|
44
60
|
/**
|
|
45
61
|
* Record a new, unreviewed expense for the signed-in person in their
|
|
46
62
|
* tenant. Needs `expenses.record`; a person-paid expense for someone else
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpenseCollection.d.ts","sourceRoot":"","sources":["../../src/collections/ExpenseCollection.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,EAAU,cAAc,EAAQ,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EACV,+BAA+B,EAC/B,gCAAgC,EAChC,wBAAwB,EACxB,wBAAwB,EACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EAEnB,KAAK,yBAAyB,
|
|
1
|
+
{"version":3,"file":"ExpenseCollection.d.ts","sourceRoot":"","sources":["../../src/collections/ExpenseCollection.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export { ExpenseReceiptCollection } from './collections/ExpenseReceiptCollection
|
|
|
46
46
|
export { Expense } from './models/Expense.js';
|
|
47
47
|
export { ExpenseReceipt } from './models/ExpenseReceipt.js';
|
|
48
48
|
export { ExpensesRecipe } from './recipes.js';
|
|
49
|
-
export
|
|
49
|
+
export type { CompleteSupplierPayableBasisInput, RecordSupplierPayableEventInput, RecordSupplierPayableInput, SupplierPayableEvent, SupplierPayableEventInput, SupplierPayablePosition, SupplierPayableReview, SupplierPayableSource, SupplierPayableState, } from './supplier-payable.js';
|
|
50
|
+
export { type AttachReceiptInput, type CommitmentPosition, type CurrencyTotal, type CurrencyTotals, DuplicateReceiptError, type DuplicateReceiptGroup, EXPENSE_PAYERS, EXPENSE_REVIEW_STATUSES, type ExpenseCostBasis, ExpenseError, type ExpenseErrorCode, type ExpenseFinancialBreakdown, type ExpenseOptions, type ExpensePayer, type ExpenseReceiptOptions, type ExpenseRefundPosition, type ExpenseRefundRecipient, type ExpenseReimbursementPosition, type ExpenseReviewStatus, type ExpenseTaxComponent, type ExpenseTotalsOptions, type FindDuplicateReceiptsOptions, type MarkDuplicateExpenseInput, type MarkReimbursedInput, type RejectExpenseInput, type ReopenExpenseInput, type ReviewExpenseInput, SUGGESTED_EXPENSE_CATEGORIES, } from './types.js';
|
|
50
51
|
export { computeContentSha256 } from './validation.js';
|
|
51
52
|
//# sourceMappingURL=index.d.ts.map
|