@cosmicdrift/kumiko-bundled-features 0.256.0 → 0.257.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.257.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -130,12 +130,12 @@
|
|
|
130
130
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
131
131
|
},
|
|
132
132
|
"dependencies": {
|
|
133
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
135
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
136
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
133
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.257.0",
|
|
134
|
+
"@cosmicdrift/kumiko-framework": "0.257.0",
|
|
135
|
+
"@cosmicdrift/kumiko-headless": "0.257.0",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer": "0.257.0",
|
|
137
|
+
"@cosmicdrift/kumiko-renderer-web": "0.257.0",
|
|
138
|
+
"@cosmicdrift/kumiko-types": "0.257.0",
|
|
139
139
|
"@mollie/api-client": "^4.5.0",
|
|
140
140
|
"@node-rs/argon2": "^2.0.2",
|
|
141
141
|
"@types/mailparser": "^3.4.6",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@testing-library/user-event": "^14.6.1",
|
|
166
166
|
"@types/qrcode": "^1.5.5",
|
|
167
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
168
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
167
|
+
"@cosmicdrift/kumiko-locale-de": "0.257.0",
|
|
168
|
+
"@cosmicdrift/kumiko-locale-es": "0.257.0"
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -64,7 +64,12 @@ async function createAccount(name: string, type: AccountType, user = admin): Pro
|
|
|
64
64
|
|
|
65
65
|
async function createTransaction(
|
|
66
66
|
lines: readonly Posting[],
|
|
67
|
-
opts: {
|
|
67
|
+
opts: {
|
|
68
|
+
date?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
subjectType?: string;
|
|
71
|
+
subjectId?: string;
|
|
72
|
+
} = {},
|
|
68
73
|
user = admin,
|
|
69
74
|
): Promise<{ id: string }> {
|
|
70
75
|
return stack.http.writeOk<{ id: string }>(
|
|
@@ -73,15 +78,20 @@ async function createTransaction(
|
|
|
73
78
|
date: opts.date ?? "2026-01-15",
|
|
74
79
|
description: opts.description ?? "Test entry",
|
|
75
80
|
lines,
|
|
81
|
+
...(opts.subjectType !== undefined && { subjectType: opts.subjectType }),
|
|
82
|
+
...(opts.subjectId !== undefined && { subjectId: opts.subjectId }),
|
|
76
83
|
},
|
|
77
84
|
user,
|
|
78
85
|
);
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
async function listTransactions(
|
|
88
|
+
async function listTransactions(
|
|
89
|
+
user = admin,
|
|
90
|
+
query: Record<string, unknown> = {},
|
|
91
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
82
92
|
const res = await stack.http.queryOk<{ rows: Array<Record<string, unknown>> }>(
|
|
83
93
|
LedgerQueries.transactionList,
|
|
84
|
-
|
|
94
|
+
query,
|
|
85
95
|
user,
|
|
86
96
|
);
|
|
87
97
|
return res.rows;
|
|
@@ -350,7 +360,13 @@ describe("ledger integration — confirm-schedule-period (recurring)", () => {
|
|
|
350
360
|
async function createSchedule(
|
|
351
361
|
debitAccountId: string,
|
|
352
362
|
creditAccountId: string,
|
|
353
|
-
over: {
|
|
363
|
+
over: {
|
|
364
|
+
amount?: number;
|
|
365
|
+
description?: string;
|
|
366
|
+
startDate?: string;
|
|
367
|
+
subjectType?: string;
|
|
368
|
+
subjectId?: string;
|
|
369
|
+
} = {},
|
|
354
370
|
): Promise<string> {
|
|
355
371
|
const s = await stack.http.writeOk<{ id: string }>(
|
|
356
372
|
LedgerHandlers.createSchedule,
|
|
@@ -361,6 +377,8 @@ describe("ledger integration — confirm-schedule-period (recurring)", () => {
|
|
|
361
377
|
amount: over.amount ?? 50000,
|
|
362
378
|
debitAccountId,
|
|
363
379
|
creditAccountId,
|
|
380
|
+
...(over.subjectType !== undefined && { subjectType: over.subjectType }),
|
|
381
|
+
...(over.subjectId !== undefined && { subjectId: over.subjectId }),
|
|
364
382
|
},
|
|
365
383
|
admin,
|
|
366
384
|
);
|
|
@@ -457,4 +475,93 @@ describe("ledger integration — confirm-schedule-period (recurring)", () => {
|
|
|
457
475
|
// Books net to zero (original cancels Storno) plus the fresh 48000 confirmation.
|
|
458
476
|
expect(trialBalance(rows)).toBe(0);
|
|
459
477
|
});
|
|
478
|
+
|
|
479
|
+
test("confirming a period on a schedule with a subject stamps the same subject onto the transaction", async () => {
|
|
480
|
+
const bank = await createAccount("Bank", "asset");
|
|
481
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
482
|
+
const scheduleId = await createSchedule(bank, rent, {
|
|
483
|
+
subjectType: "lease",
|
|
484
|
+
subjectId: "lease-1",
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
await confirm(scheduleId, "2026-01");
|
|
488
|
+
|
|
489
|
+
const rows = await listTransactions();
|
|
490
|
+
expect(rows[0]?.["subjectType"]).toBe("lease");
|
|
491
|
+
expect(rows[0]?.["subjectId"]).toBe("lease-1");
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("confirming a period on a schedule without a subject leaves the transaction without one", async () => {
|
|
495
|
+
const bank = await createAccount("Bank", "asset");
|
|
496
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
497
|
+
const scheduleId = await createSchedule(bank, rent);
|
|
498
|
+
|
|
499
|
+
await confirm(scheduleId, "2026-01");
|
|
500
|
+
|
|
501
|
+
const rows = await listTransactions();
|
|
502
|
+
expect(rows[0]?.["subjectType"]).toBeNull();
|
|
503
|
+
expect(rows[0]?.["subjectId"]).toBeNull();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe("ledger integration — subject dimension (filterable business-object reference)", () => {
|
|
508
|
+
test("a booking with subjectType/subjectId round-trips and is findable via an eq filter", async () => {
|
|
509
|
+
const bank = await createAccount("Bank", "asset");
|
|
510
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
511
|
+
|
|
512
|
+
const tx = await createTransaction(
|
|
513
|
+
[
|
|
514
|
+
{ accountId: bank, amount: 100000 },
|
|
515
|
+
{ accountId: rent, amount: -100000 },
|
|
516
|
+
],
|
|
517
|
+
{ description: "Miete WE1", subjectType: "lease", subjectId: "lease-1" },
|
|
518
|
+
);
|
|
519
|
+
await createTransaction(
|
|
520
|
+
[
|
|
521
|
+
{ accountId: bank, amount: 60000 },
|
|
522
|
+
{ accountId: rent, amount: -60000 },
|
|
523
|
+
],
|
|
524
|
+
{ description: "Miete WE2", subjectType: "lease", subjectId: "lease-2" },
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const rows = await listTransactions(admin, {
|
|
528
|
+
filter: { field: "subjectId", op: "eq", value: "lease-1" },
|
|
529
|
+
});
|
|
530
|
+
expect(rows).toHaveLength(1);
|
|
531
|
+
expect(rows[0]?.["id"]).toBe(tx.id);
|
|
532
|
+
expect(rows[0]?.["subjectType"]).toBe("lease");
|
|
533
|
+
expect(rows[0]?.["subjectId"]).toBe("lease-1");
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("an in-filter over multiple subjectId values returns exactly the matching bookings", async () => {
|
|
537
|
+
const bank = await createAccount("Bank", "asset");
|
|
538
|
+
const rent = await createAccount("Mieterträge", "income");
|
|
539
|
+
|
|
540
|
+
await createTransaction(
|
|
541
|
+
[
|
|
542
|
+
{ accountId: bank, amount: 10000 },
|
|
543
|
+
{ accountId: rent, amount: -10000 },
|
|
544
|
+
],
|
|
545
|
+
{ subjectType: "lease", subjectId: "lease-1" },
|
|
546
|
+
);
|
|
547
|
+
await createTransaction(
|
|
548
|
+
[
|
|
549
|
+
{ accountId: bank, amount: 20000 },
|
|
550
|
+
{ accountId: rent, amount: -20000 },
|
|
551
|
+
],
|
|
552
|
+
{ subjectType: "lease", subjectId: "lease-2" },
|
|
553
|
+
);
|
|
554
|
+
await createTransaction(
|
|
555
|
+
[
|
|
556
|
+
{ accountId: bank, amount: 30000 },
|
|
557
|
+
{ accountId: rent, amount: -30000 },
|
|
558
|
+
],
|
|
559
|
+
{ subjectType: "lease", subjectId: "lease-3" },
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const rows = await listTransactions(admin, {
|
|
563
|
+
filters: [{ field: "subjectId", op: "in", value: ["lease-1", "lease-3"] }],
|
|
564
|
+
});
|
|
565
|
+
expect(rows.map((r) => r["subjectId"]).sort()).toEqual(["lease-1", "lease-3"]);
|
|
566
|
+
});
|
|
460
567
|
});
|
package/src/ledger/entity.ts
CHANGED
|
@@ -78,7 +78,22 @@ export const transactionEntity = createEntity({
|
|
|
78
78
|
},
|
|
79
79
|
{ required: true },
|
|
80
80
|
),
|
|
81
|
+
// Optional business-object reference (e.g. a lease contract) so entries can
|
|
82
|
+
// be filtered by what they're about, not just grepped out of `reference`.
|
|
83
|
+
subjectType: createTextField({
|
|
84
|
+
maxLength: 64,
|
|
85
|
+
personal: false,
|
|
86
|
+
reason: "technical_reference",
|
|
87
|
+
filterable: true,
|
|
88
|
+
}),
|
|
89
|
+
subjectId: createTextField({
|
|
90
|
+
maxLength: 128,
|
|
91
|
+
personal: false,
|
|
92
|
+
reason: "technical_reference",
|
|
93
|
+
filterable: true,
|
|
94
|
+
}),
|
|
81
95
|
},
|
|
96
|
+
indexes: [{ columns: ["tenantId", "subjectType", "subjectId"] }],
|
|
82
97
|
});
|
|
83
98
|
|
|
84
99
|
// schedule — a recurring booking template (Dauerauftrag): "book `amount` from
|
|
@@ -116,5 +131,20 @@ export const scheduleEntity = createEntity({
|
|
|
116
131
|
personal: false,
|
|
117
132
|
reason: "technical_reference",
|
|
118
133
|
}),
|
|
134
|
+
// Confirmed periods inherit this from the schedule (see
|
|
135
|
+
// confirm-schedule-period.write.ts) rather than repeating it per period.
|
|
136
|
+
subjectType: createTextField({
|
|
137
|
+
maxLength: 64,
|
|
138
|
+
personal: false,
|
|
139
|
+
reason: "technical_reference",
|
|
140
|
+
filterable: true,
|
|
141
|
+
}),
|
|
142
|
+
subjectId: createTextField({
|
|
143
|
+
maxLength: 128,
|
|
144
|
+
personal: false,
|
|
145
|
+
reason: "technical_reference",
|
|
146
|
+
filterable: true,
|
|
147
|
+
}),
|
|
119
148
|
},
|
|
149
|
+
indexes: [{ columns: ["tenantId", "subjectType", "subjectId"] }],
|
|
120
150
|
});
|
|
@@ -96,6 +96,11 @@ export function createConfirmSchedulePeriodHandler(
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
const amount = payload.amount ?? Number(schedule["amount"]);
|
|
99
|
+
// The subject is a property of the schedule, not the confirm call — a
|
|
100
|
+
// period without one stays without one, no backfill/error.
|
|
101
|
+
const subjectType =
|
|
102
|
+
typeof schedule["subjectType"] === "string" ? schedule["subjectType"] : null;
|
|
103
|
+
const subjectId = typeof schedule["subjectId"] === "string" ? schedule["subjectId"] : null;
|
|
99
104
|
const created = await transactionExecutor.create(
|
|
100
105
|
{
|
|
101
106
|
id: generateId(),
|
|
@@ -107,6 +112,8 @@ export function createConfirmSchedulePeriodHandler(
|
|
|
107
112
|
{ accountId: debitAccountId, amount },
|
|
108
113
|
{ accountId: creditAccountId, amount: -amount },
|
|
109
114
|
],
|
|
115
|
+
subjectType,
|
|
116
|
+
subjectId,
|
|
110
117
|
},
|
|
111
118
|
event.user,
|
|
112
119
|
ctx.db,
|
|
@@ -35,6 +35,8 @@ export function createCreateTransactionHandler(
|
|
|
35
35
|
reference: payload.reference ?? null,
|
|
36
36
|
status: payload.status ?? "posted",
|
|
37
37
|
lines: payload.lines,
|
|
38
|
+
subjectType: payload.subjectType ?? null,
|
|
39
|
+
subjectId: payload.subjectId ?? null,
|
|
38
40
|
},
|
|
39
41
|
event.user,
|
|
40
42
|
ctx.db,
|
package/src/ledger/schemas.ts
CHANGED
|
@@ -27,6 +27,9 @@ export const createTransactionPayloadSchema = z
|
|
|
27
27
|
// the Soll/recurring work — Phase 0 only ever creates posted entries.
|
|
28
28
|
status: z.enum(TRANSACTION_STATUS).optional(),
|
|
29
29
|
lines: z.array(postingSchema).min(2),
|
|
30
|
+
// Business-object reference (e.g. a lease contract) this entry is about.
|
|
31
|
+
subjectType: z.string().max(64).optional(),
|
|
32
|
+
subjectId: z.string().max(128).optional(),
|
|
30
33
|
})
|
|
31
34
|
.refine((p) => sumIsZero(p.lines), {
|
|
32
35
|
message: "Transaction must balance: Σ of posting amounts must equal 0",
|