@happyvertical/smrt-expenses 0.60.13 → 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/dist/smrt-knowledge.json
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-expenses",
|
|
6
|
-
"packageVersion": "0.60.
|
|
6
|
+
"packageVersion": "0.60.15",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "d9dc080fcbb09279527f354b5505af4db2dcfb7166660cff2f410cd8c6e02b4b",
|
|
11
|
+
"packageJson": "c5ec867711212f50e2b5deaa0f6eb6df3a3da36ac5b912a980c2ef60ffabf779",
|
|
12
|
+
"agents": "f96ada94beee4b368ed32ff1fb334675fd4df19ad350583ddaff2fc47174cd3d",
|
|
13
|
+
"moduleDoc:agents/reviewed-allocations.md": "0bae765eb23d8aedd07c9f3106d5012d31ab19114c5506240cad53f14a244fea",
|
|
13
14
|
"moduleDoc:agents/financial-facts-test-matrix.md": "77396f7345bb43a35b412bee579f1e457b58e99561bfdc3e5dc30d28ea07d824",
|
|
14
|
-
"moduleDoc:agents/expense-ui.md": "0bdea9e6c627f8cbab2a1f21ef4da3ab7f00409764a1ef48572dd40694fb6785"
|
|
15
|
+
"moduleDoc:agents/expense-ui.md": "0bdea9e6c627f8cbab2a1f21ef4da3ab7f00409764a1ef48572dd40694fb6785",
|
|
16
|
+
"moduleDoc:agents/allocation-test-matrix.md": "ef17109f9469c3b50bc51303d766cb2cd2df237a3ee8cc6e57e139a1ef397b87"
|
|
15
17
|
},
|
|
16
18
|
"exports": [
|
|
17
19
|
".",
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"smrt"
|
|
60
62
|
],
|
|
61
63
|
"risks": [
|
|
62
|
-
"cross-package-refs:
|
|
64
|
+
"cross-package-refs:12",
|
|
63
65
|
"sensitive-fields-excluded"
|
|
64
66
|
],
|
|
65
67
|
"objects": [
|
|
@@ -86,8 +88,10 @@
|
|
|
86
88
|
"recordSupplierPayableEvent",
|
|
87
89
|
"refundPosition",
|
|
88
90
|
"reimbursementPosition",
|
|
91
|
+
"reviewAllocation",
|
|
89
92
|
"supplierPayablePosition",
|
|
90
|
-
"totalsByCurrency"
|
|
93
|
+
"totalsByCurrency",
|
|
94
|
+
"withdrawAllocation"
|
|
91
95
|
],
|
|
92
96
|
"methodSignatures": [
|
|
93
97
|
{
|
|
@@ -191,6 +195,15 @@
|
|
|
191
195
|
],
|
|
192
196
|
"returns": "Promise<ExpenseReimbursementPosition>"
|
|
193
197
|
},
|
|
198
|
+
{
|
|
199
|
+
"name": "reviewAllocation",
|
|
200
|
+
"async": true,
|
|
201
|
+
"params": [
|
|
202
|
+
"input: ReviewExpenseAllocationInput",
|
|
203
|
+
"authority: ExpenseAllocationAuthority"
|
|
204
|
+
],
|
|
205
|
+
"returns": "Promise<ExpenseAllocationResult>"
|
|
206
|
+
},
|
|
194
207
|
{
|
|
195
208
|
"name": "supplierPayablePosition",
|
|
196
209
|
"async": true,
|
|
@@ -207,6 +220,15 @@
|
|
|
207
220
|
"options?: ExpenseTotalsOptions"
|
|
208
221
|
],
|
|
209
222
|
"returns": "Promise<CurrencyTotals>"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "withdrawAllocation",
|
|
226
|
+
"async": true,
|
|
227
|
+
"params": [
|
|
228
|
+
"input: WithdrawExpenseAllocationInput",
|
|
229
|
+
"authority: ExpenseAllocationAuthority"
|
|
230
|
+
],
|
|
231
|
+
"returns": "Promise<WithdrawExpenseAllocationResult>"
|
|
210
232
|
}
|
|
211
233
|
],
|
|
212
234
|
"surfaces": [
|
|
@@ -300,6 +322,13 @@
|
|
|
300
322
|
"reason": "not listed in api.include",
|
|
301
323
|
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
302
324
|
},
|
|
325
|
+
{
|
|
326
|
+
"kind": "api",
|
|
327
|
+
"operation": "reviewAllocation",
|
|
328
|
+
"code": "not-included",
|
|
329
|
+
"reason": "not listed in api.include",
|
|
330
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
331
|
+
},
|
|
303
332
|
{
|
|
304
333
|
"kind": "api",
|
|
305
334
|
"operation": "supplierPayablePosition",
|
|
@@ -313,6 +342,13 @@
|
|
|
313
342
|
"code": "not-included",
|
|
314
343
|
"reason": "not listed in api.include",
|
|
315
344
|
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"kind": "api",
|
|
348
|
+
"operation": "withdrawAllocation",
|
|
349
|
+
"code": "not-included",
|
|
350
|
+
"reason": "not listed in api.include",
|
|
351
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection"
|
|
316
352
|
}
|
|
317
353
|
],
|
|
318
354
|
"relationshipFeatures": [
|
|
@@ -481,6 +517,57 @@
|
|
|
481
517
|
"tags": [],
|
|
482
518
|
"risks": []
|
|
483
519
|
},
|
|
520
|
+
{
|
|
521
|
+
"name": "Revisions",
|
|
522
|
+
"qualifiedName": "@happyvertical/smrt-expenses:Revisions",
|
|
523
|
+
"collection": "expenseallocationrevisions",
|
|
524
|
+
"tableName": "expense_allocation_revisions",
|
|
525
|
+
"packageName": "@happyvertical/smrt-expenses",
|
|
526
|
+
"extends": "SmrtCollection",
|
|
527
|
+
"fields": [],
|
|
528
|
+
"relationships": [],
|
|
529
|
+
"methods": [],
|
|
530
|
+
"surfaces": [],
|
|
531
|
+
"relationshipFeatures": [
|
|
532
|
+
"uuidColumns"
|
|
533
|
+
],
|
|
534
|
+
"tags": [],
|
|
535
|
+
"risks": []
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"name": "Portions",
|
|
539
|
+
"qualifiedName": "@happyvertical/smrt-expenses:Portions",
|
|
540
|
+
"collection": "expenseallocationportions",
|
|
541
|
+
"tableName": "expense_allocation_portions",
|
|
542
|
+
"packageName": "@happyvertical/smrt-expenses",
|
|
543
|
+
"extends": "SmrtCollection",
|
|
544
|
+
"fields": [],
|
|
545
|
+
"relationships": [],
|
|
546
|
+
"methods": [],
|
|
547
|
+
"surfaces": [],
|
|
548
|
+
"relationshipFeatures": [
|
|
549
|
+
"uuidColumns"
|
|
550
|
+
],
|
|
551
|
+
"tags": [],
|
|
552
|
+
"risks": []
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
"name": "Billings",
|
|
556
|
+
"qualifiedName": "@happyvertical/smrt-expenses:Billings",
|
|
557
|
+
"collection": "expenseallocationbillings",
|
|
558
|
+
"tableName": "expense_allocation_billings",
|
|
559
|
+
"packageName": "@happyvertical/smrt-expenses",
|
|
560
|
+
"extends": "SmrtCollection",
|
|
561
|
+
"fields": [],
|
|
562
|
+
"relationships": [],
|
|
563
|
+
"methods": [],
|
|
564
|
+
"surfaces": [],
|
|
565
|
+
"relationshipFeatures": [
|
|
566
|
+
"uuidColumns"
|
|
567
|
+
],
|
|
568
|
+
"tags": [],
|
|
569
|
+
"risks": []
|
|
570
|
+
},
|
|
484
571
|
{
|
|
485
572
|
"name": "Expense",
|
|
486
573
|
"qualifiedName": "@happyvertical/smrt-expenses:Expense",
|
|
@@ -678,6 +765,14 @@
|
|
|
678
765
|
"required": false,
|
|
679
766
|
"columnType": "TEXT",
|
|
680
767
|
"readonly": true
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
"name": "activeAllocationRevisionId",
|
|
771
|
+
"type": "foreignKey",
|
|
772
|
+
"required": false,
|
|
773
|
+
"related": "ExpenseAllocationRevision",
|
|
774
|
+
"columnType": "UUID",
|
|
775
|
+
"readonly": true
|
|
681
776
|
}
|
|
682
777
|
],
|
|
683
778
|
"relationships": [
|
|
@@ -736,6 +831,13 @@
|
|
|
736
831
|
"required": false,
|
|
737
832
|
"related": "Expense",
|
|
738
833
|
"columnType": "UUID"
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
"name": "activeAllocationRevisionId",
|
|
837
|
+
"type": "foreignKey",
|
|
838
|
+
"required": false,
|
|
839
|
+
"related": "ExpenseAllocationRevision",
|
|
840
|
+
"columnType": "UUID"
|
|
739
841
|
}
|
|
740
842
|
],
|
|
741
843
|
"methods": [
|
|
@@ -1062,93 +1164,189 @@
|
|
|
1062
1164
|
"risks": []
|
|
1063
1165
|
},
|
|
1064
1166
|
{
|
|
1065
|
-
"name": "
|
|
1066
|
-
"qualifiedName": "@happyvertical/smrt-expenses:
|
|
1067
|
-
"collection": "
|
|
1068
|
-
"description": "
|
|
1069
|
-
"tableName": "
|
|
1167
|
+
"name": "ExpenseAllocationRevision",
|
|
1168
|
+
"qualifiedName": "@happyvertical/smrt-expenses:ExpenseAllocationRevision",
|
|
1169
|
+
"collection": "expenseallocationrevisions",
|
|
1170
|
+
"description": "Persisted command-owned ExpenseAllocationRevision evidence.",
|
|
1171
|
+
"tableName": "expense_allocation_revisions",
|
|
1070
1172
|
"packageName": "@happyvertical/smrt-expenses",
|
|
1071
1173
|
"extends": "SmrtObject",
|
|
1072
1174
|
"fields": [
|
|
1073
1175
|
{
|
|
1074
|
-
"name": "
|
|
1176
|
+
"name": "kind",
|
|
1177
|
+
"type": "text",
|
|
1178
|
+
"required": false,
|
|
1179
|
+
"columnType": "TEXT",
|
|
1180
|
+
"default": "allocation"
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"name": "sourceCostObjectType",
|
|
1184
|
+
"type": "text",
|
|
1185
|
+
"required": false,
|
|
1186
|
+
"columnType": "TEXT"
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
"name": "sourceCostObjectId",
|
|
1190
|
+
"type": "text",
|
|
1191
|
+
"required": false,
|
|
1192
|
+
"columnType": "TEXT"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
"name": "incurredOn",
|
|
1196
|
+
"type": "text",
|
|
1197
|
+
"required": false,
|
|
1198
|
+
"columnType": "TEXT"
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
"name": "paidBy",
|
|
1075
1202
|
"type": "text",
|
|
1076
1203
|
"required": false,
|
|
1204
|
+
"columnType": "TEXT",
|
|
1205
|
+
"default": "company"
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
"name": "reimbursable",
|
|
1209
|
+
"type": "boolean",
|
|
1210
|
+
"required": true,
|
|
1211
|
+
"columnType": "BOOLEAN",
|
|
1212
|
+
"default": false
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
"name": "hasSupplierTax",
|
|
1216
|
+
"type": "boolean",
|
|
1217
|
+
"required": false,
|
|
1218
|
+
"columnType": "BOOLEAN"
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
"name": "basisEqualsGross",
|
|
1222
|
+
"type": "boolean",
|
|
1223
|
+
"required": true,
|
|
1224
|
+
"columnType": "BOOLEAN",
|
|
1225
|
+
"default": false
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
"name": "tenantId",
|
|
1229
|
+
"type": "text",
|
|
1230
|
+
"required": true,
|
|
1077
1231
|
"columnType": "UUID"
|
|
1078
1232
|
},
|
|
1079
1233
|
{
|
|
1080
|
-
"name": "
|
|
1234
|
+
"name": "sourceExpenseId",
|
|
1081
1235
|
"type": "foreignKey",
|
|
1082
1236
|
"required": true,
|
|
1083
1237
|
"related": "Expense",
|
|
1084
1238
|
"columnType": "UUID"
|
|
1085
1239
|
},
|
|
1086
1240
|
{
|
|
1087
|
-
"name": "
|
|
1088
|
-
"type": "
|
|
1089
|
-
"required":
|
|
1090
|
-
"related": "
|
|
1241
|
+
"name": "previousRevisionId",
|
|
1242
|
+
"type": "foreignKey",
|
|
1243
|
+
"required": false,
|
|
1244
|
+
"related": "ExpenseAllocationRevision",
|
|
1091
1245
|
"columnType": "UUID"
|
|
1092
1246
|
},
|
|
1093
1247
|
{
|
|
1094
|
-
"name": "
|
|
1095
|
-
"type": "
|
|
1248
|
+
"name": "requestId",
|
|
1249
|
+
"type": "text",
|
|
1096
1250
|
"required": false,
|
|
1097
|
-
"
|
|
1098
|
-
"columnType": "UUID"
|
|
1251
|
+
"columnType": "TEXT"
|
|
1099
1252
|
},
|
|
1100
1253
|
{
|
|
1101
|
-
"name": "
|
|
1254
|
+
"name": "requestFingerprint",
|
|
1102
1255
|
"type": "text",
|
|
1103
1256
|
"required": false,
|
|
1104
1257
|
"columnType": "TEXT"
|
|
1105
1258
|
},
|
|
1106
1259
|
{
|
|
1107
|
-
"name": "
|
|
1260
|
+
"name": "sourceFingerprint",
|
|
1108
1261
|
"type": "text",
|
|
1109
1262
|
"required": false,
|
|
1110
1263
|
"columnType": "TEXT"
|
|
1111
1264
|
},
|
|
1112
1265
|
{
|
|
1113
|
-
"name": "
|
|
1266
|
+
"name": "sourceFingerprintNonce",
|
|
1267
|
+
"type": "text",
|
|
1268
|
+
"required": false,
|
|
1269
|
+
"columnType": "TEXT"
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
"name": "sourceRevision",
|
|
1273
|
+
"type": "text",
|
|
1274
|
+
"required": false,
|
|
1275
|
+
"columnType": "TEXT"
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
"name": "amountMinor",
|
|
1114
1279
|
"type": "integer",
|
|
1115
1280
|
"required": false,
|
|
1116
1281
|
"columnType": "INTEGER"
|
|
1117
1282
|
},
|
|
1118
1283
|
{
|
|
1119
|
-
"name": "
|
|
1284
|
+
"name": "currency",
|
|
1120
1285
|
"type": "text",
|
|
1286
|
+
"required": false,
|
|
1287
|
+
"columnType": "TEXT"
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
"name": "reviewedByProfileId",
|
|
1291
|
+
"type": "crossPackageRef",
|
|
1292
|
+
"required": true,
|
|
1293
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1294
|
+
"columnType": "UUID"
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
"name": "reviewedByUserId",
|
|
1298
|
+
"type": "crossPackageRef",
|
|
1121
1299
|
"required": true,
|
|
1300
|
+
"related": "@happyvertical/smrt-users:User",
|
|
1301
|
+
"columnType": "UUID"
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
"name": "reviewEvidence",
|
|
1305
|
+
"type": "text",
|
|
1306
|
+
"required": false,
|
|
1122
1307
|
"columnType": "TEXT"
|
|
1123
1308
|
}
|
|
1124
1309
|
],
|
|
1125
1310
|
"relationships": [
|
|
1126
1311
|
{
|
|
1127
|
-
"name": "
|
|
1312
|
+
"name": "sourceExpenseId",
|
|
1128
1313
|
"type": "foreignKey",
|
|
1129
1314
|
"required": true,
|
|
1130
1315
|
"related": "Expense",
|
|
1131
1316
|
"columnType": "UUID"
|
|
1132
1317
|
},
|
|
1133
1318
|
{
|
|
1134
|
-
"name": "
|
|
1319
|
+
"name": "previousRevisionId",
|
|
1320
|
+
"type": "foreignKey",
|
|
1321
|
+
"required": false,
|
|
1322
|
+
"related": "ExpenseAllocationRevision",
|
|
1323
|
+
"columnType": "UUID"
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
"name": "reviewedByProfileId",
|
|
1135
1327
|
"type": "crossPackageRef",
|
|
1136
1328
|
"required": true,
|
|
1137
|
-
"related": "@happyvertical/smrt-
|
|
1329
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1138
1330
|
"columnType": "UUID"
|
|
1139
1331
|
},
|
|
1140
1332
|
{
|
|
1141
|
-
"name": "
|
|
1333
|
+
"name": "reviewedByUserId",
|
|
1142
1334
|
"type": "crossPackageRef",
|
|
1143
|
-
"required":
|
|
1144
|
-
"related": "@happyvertical/smrt-
|
|
1335
|
+
"required": true,
|
|
1336
|
+
"related": "@happyvertical/smrt-users:User",
|
|
1145
1337
|
"columnType": "UUID"
|
|
1146
1338
|
}
|
|
1147
1339
|
],
|
|
1148
1340
|
"methods": [
|
|
1341
|
+
"delete",
|
|
1149
1342
|
"save"
|
|
1150
1343
|
],
|
|
1151
1344
|
"methodSignatures": [
|
|
1345
|
+
{
|
|
1346
|
+
"name": "delete",
|
|
1347
|
+
"async": true,
|
|
1348
|
+
"returns": "Promise<void>"
|
|
1349
|
+
},
|
|
1152
1350
|
{
|
|
1153
1351
|
"name": "save",
|
|
1154
1352
|
"async": true,
|
|
@@ -1160,7 +1358,7 @@
|
|
|
1160
1358
|
],
|
|
1161
1359
|
"tenant": {
|
|
1162
1360
|
"scoped": true,
|
|
1163
|
-
"mode": "
|
|
1361
|
+
"mode": "required",
|
|
1164
1362
|
"field": "tenantId"
|
|
1165
1363
|
},
|
|
1166
1364
|
"conflictColumns": [
|
|
@@ -1175,7 +1373,7 @@
|
|
|
1175
1373
|
"operation": "save",
|
|
1176
1374
|
"code": "lifecycle-method",
|
|
1177
1375
|
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
1178
|
-
"objectName": "@happyvertical/smrt-expenses:
|
|
1376
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseAllocationRevision"
|
|
1179
1377
|
}
|
|
1180
1378
|
],
|
|
1181
1379
|
"relationshipFeatures": [
|
|
@@ -1185,83 +1383,520 @@
|
|
|
1185
1383
|
],
|
|
1186
1384
|
"tags": [],
|
|
1187
1385
|
"risks": []
|
|
1188
|
-
}
|
|
1189
|
-
],
|
|
1190
|
-
"surfaces": [
|
|
1191
|
-
{
|
|
1192
|
-
"kind": "api",
|
|
1193
|
-
"name": "expenses.record",
|
|
1194
|
-
"operation": "record",
|
|
1195
|
-
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1196
|
-
"path": "/expenses/record",
|
|
1197
|
-
"method": "POST"
|
|
1198
|
-
},
|
|
1199
|
-
{
|
|
1200
|
-
"kind": "api",
|
|
1201
|
-
"name": "expenses.attachReceipt",
|
|
1202
|
-
"operation": "attachReceipt",
|
|
1203
|
-
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1204
|
-
"path": "/expenses/attachReceipt",
|
|
1205
|
-
"method": "POST"
|
|
1206
|
-
},
|
|
1207
|
-
{
|
|
1208
|
-
"kind": "api",
|
|
1209
|
-
"name": "expenses.recordReview",
|
|
1210
|
-
"operation": "recordReview",
|
|
1211
|
-
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1212
|
-
"path": "/expenses/recordReview",
|
|
1213
|
-
"method": "POST"
|
|
1214
|
-
},
|
|
1215
|
-
{
|
|
1216
|
-
"kind": "api",
|
|
1217
|
-
"name": "expenses.recordReimbursement",
|
|
1218
|
-
"operation": "recordReimbursement",
|
|
1219
|
-
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1220
|
-
"path": "/expenses/recordReimbursement",
|
|
1221
|
-
"method": "POST"
|
|
1222
|
-
}
|
|
1223
|
-
],
|
|
1224
|
-
"prompts": [],
|
|
1225
|
-
"relationshipsV2": {
|
|
1226
|
-
"foreignKeyFields": 3,
|
|
1227
|
-
"crossPackageRefFields": 8,
|
|
1228
|
-
"junctionCollections": 1,
|
|
1229
|
-
"hierarchicalObjects": 0,
|
|
1230
|
-
"polymorphicAssociations": 0,
|
|
1231
|
-
"uuidColumns": 30
|
|
1232
|
-
},
|
|
1233
|
-
"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",
|
|
1234
|
-
"moduleDocs": [
|
|
1235
|
-
{
|
|
1236
|
-
"path": "agents/financial-facts-test-matrix.md",
|
|
1237
|
-
"module": "financial-facts-test-matrix",
|
|
1238
|
-
"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"
|
|
1239
1386
|
},
|
|
1240
1387
|
{
|
|
1241
|
-
"
|
|
1242
|
-
"
|
|
1243
|
-
"content": "# Expense entry and receipt review\n\nImport `ExpenseForm`, `ExpenseReviewPanel` and their named Props types from\n`@happyvertical/smrt-expenses/svelte`. Registry slots are `expense-form` and\n`expense-review`. `@happyvertical/smrt-expenses/ui` exposes metadata without\nSvelte; `./playground` exposes an interactive retained-draft/review example.\n\n`ExpenseDraftValues` is a complete set of native strings: `amount`, `currency`,\n`incurredOn`, `description`, `category`, `vendorId`, `commitmentId`, `paidBy`,\n`paidByProfileId`, and `correctionReason`. Initialize unused fields to `''`.\nAmount is ordinary currency text (`'125.00'` CAD), distinct from the public\nExpense model's integer minor-unit `amount` (`12500`). Server adapters parse\ncurrency precision, safe integer range and business rules. Calendar dates stay\nlossless text, normally `YYYY-MM-DD`; invalid amounts/dates never disappear due\nto browser number/date coercion. Every validation failure returns original\nstrings, not reconstructed saved-model values.\n\n```svelte\n<script lang=\"ts\">\n import { ExpenseForm, ExpenseReviewPanel } from '@happyvertical/smrt-expenses/svelte';\n let { data, form } = $props();\n</script>\n<ExpenseForm\n action=\"?/save\"\n title=\"Record purchase\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n message={form?.message}\n canEdit={data.mayEdit}\n correcting={data.correcting}\n vendors={data.authorizedVendors}\n commitments={data.authorizedCommitments}\n hiddenFields={[\n { name: 'requestId', value: form?.requestId ?? data.requestId },\n { name: 'costObjectId', value: data.costObjectId },\n { name: 'expectedTenantId', value: data.tenantId },\n ]}\n/>\n<ExpenseReviewPanel\n expense={data.expenseSummary}\n duplicates={data.authorizedDuplicateCandidates}\n history={data.history}\n receipts={data.receiptPanel}\n/>\n```\n\nVendor and commitment options are authorized Commerce `{id, label}` projections.\nResolve Profile names and application-specific award lineage in the consumer.\nUnknown retained references remain selectable/postable; selecting a reference\nnever establishes tenant ownership or commitment eligibility. `fields` maps\nlogical keys to native names; `errors` always uses logical keys. Submitter\n`intentField`/`intent` default to `intent=save`. `hiddenFields` preserves exact\nvalues and repeated names. Avoid field-name collisions. `children` adds\napplication-owned package allocation, quantity, reimbursement and correction\npolicy fields. `correcting` shows a retained reason, but does not implement a\nnew Expense correction model. The caller applies its own correction/history\npolicy and invokes existing guarded methods where appropriate.\n\nThe base form performs native POST by default, with or without JavaScript.\n`onsubmit` allows optional enhancement. `pending` or a supplied\n`smrt-ui/form-retry` `retryStatus` controls progress presentation without clearing\nvalues or creating/rotating request keys. `canEdit=false` disables fields and\nhides save; server-side authorization and replay enforcement remain mandatory.\n\nCurrency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n\ncontext. It preserves the exact retained draft string and the field name from\n`fields.currency`, including native no-JavaScript correction flows. Pass\n`currencyOptions` only when the caller must restrict the shared currency catalog.\n\n`ExpenseReviewSummary` projects public Expense description, integer minor-unit\namount, currency and incurredOn, with caller-localized `statusLabel` and `note`.\n`duplicates` and `history` are explicit authorized presentation DTOs, not a\nparallel ledger. The server supplies findings from ExpenseReceipt duplicate\nqueries and application history. Unknown labels render verbatim. No state or\naction is inferred: `canReview`, `action`, `actions` and hidden fingerprints\nexplicitly provide a native review form. `action=\"\"` posts to the current page;\nomitting `action` hides the review form; `children` can supply retained reason\nor duplicate-selection inputs. A server must reauthorize, reload the model and\ninvoke `review()`, `reject()`, `markDuplicate()` or `reopen()` under its policy.\nNever assign review fields through UI payload binding. Reviewed money remains\nfrozen by the existing model; UI props cannot bypass it.\n\n`receipts` is exactly the public Assets `AttachmentPanelProps`, supporting\nprivate view/download URLs, version notes, native upload and recovery. It is\nrendered outside the review form so upload and review remain separate native\nactions. Use authorized receipt projections; attaching a file does not review\nan expense, mark a duplicate, or change invoice approval. Assets owns file\nselection and reselect-file help; Expenses does not duplicate uploads/storage.\n\nValidation: `pnpm --filter @happyvertical/smrt-expenses test`, `test:ui`,\n`test:e2e`, `typecheck`, `build`, and `verify:pack`. PostgreSQL model validation\nremains in the existing opt-in `test:postgres` lane; this UI does not alter it.\n\n`visibleFields` optionally limits the logical `ExpenseDraftValues` fields rendered by `ExpenseForm`. Omitted renders all existing fields; an empty array renders none. Excluded fields do not emit hidden fallbacks, including `correctionReason`; fixed values can be supplied explicitly through `hiddenFields` without duplicate names. This supports policy-fixed payer fields and domain-owned reference selectors. Visibility is presentation, never authority: endpoints must validate every fixed or submitted value. Unknown retained values and validation errors remain visible for included fields.\n\nComposition validation: native Svelte SSR contracts cover defaults, omitted fields, retained malformed values, caller identity, no duplicate fixed inputs, correction/read-only/pending behavior. No persistence, transaction, tenant authority or external provider changes occur; those matrix dimensions are N/A for this presentation-only extension. Node26/Svelte5 native POST markup is the supported runtime edge.\n\n## Cost-object lifecycle (#3462)\n\nThe existing #3423/#3429 entry and review controls are extended with\n`ExpenseList`, `ExpenseReceiptCapture` and `ExpenseReviewQueue` in the same\n`./svelte` export. Their registry slots are `expense-list`,\n`expense-receipt-capture` and `expense-review-queue`; named Props and row types\nare public. The playground composes these with the existing controls.\n\n`ExpenseForm` defaults to an optional touch keypad beside amount entry. It is\navailable after hydration, with ordinary text/keyboard entry and native POST\navailable without JavaScript. `amountKeypad=false` hides it. Digits, decimal and\nbackspace edit the exact currency-unit string at its selection; no parsing or\nrounding occurs. The server validates currency precision and safe minor units.\n`costObject={{ type, id }}` enables two editable native reference inputs, with\n`costObjectFields` name adapters and `costObjectErrors` retained server errors.\nOmit it when references are fixed in `hiddenFields` (the existing consumer\ncontract). Do not supply the same reference both ways. Qualified type and bare\nid are caller strings and must be validated/authorized by the endpoint.\n\n`ExpenseList` takes caller-authorized `ExpenseListItem[]` plus exact\n`costObjectType`/`costObjectId`; both keys must match. It displays each amount\nin its own currency, never sums different currencies, and retains unknown\n`statusLabel`, optional `note`, authorized `href`, empty and load-error states.\nFiltering is presentation only: do not pass unauthorized rows to the browser.\n\n`ExpenseReceiptCapture` is a separate multipart POST action, with\n`canAttach=false` by default. `fileField` defaults to `receipt`, submitter to\n`intent=attach-receipt`. It forwards `hiddenFields` exactly, including the\nexpense and caller-owned request tokens. A native file picker works without\nJavaScript; “Use camera” mounts the existing smrt-ui `CameraCapture` only after\nan explicit click. Capture/retake/commit and stream cleanup are owned by that\nshared component. Toggle back to file selection to recover from camera denial\nor unsupported APIs. Switching modes discards the previous file; selection is\nnever uploaded automatically. Pending prevents another upload or mode change;\na committed camera photo remains held for retry. Navigation cannot restore\nfile inputs: select the file again after a failed upload or page reload. The\nserver authenticates, validates file content/limits and expense visibility,\nstores the Asset and uses `ExpenseReceiptCollection.attachReceipt()` with its\nduplicate guard; this component performs no storage, deduplication or model\nmutation. Do not place this component inside another form.\n\n`ExpenseReviewQueue` takes authorized `ExpenseReviewQueueItem[]` in caller order.\nDecisions require both queue `canReview=true`, row `canReview=true`, and a row\n`action` (empty string explicitly means this page). Each row posts its own\n`expenseId`, retained `reason`, hidden request context and clicked\n`intent=approve|reject`. Field and intent names are configurable. Rejection\nrequires a reason in native browser validation; approval bypasses that browser\nrequirement. The server must enforce the reason and reauthorize/reload before\ncalling `review()` or `reject()`; these UI gates provide no authority. Return\nrow `reason`, `message` and original request tokens after rejection or an\nuncertain outcome. Pending disables decisions. No action is inferred from\nstatus, no submitted row is removed optimistically, and no request key rotates.\nAvoid collisions between hidden field names and component identity/reason/intent\nfields. Empty, read-only, unknown status and load-error states remain visible.\n\nLifecycle evidence is in the maintained `test:ui` SSR contracts and `test:e2e`\nChromium proofs: 390px keypad, exact cost-object payload, native multipart file\nselection with and without JavaScript, camera denial recovery, and native queue\napprove/reject with retained reason and row request identity. Persistence,\ntransaction affinity and tenant authorization remain existing server contracts;\nno database/model surface is changed by these presentation components.\n"
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
"
|
|
1249
|
-
"className": "ExpensesRecipe",
|
|
1250
|
-
"label": "Expenses",
|
|
1251
|
-
"summary": "Record what the business spends, with receipts, and review it.",
|
|
1252
|
-
"synonyms": [
|
|
1253
|
-
"costs",
|
|
1254
|
-
"receipts",
|
|
1255
|
-
"spending",
|
|
1256
|
-
"reimbursements"
|
|
1257
|
-
],
|
|
1258
|
-
"models": [
|
|
1259
|
-
"@happyvertical/smrt-expenses:Expense",
|
|
1260
|
-
"@happyvertical/smrt-expenses:ExpenseReceipt"
|
|
1261
|
-
],
|
|
1262
|
-
"nav": [
|
|
1388
|
+
"name": "ExpenseAllocationPortion",
|
|
1389
|
+
"qualifiedName": "@happyvertical/smrt-expenses:ExpenseAllocationPortion",
|
|
1390
|
+
"collection": "expenseallocationportions",
|
|
1391
|
+
"description": "Persisted command-owned ExpenseAllocationPortion evidence.",
|
|
1392
|
+
"tableName": "expense_allocation_portions",
|
|
1393
|
+
"packageName": "@happyvertical/smrt-expenses",
|
|
1394
|
+
"extends": "SmrtObject",
|
|
1395
|
+
"fields": [
|
|
1263
1396
|
{
|
|
1264
|
-
"
|
|
1397
|
+
"name": "tenantId",
|
|
1398
|
+
"type": "text",
|
|
1399
|
+
"required": true,
|
|
1400
|
+
"columnType": "UUID"
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
"name": "sourceExpenseId",
|
|
1404
|
+
"type": "foreignKey",
|
|
1405
|
+
"required": true,
|
|
1406
|
+
"related": "Expense",
|
|
1407
|
+
"columnType": "UUID"
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
"name": "revisionId",
|
|
1411
|
+
"type": "foreignKey",
|
|
1412
|
+
"required": true,
|
|
1413
|
+
"related": "ExpenseAllocationRevision",
|
|
1414
|
+
"columnType": "UUID"
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
"name": "portionKey",
|
|
1418
|
+
"type": "text",
|
|
1419
|
+
"required": false,
|
|
1420
|
+
"columnType": "TEXT"
|
|
1421
|
+
},
|
|
1422
|
+
{
|
|
1423
|
+
"name": "costObjectType",
|
|
1424
|
+
"type": "text",
|
|
1425
|
+
"required": false,
|
|
1426
|
+
"columnType": "TEXT"
|
|
1427
|
+
},
|
|
1428
|
+
{
|
|
1429
|
+
"name": "costObjectId",
|
|
1430
|
+
"type": "text",
|
|
1431
|
+
"required": false,
|
|
1432
|
+
"columnType": "TEXT"
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
"name": "costCodeRef",
|
|
1436
|
+
"type": "text",
|
|
1437
|
+
"required": false,
|
|
1438
|
+
"columnType": "TEXT"
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
"name": "amountMinor",
|
|
1442
|
+
"type": "integer",
|
|
1443
|
+
"required": false,
|
|
1444
|
+
"columnType": "INTEGER"
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
"name": "currency",
|
|
1448
|
+
"type": "text",
|
|
1449
|
+
"required": false,
|
|
1450
|
+
"columnType": "TEXT"
|
|
1451
|
+
}
|
|
1452
|
+
],
|
|
1453
|
+
"relationships": [
|
|
1454
|
+
{
|
|
1455
|
+
"name": "sourceExpenseId",
|
|
1456
|
+
"type": "foreignKey",
|
|
1457
|
+
"required": true,
|
|
1458
|
+
"related": "Expense",
|
|
1459
|
+
"columnType": "UUID"
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
"name": "revisionId",
|
|
1463
|
+
"type": "foreignKey",
|
|
1464
|
+
"required": true,
|
|
1465
|
+
"related": "ExpenseAllocationRevision",
|
|
1466
|
+
"columnType": "UUID"
|
|
1467
|
+
}
|
|
1468
|
+
],
|
|
1469
|
+
"methods": [
|
|
1470
|
+
"delete",
|
|
1471
|
+
"save"
|
|
1472
|
+
],
|
|
1473
|
+
"methodSignatures": [
|
|
1474
|
+
{
|
|
1475
|
+
"name": "delete",
|
|
1476
|
+
"async": true,
|
|
1477
|
+
"returns": "Promise<void>"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
"name": "save",
|
|
1481
|
+
"async": true,
|
|
1482
|
+
"params": [
|
|
1483
|
+
"options?: SmrtSaveOptions"
|
|
1484
|
+
],
|
|
1485
|
+
"returns": "Promise<this>"
|
|
1486
|
+
}
|
|
1487
|
+
],
|
|
1488
|
+
"tenant": {
|
|
1489
|
+
"scoped": true,
|
|
1490
|
+
"mode": "required",
|
|
1491
|
+
"field": "tenantId"
|
|
1492
|
+
},
|
|
1493
|
+
"conflictColumns": [
|
|
1494
|
+
"tenant_id",
|
|
1495
|
+
"slug",
|
|
1496
|
+
"context"
|
|
1497
|
+
],
|
|
1498
|
+
"surfaces": [],
|
|
1499
|
+
"withheldSurfaces": [
|
|
1500
|
+
{
|
|
1501
|
+
"kind": "api",
|
|
1502
|
+
"operation": "save",
|
|
1503
|
+
"code": "lifecycle-method",
|
|
1504
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
1505
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseAllocationPortion"
|
|
1506
|
+
}
|
|
1507
|
+
],
|
|
1508
|
+
"relationshipFeatures": [
|
|
1509
|
+
"foreignKey",
|
|
1510
|
+
"uuidColumns"
|
|
1511
|
+
],
|
|
1512
|
+
"tags": [],
|
|
1513
|
+
"risks": []
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
"name": "ExpenseAllocationBilling",
|
|
1517
|
+
"qualifiedName": "@happyvertical/smrt-expenses:ExpenseAllocationBilling",
|
|
1518
|
+
"collection": "expenseallocationbillings",
|
|
1519
|
+
"description": "Persisted command-owned ExpenseAllocationBilling evidence.",
|
|
1520
|
+
"tableName": "expense_allocation_billings",
|
|
1521
|
+
"packageName": "@happyvertical/smrt-expenses",
|
|
1522
|
+
"extends": "SmrtObject",
|
|
1523
|
+
"fields": [
|
|
1524
|
+
{
|
|
1525
|
+
"name": "tenantId",
|
|
1526
|
+
"type": "text",
|
|
1527
|
+
"required": true,
|
|
1528
|
+
"columnType": "UUID"
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
"name": "sourceExpenseId",
|
|
1532
|
+
"type": "foreignKey",
|
|
1533
|
+
"required": true,
|
|
1534
|
+
"related": "Expense",
|
|
1535
|
+
"columnType": "UUID"
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
"name": "portionId",
|
|
1539
|
+
"type": "foreignKey",
|
|
1540
|
+
"required": true,
|
|
1541
|
+
"related": "ExpenseAllocationPortion",
|
|
1542
|
+
"columnType": "UUID"
|
|
1543
|
+
},
|
|
1544
|
+
{
|
|
1545
|
+
"name": "invoiceId",
|
|
1546
|
+
"type": "crossPackageRef",
|
|
1547
|
+
"required": false,
|
|
1548
|
+
"related": "@happyvertical/smrt-commerce:Invoice",
|
|
1549
|
+
"columnType": "UUID"
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
"name": "draftType",
|
|
1553
|
+
"type": "text",
|
|
1554
|
+
"required": false,
|
|
1555
|
+
"columnType": "TEXT"
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
"name": "draftId",
|
|
1559
|
+
"type": "text",
|
|
1560
|
+
"required": false,
|
|
1561
|
+
"columnType": "TEXT"
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
"name": "invoiceLineId",
|
|
1565
|
+
"type": "crossPackageRef",
|
|
1566
|
+
"required": false,
|
|
1567
|
+
"related": "@happyvertical/smrt-commerce:InvoiceLineItem",
|
|
1568
|
+
"columnType": "UUID"
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
"name": "requestId",
|
|
1572
|
+
"type": "text",
|
|
1573
|
+
"required": false,
|
|
1574
|
+
"columnType": "TEXT"
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
"name": "requestFingerprint",
|
|
1578
|
+
"type": "text",
|
|
1579
|
+
"required": false,
|
|
1580
|
+
"columnType": "TEXT"
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
"name": "relatedReservationId",
|
|
1584
|
+
"type": "foreignKey",
|
|
1585
|
+
"required": false,
|
|
1586
|
+
"related": "ExpenseAllocationBilling",
|
|
1587
|
+
"columnType": "UUID"
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
"name": "kind",
|
|
1591
|
+
"type": "text",
|
|
1592
|
+
"required": false,
|
|
1593
|
+
"columnType": "TEXT"
|
|
1594
|
+
}
|
|
1595
|
+
],
|
|
1596
|
+
"relationships": [
|
|
1597
|
+
{
|
|
1598
|
+
"name": "sourceExpenseId",
|
|
1599
|
+
"type": "foreignKey",
|
|
1600
|
+
"required": true,
|
|
1601
|
+
"related": "Expense",
|
|
1602
|
+
"columnType": "UUID"
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
"name": "portionId",
|
|
1606
|
+
"type": "foreignKey",
|
|
1607
|
+
"required": true,
|
|
1608
|
+
"related": "ExpenseAllocationPortion",
|
|
1609
|
+
"columnType": "UUID"
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
"name": "invoiceId",
|
|
1613
|
+
"type": "crossPackageRef",
|
|
1614
|
+
"required": false,
|
|
1615
|
+
"related": "@happyvertical/smrt-commerce:Invoice",
|
|
1616
|
+
"columnType": "UUID"
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
"name": "invoiceLineId",
|
|
1620
|
+
"type": "crossPackageRef",
|
|
1621
|
+
"required": false,
|
|
1622
|
+
"related": "@happyvertical/smrt-commerce:InvoiceLineItem",
|
|
1623
|
+
"columnType": "UUID"
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
"name": "relatedReservationId",
|
|
1627
|
+
"type": "foreignKey",
|
|
1628
|
+
"required": false,
|
|
1629
|
+
"related": "ExpenseAllocationBilling",
|
|
1630
|
+
"columnType": "UUID"
|
|
1631
|
+
}
|
|
1632
|
+
],
|
|
1633
|
+
"methods": [
|
|
1634
|
+
"delete",
|
|
1635
|
+
"save"
|
|
1636
|
+
],
|
|
1637
|
+
"methodSignatures": [
|
|
1638
|
+
{
|
|
1639
|
+
"name": "delete",
|
|
1640
|
+
"async": true,
|
|
1641
|
+
"returns": "Promise<void>"
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
"name": "save",
|
|
1645
|
+
"async": true,
|
|
1646
|
+
"params": [
|
|
1647
|
+
"options?: SmrtSaveOptions"
|
|
1648
|
+
],
|
|
1649
|
+
"returns": "Promise<this>"
|
|
1650
|
+
}
|
|
1651
|
+
],
|
|
1652
|
+
"tenant": {
|
|
1653
|
+
"scoped": true,
|
|
1654
|
+
"mode": "required",
|
|
1655
|
+
"field": "tenantId"
|
|
1656
|
+
},
|
|
1657
|
+
"conflictColumns": [
|
|
1658
|
+
"tenant_id",
|
|
1659
|
+
"slug",
|
|
1660
|
+
"context"
|
|
1661
|
+
],
|
|
1662
|
+
"surfaces": [],
|
|
1663
|
+
"withheldSurfaces": [
|
|
1664
|
+
{
|
|
1665
|
+
"kind": "api",
|
|
1666
|
+
"operation": "save",
|
|
1667
|
+
"code": "lifecycle-method",
|
|
1668
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
1669
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseAllocationBilling"
|
|
1670
|
+
}
|
|
1671
|
+
],
|
|
1672
|
+
"relationshipFeatures": [
|
|
1673
|
+
"crossPackageRef",
|
|
1674
|
+
"foreignKey",
|
|
1675
|
+
"uuidColumns"
|
|
1676
|
+
],
|
|
1677
|
+
"tags": [],
|
|
1678
|
+
"risks": []
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
"name": "ExpenseReceipt",
|
|
1682
|
+
"qualifiedName": "@happyvertical/smrt-expenses:ExpenseReceipt",
|
|
1683
|
+
"collection": "expensereceipts",
|
|
1684
|
+
"description": "A receipt file attached to an expense.",
|
|
1685
|
+
"tableName": "expense_receipts",
|
|
1686
|
+
"packageName": "@happyvertical/smrt-expenses",
|
|
1687
|
+
"extends": "SmrtObject",
|
|
1688
|
+
"fields": [
|
|
1689
|
+
{
|
|
1690
|
+
"name": "tenantId",
|
|
1691
|
+
"type": "text",
|
|
1692
|
+
"required": false,
|
|
1693
|
+
"columnType": "UUID"
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
"name": "expenseId",
|
|
1697
|
+
"type": "foreignKey",
|
|
1698
|
+
"required": true,
|
|
1699
|
+
"related": "Expense",
|
|
1700
|
+
"columnType": "UUID"
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
"name": "assetId",
|
|
1704
|
+
"type": "crossPackageRef",
|
|
1705
|
+
"required": true,
|
|
1706
|
+
"related": "@happyvertical/smrt-assets:Asset",
|
|
1707
|
+
"columnType": "UUID"
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
"name": "uploadedByProfileId",
|
|
1711
|
+
"type": "crossPackageRef",
|
|
1712
|
+
"required": false,
|
|
1713
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1714
|
+
"columnType": "UUID"
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
"name": "filename",
|
|
1718
|
+
"type": "text",
|
|
1719
|
+
"required": false,
|
|
1720
|
+
"columnType": "TEXT"
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
"name": "mimeType",
|
|
1724
|
+
"type": "text",
|
|
1725
|
+
"required": false,
|
|
1726
|
+
"columnType": "TEXT"
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
"name": "byteCount",
|
|
1730
|
+
"type": "integer",
|
|
1731
|
+
"required": false,
|
|
1732
|
+
"columnType": "INTEGER"
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
"name": "contentSha256",
|
|
1736
|
+
"type": "text",
|
|
1737
|
+
"required": true,
|
|
1738
|
+
"columnType": "TEXT"
|
|
1739
|
+
}
|
|
1740
|
+
],
|
|
1741
|
+
"relationships": [
|
|
1742
|
+
{
|
|
1743
|
+
"name": "expenseId",
|
|
1744
|
+
"type": "foreignKey",
|
|
1745
|
+
"required": true,
|
|
1746
|
+
"related": "Expense",
|
|
1747
|
+
"columnType": "UUID"
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
"name": "assetId",
|
|
1751
|
+
"type": "crossPackageRef",
|
|
1752
|
+
"required": true,
|
|
1753
|
+
"related": "@happyvertical/smrt-assets:Asset",
|
|
1754
|
+
"columnType": "UUID"
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
"name": "uploadedByProfileId",
|
|
1758
|
+
"type": "crossPackageRef",
|
|
1759
|
+
"required": false,
|
|
1760
|
+
"related": "@happyvertical/smrt-profiles:Profile",
|
|
1761
|
+
"columnType": "UUID"
|
|
1762
|
+
}
|
|
1763
|
+
],
|
|
1764
|
+
"methods": [
|
|
1765
|
+
"delete",
|
|
1766
|
+
"save"
|
|
1767
|
+
],
|
|
1768
|
+
"methodSignatures": [
|
|
1769
|
+
{
|
|
1770
|
+
"name": "delete",
|
|
1771
|
+
"async": true,
|
|
1772
|
+
"params": [
|
|
1773
|
+
"options?: object"
|
|
1774
|
+
],
|
|
1775
|
+
"returns": "Promise<void>"
|
|
1776
|
+
},
|
|
1777
|
+
{
|
|
1778
|
+
"name": "save",
|
|
1779
|
+
"async": true,
|
|
1780
|
+
"params": [
|
|
1781
|
+
"options?: SmrtSaveOptions"
|
|
1782
|
+
],
|
|
1783
|
+
"returns": "Promise<this>"
|
|
1784
|
+
}
|
|
1785
|
+
],
|
|
1786
|
+
"tenant": {
|
|
1787
|
+
"scoped": true,
|
|
1788
|
+
"mode": "optional",
|
|
1789
|
+
"field": "tenantId"
|
|
1790
|
+
},
|
|
1791
|
+
"conflictColumns": [
|
|
1792
|
+
"tenant_id",
|
|
1793
|
+
"slug",
|
|
1794
|
+
"context"
|
|
1795
|
+
],
|
|
1796
|
+
"surfaces": [],
|
|
1797
|
+
"withheldSurfaces": [
|
|
1798
|
+
{
|
|
1799
|
+
"kind": "api",
|
|
1800
|
+
"operation": "save",
|
|
1801
|
+
"code": "lifecycle-method",
|
|
1802
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
1803
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseReceipt"
|
|
1804
|
+
}
|
|
1805
|
+
],
|
|
1806
|
+
"relationshipFeatures": [
|
|
1807
|
+
"crossPackageRef",
|
|
1808
|
+
"foreignKey",
|
|
1809
|
+
"uuidColumns"
|
|
1810
|
+
],
|
|
1811
|
+
"tags": [],
|
|
1812
|
+
"risks": []
|
|
1813
|
+
}
|
|
1814
|
+
],
|
|
1815
|
+
"surfaces": [
|
|
1816
|
+
{
|
|
1817
|
+
"kind": "api",
|
|
1818
|
+
"name": "expenses.record",
|
|
1819
|
+
"operation": "record",
|
|
1820
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1821
|
+
"path": "/expenses/record",
|
|
1822
|
+
"method": "POST"
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
"kind": "api",
|
|
1826
|
+
"name": "expenses.attachReceipt",
|
|
1827
|
+
"operation": "attachReceipt",
|
|
1828
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1829
|
+
"path": "/expenses/attachReceipt",
|
|
1830
|
+
"method": "POST"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
"kind": "api",
|
|
1834
|
+
"name": "expenses.recordReview",
|
|
1835
|
+
"operation": "recordReview",
|
|
1836
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1837
|
+
"path": "/expenses/recordReview",
|
|
1838
|
+
"method": "POST"
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
"kind": "api",
|
|
1842
|
+
"name": "expenses.recordReimbursement",
|
|
1843
|
+
"operation": "recordReimbursement",
|
|
1844
|
+
"objectName": "@happyvertical/smrt-expenses:ExpenseCollection",
|
|
1845
|
+
"path": "/expenses/recordReimbursement",
|
|
1846
|
+
"method": "POST"
|
|
1847
|
+
}
|
|
1848
|
+
],
|
|
1849
|
+
"prompts": [],
|
|
1850
|
+
"relationshipsV2": {
|
|
1851
|
+
"foreignKeyFields": 11,
|
|
1852
|
+
"crossPackageRefFields": 12,
|
|
1853
|
+
"junctionCollections": 1,
|
|
1854
|
+
"hierarchicalObjects": 0,
|
|
1855
|
+
"polymorphicAssociations": 0,
|
|
1856
|
+
"uuidColumns": 66
|
|
1857
|
+
},
|
|
1858
|
+
"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 original cost object per expense. Reviewed relational allocation revisions\n can attribute that source to multiple targets; target totals count portions\n instead of the original attribution. See [reviewed allocations](agents/reviewed-allocations.md).\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\n## Reviewed allocations (#4746)\n\nSee [reviewed allocations](agents/reviewed-allocations.md) and its\n[behavior matrix](agents/allocation-test-matrix.md). Source accounting remains\nsingular; immutable portions own attribution and canonical billing identities.\n\nUnbilled `withdrawAllocation` audits a full return to the original unsplit target,\nrequires source/all-old/restored-target authority, refuses any active billing\nclaim, and permits later explicit source correction without losing history.\nRevision kind/predecessor + typed source snapshots preserve old portion facts\nwhen the active head is null; receipt provenance stays frozen after withdrawal.\n",
|
|
1859
|
+
"moduleDocs": [
|
|
1860
|
+
{
|
|
1861
|
+
"path": "agents/reviewed-allocations.md",
|
|
1862
|
+
"module": "reviewed-allocations",
|
|
1863
|
+
"content": "# Reviewed expense attribution\n\n`ExpenseCollection.reviewAllocation(input, authority)` reviews a **full** split\nof a positive, reviewed original. The source keeps its original amount, payer,\ncommitment, reimbursement, refund capacity, supplier tax facts and receipt graph.\nChildren are attribution, never additional expenses or another cash authority.\n\nThe input names `sourceExpenseId`, the loaded `updated_at` as\n`expectedSourceRevision`, `requestId`, `reviewEvidence`, and 1–1000 portions.\nEach portion has an exact positive safe-integer `amountMinor`, unique\n`portionKey`, registered same-tenant `costObjectType`/`costObjectId`, and explicit\n`costCodeRef`. Portions must sum exactly to source gross money in source currency.\nNo remainder, tax allocation, customer markup or tax policy is inferred.\n\nAmbient `expenses.recordReview` authorizes a person in the active tenant. The\nmandatory host `authorizeSource(principal,sourceExpenseId,executor,context)`\ncallback checks original source privacy for full reviews. Its context names the\nexplicit review/reserve/bind/release action; billing contexts include only the\nbounded authorized portion so target finance users need no unrelated original\nproject or full receipt disclosure. The mandatory\n`authorizeTarget(principal,target,executor)` callback must enforce\ncurrent project grants for **every old and proposed target**. The supplied\nexecutor is the command's caller-affine transaction. Callback denial rolls back\nall children/head/revision claims. Never perform provider writes in the callback.\n\nA source lock and revision compare-and-swap serialize allocations and billing\nreservations. Same request/payload returns the same immutable IDs after current\nauthority checks; changed payload conflicts. On serialization/revision failure,\nretry the whole caller transaction with fresh instances. Unreserved corrections\nappend a revision linked by `previousRevisionId`; prior children remain readable.\nWhile an allocation is active, source review cannot be reopened/rejected.\nUse explicit audited unbilled withdrawal before source correction; immutable\nallocation provenance prevents physical source deletion.\n\n`totalsByCurrency()` counts each source once. Target filters project the current\nportions instead of counting the source's original attribution. Commitment\npositions, supplier-tax reports and reimbursement/refund positions retain the\noriginal accounting source exactly once. `forCostObject()` remains an original\nsource/evidence query; use target-filtered totals for allocated money. Do not sum\nsource amounts obtained as evidence to calculate target costs.\n\n`resolveExpenseAllocationPortion(db,portionId,authority)` returns only that\nportion's immutable source/revision/fingerprint/target/code/amount/currency and\ncurrent `active` flag after project authority; it does not disclose receipt URLs\nor other projects. Receipt access still needs the original source privacy check.\n`getExpenseAllocationPortionPosition` adds current reservation UUID, host draft\nreferences and bound invoice references/status for that same target portion. It\nis advisory; reservation mutations always recheck capacity atomically. Historical\nor issued claims never become available implicitly.\n\n## Billing\n\n`reserveExpenseAllocationPortion` reserves a whole portion once against a host\n`draftType`/`draftId`, requiring `authority.authorizeDraft`; it can alternatively\nreserve and bind directly to an existing invoice/line. Customer price remains\nindependent of source capacity. `bindExpenseAllocationPortion` assigns that same\nreservation UUID to one same-tenant, same-currency commerce draft line after\nmandatory `authorizeInvoice` current-grant/project checks. The line must use\n`EXPENSE_ALLOCATION_INVOICE_SOURCE` and `sourceId = portionId`.\n\nCommerce stores only the reservation pointer, with trusted source-handler checks\non line mutations and invoice issue. Missing handlers, unbound protected lines,\nchanged/cleared bound identities, copied pointers and duplicate line claims fail.\nAll reservation and binding events are immutable relational rows; source locking,\nCAS and unique event keys prevent concurrent double assignment. There is no\ncommerce-side source availability balance.\n\n`releaseExpenseAllocationPortion` records withdrawal and removes a bound draft\npointer in the same transaction. It retains claim/binding history. Active claims\nblock allocation supersession. Once issued, a claim remains consumed: ordinary\nrelease/reallocation is refused and corrections require an explicit credit or\nreversal policy; the API never rewrites issued invoices or infers such policy.\n\nMigrate schemas before use, including the three new allocation tables, nullable\nExpense head and nullable commerce line pointer. Existing unsplit rows retain\nNULL heads/pointers and unchanged accounting semantics. See the\n[behavior matrix](allocation-test-matrix.md) for release evidence requirements.\n\nBounded readers use the explicitly published `expenses.read` catalog permission\n(registered through public `registerPermissionDefinitions`; `get`/`list`\nnormalize to `.read`) and the host's mandatory current target-cost\nvisibility callback. They never require review authority or original-source\nprivacy access. Mutations retain `expenses.recordReview`. Consumers must grant\nthese real permissions through their role configuration; never fabricate a\npermission snapshot to call the library.\n\nBounded identities include source date, payer **category** (no person/name),\nreimbursement eligibility, nullable supplier-tax presence, refund presence and\nnullable exact `costBasisMinor`. Basis equals portion gross only when the\noriginal's complete known basis equals gross and no linked refunds exist. It is\notherwise unknown; tax is never apportioned implicitly. New reserve/bind commands\nrefuse a source with any linked refunds pending explicit reversal policy.\nReimbursement eligibility freezes with allocation provenance. Existing issued\nreservations remain consumed and immutable when later refund evidence arrives.\n\nThe source fingerprint includes a private random per-source nonce retained only\nin full source-authorized revision evidence. Bounded readers receive its digest,\nnot the nonce, preventing offline guesses of another project's source total.\nUnbilled supersession reuses the nonce and retains the same source fingerprint.\n\n## Explicit unbilled withdrawal before source correction\n\n`ExpenseCollection.withdrawAllocation` (root `withdrawExpenseAllocation`) accepts\n`sourceExpenseId`, `expectedActiveRevisionId`, `expectedSourceRevision`,\n`requestId` and `reviewEvidence`. It appends an immutable revision with\n`kind: 'withdrawal'` and clears the active head atomically on the caller executor.\nThe result is `{revision, withdrawnRevisionId}`; reload the original source for\nits current state/revision. No portion row is changed or deleted.\n\nWithdrawal requires current source privacy/edit authority with action\n`withdrawAllocation`, authority for **every old portion target**, and a separate\nmandatory `authorizeUnsplitTarget(principal,{sourceExpenseId,costObjectType,\ncostObjectId},executor)` callback for the restored original destination. The\nlatter has no invented cost code. Assigned destinations must exist in the active\ntenant; an explicitly unassigned original has both destination strings empty and\nstill requires the callback. Replays reauthorize all of these paths; changed\npayload, stale source/head or a conflicting request is refused.\n\nAny active reservation, bound claim or issued claim refuses withdrawal. First\nabandon/release an authorized unissued draft explicitly; withdrawal never\nsilently releases capacity or rewrites issued invoices. **This is not billed\nreversal support.** Existing settlement/refund/reimbursement guards also remain.\n\nGlobal and commitment totals still count the original source once. Its monetary\nattribution switches atomically back to the original unsplit destination. Source\nreview status is unchanged: no financial approval is implied. The caller may\nthen explicitly reopen and correct the original amount/date/payer or reject a\nduplicate using existing guarded source methods. Corrected unreviewed money stays\nexcluded until separately reviewed. Original receipt evidence remains immutable\nonce allocation history exists, including after withdrawal.\n\nThe immutable predecessor chain has one successor per revision. Its unique leaf\nretains the latest withdrawal independently of a null active head; the next\nreview links to that withdrawal deterministically. Typed source snapshot facts\non each revision preserve historical portion date/payer/basis attestation across\nlater source correction. Inactive historical portions remain bounded-readable\nwith `active:false` when the source is reopened/rejected; they are never available\nfor fresh billing. Active portions still require a usable reviewed source.\n"
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
"path": "agents/financial-facts-test-matrix.md",
|
|
1867
|
+
"module": "financial-facts-test-matrix",
|
|
1868
|
+
"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"
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
"path": "agents/expense-ui.md",
|
|
1872
|
+
"module": "expense-ui",
|
|
1873
|
+
"content": "# Expense entry and receipt review\n\nImport `ExpenseForm`, `ExpenseReviewPanel` and their named Props types from\n`@happyvertical/smrt-expenses/svelte`. Registry slots are `expense-form` and\n`expense-review`. `@happyvertical/smrt-expenses/ui` exposes metadata without\nSvelte; `./playground` exposes an interactive retained-draft/review example.\n\n`ExpenseDraftValues` is a complete set of native strings: `amount`, `currency`,\n`incurredOn`, `description`, `category`, `vendorId`, `commitmentId`, `paidBy`,\n`paidByProfileId`, and `correctionReason`. Initialize unused fields to `''`.\nAmount is ordinary currency text (`'125.00'` CAD), distinct from the public\nExpense model's integer minor-unit `amount` (`12500`). Server adapters parse\ncurrency precision, safe integer range and business rules. Calendar dates stay\nlossless text, normally `YYYY-MM-DD`; invalid amounts/dates never disappear due\nto browser number/date coercion. Every validation failure returns original\nstrings, not reconstructed saved-model values.\n\n```svelte\n<script lang=\"ts\">\n import { ExpenseForm, ExpenseReviewPanel } from '@happyvertical/smrt-expenses/svelte';\n let { data, form } = $props();\n</script>\n<ExpenseForm\n action=\"?/save\"\n title=\"Record purchase\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n message={form?.message}\n canEdit={data.mayEdit}\n correcting={data.correcting}\n vendors={data.authorizedVendors}\n commitments={data.authorizedCommitments}\n hiddenFields={[\n { name: 'requestId', value: form?.requestId ?? data.requestId },\n { name: 'costObjectId', value: data.costObjectId },\n { name: 'expectedTenantId', value: data.tenantId },\n ]}\n/>\n<ExpenseReviewPanel\n expense={data.expenseSummary}\n duplicates={data.authorizedDuplicateCandidates}\n history={data.history}\n receipts={data.receiptPanel}\n/>\n```\n\nVendor and commitment options are authorized Commerce `{id, label}` projections.\nResolve Profile names and application-specific award lineage in the consumer.\nUnknown retained references remain selectable/postable; selecting a reference\nnever establishes tenant ownership or commitment eligibility. `fields` maps\nlogical keys to native names; `errors` always uses logical keys. Submitter\n`intentField`/`intent` default to `intent=save`. `hiddenFields` preserves exact\nvalues and repeated names. Avoid field-name collisions. `children` adds\napplication-owned package allocation, quantity, reimbursement and correction\npolicy fields. `correcting` shows a retained reason, but does not implement a\nnew Expense correction model. The caller applies its own correction/history\npolicy and invokes existing guarded methods where appropriate.\n\nThe base form performs native POST by default, with or without JavaScript.\n`onsubmit` allows optional enhancement. `pending` or a supplied\n`smrt-ui/form-retry` `retryStatus` controls progress presentation without clearing\nvalues or creating/rotating request keys. `canEdit=false` disables fields and\nhides save; server-side authorization and replay enforcement remain mandatory.\n\nCurrency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n\ncontext. It preserves the exact retained draft string and the field name from\n`fields.currency`, including native no-JavaScript correction flows. Pass\n`currencyOptions` only when the caller must restrict the shared currency catalog.\n\n`ExpenseReviewSummary` projects public Expense description, integer minor-unit\namount, currency and incurredOn, with caller-localized `statusLabel` and `note`.\n`duplicates` and `history` are explicit authorized presentation DTOs, not a\nparallel ledger. The server supplies findings from ExpenseReceipt duplicate\nqueries and application history. Unknown labels render verbatim. No state or\naction is inferred: `canReview`, `action`, `actions` and hidden fingerprints\nexplicitly provide a native review form. `action=\"\"` posts to the current page;\nomitting `action` hides the review form; `children` can supply retained reason\nor duplicate-selection inputs. A server must reauthorize, reload the model and\ninvoke `review()`, `reject()`, `markDuplicate()` or `reopen()` under its policy.\nNever assign review fields through UI payload binding. Reviewed money remains\nfrozen by the existing model; UI props cannot bypass it.\n\n`receipts` is exactly the public Assets `AttachmentPanelProps`, supporting\nprivate view/download URLs, version notes, native upload and recovery. It is\nrendered outside the review form so upload and review remain separate native\nactions. Use authorized receipt projections; attaching a file does not review\nan expense, mark a duplicate, or change invoice approval. Assets owns file\nselection and reselect-file help; Expenses does not duplicate uploads/storage.\n\nValidation: `pnpm --filter @happyvertical/smrt-expenses test`, `test:ui`,\n`test:e2e`, `typecheck`, `build`, and `verify:pack`. PostgreSQL model validation\nremains in the existing opt-in `test:postgres` lane; this UI does not alter it.\n\n`visibleFields` optionally limits the logical `ExpenseDraftValues` fields rendered by `ExpenseForm`. Omitted renders all existing fields; an empty array renders none. Excluded fields do not emit hidden fallbacks, including `correctionReason`; fixed values can be supplied explicitly through `hiddenFields` without duplicate names. This supports policy-fixed payer fields and domain-owned reference selectors. Visibility is presentation, never authority: endpoints must validate every fixed or submitted value. Unknown retained values and validation errors remain visible for included fields.\n\nComposition validation: native Svelte SSR contracts cover defaults, omitted fields, retained malformed values, caller identity, no duplicate fixed inputs, correction/read-only/pending behavior. No persistence, transaction, tenant authority or external provider changes occur; those matrix dimensions are N/A for this presentation-only extension. Node26/Svelte5 native POST markup is the supported runtime edge.\n\n## Cost-object lifecycle (#3462)\n\nThe existing #3423/#3429 entry and review controls are extended with\n`ExpenseList`, `ExpenseReceiptCapture` and `ExpenseReviewQueue` in the same\n`./svelte` export. Their registry slots are `expense-list`,\n`expense-receipt-capture` and `expense-review-queue`; named Props and row types\nare public. The playground composes these with the existing controls.\n\n`ExpenseForm` defaults to an optional touch keypad beside amount entry. It is\navailable after hydration, with ordinary text/keyboard entry and native POST\navailable without JavaScript. `amountKeypad=false` hides it. Digits, decimal and\nbackspace edit the exact currency-unit string at its selection; no parsing or\nrounding occurs. The server validates currency precision and safe minor units.\n`costObject={{ type, id }}` enables two editable native reference inputs, with\n`costObjectFields` name adapters and `costObjectErrors` retained server errors.\nOmit it when references are fixed in `hiddenFields` (the existing consumer\ncontract). Do not supply the same reference both ways. Qualified type and bare\nid are caller strings and must be validated/authorized by the endpoint.\n\n`ExpenseList` takes caller-authorized `ExpenseListItem[]` plus exact\n`costObjectType`/`costObjectId`; both keys must match. It displays each amount\nin its own currency, never sums different currencies, and retains unknown\n`statusLabel`, optional `note`, authorized `href`, empty and load-error states.\nFiltering is presentation only: do not pass unauthorized rows to the browser.\n\n`ExpenseReceiptCapture` is a separate multipart POST action, with\n`canAttach=false` by default. `fileField` defaults to `receipt`, submitter to\n`intent=attach-receipt`. It forwards `hiddenFields` exactly, including the\nexpense and caller-owned request tokens. A native file picker works without\nJavaScript; “Use camera” mounts the existing smrt-ui `CameraCapture` only after\nan explicit click. Capture/retake/commit and stream cleanup are owned by that\nshared component. Toggle back to file selection to recover from camera denial\nor unsupported APIs. Switching modes discards the previous file; selection is\nnever uploaded automatically. Pending prevents another upload or mode change;\na committed camera photo remains held for retry. Navigation cannot restore\nfile inputs: select the file again after a failed upload or page reload. The\nserver authenticates, validates file content/limits and expense visibility,\nstores the Asset and uses `ExpenseReceiptCollection.attachReceipt()` with its\nduplicate guard; this component performs no storage, deduplication or model\nmutation. Do not place this component inside another form.\n\n`ExpenseReviewQueue` takes authorized `ExpenseReviewQueueItem[]` in caller order.\nDecisions require both queue `canReview=true`, row `canReview=true`, and a row\n`action` (empty string explicitly means this page). Each row posts its own\n`expenseId`, retained `reason`, hidden request context and clicked\n`intent=approve|reject`. Field and intent names are configurable. Rejection\nrequires a reason in native browser validation; approval bypasses that browser\nrequirement. The server must enforce the reason and reauthorize/reload before\ncalling `review()` or `reject()`; these UI gates provide no authority. Return\nrow `reason`, `message` and original request tokens after rejection or an\nuncertain outcome. Pending disables decisions. No action is inferred from\nstatus, no submitted row is removed optimistically, and no request key rotates.\nAvoid collisions between hidden field names and component identity/reason/intent\nfields. Empty, read-only, unknown status and load-error states remain visible.\n\nLifecycle evidence is in the maintained `test:ui` SSR contracts and `test:e2e`\nChromium proofs: 390px keypad, exact cost-object payload, native multipart file\nselection with and without JavaScript, camera denial recovery, and native queue\napprove/reject with retained reason and row request identity. Persistence,\ntransaction affinity and tenant authorization remain existing server contracts;\nno database/model surface is changed by these presentation components.\n"
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
"path": "agents/allocation-test-matrix.md",
|
|
1877
|
+
"module": "allocation-test-matrix",
|
|
1878
|
+
"content": "# Reviewed expense allocations (#4746)\n\nRisk: **high** — public financial contract, tenant authority, concurrent monetary\nreservations and issued invoice lineage. The integration coordinator owns full\nrelease validation and independent review; worker evidence is narrow only.\n\nAll rows exercise real persistence. Commands: `pnpm --filter\n@happyvertical/smrt-expenses exec vitest run allocation` (SQLite), then the\npackage PostgreSQL lane at integration. DuckDB is not a declared expense release\nlane; its status must be explicitly assessed before release. No provider writes.\nNew feature: base-fail comparison is N/A (no allocation API exists on base).\n\n| Behavior / reachable trigger | Positive | Negative / failure | Actor/context | Executor / transaction | Runtime/dialect | Contract edge / level |\n| --- | --- | --- | --- | --- | --- | --- |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n\nUI/browser N/A: no new user interface. External provider failure N/A: allocation\nrecords local evidence and performs no external execution.\n\n## Invoice binding extension (captured before its implementation)\n\nCommerce owns only a nullable guarded `InvoiceLineItem.sourceReservationId`\npointer. The expense reservation remains the single capacity authority. A host\nmay reserve against its own draft, then bind the same reservation UUID to one\ncommerce line. Trusted package registration allowlists the `billable:` qualified\nsource type; client strings never dispatch arbitrary model methods.\n\n| Behavior / trigger | Positive | Negative / failure | Actor/context | Executor / transaction | Runtime/dialect | Contract edge / level |\n| --- | --- | --- | --- | --- | --- | --- |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n\nInitial SQLite evidence: allocation-fourth.log (6/6); extended billing first run\nallocation-billing-first.log (8/8). These are worker evidence, not release gates.\n\n## Dialect assessment evidence\n\nExisting package AGENTS validation defines SQLite and migrated PostgreSQL lanes;\nits supplier-payable matrix explicitly uses Node26 SQLite+PG. No explicit\nDuckDB compatibility promise was found in the owning README/AGENTS. A real\nDuckDB graph bootstrap was attempted: `allocation-dialects.log` records failure\nbefore allocation behavior on the existing\n`_smrt_billing_accounts.customer_id ON UPDATE CASCADE` relationship. Core\n`schema/foreign-key-planner.ts` deliberately refuses that unsupported DuckDB\ncontract and directs callers to PostgreSQL/SQLite. This diagnostic is preserved;\nno FK/schema weakening or alternate toolchain was used. It does not constitute\nDuckDB execution evidence for allocations, and release review must reconcile the\nsupported-dialect contract rather than claim universal compatibility.\n\n| Additional bounded contract | Positive | Negative/failure | Context/actor | Executor | Dialect/runtime | Validation |\n| --- | --- | --- | --- | --- | --- | --- |\n| 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 |\n| 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 |\n| 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 |\n\n## Explicit unbilled withdrawal (before implementation)\n\nHigh-risk financial/tenancy/contract continuation. Withdraw is not a billed\nreversal and never releases claims implicitly. Public input includes exact source\nrevision, expected active allocation revision, request key and audit evidence.\n\n| Behavior / trigger | Positive | Negative/failure | Actor/context | Executor/atomicity | Dialect | Evidence |\n| --- | --- | --- | --- | --- | --- | --- |\n| 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 |\n| 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 |\n| 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 |\n| 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 |\n\nNew API absent on prior source is feature baseline N/A for existing API\nregression, but executable absent-command/blocked-correction evidence is captured.\nNo provider writes, browser/UI N/A; root owns PG/fullvalidation/review.\n"
|
|
1879
|
+
}
|
|
1880
|
+
],
|
|
1881
|
+
"recipes": [
|
|
1882
|
+
{
|
|
1883
|
+
"id": "expenses.tracking",
|
|
1884
|
+
"className": "ExpensesRecipe",
|
|
1885
|
+
"label": "Expenses",
|
|
1886
|
+
"summary": "Record what the business spends, with receipts, and review it.",
|
|
1887
|
+
"synonyms": [
|
|
1888
|
+
"costs",
|
|
1889
|
+
"receipts",
|
|
1890
|
+
"spending",
|
|
1891
|
+
"reimbursements"
|
|
1892
|
+
],
|
|
1893
|
+
"models": [
|
|
1894
|
+
"@happyvertical/smrt-expenses:Expense",
|
|
1895
|
+
"@happyvertical/smrt-expenses:ExpenseReceipt"
|
|
1896
|
+
],
|
|
1897
|
+
"nav": [
|
|
1898
|
+
{
|
|
1899
|
+
"label": "Expenses",
|
|
1265
1900
|
"model": "@happyvertical/smrt-expenses:Expense",
|
|
1266
1901
|
"icon": "receipt",
|
|
1267
1902
|
"description": "Costs you have paid, with the receipt and who paid."
|
|
@@ -1322,6 +1957,9 @@
|
|
|
1322
1957
|
"costObjectType": {
|
|
1323
1958
|
"visibility": "hidden"
|
|
1324
1959
|
},
|
|
1960
|
+
"activeAllocationRevisionId": {
|
|
1961
|
+
"visibility": "hidden"
|
|
1962
|
+
},
|
|
1325
1963
|
"financialBreakdown": {
|
|
1326
1964
|
"visibility": "hidden"
|
|
1327
1965
|
},
|