@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
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-expenses",
|
|
6
|
-
"packageVersion": "0.57.
|
|
6
|
+
"packageVersion": "0.57.5",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/financial-facts-test-matrix.md": "
|
|
10
|
+
"manifest": "da9fd2a0da4b9c8ceed2dbe761c1f8ea837d7df89ad0e14ba1a7ef617f8e3076",
|
|
11
|
+
"packageJson": "9b80d759b93bd620285e139401b10138a164c293d6e138e734aa629598ca6ea1",
|
|
12
|
+
"agents": "f6a889c43d603b553f30e90de45eb046daa712bd1a5a2244a4f3b8f9ed1232a3",
|
|
13
|
+
"moduleDoc:agents/financial-facts-test-matrix.md": "77396f7345bb43a35b412bee579f1e457b58e99561bfdc3e5dc30d28ea07d824",
|
|
14
14
|
"moduleDoc:agents/expense-ui.md": "0bdea9e6c627f8cbab2a1f21ef4da3ab7f00409764a1ef48572dd40694fb6785"
|
|
15
15
|
},
|
|
16
16
|
"exports": [
|
|
@@ -76,11 +76,17 @@
|
|
|
76
76
|
"methods": [
|
|
77
77
|
"attachReceipt",
|
|
78
78
|
"commitmentPosition",
|
|
79
|
+
"completeSupplierPayableBasis",
|
|
79
80
|
"forCostObject",
|
|
80
81
|
"listReviewedTaxFacts",
|
|
81
82
|
"record",
|
|
82
83
|
"recordReimbursement",
|
|
83
84
|
"recordReview",
|
|
85
|
+
"recordSupplierPayable",
|
|
86
|
+
"recordSupplierPayableEvent",
|
|
87
|
+
"refundPosition",
|
|
88
|
+
"reimbursementPosition",
|
|
89
|
+
"supplierPayablePosition",
|
|
84
90
|
"totalsByCurrency"
|
|
85
91
|
],
|
|
86
92
|
"methodSignatures": [
|
|
@@ -101,6 +107,15 @@
|
|
|
101
107
|
],
|
|
102
108
|
"returns": "Promise<CommitmentPosition>"
|
|
103
109
|
},
|
|
110
|
+
{
|
|
111
|
+
"name": "completeSupplierPayableBasis",
|
|
112
|
+
"async": true,
|
|
113
|
+
"params": [
|
|
114
|
+
"expenseId: string",
|
|
115
|
+
"input: CompleteSupplierPayableBasisInput"
|
|
116
|
+
],
|
|
117
|
+
"returns": "Promise<SupplierPayablePosition>"
|
|
118
|
+
},
|
|
104
119
|
{
|
|
105
120
|
"name": "forCostObject",
|
|
106
121
|
"async": true,
|
|
@@ -142,6 +157,49 @@
|
|
|
142
157
|
],
|
|
143
158
|
"returns": "Promise<Expense>"
|
|
144
159
|
},
|
|
160
|
+
{
|
|
161
|
+
"name": "recordSupplierPayable",
|
|
162
|
+
"async": true,
|
|
163
|
+
"params": [
|
|
164
|
+
"expenseId: string",
|
|
165
|
+
"input: RecordSupplierPayableInput"
|
|
166
|
+
],
|
|
167
|
+
"returns": "Promise<SupplierPayablePosition>"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "recordSupplierPayableEvent",
|
|
171
|
+
"async": true,
|
|
172
|
+
"params": [
|
|
173
|
+
"expenseId: string",
|
|
174
|
+
"input: RecordSupplierPayableEventInput"
|
|
175
|
+
],
|
|
176
|
+
"returns": "Promise<SupplierPayablePosition>"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "refundPosition",
|
|
180
|
+
"async": true,
|
|
181
|
+
"params": [
|
|
182
|
+
"sourceId: string"
|
|
183
|
+
],
|
|
184
|
+
"returns": "Promise<ExpenseRefundPosition>"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "reimbursementPosition",
|
|
188
|
+
"async": true,
|
|
189
|
+
"params": [
|
|
190
|
+
"sourceId: string"
|
|
191
|
+
],
|
|
192
|
+
"returns": "Promise<ExpenseReimbursementPosition>"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "supplierPayablePosition",
|
|
196
|
+
"async": true,
|
|
197
|
+
"params": [
|
|
198
|
+
"expenseId: string",
|
|
199
|
+
"input?: object"
|
|
200
|
+
],
|
|
201
|
+
"returns": "Promise<SupplierPayablePosition | null>"
|
|
202
|
+
},
|
|
145
203
|
{
|
|
146
204
|
"name": "totalsByCurrency",
|
|
147
205
|
"async": true,
|
|
@@ -193,6 +251,13 @@
|
|
|
193
251
|
"reason": "not listed in api.include",
|
|
194
252
|
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
195
253
|
},
|
|
254
|
+
{
|
|
255
|
+
"kind": "api",
|
|
256
|
+
"operation": "completeSupplierPayableBasis",
|
|
257
|
+
"code": "not-included",
|
|
258
|
+
"reason": "not listed in api.include",
|
|
259
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
260
|
+
},
|
|
196
261
|
{
|
|
197
262
|
"kind": "api",
|
|
198
263
|
"operation": "forCostObject",
|
|
@@ -207,6 +272,41 @@
|
|
|
207
272
|
"reason": "not listed in api.include",
|
|
208
273
|
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
209
274
|
},
|
|
275
|
+
{
|
|
276
|
+
"kind": "api",
|
|
277
|
+
"operation": "recordSupplierPayable",
|
|
278
|
+
"code": "not-included",
|
|
279
|
+
"reason": "not listed in api.include",
|
|
280
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"kind": "api",
|
|
284
|
+
"operation": "recordSupplierPayableEvent",
|
|
285
|
+
"code": "not-included",
|
|
286
|
+
"reason": "not listed in api.include",
|
|
287
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"kind": "api",
|
|
291
|
+
"operation": "refundPosition",
|
|
292
|
+
"code": "not-included",
|
|
293
|
+
"reason": "not listed in api.include",
|
|
294
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"kind": "api",
|
|
298
|
+
"operation": "reimbursementPosition",
|
|
299
|
+
"code": "not-included",
|
|
300
|
+
"reason": "not listed in api.include",
|
|
301
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"kind": "api",
|
|
305
|
+
"operation": "supplierPayablePosition",
|
|
306
|
+
"code": "not-included",
|
|
307
|
+
"reason": "not listed in api.include",
|
|
308
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
309
|
+
},
|
|
210
310
|
{
|
|
211
311
|
"kind": "api",
|
|
212
312
|
"operation": "totalsByCurrency",
|
|
@@ -438,6 +538,13 @@
|
|
|
438
538
|
"required": false,
|
|
439
539
|
"columnType": "TEXT"
|
|
440
540
|
},
|
|
541
|
+
{
|
|
542
|
+
"name": "supplierPayableJson",
|
|
543
|
+
"type": "text",
|
|
544
|
+
"required": false,
|
|
545
|
+
"columnType": "TEXT",
|
|
546
|
+
"readonly": true
|
|
547
|
+
},
|
|
441
548
|
{
|
|
442
549
|
"name": "paidByName",
|
|
443
550
|
"type": "text",
|
|
@@ -451,6 +558,12 @@
|
|
|
451
558
|
"related": "Expense",
|
|
452
559
|
"columnType": "UUID"
|
|
453
560
|
},
|
|
561
|
+
{
|
|
562
|
+
"name": "refundRecipient",
|
|
563
|
+
"type": "text",
|
|
564
|
+
"required": false,
|
|
565
|
+
"columnType": "TEXT"
|
|
566
|
+
},
|
|
454
567
|
{
|
|
455
568
|
"name": "incurredOn",
|
|
456
569
|
"type": "text",
|
|
@@ -545,17 +658,26 @@
|
|
|
545
658
|
"required": false,
|
|
546
659
|
"columnType": "BOOLEAN"
|
|
547
660
|
},
|
|
661
|
+
{
|
|
662
|
+
"name": "reimbursedAmount",
|
|
663
|
+
"type": "integer",
|
|
664
|
+
"required": false,
|
|
665
|
+
"columnType": "INTEGER",
|
|
666
|
+
"readonly": true
|
|
667
|
+
},
|
|
548
668
|
{
|
|
549
669
|
"name": "reimbursedAt",
|
|
550
670
|
"type": "datetime",
|
|
551
671
|
"required": false,
|
|
552
|
-
"columnType": "TIMESTAMP"
|
|
672
|
+
"columnType": "TIMESTAMP",
|
|
673
|
+
"readonly": true
|
|
553
674
|
},
|
|
554
675
|
{
|
|
555
676
|
"name": "reimbursementReference",
|
|
556
677
|
"type": "text",
|
|
557
678
|
"required": false,
|
|
558
|
-
"columnType": "TEXT"
|
|
679
|
+
"columnType": "TEXT",
|
|
680
|
+
"readonly": true
|
|
559
681
|
}
|
|
560
682
|
],
|
|
561
683
|
"relationships": [
|
|
@@ -617,20 +739,45 @@
|
|
|
617
739
|
}
|
|
618
740
|
],
|
|
619
741
|
"methods": [
|
|
742
|
+
"completeSupplierPayableBasis",
|
|
743
|
+
"delete",
|
|
620
744
|
"getCostBasis",
|
|
621
745
|
"getCostObject",
|
|
622
746
|
"getFinancialBreakdown",
|
|
747
|
+
"getRefundPosition",
|
|
748
|
+
"getRefundRecipient",
|
|
749
|
+
"getReimbursementPosition",
|
|
750
|
+
"getSupplierPayablePosition",
|
|
623
751
|
"isCountable",
|
|
624
752
|
"isReimbursed",
|
|
625
753
|
"markDuplicate",
|
|
626
754
|
"markReimbursed",
|
|
755
|
+
"recordSupplierPayable",
|
|
756
|
+
"recordSupplierPayableEvent",
|
|
627
757
|
"reject",
|
|
628
758
|
"reopen",
|
|
629
759
|
"review",
|
|
630
760
|
"save",
|
|
631
|
-
"setFinancialBreakdown"
|
|
761
|
+
"setFinancialBreakdown",
|
|
762
|
+
"setRefundRecipient"
|
|
632
763
|
],
|
|
633
764
|
"methodSignatures": [
|
|
765
|
+
{
|
|
766
|
+
"name": "completeSupplierPayableBasis",
|
|
767
|
+
"async": true,
|
|
768
|
+
"params": [
|
|
769
|
+
"input: CompleteSupplierPayableBasisInput"
|
|
770
|
+
],
|
|
771
|
+
"returns": "Promise<SupplierPayablePosition>"
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
"name": "delete",
|
|
775
|
+
"async": true,
|
|
776
|
+
"params": [
|
|
777
|
+
"options?: object"
|
|
778
|
+
],
|
|
779
|
+
"returns": "Promise<void>"
|
|
780
|
+
},
|
|
634
781
|
{
|
|
635
782
|
"name": "getCostBasis",
|
|
636
783
|
"returns": "ExpenseCostBasis | null"
|
|
@@ -644,6 +791,28 @@
|
|
|
644
791
|
"name": "getFinancialBreakdown",
|
|
645
792
|
"returns": "ExpenseFinancialBreakdown | null"
|
|
646
793
|
},
|
|
794
|
+
{
|
|
795
|
+
"name": "getRefundPosition",
|
|
796
|
+
"async": true,
|
|
797
|
+
"returns": "Promise<ExpenseRefundPosition>"
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
"name": "getRefundRecipient",
|
|
801
|
+
"returns": "ExpenseRefundRecipient | null"
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
"name": "getReimbursementPosition",
|
|
805
|
+
"async": true,
|
|
806
|
+
"returns": "Promise<ExpenseReimbursementPosition>"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
"name": "getSupplierPayablePosition",
|
|
810
|
+
"async": true,
|
|
811
|
+
"params": [
|
|
812
|
+
"input?: object"
|
|
813
|
+
],
|
|
814
|
+
"returns": "Promise<SupplierPayablePosition | null>"
|
|
815
|
+
},
|
|
647
816
|
{
|
|
648
817
|
"name": "isCountable",
|
|
649
818
|
"returns": "boolean"
|
|
@@ -668,6 +837,22 @@
|
|
|
668
837
|
],
|
|
669
838
|
"returns": "Promise<this>"
|
|
670
839
|
},
|
|
840
|
+
{
|
|
841
|
+
"name": "recordSupplierPayable",
|
|
842
|
+
"async": true,
|
|
843
|
+
"params": [
|
|
844
|
+
"input: RecordSupplierPayableInput"
|
|
845
|
+
],
|
|
846
|
+
"returns": "Promise<SupplierPayablePosition>"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
"name": "recordSupplierPayableEvent",
|
|
850
|
+
"async": true,
|
|
851
|
+
"params": [
|
|
852
|
+
"input: RecordSupplierPayableEventInput"
|
|
853
|
+
],
|
|
854
|
+
"returns": "Promise<SupplierPayablePosition>"
|
|
855
|
+
},
|
|
671
856
|
{
|
|
672
857
|
"name": "reject",
|
|
673
858
|
"async": true,
|
|
@@ -703,6 +888,13 @@
|
|
|
703
888
|
"value: ExpenseFinancialBreakdown | null"
|
|
704
889
|
],
|
|
705
890
|
"returns": "void"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"name": "setRefundRecipient",
|
|
894
|
+
"params": [
|
|
895
|
+
"value: ExpenseRefundRecipient | null"
|
|
896
|
+
],
|
|
897
|
+
"returns": "void"
|
|
706
898
|
}
|
|
707
899
|
],
|
|
708
900
|
"tenant": {
|
|
@@ -717,6 +909,13 @@
|
|
|
717
909
|
],
|
|
718
910
|
"surfaces": [],
|
|
719
911
|
"withheldSurfaces": [
|
|
912
|
+
{
|
|
913
|
+
"kind": "api",
|
|
914
|
+
"operation": "completeSupplierPayableBasis",
|
|
915
|
+
"code": "not-included",
|
|
916
|
+
"reason": "not listed in api.include",
|
|
917
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
918
|
+
},
|
|
720
919
|
{
|
|
721
920
|
"kind": "api",
|
|
722
921
|
"operation": "getCostBasis",
|
|
@@ -738,6 +937,34 @@
|
|
|
738
937
|
"reason": "not listed in api.include",
|
|
739
938
|
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
740
939
|
},
|
|
940
|
+
{
|
|
941
|
+
"kind": "api",
|
|
942
|
+
"operation": "getRefundPosition",
|
|
943
|
+
"code": "not-included",
|
|
944
|
+
"reason": "not listed in api.include",
|
|
945
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
"kind": "api",
|
|
949
|
+
"operation": "getRefundRecipient",
|
|
950
|
+
"code": "not-included",
|
|
951
|
+
"reason": "not listed in api.include",
|
|
952
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
"kind": "api",
|
|
956
|
+
"operation": "getReimbursementPosition",
|
|
957
|
+
"code": "not-included",
|
|
958
|
+
"reason": "not listed in api.include",
|
|
959
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"kind": "api",
|
|
963
|
+
"operation": "getSupplierPayablePosition",
|
|
964
|
+
"code": "not-included",
|
|
965
|
+
"reason": "not listed in api.include",
|
|
966
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
967
|
+
},
|
|
741
968
|
{
|
|
742
969
|
"kind": "api",
|
|
743
970
|
"operation": "isCountable",
|
|
@@ -766,6 +993,20 @@
|
|
|
766
993
|
"reason": "not listed in api.include",
|
|
767
994
|
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
768
995
|
},
|
|
996
|
+
{
|
|
997
|
+
"kind": "api",
|
|
998
|
+
"operation": "recordSupplierPayable",
|
|
999
|
+
"code": "not-included",
|
|
1000
|
+
"reason": "not listed in api.include",
|
|
1001
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
"kind": "api",
|
|
1005
|
+
"operation": "recordSupplierPayableEvent",
|
|
1006
|
+
"code": "not-included",
|
|
1007
|
+
"reason": "not listed in api.include",
|
|
1008
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
1009
|
+
},
|
|
769
1010
|
{
|
|
770
1011
|
"kind": "api",
|
|
771
1012
|
"operation": "reject",
|
|
@@ -800,6 +1041,13 @@
|
|
|
800
1041
|
"code": "not-included",
|
|
801
1042
|
"reason": "not listed in api.include",
|
|
802
1043
|
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"kind": "api",
|
|
1047
|
+
"operation": "setRefundRecipient",
|
|
1048
|
+
"code": "not-included",
|
|
1049
|
+
"reason": "not listed in api.include",
|
|
1050
|
+
"objectName": "@happyvertical/smrt-expenses:Expense"
|
|
803
1051
|
}
|
|
804
1052
|
],
|
|
805
1053
|
"relationshipFeatures": [
|
|
@@ -976,12 +1224,12 @@
|
|
|
976
1224
|
"polymorphicAssociations": 0,
|
|
977
1225
|
"uuidColumns": 30
|
|
978
1226
|
},
|
|
979
|
-
"agentDoc": "# @happyvertical/smrt-expenses\n\nIncurred costs against a cost object, with deduplicated receipts, an explicit\nreview, reimbursement recording, and smrt-commerce commitment drawdown (#3289).\n\n## Models\n\n- **Expense** (`expenses`): `costObjectType` (qualified class name) +\n `costObjectId` (bare string), `category` (lowercase kebab-case; suggestions\n in `SUGGESTED_EXPENSE_CATEGORIES`), `amount` integer minor units + ISO 4217\n `currency`, `incurredOn` (`YYYY-MM-DD` text: a calendar date, not an\n instant), `recordedAt`, `recordedByProfileId`, `paidBy` (`company` |\n `person` | `third-party`) + `paidByProfileId` / `paidByName`, `vendorId`, `commitmentId` /\n `commitmentLineId`, review fields, `reimbursable` / `reimbursedAt` /\n `reimbursementReference`.\n- **ExpenseReceipt** (`expense_receipts`): noun-specific asset join\n (`expenseId` → Expense, `assetId` → smrt-assets Asset) with uploader,\n `filename`, `mimeType`, `byteCount`, `contentSha256`. Collection extends\n `SmrtJunction` (left expense, right asset, ordered by `created_at`).\n\n## Recipe\n\n`src/recipes.ts` declares one `SmrtRecipe`, `expenses.tracking` (label\n\"Expenses\", section `accounting`, shared with `ledgers.bookkeeping`), emitted\ninto the built `manifest.json` and `smrt-knowledge.json` `recipes`. It brings\n`Expense` with `ExpenseReceipt` as a child model, hides the cost-object type,\ncommitment and reviewer plumbing, and leaves the read-only review fields as\nthey are (review is method-only; see below). Help lives in\n`src/tracking.recipe.md` (#3591). See [core recipes](../core/agents/recipes.md).\n\n## Invariants\n\n- **Review is method-only.** `review()`, `reject()`, `markDuplicate()`,\n `reopen()` read the persisted status, check the move, and authorize exactly\n one save through a module `WeakMap`. Every transition assigns every review\n field, and the token carries that complete resulting state, so a field set\n directly beside `review()`/`reject()` (e.g. an injected `duplicateOfId`)\n is refused. A `reviewed` expense never carries `duplicateOfId`. `save()` compares every review field\n (status, reviewer, time, note, `duplicateOfId`) with the stored row and\n refuses an unauthorized change (`EXPENSE_REVIEW_FIELDS_LOCKED`), including\n on a brand-new row. Review fields are also `readonly` for generated writes.\n- **Writes are pinned to the guarded row.** The guards read the row stored\n under `id`, but core's natural-key save adopts any same-owner row on\n `(tenant_id, slug, context)` and upserts it with no revision predicate;\n these models' slugs come from the id. So `pinNaturalKey()` lets a fresh\n (not `isPersisted`) instance only INSERT (`requireInsertOnSave()`): naming\n an existing row's id is `EXPENSE_IDENTITY_CONFLICT`, and a row appearing\n mid-save fails the INSERT instead of being adopted. An existing row changes\n only through a loaded instance (revision compare-and-swap) that keeps its\n slug/context. Never `create({ id: <existing> })` or `getOrUpsert()` these\n models; load and save instead.\n- **Reviewed money is frozen.** While the stored status is `reviewed`,\n `LOCKED_WHEN_REVIEWED` fields (money, currency, both dates, cost object,\n vendor, commitment, payer) cannot change; `reopen()` first. `recordedAt` is\n compared as an instant, since drivers return it as a `Date` or a string.\n- **Drawdown counts once.** `ExpenseCollection.commitmentPosition()` sums only\n `reviewed` rows with no `duplicateOfId`, in the commitment's currency, deduped\n by id. Committed is the contract's `totalAmount` (or the line's `amount`).\n- **Currencies never mix.** A commitment match must share the currency and\n tenant (`EXPENSE_COMMITMENT_MISMATCH`), checked when the match, line or\n currency is set or changed — not on every save, so a later commitment edit\n never blocks e.g. `markReimbursed()`. `totalsByCurrency()` keys by currency.\n Rows left in another currency by a later commitment change land in\n `otherCurrencies`, never in `drawn`.\n- **Duplicate receipts.** Same `(expense, sha256)` is refused by a pre-insert\n read plus the unique index `expense_receipts_expense_sha256_key`\n `(expense_id, content_sha256)`; a unique violation is rethrown as\n `DuplicateReceiptError`, classified from the error itself (violated key\n name, or `details.fieldName` columns on core's typed error) because on\n PostgreSQL the violation aborts a caller's transaction and nothing can be\n re-read; only an error naming no key falls back to a re-read. The index omits `tenant_id` deliberately: the\n expense already fixes the tenant, and a nullable leading `tenant_id` would\n leave NULL-tenant (global) receipts unenforced. Same hash on different\n expenses is allowed; `findDuplicateReceipts()` reports it (facet on\n `contentSha256`, count > 1). Receipt `tenantId` (derived from the\n expense on insert), `expenseId`, `assetId` and `contentSha256` are\n immutable. The inherited `SmrtJunction.attach()` and\n `setLinks()` are overridden to throw (`setLinks()` would delete every\n receipt, then fail to re-attach without hashes); `attachReceipt()` adds and\n `detach()` removes a link, keeping the asset. Because `ExpenseReceipt`\n overrides `save()`, `hasBaseJunctionLifecycle()` is false and no batch\n junction path can write a hashless row around it.\n- **Vendor ownership.** `vendorId` is a `crossPackageRef` (no FK, no\n validation), so `assertVendor()` checks the vendor is visible in and owned\n by the expense's tenant whenever it is set or changed\n (`EXPENSE_VENDOR_MISMATCH`).\n- **Tenant never moves.** Expense and receipt tenants are fixed once\n stored, compared as `effectiveTenant()` (the field, or the active context\n the `beforeSave` interceptor would fill an unset one from; unset is falsy,\n so `''` counts). `normalizeTenantId()` stores `''` as NULL (global), never\n as a pseudo-tenant, matching core/tenancy's falsy \"unset\". Otherwise a\n global (NULL-tenant) row saved inside `withTenant()` would be silently\n populated and split from its global expense or receipts.\n- **Tenancy.** Optional (`@TenantScoped({ mode: 'optional' })`, nullable\n `tenantId`), like smrt-commerce. A receipt takes its expense's tenant; its\n expense and asset must be visible in that tenant. A commitment must belong\n to the expense's tenant; before `super.save()` that is\n `tenantId ?? getTenantId()`, because the interceptor has not populated it.\n Consumers make it required with a qualified\n `registerTenantScopedClass(…, { mode: 'required' })`.\n- **Closed generated surface.** Models AND collections declare\n `mcp: { include: [] }`, `cli: false` and no generated CRUD or reads\n (generated reads check no read permission, and cost visibility is\n permission-gated). Collection classes are registered too: an undecorated\n collection publishes its CRUD and public methods as MCP tools. Core has no\n consumer-side switch to reopen a package model's surface.\n- **Generated write actions (#4001).** `ExpenseCollection` hosts the only\n generated writes, on REST: `record`, `attachReceipt`, `recordReview`\n (`decision: 'approve' | 'reject'`, reject needs `reason`) and\n `recordReimbursement`. `Expense` and `ExpenseCollection` both list them in\n `api.include` (runtime REST gates on the item, SvelteKit on the collection).\n Their names differ from `Expense.review()` / `markReimbursed()` on purpose:\n allowlisting a collection action must never open an instance method that\n takes the actor from its input. `src/actions/` delegates to the guarded\n model flow; identity comes from the tenancy/session ALS, never the body;\n each authorizes `expenses.<action>` via `assertOperationPermission` (system\n context never bypasses; review and reimbursement refuse super-admin bypass).\n Acting on someone else's expense needs `recordReview`. There is no submit\n state: a recorded expense is `unreviewed`, i.e. awaiting review.\n- **Cost object is not dereferenced on save.** `getCostObject()` resolves it\n through the registry and returns `null` for a foreign-tenant row. One cost\n object per expense (columns, not a `SmrtPolymorphicAssociation` join) so a\n cost can never roll up into two jobs.\n\n## Optional financial facts\n\n`financialBreakdown` is guarded JSON text (`get/setFinancialBreakdown`), absent\nfor legacy rows. Exact `getCostBasis()` returns null without facts. Complete facts\nreconcile subtotal + signed tax components to amount/currency; recoverable amounts\nare explicit. No tax law or customer pricing is inferred. `third-party` payer\nrequires `paidByName` and is not reimbursable. Negative amounts require `refundOfId`\n(same-owner/currency positive source, checked through the bound executor).\nSource edits also check inbound same-owner refunds: a referenced source stays\npositive, non-refund, and in their currency. `save()` encloses guards and write\nin the public `withTransaction()` boundary; PostgreSQL locks its row and proposed\nsource in id order before either refund check, while SQLite serializes writes.\nThe inbound projected lookup uses the existing `refund_of_id` index and an\nexplicit owner filter. Keep caller transaction affinity and per-row revision\nchecks; neither an unlocked lookup nor revision CAS alone protects this pair.\n\n`ExpenseCollection.listReviewedTaxFacts({ tenantId, from, to, currency? })`\nis the reporting input for supplier facts: it returns only that tenant's reviewed,\nnon-duplicate expenses in the inclusive calendar-date range. A null\n`financialBreakdown` remains an unknown legacy fact, never zero tax.\nEach fact carries `expenseId` as the commerce reporting identity; `id` remains\nthe compatible owner alias.\nAll three fields participate in the reviewed-money freeze. See the README and\n[financial facts test matrix](agents/financial-facts-test-matrix.md).\n\nLinking a new refund (or changing its source) also claims the source revision in\nthat same transaction through `claimRevision(source.updated_at)`. This makes a\nsource edit under an older PostgreSQL `REPEATABLE READ` snapshot fail with a\nserialization error instead of missing the new refund. Reload a source after\nlinking a refund before editing/reviewing it; on a serialization failure, retry\nthe entire caller transaction with fresh instances. Failed outer transactions\nroll back both the refund and the source revision claim. Existing-link saves do\nnot advance the source revision.\n\n## Cross-package references\n\n- `vendorId` → `@crossPackageRef('@happyvertical/smrt-commerce:Vendor')`\n- `commitmentId` → `…smrt-commerce:PurchaseContract` (`PurchaseOrder` or `ProductionOrder`)\n- `commitmentLineId` → `…smrt-commerce:ContractLineItem`\n- `assetId` → `…smrt-assets:Asset`\n- profile ids → `…smrt-profiles:Profile` (no dependency; references only)\n- `expenseId`, `duplicateOfId` → `@foreignKey` within the package\n\n## Scope\n\nAllowance (a planned amount) is not an expense kind; it belongs with\nbudget/commitment records. Labour belongs to `@happyvertical/smrt-timesheets`\n(#3288). No OCR, no accounting-provider posting.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-expenses test\npnpm --filter @happyvertical/smrt-expenses typecheck\npnpm --filter @happyvertical/smrt-expenses test:postgres\n```\n\n`src/__tests__/helpers/expense-suite.ts` is the shared behaviour suite: the\nSQLite file and the PostgreSQL lane (`*.optional.test.ts`, schema from\n`migrateSmrtSchemas` plus live parity) both run it. Use UUID tenant and\nprofile ids in tests: PostgreSQL stores them as `uuid`.\n\n## UI\n\n`src/svelte/` owns native expense entry and review presentation. See\n[the UI contract](agents/expense-ui.md) for DTOs, native payloads and recovery.\nDraft amounts are lossless currency-unit text; model/review amounts remain integer\nminor units. Receipt upload is composed from Assets, never reimplemented here.\n`test` includes backend and UI SSR; `test:e2e` proves native browser submissions.\n",
|
|
1227
|
+
"agentDoc": "# @happyvertical/smrt-expenses\n\nIncurred costs against a cost object, with deduplicated receipts, an explicit\nreview, reimbursement recording, and smrt-commerce commitment drawdown (#3289).\n\n## Models\n\n- **Expense** (`expenses`): `costObjectType` (qualified class name) +\n `costObjectId` (bare string), `category` (lowercase kebab-case; suggestions\n in `SUGGESTED_EXPENSE_CATEGORIES`), `amount` integer minor units + ISO 4217\n `currency`, `incurredOn` (`YYYY-MM-DD` text: a calendar date, not an\n instant), `recordedAt`, `recordedByProfileId`, `paidBy` (`company` |\n `person` | `third-party`) + `paidByProfileId` / `paidByName`, `vendorId`, `commitmentId` /\n `commitmentLineId`, review fields, `reimbursable` / `reimbursedAt` /\n `reimbursementReference`.\n- **ExpenseReceipt** (`expense_receipts`): noun-specific asset join\n (`expenseId` → Expense, `assetId` → smrt-assets Asset) with uploader,\n `filename`, `mimeType`, `byteCount`, `contentSha256`. Collection extends\n `SmrtJunction` (left expense, right asset, ordered by `created_at`).\n\n## Recipe\n\n`src/recipes.ts` declares one `SmrtRecipe`, `expenses.tracking` (label\n\"Expenses\", section `accounting`, shared with `ledgers.bookkeeping`), emitted\ninto the built `manifest.json` and `smrt-knowledge.json` `recipes`. It brings\n`Expense` with `ExpenseReceipt` as a child model, hides the cost-object type,\ncommitment and reviewer plumbing, and leaves the read-only review fields as\nthey are (review is method-only; see below). Help lives in\n`src/tracking.recipe.md` (#3591). See [core recipes](../core/agents/recipes.md).\n\n## Invariants\n\n- **Review is method-only.** `review()`, `reject()`, `markDuplicate()`,\n `reopen()` read the persisted status, check the move, and authorize exactly\n one save through a module `WeakMap`. Every transition assigns every review\n field, and the token carries that complete resulting state, so a field set\n directly beside `review()`/`reject()` (e.g. an injected `duplicateOfId`)\n is refused. A `reviewed` expense never carries `duplicateOfId`. `save()` compares every review field\n (status, reviewer, time, note, `duplicateOfId`) with the stored row and\n refuses an unauthorized change (`EXPENSE_REVIEW_FIELDS_LOCKED`), including\n on a brand-new row. Review fields are also `readonly` for generated writes.\n- **Writes are pinned to the guarded row.** The guards read the row stored\n under `id`, but core's natural-key save adopts any same-owner row on\n `(tenant_id, slug, context)` and upserts it with no revision predicate;\n these models' slugs come from the id. So `pinNaturalKey()` lets a fresh\n (not `isPersisted`) instance only INSERT (`requireInsertOnSave()`): naming\n an existing row's id is `EXPENSE_IDENTITY_CONFLICT`, and a row appearing\n mid-save fails the INSERT instead of being adopted. An existing row changes\n only through a loaded instance (revision compare-and-swap) that keeps its\n slug/context. Never `create({ id: <existing> })` or `getOrUpsert()` these\n models; load and save instead.\n- **Reviewed money is frozen.** While the stored status is `reviewed`,\n `LOCKED_WHEN_REVIEWED` fields (money, currency, both dates, cost object,\n vendor, commitment, payer) cannot change; `reopen()` first. `recordedAt` is\n compared as an instant, since drivers return it as a `Date` or a string.\n- **Drawdown counts once.** `ExpenseCollection.commitmentPosition()` sums only\n `reviewed` rows with no `duplicateOfId`, in the commitment's currency, deduped\n by id. Committed is the contract's `totalAmount` (or the line's `amount`).\n- **Currencies never mix.** A commitment match must share the currency and\n tenant (`EXPENSE_COMMITMENT_MISMATCH`), checked when the match, line or\n currency is set or changed — not on every save, so a later commitment edit\n never blocks e.g. `markReimbursed()`. `totalsByCurrency()` keys by currency.\n Rows left in another currency by a later commitment change land in\n `otherCurrencies`, never in `drawn`.\n- **Duplicate receipts.** Same `(expense, sha256)` is refused by a pre-insert\n read plus the unique index `expense_receipts_expense_sha256_key`\n `(expense_id, content_sha256)`; a unique violation is rethrown as\n `DuplicateReceiptError`, classified from the error itself (violated key\n name, or `details.fieldName` columns on core's typed error) because on\n PostgreSQL the violation aborts a caller's transaction and nothing can be\n re-read; only an error naming no key falls back to a re-read. The index omits `tenant_id` deliberately: the\n expense already fixes the tenant, and a nullable leading `tenant_id` would\n leave NULL-tenant (global) receipts unenforced. Same hash on different\n expenses is allowed; `findDuplicateReceipts()` reports it (facet on\n `contentSha256`, count > 1). Receipt `tenantId` (derived from the\n expense on insert), `expenseId`, `assetId` and `contentSha256` are\n immutable. The inherited `SmrtJunction.attach()` and\n `setLinks()` are overridden to throw (`setLinks()` would delete every\n receipt, then fail to re-attach without hashes); `attachReceipt()` adds and\n `detach()` removes a link, keeping the asset. Because `ExpenseReceipt`\n overrides `save()`, `hasBaseJunctionLifecycle()` is false and no batch\n junction path can write a hashless row around it.\n- **Vendor ownership.** `vendorId` is a `crossPackageRef` (no FK, no\n validation), so `assertVendor()` checks the vendor is visible in and owned\n by the expense's tenant whenever it is set or changed\n (`EXPENSE_VENDOR_MISMATCH`).\n- **Tenant never moves.** Expense and receipt tenants are fixed once\n stored, compared as `effectiveTenant()` (the field, or the active context\n the `beforeSave` interceptor would fill an unset one from; unset is falsy,\n so `''` counts). `normalizeTenantId()` stores `''` as NULL (global), never\n as a pseudo-tenant, matching core/tenancy's falsy \"unset\". Otherwise a\n global (NULL-tenant) row saved inside `withTenant()` would be silently\n populated and split from its global expense or receipts.\n- **Tenancy.** Optional (`@TenantScoped({ mode: 'optional' })`, nullable\n `tenantId`), like smrt-commerce. A receipt takes its expense's tenant; its\n expense and asset must be visible in that tenant. A commitment must belong\n to the expense's tenant; before `super.save()` that is\n `tenantId ?? getTenantId()`, because the interceptor has not populated it.\n Consumers make it required with a qualified\n `registerTenantScopedClass(…, { mode: 'required' })`.\n- **Closed generated surface.** Models AND collections declare\n `mcp: { include: [] }`, `cli: false` and no generated CRUD or reads\n (generated reads check no read permission, and cost visibility is\n permission-gated). Collection classes are registered too: an undecorated\n collection publishes its CRUD and public methods as MCP tools. Core has no\n consumer-side switch to reopen a package model's surface.\n- **Generated write actions (#4001).** `ExpenseCollection` hosts the only\n generated writes, on REST: `record`, `attachReceipt`, `recordReview`\n (`decision: 'approve' | 'reject'`, reject needs `reason`) and\n `recordReimbursement`. `Expense` and `ExpenseCollection` both list them in\n `api.include` (runtime REST gates on the item, SvelteKit on the collection).\n Their names differ from `Expense.review()` / `markReimbursed()` on purpose:\n allowlisting a collection action must never open an instance method that\n takes the actor from its input. `src/actions/` delegates to the guarded\n model flow; identity comes from the tenancy/session ALS, never the body;\n each authorizes `expenses.<action>` via `assertOperationPermission` (system\n context never bypasses; review and reimbursement refuse super-admin bypass).\n Acting on someone else's expense needs `recordReview`. There is no submit\n state: a recorded expense is `unreviewed`, i.e. awaiting review.\n- **Cost object is not dereferenced on save.** `getCostObject()` resolves it\n through the registry and returns `null` for a foreign-tenant row. One cost\n object per expense (columns, not a `SmrtPolymorphicAssociation` join) so a\n cost can never roll up into two jobs.\n\n## Optional financial facts\n\n`financialBreakdown` is guarded JSON text (`get/setFinancialBreakdown`), absent\nfor legacy rows. Exact `getCostBasis()` returns null without facts. Complete facts\nreconcile subtotal + signed tax components to amount/currency; recoverable amounts\nare explicit. No tax law or customer pricing is inferred. `third-party` payer\nrequires `paidByName` and is not reimbursable. Negative amounts require `refundOfId`\n(same-owner/currency positive source, checked through the bound executor).\nSource edits also check inbound same-owner refunds: a referenced source stays\npositive, non-refund, and in their currency. `save()` encloses guards and write\nin the public `withTransaction()` boundary; PostgreSQL locks its row and proposed\nsource in id order before either refund check, while SQLite serializes writes.\nThe inbound projected lookup uses the existing `refund_of_id` index and an\nexplicit owner filter. Keep caller transaction affinity and per-row revision\nchecks; neither an unlocked lookup nor revision CAS alone protects this pair.\n\n`ExpenseCollection.listReviewedTaxFacts({ tenantId, from, to, currency? })`\nis the reporting input for supplier facts: it returns only that tenant's reviewed,\nnon-duplicate expenses in the inclusive calendar-date range. A null\n`financialBreakdown` remains an unknown legacy fact, never zero tax.\nEach fact carries `expenseId` as the commerce reporting identity; `id` remains\nthe compatible owner alias.\nAll three fields participate in the reviewed-money freeze. See the README and\n[financial facts test matrix](agents/financial-facts-test-matrix.md).\n\nLinking a refund, changing its source, or editing amount/financial facts claims the source revision in\nthat same transaction through `claimRevision(source.updated_at)`. This makes a\nsource edit under an older PostgreSQL `REPEATABLE READ` snapshot fail with a\nserialization error instead of missing the new refund. Reload a source after\nlinking a refund before editing/reviewing it; on a serialization failure, retry\nthe entire caller transaction with fresh instances. Failed outer transactions\nroll back both the refund and the source revision claim. Saves without capacity-relevant changes do not advance the source revision.\n\n## Cross-package references\n\n- `vendorId` → `@crossPackageRef('@happyvertical/smrt-commerce:Vendor')`\n- `commitmentId` → `…smrt-commerce:PurchaseContract` (`PurchaseOrder` or `ProductionOrder`)\n- `commitmentLineId` → `…smrt-commerce:ContractLineItem`\n- `assetId` → `…smrt-assets:Asset`\n- profile ids → `…smrt-profiles:Profile` (no dependency; references only)\n- `expenseId`, `duplicateOfId` → `@foreignKey` within the package\n\n## Scope\n\nAllowance (a planned amount) is not an expense kind; it belongs with\nbudget/commitment records. Labour belongs to `@happyvertical/smrt-timesheets`\n(#3288). No OCR, no accounting-provider posting.\n\n## Validation\n\n```bash\npnpm --filter @happyvertical/smrt-expenses test\npnpm --filter @happyvertical/smrt-expenses typecheck\npnpm --filter @happyvertical/smrt-expenses test:postgres\n```\n\n`src/__tests__/helpers/expense-suite.ts` is the shared behaviour suite: the\nSQLite file and the PostgreSQL lane (`*.optional.test.ts`, schema from\n`migrateSmrtSchemas` plus live parity) both run it. Use UUID tenant and\nprofile ids in tests: PostgreSQL stores them as `uuid`.\n\n## UI\n\n`src/svelte/` owns native expense entry and review presentation. See\n[the UI contract](agents/expense-ui.md) for DTOs, native payloads and recovery.\nDraft amounts are lossless currency-unit text; model/review amounts remain integer\nminor units. Receipt upload is composed from Assets, never reimplemented here.\n`test` includes backend and UI SSR; `test:e2e` proves native browser submissions.\n\n\n## Exact refund capacity (#4070)\n\nAll persisted linked refunds reserve gross capacity regardless of review state.\n`refundPosition(sourceId)` / `getRefundPosition()` expose exact positive cash\nmagnitudes and nullable complete financial residuals. `src/refund-position.ts`\nbounds known subtotal and tax-code charged/recoverable/nonrecoverable reversals\nseparately; source component identities cannot compensate for one another.\nUnknown facts stay null, and known linked facts cannot be cleared. Source edits\nand refund edits revalidate the prospective position under the existing source\nlocks and revision claims. No customer credit or cash recipient is inferred.\n\n## Cash reimbursement positions (#4073)\n\n`refundRecipient` guarded JSON is explicit company/person/third-party cash receipt evidence,\nseparate from payer. Third-party names are exact bounded source evidence, never\nverified identities or fuzzy joins. Missing/other-person/third-party direction\nstays unknown for person reimbursement; third-party yields\n`unknown-third-party-recipient`. Public\n`reimbursementPosition` / `getReimbursementPosition` returns nonnegative remaining\nclaim and separate money owed to company; never offset economic cost basis or\ninfer historical payment. `markReimbursed` stores exact full remaining\n`reimbursedAmount` with date/reference; direct payment-field writes, replacement\npayments, and later source cash-claim/payer edits are refused. Legacy payment\nmarkers with null amount remain unknown. Recipient changes share refund source\nlocks/CAS, including rollback and stale REPEATABLE READ decisions.\n\n## Supplier payable authority (#4096)\n\n`src/supplier-payable.ts` owns exact dated retention/settlement/refund-direction\nprojection, separately from incurred cost and employee reimbursement. The guarded\n`supplierPayableJson` is method-only, original snapshot + append-only child UUIDs\n+ optional one-time unknown-basis completion. Current source-row locks/CAS also\nserialize this authority with linked refunds. Direct field writes/deletes and\nmanaged money/payer/refund mutations fail; no generated action exposes these\nmethods. Unknown basis or unreconciled refund remains unknown and blocks new\nsettlement. A settlement records evidence; it executes no bank/provider/journal\noperation. Preserve explicit chronological prefixes and same-day ordering.\nSee README and `agents/supplier-payable-test-matrix.md` for the full contract.\n",
|
|
980
1228
|
"moduleDocs": [
|
|
981
1229
|
{
|
|
982
1230
|
"path": "agents/financial-facts-test-matrix.md",
|
|
983
1231
|
"module": "financial-facts-test-matrix",
|
|
984
|
-
"content": "# Expense financial facts test design (#3792)\n\nRisk: high — persisted financial mutation, tenancy, reviewed immutability, and\nschema migration. Synthetic data only. Scope is source facts, not tax law or\nsettlement. Existing generated mutation surfaces remain closed.\n\nAll integration rows run in the shared expense behavior suite on SQLite and\nPostgreSQL, using the caller-bound database executor and active tenant. Commands:\n`pnpm --filter @happyvertical/smrt-expenses test` (S),\n`pnpm --filter @happyvertical/smrt-expenses test:postgres` (P).\n\n| Behavior / reachable trigger | Positive | Negative / failure | Actor / context | Executor / transaction | Runtime / dialect | External edge | Level / command |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| Optional source facts through create/save/load | Legacy amount roundtrip has no breakdown or inferred basis; explicit facts survive reload | Unknown keys, malformed JSON/types/currency, mismatched total/sign, duplicate component, overflow rejected | Consumer with same active tenant | Bound collection database, no multirow mutation | Node SQLite/PostgreSQL | Optional missing facts; unknown values fail closed | Integration S/P |\n| Exact basis through getCostBasis | Partial recovery and negative refund exact minor units | Unsafe numbers and inconsistent recovery rejected | Consumer reading source facts | Pure synchronous calculation; N/A transaction (no writes) | Node plus persisted roundtrip both dialects | Structural typed result usable without dependency | Unit/integration S/P |\n| Refund zero-tax recovery boundary | Zero charged and zero recovered accepted | Negative recovery against zero tax rejected by setter and save; persisted facts unchanged | Same tenant author | Bound collection database; retry after correction | SQLite/PostgreSQL | Exact magnitude bound includes zero | Regression integration S/P |\n| Named third-party payer save | Name persists without employee reimbursement | Missing name, incompatible profile/name, reimbursable rejected; company/person unchanged | Same tenant expense author | Bound database | SQLite/PostgreSQL | New enum explicit, unknown rejected | Integration S/P |\n| Refund source save | Negative refund references positive same-owner/currency source | Self, missing, cross-tenant/global, currency mismatch, refund-of-refund rejected | Active tenant × same/foreign/global resource | Lookup uses same database executor; transaction rollback and retry | SQLite/PostgreSQL | No settlement or cumulative refund entitlement inferred | Integration S/P |\n| Reviewed facts save/reopen/review | Reopen, correct, review permitted | Direct financial/payer/refund mutation and mutation alongside reopen rejected; failed save preserves persisted row | Consumer review method; existing authorization contract unchanged | Revision-guarded single-row save; transaction rollback | SQLite/PostgreSQL | Existing closed generated surface retained | Integration S/P |\n| Reviewed refund roll-up | Signed gross totals and commitment drawdown count source/refund once, retained basis does not replace legacy amount | Existing cross-currency/tenant commitment denials remain active | Same tenant reviewer | Bound collections and revision-guarded saves | SQLite/PostgreSQL | Existing aggregate semantics preserved | Integration S/P |\n| Additive schema migration | Existing row retains amount and absent financial facts; repeat migration no-op | Live parity and column types asserted | Schema operator, no end-user authorization | Public dbMigrate migration path | SQLite/PostgreSQL | Existing deployed schema upgraded | Migration S/P |\n\nAdditional gates: package typecheck, build, browser harness; root combined release\nvalidation and exact-head review belong to the coordinator. Original additive\nfeature rows do not require a bug-fix comparison; the zero-tax correction has\nexplicit failing-base evidence below. No network provider is\nintroduced: upstream provider failure is N/A. Refund lookup failure is tested.\n\n## Executed release evidence\n\nExecuted on Node 26.10.0 / pnpm 11.25.0, with SQLite and PostgreSQL. All\nbehavior rows above run in the shared suite; PostgreSQL additionally checks\nnative column types and live parity. Both migration fixtures start from the\nprevious expense schema, preserve a seeded legacy row, exercise the migrated\nrefund FK's RESTRICT behavior, and prove repeat migration is a no-op.\n\n- `test`: 61 backend tests and 15 UI tests passed. The optional PostgreSQL file\n is deliberately skipped here and executed by its dedicated command.\n- `test:postgres`: 55 passed; no skipped tests.\n- `test:e2e`: 11 passed using the supported system Chromium, including native\n submissions without JavaScript.\n- Package build, typecheck (including Svelte), and scoped Biome checks passed.\n- SQLite migration requires the generic new nullable FK column fix in #3795;\n the initial executable failure and passing dependency-head run are retained.\n\nFull output is retained outside the checkout under\n`/home/will/.local/state/happyvertical/tradeschief-277-release1/expenses/`:\n`test-final.log`, `postgres-final-with-core.log`, `e2e.log`,\n`package-build.log`, `typecheck-delivery.log`, `lint-final.log`, and\n`core-build.log`. Initial diagnostic runs remain there for failure provenance.\nThe redundant whole-workspace build was stopped to reduce memory contention;\nthe coordinator owns the final combined root gates and exact-head review.\n\n\n## Zero-tax refund correction\n\nThe added regression fails against feature commit `08a791a93`: a refund with\nsubtotal/total -100, tax 0, and recoverable tax -1 was accepted. Comparing exact\nBigInt magnitudes fixes the bound without changing the sign requirements.\nThe shared regression also rejects raw-save bypass, verifies stored facts are\nunchanged after failure, and permits retry with zero recovery. Evidence is in\n`zero-tax-base.log` (expected failure), `zero-tax-test.log`,\n`zero-tax-postgres.log`, `zero-tax-typecheck.log`, and `zero-tax-build.log`\nunder the evidence directory above.\nFinal correction validation: 62 backend + 15 UI tests and 56 PostgreSQL tests\npassed, including both migration lanes. The unchanged browser contract retains\nthe 11-pass evidence above.\n\n\n## Inbound refund source invariant — review thread 4243223180\n\nRisk remains high (financial relationship mutation). Baseline is release\n`d372e2df68f1000bbfccb9dbe88afcd7d78ece9b`. Execute before/after evidence for\nthe new regressions; the existing per-row revision guard alone cannot protect a\nsource/refund pair from concurrent edits.\n\n| Behavior / reachable trigger | Positive | Negative / failure | Actor / context | Executor / transaction | Runtime / dialect | External edge | Level / command |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| Source save with inbound refunds | Unrelated edits and positive same-currency corrections remain valid; unreferenced sources remain editable | Zero amount, changed currency, or conversion to refund refused; source and reviewed refund unchanged | Same owner, source unreviewed or reopened, refund reviewed | Bound transaction; existing revision CAS retained | SQLite/PostgreSQL | Refund source eligibility preserved in both directions | Shared integration S/P |\n| Inbound lookup ownership | NULL owner and tenant-owned references enforced outside context too | Malformed foreign-owner inbound fixture must not block another tenant; ordinary cross-tenant creation remains refused | Active tenant / no context × own / foreign / global | Public projected list with owner predicate, existing refund FK index | SQLite/PostgreSQL | No cross-tenant lookup bypass or existence disclosure | Shared integration S/P |\n| Caller transaction affinity | Guard sees uncommitted inbound refund; valid retry after rollback | Invalid source edit rolls back sibling refund mutation; original rows remain valid | Same tenant author | Existing public nested transaction and withDatabase APIs | SQLite/PostgreSQL | No independent root transaction or partial commit | Shared integration S/P |\n| Source/refund concurrent save | Winner preserves eligible persisted relationship | Refund-first blocks source edit; source-first makes later refund fail on current source | Same tenant, distinct real connections | Ordered PostgreSQL row locks held from validation through commit; SQLite write transaction serialization | PostgreSQL interleaving plus shared SQLite atomicity | No blanket source freeze or settlement limits | Real-PG integration P |\n| Stale repeatable-read source snapshot | Fresh same-currency positive edit succeeds | Snapshot pinned before independent refund commit; stale currency edit aborts serialization; fresh invalid edit rejects | Same tenant | Public source revision claim in refund transaction | PostgreSQL REPEATABLE READ | No missed committed refund under stale snapshot | Real-PG integration P |\n\nThe source revision advances atomically on a new/changed refund link through the\npublic revision API, including rollback with a caller transaction. Source\ninstances loaded before linking must be reloaded for subsequent valid edits.\n\nReproduction evidence in the external `inbound-refunds/` directory: baseline\nSQLite fails all four new shared regressions, baseline PostgreSQL fails those\nfour plus both writer-order cases. Row locks without a source revision claim\nalso fail the repeatable-read regression (`base-rr.log`). The corrected focused\nruns pass four SQLite and seven PostgreSQL cases (`fixed-sqlite.log`,\n`fixed-postgres.log`). Final committed-head package build, full test, PostgreSQL,\ntypecheck, and browser commands are recorded with HEAD and exit status in\n`validation-records.json` and their named logs in that same evidence directory.\n"
|
|
1232
|
+
"content": "# Expense financial facts test design (#3792)\n\nRisk: high — persisted financial mutation, tenancy, reviewed immutability, and\nschema migration. Synthetic data only. Scope is source facts, not tax law or\nsettlement. Existing generated mutation surfaces remain closed.\n\nAll integration rows run in the shared expense behavior suite on SQLite and\nPostgreSQL, using the caller-bound database executor and active tenant. Commands:\n`pnpm --filter @happyvertical/smrt-expenses test` (S),\n`pnpm --filter @happyvertical/smrt-expenses test:postgres` (P).\n\n| Behavior / reachable trigger | Positive | Negative / failure | Actor / context | Executor / transaction | Runtime / dialect | External edge | Level / command |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| Optional source facts through create/save/load | Legacy amount roundtrip has no breakdown or inferred basis; explicit facts survive reload | Unknown keys, malformed JSON/types/currency, mismatched total/sign, duplicate component, overflow rejected | Consumer with same active tenant | Bound collection database, no multirow mutation | Node SQLite/PostgreSQL | Optional missing facts; unknown values fail closed | Integration S/P |\n| Exact basis through getCostBasis | Partial recovery and negative refund exact minor units | Unsafe numbers and inconsistent recovery rejected | Consumer reading source facts | Pure synchronous calculation; N/A transaction (no writes) | Node plus persisted roundtrip both dialects | Structural typed result usable without dependency | Unit/integration S/P |\n| Refund zero-tax recovery boundary | Zero charged and zero recovered accepted | Negative recovery against zero tax rejected by setter and save; persisted facts unchanged | Same tenant author | Bound collection database; retry after correction | SQLite/PostgreSQL | Exact magnitude bound includes zero | Regression integration S/P |\n| Named third-party payer save | Name persists without employee reimbursement | Missing name, incompatible profile/name, reimbursable rejected; company/person unchanged | Same tenant expense author | Bound database | SQLite/PostgreSQL | New enum explicit, unknown rejected | Integration S/P |\n| Refund source save | Negative refund references positive same-owner/currency source | Self, missing, cross-tenant/global, currency mismatch, refund-of-refund rejected | Active tenant × same/foreign/global resource | Lookup uses same database executor; transaction rollback and retry | SQLite/PostgreSQL | No settlement or cumulative refund entitlement inferred | Integration S/P |\n| Reviewed facts save/reopen/review | Reopen, correct, review permitted | Direct financial/payer/refund mutation and mutation alongside reopen rejected; failed save preserves persisted row | Consumer review method; existing authorization contract unchanged | Revision-guarded single-row save; transaction rollback | SQLite/PostgreSQL | Existing closed generated surface retained | Integration S/P |\n| Reviewed refund roll-up | Signed gross totals and commitment drawdown count source/refund once, retained basis does not replace legacy amount | Existing cross-currency/tenant commitment denials remain active | Same tenant reviewer | Bound collections and revision-guarded saves | SQLite/PostgreSQL | Existing aggregate semantics preserved | Integration S/P |\n| Additive schema migration | Existing row retains amount and absent financial facts; repeat migration no-op | Live parity and column types asserted | Schema operator, no end-user authorization | Public dbMigrate migration path | SQLite/PostgreSQL | Existing deployed schema upgraded | Migration S/P |\n\nAdditional gates: package typecheck, build, browser harness; root combined release\nvalidation and exact-head review belong to the coordinator. Original additive\nfeature rows do not require a bug-fix comparison; the zero-tax correction has\nexplicit failing-base evidence below. No network provider is\nintroduced: upstream provider failure is N/A. Refund lookup failure is tested.\n\n## Executed release evidence\n\nExecuted on Node 26.10.0 / pnpm 11.25.0, with SQLite and PostgreSQL. All\nbehavior rows above run in the shared suite; PostgreSQL additionally checks\nnative column types and live parity. Both migration fixtures start from the\nprevious expense schema, preserve a seeded legacy row, exercise the migrated\nrefund FK's RESTRICT behavior, and prove repeat migration is a no-op.\n\n- `test`: 61 backend tests and 15 UI tests passed. The optional PostgreSQL file\n is deliberately skipped here and executed by its dedicated command.\n- `test:postgres`: 55 passed; no skipped tests.\n- `test:e2e`: 11 passed using the supported system Chromium, including native\n submissions without JavaScript.\n- Package build, typecheck (including Svelte), and scoped Biome checks passed.\n- SQLite migration requires the generic new nullable FK column fix in #3795;\n the initial executable failure and passing dependency-head run are retained.\n\nFull output is retained outside the checkout under\n`/home/will/.local/state/happyvertical/tradeschief-277-release1/expenses/`:\n`test-final.log`, `postgres-final-with-core.log`, `e2e.log`,\n`package-build.log`, `typecheck-delivery.log`, `lint-final.log`, and\n`core-build.log`. Initial diagnostic runs remain there for failure provenance.\nThe redundant whole-workspace build was stopped to reduce memory contention;\nthe coordinator owns the final combined root gates and exact-head review.\n\n\n## Zero-tax refund correction\n\nThe added regression fails against feature commit `08a791a93`: a refund with\nsubtotal/total -100, tax 0, and recoverable tax -1 was accepted. Comparing exact\nBigInt magnitudes fixes the bound without changing the sign requirements.\nThe shared regression also rejects raw-save bypass, verifies stored facts are\nunchanged after failure, and permits retry with zero recovery. Evidence is in\n`zero-tax-base.log` (expected failure), `zero-tax-test.log`,\n`zero-tax-postgres.log`, `zero-tax-typecheck.log`, and `zero-tax-build.log`\nunder the evidence directory above.\nFinal correction validation: 62 backend + 15 UI tests and 56 PostgreSQL tests\npassed, including both migration lanes. The unchanged browser contract retains\nthe 11-pass evidence above.\n\n\n## Inbound refund source invariant — review thread 4243223180\n\nRisk remains high (financial relationship mutation). Baseline is release\n`d372e2df68f1000bbfccb9dbe88afcd7d78ece9b`. Execute before/after evidence for\nthe new regressions; the existing per-row revision guard alone cannot protect a\nsource/refund pair from concurrent edits.\n\n| Behavior / reachable trigger | Positive | Negative / failure | Actor / context | Executor / transaction | Runtime / dialect | External edge | Level / command |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n| Source save with inbound refunds | Unrelated edits and positive same-currency corrections remain valid; unreferenced sources remain editable | Zero amount, changed currency, or conversion to refund refused; source and reviewed refund unchanged | Same owner, source unreviewed or reopened, refund reviewed | Bound transaction; existing revision CAS retained | SQLite/PostgreSQL | Refund source eligibility preserved in both directions | Shared integration S/P |\n| Inbound lookup ownership | NULL owner and tenant-owned references enforced outside context too | Malformed foreign-owner inbound fixture must not block another tenant; ordinary cross-tenant creation remains refused | Active tenant / no context × own / foreign / global | Public projected list with owner predicate, existing refund FK index | SQLite/PostgreSQL | No cross-tenant lookup bypass or existence disclosure | Shared integration S/P |\n| Caller transaction affinity | Guard sees uncommitted inbound refund; valid retry after rollback | Invalid source edit rolls back sibling refund mutation; original rows remain valid | Same tenant author | Existing public nested transaction and withDatabase APIs | SQLite/PostgreSQL | No independent root transaction or partial commit | Shared integration S/P |\n| Source/refund concurrent save | Winner preserves eligible persisted relationship | Refund-first blocks source edit; source-first makes later refund fail on current source | Same tenant, distinct real connections | Ordered PostgreSQL row locks held from validation through commit; SQLite write transaction serialization | PostgreSQL interleaving plus shared SQLite atomicity | No blanket source freeze or settlement limits | Real-PG integration P |\n| Stale repeatable-read source snapshot | Fresh same-currency positive edit succeeds | Snapshot pinned before independent refund commit; stale currency edit aborts serialization; fresh invalid edit rejects | Same tenant | Public source revision claim in refund transaction | PostgreSQL REPEATABLE READ | No missed committed refund under stale snapshot | Real-PG integration P |\n\nThe source revision advances atomically on a new/changed refund link through the\npublic revision API, including rollback with a caller transaction. Source\ninstances loaded before linking must be reloaded for subsequent valid edits.\n\nReproduction evidence in the external `inbound-refunds/` directory: baseline\nSQLite fails all four new shared regressions, baseline PostgreSQL fails those\nfour plus both writer-order cases. Row locks without a source revision claim\nalso fail the repeatable-read regression (`base-rr.log`). The corrected focused\nruns pass four SQLite and seven PostgreSQL cases (`fixed-sqlite.log`,\n`fixed-postgres.log`). Final committed-head package build, full test, PostgreSQL,\ntypecheck, and browser commands are recorded with HEAD and exit status in\n`validation-records.json` and their named logs in that same evidence directory.\n\n\n## Exact refund positions (#4070)\n\nRisk: high — cumulative financial reversals and concurrent edits. The shared\nSQLite/PostgreSQL suite covers partial/full/excess cash reversals (including\nrejected rows), independent subtotal/charged/recovery/nonrecoverable caps, exact\ncomponent identities, signed residuals, unknown legacy/partial facts, source\ncorrections, refund edits/reparenting, and rollback of refund plus source revision.\nPostgreSQL additionally proves stale sibling amount and financial-facts edits\nabort under REPEATABLE READ, then fail the appropriate cap on a fresh retry.\nNo missing financial facts are synthesized. A former freeze fixture now uses a\nsource gross sufficient for its refund and retains known linked facts after\nreopen: clearing them would erase already-proved reversal bounds.\n\nFocused commands: `pnpm --filter @happyvertical/smrt-expenses exec vitest run\nsrc/__tests__/expenses.test.ts`, the same PostgreSQL optional test through\n`scripts/run-with-ci-postgres.mjs`, and owning-package `tsc -p tsconfig.json\n--noEmit`. External receipts: `/home/will/Work/tmp/tc-mvp-20261016/expense-refund-caps/`.\nCoordinator owns combined prerequisites, release validation, and review.\n\n## Refund cash direction and reimbursement positions (#4073)\n\nRisk: high — retained payment amount, liability/receivable direction, additive\nschema migration, and recipient-vs-payment concurrency. Shared SQLite/PostgreSQL\ncases prove company receipts preserve gross personal claim, person receipts\nreduce it, exact full-remaining payment is retained independently of economic\ncost basis, later personal receipt creates a separate company receivable,\nunknown/other-person recipients and historical missing paid amounts remain\nunknown, direct overwrite/source-claim rewrite fail closed, recipient review\nfreeze/shape apply, and caller rollback restores an unpaid claim. PostgreSQL\npins a REPEATABLE READ reimbursement before an independent recipient correction\nand requires serialization rejection, followed by exact fresh payment. Existing\nservice-backed permission tests run unchanged; optional expected amount remains\nbehind the same authorization. Migration verifies new fields stay null on legacy\nrows; no historical payer or paid cash is inferred.\n\nFocused evidence: `/home/will/Work/tmp/tc-mvp-20261016/expense-reimbursement-position/`.\nNo worker broad build/suite/review/PR; coordinator owns combined release gates.\n\n## Explicit third-party refund recipients (#4082)\n\nShared SQLite/PostgreSQL cases retain exact bounded third-party source names,\nrefuse blank/malformed/overlong names and extra fields, persist/reload guarded\nJSON and preserve reviewed provenance. Recipient edits claim the source revision;\ncaller rollback restores both evidence and revision. Person reimbursement with\nthird-party cash receipt returns unknown-third-party-recipient, nullable cash\npositions and retained actual paid cash; it cannot infer company income, person\npayment or settlement. An exact source name is evidence, not verified customer\nidentity. Existing company/person and unknown legacy cases remain covered.\n\n## Complete programmatic refund reads\n\nA shared 201-refund SQLite/PostgreSQL case verifies full cumulative cash capacity,\nexact person reimbursement, over-refund refusal and source-reduction refusal.\nThe internal collection reads intentionally use the unbounded programmatic\n`list()` contract; generated API page defaults do not apply to those reads.\n"
|
|
985
1233
|
},
|
|
986
1234
|
{
|
|
987
1235
|
"path": "agents/expense-ui.md",
|
|
@@ -1071,6 +1319,15 @@
|
|
|
1071
1319
|
"financialBreakdown": {
|
|
1072
1320
|
"visibility": "hidden"
|
|
1073
1321
|
},
|
|
1322
|
+
"supplierPayableJson": {
|
|
1323
|
+
"visibility": "hidden"
|
|
1324
|
+
},
|
|
1325
|
+
"refundRecipient": {
|
|
1326
|
+
"visibility": "hidden"
|
|
1327
|
+
},
|
|
1328
|
+
"reimbursedAmount": {
|
|
1329
|
+
"help": "The recorded amount paid back to the person who paid."
|
|
1330
|
+
},
|
|
1074
1331
|
"refundOfId": {
|
|
1075
1332
|
"visibility": "hidden"
|
|
1076
1333
|
},
|