@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.256.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.256.0",
134
- "@cosmicdrift/kumiko-framework": "0.256.0",
135
- "@cosmicdrift/kumiko-headless": "0.256.0",
136
- "@cosmicdrift/kumiko-renderer": "0.256.0",
137
- "@cosmicdrift/kumiko-renderer-web": "0.256.0",
138
- "@cosmicdrift/kumiko-types": "0.256.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.256.0",
168
- "@cosmicdrift/kumiko-locale-es": "0.256.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: { date?: string; description?: string } = {},
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(user = admin): Promise<Array<Record<string, unknown>>> {
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: { amount?: number; description?: string; startDate?: string } = {},
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
  });
@@ -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,
@@ -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",