@happyvertical/smrt-expenses 0.60.14 → 0.60.15
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 +15 -2
- package/README.md +8 -0
- package/agents/allocation-test-matrix.md +82 -0
- package/agents/reviewed-allocations.md +138 -0
- package/dist/allocation-internal.d.ts +6 -0
- package/dist/allocation-internal.d.ts.map +1 -0
- package/dist/chunks/{Expense-CimA4lxE.js → Expense-Deq19w2r.js} +28 -3
- package/dist/chunks/Expense-Deq19w2r.js.map +1 -0
- package/dist/chunks/{ExpenseReceiptCollection-CSBrNDzv.js → ExpenseReceiptCollection-D4Na93BF.js} +37 -3
- package/dist/chunks/ExpenseReceiptCollection-D4Na93BF.js.map +1 -0
- package/dist/chunks/{expense-actions-DiIhw0xO.js → expense-actions-DszuHacx.js} +2 -2
- package/dist/chunks/{expense-actions-DiIhw0xO.js.map → expense-actions-DszuHacx.js.map} +1 -1
- package/dist/collections/ExpenseCollection.d.ts +5 -5
- package/dist/collections/ExpenseCollection.d.ts.map +1 -1
- package/dist/expense-allocation.d.ts +133 -0
- package/dist/expense-allocation.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1001 -21
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2359 -627
- package/dist/models/Expense.d.ts +2 -6
- package/dist/models/Expense.d.ts.map +1 -1
- package/dist/models/ExpenseAllocation.d.ts +117 -0
- package/dist/models/ExpenseAllocation.d.ts.map +1 -0
- package/dist/models/ExpenseReceipt.d.ts +7 -0
- package/dist/models/ExpenseReceipt.d.ts.map +1 -1
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/recipes.d.ts +3 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +746 -108
- package/dist/smrt-knowledge.okf.md +17 -3
- package/dist/types.d.ts +2 -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-CimA4lxE.js.map +0 -1
- package/dist/chunks/ExpenseReceiptCollection-CSBrNDzv.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -119,8 +119,9 @@ they are (review is method-only; see below). Help lives in
|
|
|
119
119
|
state: a recorded expense is `unreviewed`, i.e. awaiting review.
|
|
120
120
|
- **Cost object is not dereferenced on save.** `getCostObject()` resolves it
|
|
121
121
|
through the registry and returns `null` for a foreign-tenant row. One cost
|
|
122
|
-
object per expense
|
|
123
|
-
|
|
122
|
+
original cost object per expense. Reviewed relational allocation revisions
|
|
123
|
+
can attribute that source to multiple targets; target totals count portions
|
|
124
|
+
instead of the original attribution. See [reviewed allocations](agents/reviewed-allocations.md).
|
|
124
125
|
|
|
125
126
|
## Optional financial facts
|
|
126
127
|
|
|
@@ -230,3 +231,15 @@ methods. Unknown basis or unreconciled refund remains unknown and blocks new
|
|
|
230
231
|
settlement. A settlement records evidence; it executes no bank/provider/journal
|
|
231
232
|
operation. Preserve explicit chronological prefixes and same-day ordering.
|
|
232
233
|
See README and `agents/supplier-payable-test-matrix.md` for the full contract.
|
|
234
|
+
|
|
235
|
+
## Reviewed allocations (#4746)
|
|
236
|
+
|
|
237
|
+
See [reviewed allocations](agents/reviewed-allocations.md) and its
|
|
238
|
+
[behavior matrix](agents/allocation-test-matrix.md). Source accounting remains
|
|
239
|
+
singular; immutable portions own attribution and canonical billing identities.
|
|
240
|
+
|
|
241
|
+
Unbilled `withdrawAllocation` audits a full return to the original unsplit target,
|
|
242
|
+
requires source/all-old/restored-target authority, refuses any active billing
|
|
243
|
+
claim, and permits later explicit source correction without losing history.
|
|
244
|
+
Revision kind/predecessor + typed source snapshots preserve old portion facts
|
|
245
|
+
when the active head is null; receipt provenance stays frozen after withdrawal.
|
package/README.md
CHANGED
|
@@ -394,3 +394,11 @@ source and current completion/event evidence cannot be replaced or deleted;
|
|
|
394
394
|
managed source money/payer/context and linked refund facts cannot be edited
|
|
395
395
|
around the position. Caller rollback includes both evidence and source revision.
|
|
396
396
|
Expenses with no supplier-payable authority keep their existing behavior.
|
|
397
|
+
|
|
398
|
+
## Reviewed expense allocations
|
|
399
|
+
|
|
400
|
+
A reviewed original may be attributed through immutable, fully reconciled portions
|
|
401
|
+
to multiple authorized same-tenant cost objects, without cloning its spend or
|
|
402
|
+
receipt evidence. See [the allocation contract](agents/reviewed-allocations.md)
|
|
403
|
+
for current actor/target authority, retries, corrections, reporting and canonical
|
|
404
|
+
draft-to-issued billing reservations. Existing unsplit expense APIs remain valid.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Reviewed expense allocations (#4746)
|
|
2
|
+
|
|
3
|
+
Risk: **high** — public financial contract, tenant authority, concurrent monetary
|
|
4
|
+
reservations and issued invoice lineage. The integration coordinator owns full
|
|
5
|
+
release validation and independent review; worker evidence is narrow only.
|
|
6
|
+
|
|
7
|
+
All rows exercise real persistence. Commands: `pnpm --filter
|
|
8
|
+
@happyvertical/smrt-expenses exec vitest run allocation` (SQLite), then the
|
|
9
|
+
package PostgreSQL lane at integration. DuckDB is not a declared expense release
|
|
10
|
+
lane; its status must be explicitly assessed before release. No provider writes.
|
|
11
|
+
New feature: base-fail comparison is N/A (no allocation API exists on base).
|
|
12
|
+
|
|
13
|
+
| Behavior / reachable trigger | Positive | Negative / failure | Actor/context | Executor / transaction | Runtime/dialect | Contract edge / level |
|
|
14
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
15
|
+
| Review split | 100 → 60/40, reload, one receipt graph | zero/fraction/unsafe/duplicate keys/unknown fields, wrong sum | tenant reviewer | source row lock + revision CAS, all children same tx | SQLite/PostgreSQL | strict public input; integration |
|
|
16
|
+
| Current authority | every old and new target authorized | foreign tenant, revoked actor/grants, denied second target | ambient authenticated actor × owner × active tenant | callback receives bound executor | SQLite/PostgreSQL | required host callback; integration |
|
|
17
|
+
| Retry/concurrency | identical key returns same UUIDs | changed payload conflict; stale source; concurrent competing revisions | same tenant reviewer | lock then CAS; caller retry whole tx | SQLite/PostgreSQL | idempotency contract; integration |
|
|
18
|
+
| Atomicity | caller commit persists all | throw after first portion; outer rollback leaves no head/children | authorized reviewer | caller-owned executor | SQLite/PostgreSQL | injected failure; integration |
|
|
19
|
+
| Immutable revisions/source | unbilled supersession preserves history | direct save/delete, source review/money/tenant rewrite | owner and foreign actor | revision/head atomic | SQLite/PostgreSQL | guarded models; integration |
|
|
20
|
+
| Totals and accounting | global 100, targets 60/40; commitment100 once | source never added to portions, no partial remainder | current tenant | same bound executor reads | SQLite/PostgreSQL | legacy unsplit APIs; integration |
|
|
21
|
+
| Refund/reimbursement/reporting | source cash/tax facts count100 once, separate refund follows existing model | no allocated source review/money rewrite; refund capacity unchanged | tenant owner | source locks/CAS preserved | SQLite/PostgreSQL | original source remains accounting authority; integration |
|
|
22
|
+
| Canonical billable identity | portion resolves target/source/fingerprint/currency; reserve once | foreign/missing line, wrong tenant/target, reused portion | current tenant billing actor | source+invoice lock/CAS; reservation atomic | SQLite/PostgreSQL | public invoice/line contract; integration |
|
|
23
|
+
| Billed lineage | draft release then supersede; issued reservation retained | issued release/rewrite and supersede blocked | current tenant billing actor | bound transaction | SQLite/PostgreSQL | immutable issued snapshot; integration |
|
|
24
|
+
| Generated surface / migration | new tables/FKs/indexes; old rows unsplit | direct generated CRUD absent | no actor transport surface | real generated schema | SQLite/PostgreSQL | closed API/MCP/CLI; integration |
|
|
25
|
+
|
|
26
|
+
UI/browser N/A: no new user interface. External provider failure N/A: allocation
|
|
27
|
+
records local evidence and performs no external execution.
|
|
28
|
+
|
|
29
|
+
## Invoice binding extension (captured before its implementation)
|
|
30
|
+
|
|
31
|
+
Commerce owns only a nullable guarded `InvoiceLineItem.sourceReservationId`
|
|
32
|
+
pointer. The expense reservation remains the single capacity authority. A host
|
|
33
|
+
may reserve against its own draft, then bind the same reservation UUID to one
|
|
34
|
+
commerce line. Trusted package registration allowlists the `billable:` qualified
|
|
35
|
+
source type; client strings never dispatch arbitrary model methods.
|
|
36
|
+
|
|
37
|
+
| Behavior / trigger | Positive | Negative / failure | Actor/context | Executor / transaction | Runtime/dialect | Contract edge / level |
|
|
38
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
39
|
+
| Host-draft bridge | reserve once, later bind same UUID | second capacity claim, two concurrent bind targets | ambient reviewer + host draft/invoice authority | source lock/CAS, unique `(relatedReservationId,kind)`, same executor | SQLite/PostgreSQL | public compose API; integration |
|
|
40
|
+
| Commerce pointer guards | authorized bind then issue | direct pointer assignment, old-pointer clearing with source namespace change, clone/duplicate, model and collection delete, cascading invoice deletion | active tenant | document lock, owning reservation checks | SQLite/PostgreSQL | existing public mutations; integration |
|
|
41
|
+
| Missing capability | registered protected type resolves | unknown/missing handler; unbound protected line at issue | active tenant | same issue transaction | SQLite/PostgreSQL | fail-closed protocol; integration |
|
|
42
|
+
| Withdrawal | release + pointer removal then supersede | issued release, rollback after release before unbind | host current authority | one source/invoice transaction, immutable event history | SQLite/PostgreSQL | no second ledger; integration |
|
|
43
|
+
|
|
44
|
+
Initial SQLite evidence: allocation-fourth.log (6/6); extended billing first run
|
|
45
|
+
allocation-billing-first.log (8/8). These are worker evidence, not release gates.
|
|
46
|
+
|
|
47
|
+
## Dialect assessment evidence
|
|
48
|
+
|
|
49
|
+
Existing package AGENTS validation defines SQLite and migrated PostgreSQL lanes;
|
|
50
|
+
its supplier-payable matrix explicitly uses Node26 SQLite+PG. No explicit
|
|
51
|
+
DuckDB compatibility promise was found in the owning README/AGENTS. A real
|
|
52
|
+
DuckDB graph bootstrap was attempted: `allocation-dialects.log` records failure
|
|
53
|
+
before allocation behavior on the existing
|
|
54
|
+
`_smrt_billing_accounts.customer_id ON UPDATE CASCADE` relationship. Core
|
|
55
|
+
`schema/foreign-key-planner.ts` deliberately refuses that unsupported DuckDB
|
|
56
|
+
contract and directs callers to PostgreSQL/SQLite. This diagnostic is preserved;
|
|
57
|
+
no FK/schema weakening or alternate toolchain was used. It does not constitute
|
|
58
|
+
DuckDB execution evidence for allocations, and release review must reconcile the
|
|
59
|
+
supported-dialect contract rather than claim universal compatibility.
|
|
60
|
+
|
|
61
|
+
| Additional bounded contract | Positive | Negative/failure | Context/actor | Executor | Dialect/runtime | Validation |
|
|
62
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
63
|
+
| Source privacy by action | full review source grant; bounded billing context | denied original source even when destinations allowed | real ambient review actor; callback action review/reserve/bind/release | caller bound | SQLite/PG Node26 | allocation shared suite |
|
|
64
|
+
| Read authority | actual expenses.read + host target grant | read-only principal cannot reserve/review; source privacy denial does not hide own bounded portion | read actor × own target | same scoped DB | SQLite/PG Node26 | allocation shared suite |
|
|
65
|
+
| Exact economic attestation | known zero-tax gross basis60, company category only | unknown/recoverable/refunded basis stays null; refunded new bill denied | target-authorized reader/biller | source lock/CAS for mutation | SQLite/PG Node26 | allocation shared suite |
|
|
66
|
+
|
|
67
|
+
## Explicit unbilled withdrawal (before implementation)
|
|
68
|
+
|
|
69
|
+
High-risk financial/tenancy/contract continuation. Withdraw is not a billed
|
|
70
|
+
reversal and never releases claims implicitly. Public input includes exact source
|
|
71
|
+
revision, expected active allocation revision, request key and audit evidence.
|
|
72
|
+
|
|
73
|
+
| Behavior / trigger | Positive | Negative/failure | Actor/context | Executor/atomicity | Dialect | Evidence |
|
|
74
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
75
|
+
| Unbilled withdrawal | released draft claim → audited withdrawal → reopen/amount correction or duplicate rejection | active reserved/bound/issued capacity refuses; no auto-release | source-private reviewer + ALL old target grants + mandatory restored-unsplit destination grant | source lock/CAS, same caller executor, immutable event + null head in one transaction | SQLite/PG shared suite | narrow allocation command |
|
|
76
|
+
| Retry/concurrency | identical replay reauthorizes and returns same audit UUID | changed payload/key collision, stale active/source revisions, competing withdrawals | current actor/source/old targets/restored destination | unique request, unique predecessor successor; source CAS | SQLite/PG | shared feature proof |
|
|
77
|
+
| Atomic reporting | source100 returns once to unsplit target; commitments/global totals unchanged | injected failure after audit insert leaves old head/targets, caller rollback restores all | same tenant | no transient committed source+portion cost | SQLite/PG | shared real persistence |
|
|
78
|
+
| History after correction | next review links latest withdrawal deterministically; old portion facts remain immutable across source edits/reject/reopen | foreign/revoked old/restored target denies, ambiguous chain fails closed | bounded old-target reader vs full source editor | immutable relational predecessor chain; typed revision source snapshot | SQLite/PG | shared lineage/reload proof |
|
|
79
|
+
|
|
80
|
+
New API absent on prior source is feature baseline N/A for existing API
|
|
81
|
+
regression, but executable absent-command/blocked-correction evidence is captured.
|
|
82
|
+
No provider writes, browser/UI N/A; root owns PG/fullvalidation/review.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Reviewed expense attribution
|
|
2
|
+
|
|
3
|
+
`ExpenseCollection.reviewAllocation(input, authority)` reviews a **full** split
|
|
4
|
+
of a positive, reviewed original. The source keeps its original amount, payer,
|
|
5
|
+
commitment, reimbursement, refund capacity, supplier tax facts and receipt graph.
|
|
6
|
+
Children are attribution, never additional expenses or another cash authority.
|
|
7
|
+
|
|
8
|
+
The input names `sourceExpenseId`, the loaded `updated_at` as
|
|
9
|
+
`expectedSourceRevision`, `requestId`, `reviewEvidence`, and 1–1000 portions.
|
|
10
|
+
Each portion has an exact positive safe-integer `amountMinor`, unique
|
|
11
|
+
`portionKey`, registered same-tenant `costObjectType`/`costObjectId`, and explicit
|
|
12
|
+
`costCodeRef`. Portions must sum exactly to source gross money in source currency.
|
|
13
|
+
No remainder, tax allocation, customer markup or tax policy is inferred.
|
|
14
|
+
|
|
15
|
+
Ambient `expenses.recordReview` authorizes a person in the active tenant. The
|
|
16
|
+
mandatory host `authorizeSource(principal,sourceExpenseId,executor,context)`
|
|
17
|
+
callback checks original source privacy for full reviews. Its context names the
|
|
18
|
+
explicit review/reserve/bind/release action; billing contexts include only the
|
|
19
|
+
bounded authorized portion so target finance users need no unrelated original
|
|
20
|
+
project or full receipt disclosure. The mandatory
|
|
21
|
+
`authorizeTarget(principal,target,executor)` callback must enforce
|
|
22
|
+
current project grants for **every old and proposed target**. The supplied
|
|
23
|
+
executor is the command's caller-affine transaction. Callback denial rolls back
|
|
24
|
+
all children/head/revision claims. Never perform provider writes in the callback.
|
|
25
|
+
|
|
26
|
+
A source lock and revision compare-and-swap serialize allocations and billing
|
|
27
|
+
reservations. Same request/payload returns the same immutable IDs after current
|
|
28
|
+
authority checks; changed payload conflicts. On serialization/revision failure,
|
|
29
|
+
retry the whole caller transaction with fresh instances. Unreserved corrections
|
|
30
|
+
append a revision linked by `previousRevisionId`; prior children remain readable.
|
|
31
|
+
While an allocation is active, source review cannot be reopened/rejected.
|
|
32
|
+
Use explicit audited unbilled withdrawal before source correction; immutable
|
|
33
|
+
allocation provenance prevents physical source deletion.
|
|
34
|
+
|
|
35
|
+
`totalsByCurrency()` counts each source once. Target filters project the current
|
|
36
|
+
portions instead of counting the source's original attribution. Commitment
|
|
37
|
+
positions, supplier-tax reports and reimbursement/refund positions retain the
|
|
38
|
+
original accounting source exactly once. `forCostObject()` remains an original
|
|
39
|
+
source/evidence query; use target-filtered totals for allocated money. Do not sum
|
|
40
|
+
source amounts obtained as evidence to calculate target costs.
|
|
41
|
+
|
|
42
|
+
`resolveExpenseAllocationPortion(db,portionId,authority)` returns only that
|
|
43
|
+
portion's immutable source/revision/fingerprint/target/code/amount/currency and
|
|
44
|
+
current `active` flag after project authority; it does not disclose receipt URLs
|
|
45
|
+
or other projects. Receipt access still needs the original source privacy check.
|
|
46
|
+
`getExpenseAllocationPortionPosition` adds current reservation UUID, host draft
|
|
47
|
+
references and bound invoice references/status for that same target portion. It
|
|
48
|
+
is advisory; reservation mutations always recheck capacity atomically. Historical
|
|
49
|
+
or issued claims never become available implicitly.
|
|
50
|
+
|
|
51
|
+
## Billing
|
|
52
|
+
|
|
53
|
+
`reserveExpenseAllocationPortion` reserves a whole portion once against a host
|
|
54
|
+
`draftType`/`draftId`, requiring `authority.authorizeDraft`; it can alternatively
|
|
55
|
+
reserve and bind directly to an existing invoice/line. Customer price remains
|
|
56
|
+
independent of source capacity. `bindExpenseAllocationPortion` assigns that same
|
|
57
|
+
reservation UUID to one same-tenant, same-currency commerce draft line after
|
|
58
|
+
mandatory `authorizeInvoice` current-grant/project checks. The line must use
|
|
59
|
+
`EXPENSE_ALLOCATION_INVOICE_SOURCE` and `sourceId = portionId`.
|
|
60
|
+
|
|
61
|
+
Commerce stores only the reservation pointer, with trusted source-handler checks
|
|
62
|
+
on line mutations and invoice issue. Missing handlers, unbound protected lines,
|
|
63
|
+
changed/cleared bound identities, copied pointers and duplicate line claims fail.
|
|
64
|
+
All reservation and binding events are immutable relational rows; source locking,
|
|
65
|
+
CAS and unique event keys prevent concurrent double assignment. There is no
|
|
66
|
+
commerce-side source availability balance.
|
|
67
|
+
|
|
68
|
+
`releaseExpenseAllocationPortion` records withdrawal and removes a bound draft
|
|
69
|
+
pointer in the same transaction. It retains claim/binding history. Active claims
|
|
70
|
+
block allocation supersession. Once issued, a claim remains consumed: ordinary
|
|
71
|
+
release/reallocation is refused and corrections require an explicit credit or
|
|
72
|
+
reversal policy; the API never rewrites issued invoices or infers such policy.
|
|
73
|
+
|
|
74
|
+
Migrate schemas before use, including the three new allocation tables, nullable
|
|
75
|
+
Expense head and nullable commerce line pointer. Existing unsplit rows retain
|
|
76
|
+
NULL heads/pointers and unchanged accounting semantics. See the
|
|
77
|
+
[behavior matrix](allocation-test-matrix.md) for release evidence requirements.
|
|
78
|
+
|
|
79
|
+
Bounded readers use the explicitly published `expenses.read` catalog permission
|
|
80
|
+
(registered through public `registerPermissionDefinitions`; `get`/`list`
|
|
81
|
+
normalize to `.read`) and the host's mandatory current target-cost
|
|
82
|
+
visibility callback. They never require review authority or original-source
|
|
83
|
+
privacy access. Mutations retain `expenses.recordReview`. Consumers must grant
|
|
84
|
+
these real permissions through their role configuration; never fabricate a
|
|
85
|
+
permission snapshot to call the library.
|
|
86
|
+
|
|
87
|
+
Bounded identities include source date, payer **category** (no person/name),
|
|
88
|
+
reimbursement eligibility, nullable supplier-tax presence, refund presence and
|
|
89
|
+
nullable exact `costBasisMinor`. Basis equals portion gross only when the
|
|
90
|
+
original's complete known basis equals gross and no linked refunds exist. It is
|
|
91
|
+
otherwise unknown; tax is never apportioned implicitly. New reserve/bind commands
|
|
92
|
+
refuse a source with any linked refunds pending explicit reversal policy.
|
|
93
|
+
Reimbursement eligibility freezes with allocation provenance. Existing issued
|
|
94
|
+
reservations remain consumed and immutable when later refund evidence arrives.
|
|
95
|
+
|
|
96
|
+
The source fingerprint includes a private random per-source nonce retained only
|
|
97
|
+
in full source-authorized revision evidence. Bounded readers receive its digest,
|
|
98
|
+
not the nonce, preventing offline guesses of another project's source total.
|
|
99
|
+
Unbilled supersession reuses the nonce and retains the same source fingerprint.
|
|
100
|
+
|
|
101
|
+
## Explicit unbilled withdrawal before source correction
|
|
102
|
+
|
|
103
|
+
`ExpenseCollection.withdrawAllocation` (root `withdrawExpenseAllocation`) accepts
|
|
104
|
+
`sourceExpenseId`, `expectedActiveRevisionId`, `expectedSourceRevision`,
|
|
105
|
+
`requestId` and `reviewEvidence`. It appends an immutable revision with
|
|
106
|
+
`kind: 'withdrawal'` and clears the active head atomically on the caller executor.
|
|
107
|
+
The result is `{revision, withdrawnRevisionId}`; reload the original source for
|
|
108
|
+
its current state/revision. No portion row is changed or deleted.
|
|
109
|
+
|
|
110
|
+
Withdrawal requires current source privacy/edit authority with action
|
|
111
|
+
`withdrawAllocation`, authority for **every old portion target**, and a separate
|
|
112
|
+
mandatory `authorizeUnsplitTarget(principal,{sourceExpenseId,costObjectType,
|
|
113
|
+
costObjectId},executor)` callback for the restored original destination. The
|
|
114
|
+
latter has no invented cost code. Assigned destinations must exist in the active
|
|
115
|
+
tenant; an explicitly unassigned original has both destination strings empty and
|
|
116
|
+
still requires the callback. Replays reauthorize all of these paths; changed
|
|
117
|
+
payload, stale source/head or a conflicting request is refused.
|
|
118
|
+
|
|
119
|
+
Any active reservation, bound claim or issued claim refuses withdrawal. First
|
|
120
|
+
abandon/release an authorized unissued draft explicitly; withdrawal never
|
|
121
|
+
silently releases capacity or rewrites issued invoices. **This is not billed
|
|
122
|
+
reversal support.** Existing settlement/refund/reimbursement guards also remain.
|
|
123
|
+
|
|
124
|
+
Global and commitment totals still count the original source once. Its monetary
|
|
125
|
+
attribution switches atomically back to the original unsplit destination. Source
|
|
126
|
+
review status is unchanged: no financial approval is implied. The caller may
|
|
127
|
+
then explicitly reopen and correct the original amount/date/payer or reject a
|
|
128
|
+
duplicate using existing guarded source methods. Corrected unreviewed money stays
|
|
129
|
+
excluded until separately reviewed. Original receipt evidence remains immutable
|
|
130
|
+
once allocation history exists, including after withdrawal.
|
|
131
|
+
|
|
132
|
+
The immutable predecessor chain has one successor per revision. Its unique leaf
|
|
133
|
+
retains the latest withdrawal independently of a null active head; the next
|
|
134
|
+
review links to that withdrawal deterministically. Typed source snapshot facts
|
|
135
|
+
on each revision preserve historical portion date/payer/basis attestation across
|
|
136
|
+
later source correction. Inactive historical portions remain bounded-readable
|
|
137
|
+
with `active:false` when the source is reopened/rejected; they are never available
|
|
138
|
+
for fresh billing. Active portions still require a usable reviewed source.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Package-private command capabilities; never exported from the package root. */
|
|
2
|
+
import type { SmrtObject } from '@happyvertical/smrt-core';
|
|
3
|
+
export declare const allocationWrites: WeakSet<SmrtObject>;
|
|
4
|
+
export declare const allocationHeads: WeakMap<SmrtObject, string | null>;
|
|
5
|
+
export declare function insertAllocation<T extends SmrtObject>(row: T): Promise<T>;
|
|
6
|
+
//# sourceMappingURL=allocation-internal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocation-internal.d.ts","sourceRoot":"","sources":["../src/allocation-internal.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,eAAO,MAAM,gBAAgB,qBAA4B,CAAC;AAC1D,eAAO,MAAM,eAAe,oCAA2C,CAAC;AACxE,wBAAsB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EACzD,GAAG,EAAE,CAAC,GACL,OAAO,CAAC,CAAC,CAAC,CASZ"}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { ExpenseError } from "../types.js";
|
|
2
2
|
import { ObjectRegistry, SmrtObject, crossPackageRef, field, foreignKey, isPostgresDatabase, isUniqueViolationError, smrt } from "@happyvertical/smrt-core";
|
|
3
3
|
import { TenantScoped, getTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
4
|
-
import { ContractLineItemCollection, PurchaseContractCollection, VendorCollection } from "@happyvertical/smrt-commerce";
|
|
5
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { ContractLineItemCollection, PurchaseContractCollection, VendorCollection } from "@happyvertical/smrt-commerce";
|
|
6
|
+
//#region src/allocation-internal.ts
|
|
7
|
+
var allocationWrites = /* @__PURE__ */ new WeakSet();
|
|
8
|
+
var allocationHeads = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
async function insertAllocation(row) {
|
|
10
|
+
await row.initialize();
|
|
11
|
+
allocationWrites.add(row);
|
|
12
|
+
try {
|
|
13
|
+
await row.save();
|
|
14
|
+
return row;
|
|
15
|
+
} finally {
|
|
16
|
+
allocationWrites.delete(row);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
6
20
|
//#region src/validation.ts
|
|
7
21
|
var CATEGORY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
8
22
|
var ISO_CURRENCY = /^[A-Z]{3}$/;
|
|
@@ -643,8 +657,10 @@ var Expense = class extends SmrtObject {
|
|
|
643
657
|
reimbursedAmount = null;
|
|
644
658
|
reimbursedAt = null;
|
|
645
659
|
reimbursementReference = "";
|
|
660
|
+
activeAllocationRevisionId = null;
|
|
646
661
|
constructor(options = {}) {
|
|
647
662
|
super(options);
|
|
663
|
+
if (options.activeAllocationRevisionId !== void 0) this.activeAllocationRevisionId = options.activeAllocationRevisionId;
|
|
648
664
|
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
649
665
|
if (options.costObjectType !== void 0) this.costObjectType = options.costObjectType;
|
|
650
666
|
if (options.costObjectId !== void 0) this.costObjectId = options.costObjectId;
|
|
@@ -867,6 +883,10 @@ var Expense = class extends SmrtObject {
|
|
|
867
883
|
const persisted = await this.readPersisted();
|
|
868
884
|
const inserting = await pinNaturalKey("Expense", this, persisted);
|
|
869
885
|
if (persisted && String(persisted.tenant_id ?? "") !== String(effectiveTenant(this.tenantId) ?? "")) throw new ExpenseError("EXPENSE_INVALID", `Expense ${this.id}: its tenant cannot change after it is recorded.`);
|
|
886
|
+
const storedAllocation = asNullableString(persisted?.active_allocation_revision_id);
|
|
887
|
+
if (storedAllocation && persisted && Boolean(persisted.reimbursable) !== this.reimbursable) throw new ExpenseError("EXPENSE_INVALID", "Allocated source reimbursement eligibility is frozen.");
|
|
888
|
+
if (storedAllocation !== this.activeAllocationRevisionId && !(allocationHeads.has(this) && allocationHeads.get(this) === this.activeAllocationRevisionId)) throw new ExpenseError("EXPENSE_INVALID", "Allocation head is command-owned.");
|
|
889
|
+
if (storedAllocation && persisted && (String(persisted.review_status) !== this.reviewStatus || String(persisted.duplicate_of_id ?? "") !== String(this.duplicateOfId ?? ""))) throw new ExpenseError("EXPENSE_INVALID", "Allocated source review cannot change; supersede its allocation instead.");
|
|
870
890
|
await this.assertSupplierPayable(persisted);
|
|
871
891
|
this.assertReviewFields(persisted);
|
|
872
892
|
this.assertReviewedLock(persisted);
|
|
@@ -994,6 +1014,7 @@ var Expense = class extends SmrtObject {
|
|
|
994
1014
|
await this.lockRefundRows();
|
|
995
1015
|
const row = await this.readPersisted();
|
|
996
1016
|
if (row && String(row.tenant_id ?? "") !== String(effectiveTenant(this.tenantId) ?? "")) payableInvalid("owned source is required.");
|
|
1017
|
+
if (row?.active_allocation_revision_id) throw new ExpenseError("EXPENSE_INVALID", "Allocated source evidence cannot be deleted.");
|
|
997
1018
|
if (row?.supplier_payable_json) payableInvalid("supplier authority cannot be deleted.");
|
|
998
1019
|
const parentId = asNullableString(row?.refund_of_id);
|
|
999
1020
|
if (parentId) {
|
|
@@ -1312,6 +1333,10 @@ __decorateClass([field({
|
|
|
1312
1333
|
type: "text",
|
|
1313
1334
|
readonly: true
|
|
1314
1335
|
})], Expense.prototype, "reimbursementReference", 2);
|
|
1336
|
+
__decorateClass([foreignKey("ExpenseAllocationRevision", {
|
|
1337
|
+
nullable: true,
|
|
1338
|
+
onDelete: "RESTRICT"
|
|
1339
|
+
}), field({ readonly: true })], Expense.prototype, "activeAllocationRevisionId", 2);
|
|
1315
1340
|
Expense = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
|
|
1316
1341
|
tableName: "expenses",
|
|
1317
1342
|
api: { include: [
|
|
@@ -1359,6 +1384,6 @@ function sameTenant(a, b) {
|
|
|
1359
1384
|
return (a ?? null) === (b ?? null);
|
|
1360
1385
|
}
|
|
1361
1386
|
//#endregion
|
|
1362
|
-
export {
|
|
1387
|
+
export { effectiveTenant as a, normalizeSha256 as c, allocationHeads as d, allocationWrites as f, computeContentSha256 as i, normalizeTenantId as l, assertMinorUnits as n, identityConflict as o, insertAllocation as p, assertQualifiedClassName as r, instantMs as s, Expense as t, pinNaturalKey as u };
|
|
1363
1388
|
|
|
1364
|
-
//# sourceMappingURL=Expense-
|
|
1389
|
+
//# sourceMappingURL=Expense-Deq19w2r.js.map
|