cabloy 5.1.112 → 5.1.113

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.
Files changed (41) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +2 -0
  3. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +9 -0
  4. package/.github/workflows/playwright-e2e.yml +34 -2
  5. package/.github/workflows/vona-test-pg.yml +8 -0
  6. package/CHANGELOG.md +13 -0
  7. package/CLAUDE.md +2 -0
  8. package/cabloy-docs/backend/migration-and-changes.md +13 -9
  9. package/cabloy-docs/backend/unit-testing.md +25 -0
  10. package/e2e/scripts/startE2eVona.ts +51 -13
  11. package/e2e/specs/a-commerce/commerce.spec.ts +1 -0
  12. package/e2e/specs/cabloy-basic/basic.spec.ts +12 -7
  13. package/package.json +1 -1
  14. package/vona/packages-cli/cli/package.json +1 -1
  15. package/vona/packages-cli/cli-set-api/package.json +1 -1
  16. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +6 -9
  17. package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +50 -21
  18. package/vona/packages-vona/vona/package.json +1 -1
  19. package/vona/pnpm-lock.yaml +67 -67
  20. package/vona/src/backend/config/config/config.test.ts +1 -1
  21. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +234 -0
  22. package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +3 -3
  23. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +4 -0
  24. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +4 -0
  25. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +4 -0
  26. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockAudit.tsx +39 -7
  27. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +38 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +268 -65
  29. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +276 -81
  30. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  31. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +2 -2
  32. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  33. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/version.ts +12 -12
  34. package/vona/src/suite-vendor/a-vona/modules/a-version/src/types/version.ts +4 -4
  35. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  36. package/zova/pnpm-lock.yaml +14 -14
  37. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +0 -1
  38. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +1 -0
  39. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +17 -7
  40. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.header.tsx +1 -1
  41. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +17 -7
@@ -5,32 +5,70 @@ import assert from 'node:assert';
5
5
  import { describe, it } from 'node:test';
6
6
  import { app } from 'vona-mock';
7
7
 
8
- async function createSku(suffix: string): Promise<number> {
9
- const categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
8
+ interface IStockFixture {
9
+ categoryId: number;
10
+ productId: number;
11
+ skuId: number;
12
+ }
13
+
14
+ type IStockFixturePartial = Partial<IStockFixture>;
15
+
16
+ async function createSku(suffix: string, fixtures: IStockFixturePartial[]): Promise<IStockFixture> {
17
+ const fixture: IStockFixturePartial = {};
18
+ fixtures.push(fixture);
19
+ fixture.categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
10
20
  body: { name: `reservation-category-${suffix}`, published: true },
11
21
  });
12
- const productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
13
- body: { categoryId, title: `reservation-product-${suffix}`, published: true },
22
+ fixture.productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
23
+ body: {
24
+ categoryId: fixture.categoryId,
25
+ title: `reservation-product-${suffix}`,
26
+ published: true,
27
+ },
14
28
  });
15
- return await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
29
+ fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
16
30
  body: {
17
- productId,
31
+ productId: fixture.productId,
18
32
  code: `reservation-sku-${suffix}`,
19
33
  priceCents: 100,
20
34
  lifecycle: 'active',
21
35
  },
22
36
  });
37
+ return fixture as IStockFixture;
23
38
  }
24
39
 
25
- async function prepareStock(suffix: string, quantity = 3): Promise<number> {
26
- const skuId = await createSku(suffix);
40
+ async function prepareStock(
41
+ suffix: string,
42
+ fixtures: IStockFixturePartial[],
43
+ quantity = 3,
44
+ ): Promise<IStockFixture> {
45
+ const fixture = await createSku(suffix, fixtures);
27
46
  await app.scope('commerce-trade').service.stockBalance.adjustStock({
28
- skuId,
47
+ skuId: fixture.skuId,
29
48
  delta: quantity,
30
49
  reason: 'reservation test setup',
31
50
  correlationId: `reservation-setup-${suffix}`,
32
51
  });
33
- return skuId;
52
+ return fixture;
53
+ }
54
+
55
+ async function dropStockFixtures(fixtures: IStockFixturePartial[]) {
56
+ const scopeTrade = app.scope('commerce-trade');
57
+ const scopeCatalog = app.scope('commerce-catalog');
58
+ for (const fixture of fixtures.toReversed()) {
59
+ if (fixture.skuId !== undefined) {
60
+ await scopeTrade.model.stockAudit.delete({ skuId: fixture.skuId });
61
+ await scopeTrade.model.stockReservation.delete({ skuId: fixture.skuId });
62
+ await scopeTrade.model.stockBalance.delete({ skuId: fixture.skuId });
63
+ await scopeCatalog.model.sku.delete({ id: fixture.skuId });
64
+ }
65
+ if (fixture.productId !== undefined) {
66
+ await scopeCatalog.model.product.delete({ id: fixture.productId });
67
+ }
68
+ if (fixture.categoryId !== undefined) {
69
+ await scopeCatalog.model.category.delete({ id: fixture.categoryId });
70
+ }
71
+ }
34
72
  }
35
73
 
36
74
  async function reserve(
@@ -62,14 +100,15 @@ async function assertReservationRejected(skuId: number, suffix: string, expected
62
100
  describe('stockReservation.test.ts', () => {
63
101
  it('reserves and consumes stock exactly once with traceable audits', async () => {
64
102
  await app.bean.executor.mockCtx(async () => {
65
- const suffix = `${Date.now()}`;
103
+ const fixtures: IStockFixturePartial[] = [];
66
104
  await app.bean.passport.signinMock();
67
105
  try {
68
- const skuId = await prepareStock(suffix);
69
- const reservation = await reserve(skuId, suffix);
106
+ const suffix = `${Date.now()}`;
107
+ const fixture = await prepareStock(suffix, fixtures);
108
+ const reservation = await reserve(fixture.skuId, suffix);
70
109
  const balanceAfterReserve = await app
71
110
  .scope('commerce-trade')
72
- .model.stockBalance.get({ skuId });
111
+ .model.stockBalance.get({ skuId: fixture.skuId });
73
112
  assert.deepEqual(
74
113
  [
75
114
  balanceAfterReserve?.onHand,
@@ -79,7 +118,7 @@ describe('stockReservation.test.ts', () => {
79
118
  [3, 2, 1],
80
119
  );
81
120
 
82
- const duplicate = await reserve(skuId, suffix);
121
+ const duplicate = await reserve(fixture.skuId, suffix);
83
122
  assert.equal(duplicate.id, reservation.id);
84
123
  const consumed = await app.scope('commerce-trade').service.stockBalance.consume({
85
124
  reservationId: reservation.id,
@@ -94,7 +133,7 @@ describe('stockReservation.test.ts', () => {
94
133
 
95
134
  const balanceAfterConsume = await app
96
135
  .scope('commerce-trade')
97
- .model.stockBalance.get({ skuId });
136
+ .model.stockBalance.get({ skuId: fixture.skuId });
98
137
  assert.deepEqual(
99
138
  [
100
139
  balanceAfterConsume?.onHand,
@@ -103,15 +142,16 @@ describe('stockReservation.test.ts', () => {
103
142
  ],
104
143
  [1, 0, 1],
105
144
  );
106
- const audits = await app
107
- .scope('commerce-trade')
108
- .model.stockAudit.select({ where: { skuId } });
145
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
146
+ where: { skuId: fixture.skuId },
147
+ });
109
148
  assert.deepEqual(
110
149
  audits.map(audit => audit.operation),
111
150
  ['adjust', 'reserve', 'consume'],
112
151
  );
113
152
  assert.equal(audits[1].stockReservationId, reservation.id);
114
153
  } finally {
154
+ await dropStockFixtures(fixtures);
115
155
  await app.bean.passport.signout();
116
156
  }
117
157
  });
@@ -119,36 +159,29 @@ describe('stockReservation.test.ts', () => {
119
159
 
120
160
  it('rejects reservations for inactive or unpublished catalog data without writes', async () => {
121
161
  await app.bean.executor.mockCtx(async () => {
122
- const suffix = `${Date.now()}`;
162
+ const fixtures: IStockFixturePartial[] = [];
123
163
  await app.bean.passport.signinMock();
124
164
  try {
125
- const inactiveSkuId = await prepareStock(`${suffix}-inactive`);
126
- await app.scope('commerce-catalog').model.sku.updateById(inactiveSkuId, {
165
+ const suffix = `${Date.now()}`;
166
+ const inactiveSku = await prepareStock(`${suffix}-inactive`, fixtures);
167
+ await app.scope('commerce-catalog').model.sku.updateById(inactiveSku.skuId, {
127
168
  lifecycle: 'inactive',
128
169
  });
129
- await assertReservationRejected(inactiveSkuId, `${suffix}-inactive`, 404);
170
+ await assertReservationRejected(inactiveSku.skuId, `${suffix}-inactive`, 404);
130
171
 
131
- const unpublishedProductSkuId = await prepareStock(`${suffix}-product`);
132
- const unpublishedProductSku = await app
133
- .scope('commerce-catalog')
134
- .model.sku.getById(unpublishedProductSkuId);
172
+ const unpublishedProductSku = await prepareStock(`${suffix}-product`, fixtures);
135
173
  await app
136
174
  .scope('commerce-catalog')
137
- .model.product.updateById(unpublishedProductSku!.productId, { published: false });
138
- await assertReservationRejected(unpublishedProductSkuId, `${suffix}-product`, 409);
175
+ .model.product.updateById(unpublishedProductSku.productId, { published: false });
176
+ await assertReservationRejected(unpublishedProductSku.skuId, `${suffix}-product`, 409);
139
177
 
140
- const unpublishedCategorySkuId = await prepareStock(`${suffix}-category`);
141
- const unpublishedCategorySku = await app
142
- .scope('commerce-catalog')
143
- .model.sku.getById(unpublishedCategorySkuId);
144
- const unpublishedCategoryProduct = await app
145
- .scope('commerce-catalog')
146
- .model.product.getById(unpublishedCategorySku!.productId);
178
+ const unpublishedCategorySku = await prepareStock(`${suffix}-category`, fixtures);
147
179
  await app
148
180
  .scope('commerce-catalog')
149
- .model.category.updateById(unpublishedCategoryProduct!.categoryId, { published: false });
150
- await assertReservationRejected(unpublishedCategorySkuId, `${suffix}-category`, 409);
181
+ .model.category.updateById(unpublishedCategorySku.categoryId, { published: false });
182
+ await assertReservationRejected(unpublishedCategorySku.skuId, `${suffix}-category`, 409);
151
183
  } finally {
184
+ await dropStockFixtures(fixtures);
152
185
  await app.bean.passport.signout();
153
186
  }
154
187
  });
@@ -156,11 +189,12 @@ describe('stockReservation.test.ts', () => {
156
189
 
157
190
  it('releases and restores stock only from legal reservation states', async () => {
158
191
  await app.bean.executor.mockCtx(async () => {
159
- const suffix = `${Date.now()}`;
192
+ const fixtures: IStockFixturePartial[] = [];
160
193
  await app.bean.passport.signinMock();
161
194
  try {
162
- const skuId = await prepareStock(suffix);
163
- const releasedReservation = await reserve(skuId, `${suffix}-release`);
195
+ const suffix = `${Date.now()}`;
196
+ const fixture = await prepareStock(suffix, fixtures);
197
+ const releasedReservation = await reserve(fixture.skuId, `${suffix}-release`);
164
198
  const released = await app.scope('commerce-trade').service.stockBalance.release({
165
199
  reservationId: releasedReservation.id,
166
200
  reason: 'payment failed',
@@ -172,7 +206,7 @@ describe('stockReservation.test.ts', () => {
172
206
  });
173
207
  assert.equal(duplicateRelease.state, 'released');
174
208
 
175
- const consumedReservation = await reserve(skuId, `${suffix}-restore`, 1);
209
+ const consumedReservation = await reserve(fixture.skuId, `${suffix}-restore`, 1);
176
210
  await app.scope('commerce-trade').service.stockBalance.consume({
177
211
  reservationId: consumedReservation.id,
178
212
  reason: 'payment success',
@@ -182,6 +216,11 @@ describe('stockReservation.test.ts', () => {
182
216
  reason: 'refund success',
183
217
  });
184
218
  assert.equal(restored.state, 'restored');
219
+ const duplicateRestore = await app.scope('commerce-trade').service.stockBalance.restore({
220
+ reservationId: consumedReservation.id,
221
+ reason: 'refund success retry',
222
+ });
223
+ assert.equal(duplicateRestore.state, 'restored');
185
224
 
186
225
  const [_, err] = await catchError(() =>
187
226
  app.scope('commerce-trade').service.stockBalance.consume({
@@ -190,71 +229,227 @@ describe('stockReservation.test.ts', () => {
190
229
  }),
191
230
  );
192
231
  assert.equal(err?.code, 409);
193
- const balance = await app.scope('commerce-trade').model.stockBalance.get({ skuId });
232
+ const balance = await app
233
+ .scope('commerce-trade')
234
+ .model.stockBalance.get({ skuId: fixture.skuId });
194
235
  assert.deepEqual([balance?.onHand, balance?.reserved, balance?.available], [3, 0, 3]);
236
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
237
+ where: { skuId: fixture.skuId },
238
+ });
239
+ assert.equal(audits.length, 6);
240
+ assert.deepEqual(
241
+ audits.map(audit => audit.operation),
242
+ ['adjust', 'reserve', 'release', 'reserve', 'consume', 'restore'],
243
+ );
244
+ assert.deepEqual(
245
+ audits.map(audit => audit.stockBalanceId),
246
+ audits.map(() => balance?.id),
247
+ );
248
+ assert.deepEqual(
249
+ audits.map(audit => audit.stockReservationId),
250
+ [
251
+ null,
252
+ releasedReservation.id,
253
+ releasedReservation.id,
254
+ consumedReservation.id,
255
+ consumedReservation.id,
256
+ consumedReservation.id,
257
+ ],
258
+ );
259
+ assert.deepEqual(
260
+ audits.map(audit => audit.correlationId),
261
+ [
262
+ `reservation-setup-${suffix}`,
263
+ `reservation-${suffix}-release`,
264
+ `reservation-${suffix}-release`,
265
+ `reservation-${suffix}-restore`,
266
+ `reservation-${suffix}-restore`,
267
+ `reservation-${suffix}-restore`,
268
+ ],
269
+ );
270
+ assert.deepEqual(
271
+ audits.map(audit => audit.reason),
272
+ [
273
+ 'reservation test setup',
274
+ 'reservation test',
275
+ 'payment failed',
276
+ 'reservation test',
277
+ 'payment success',
278
+ 'refund success',
279
+ ],
280
+ );
281
+ assert.deepEqual(
282
+ audits.map(audit => [
283
+ audit.delta,
284
+ audit.priorOnHand,
285
+ audit.priorReserved,
286
+ audit.priorAvailable,
287
+ audit.onHand,
288
+ audit.reserved,
289
+ audit.available,
290
+ ]),
291
+ [
292
+ [3, 0, 0, 0, 3, 0, 3],
293
+ [-2, 3, 0, 3, 3, 2, 1],
294
+ [0, 3, 2, 1, 3, 0, 3],
295
+ [-1, 3, 0, 3, 3, 1, 2],
296
+ [-1, 3, 1, 2, 2, 0, 2],
297
+ [1, 2, 0, 2, 3, 0, 3],
298
+ ],
299
+ );
195
300
  } finally {
301
+ await dropStockFixtures(fixtures);
196
302
  await app.bean.passport.signout();
197
303
  }
198
304
  });
199
305
  });
200
306
 
201
- it('allows exactly one competing reservation for the final unit', async () => {
202
- await app.bean.executor.mockCtx(async () => {
203
- const suffix = `${Date.now()}`;
204
- await app.bean.passport.signinMock();
205
- try {
206
- const skuId = await prepareStock(suffix, 1);
207
- const results = await Promise.allSettled([
208
- reserve(skuId, `${suffix}-first`, 1),
209
- reserve(skuId, `${suffix}-second`, 1),
210
- ]);
211
- assert.equal(results.filter(result => result.status === 'fulfilled').length, 1);
212
- assert.equal(results.filter(result => result.status === 'rejected').length, 1);
307
+ it('allows exactly one independent reservation for the final unit', async t => {
308
+ if (process.env.DATABASE_DEFAULT_CLIENT !== 'pg') {
309
+ t.skip('requires PostgreSQL row-lock contention');
310
+ return;
311
+ }
312
+ const fixtures: IStockFixturePartial[] = [];
313
+ const suffix = `${Date.now()}`;
314
+ let fixture!: IStockFixture;
315
+ try {
316
+ await app.bean.executor.mockCtx(async () => {
317
+ await app.bean.passport.signinMock();
318
+ try {
319
+ fixture = await prepareStock(suffix, fixtures, 1);
320
+ } finally {
321
+ await app.bean.passport.signout();
322
+ }
323
+ });
324
+
325
+ const reserveInContext = async (correlationSuffix: string) => {
326
+ return await app.bean.executor.mockCtx(async () => {
327
+ await app.bean.passport.signinMock();
328
+ try {
329
+ return await reserve(fixture.skuId, correlationSuffix, 1);
330
+ } finally {
331
+ await app.bean.passport.signout();
332
+ }
333
+ });
334
+ };
335
+ const results = await Promise.allSettled([
336
+ reserveInContext(`${suffix}-first`),
337
+ reserveInContext(`${suffix}-second`),
338
+ ]);
339
+ assert.equal(
340
+ results.filter(result => result.status === 'fulfilled').length,
341
+ 1,
342
+ JSON.stringify(results),
343
+ );
344
+ assert.equal(
345
+ results.filter(result => result.status === 'rejected').length,
346
+ 1,
347
+ JSON.stringify(results),
348
+ );
349
+ const rejected = results.find(result => result.status === 'rejected');
350
+ assert.equal(
351
+ (rejected as PromiseRejectedResult | undefined)?.reason?.code,
352
+ 409,
353
+ String((rejected as PromiseRejectedResult | undefined)?.reason),
354
+ );
213
355
 
214
- const balance = await app.scope('commerce-trade').model.stockBalance.get({ skuId });
356
+ await app.bean.executor.mockCtx(async () => {
357
+ const balance = await app
358
+ .scope('commerce-trade')
359
+ .model.stockBalance.get({ skuId: fixture.skuId });
215
360
  assert.deepEqual([balance?.onHand, balance?.reserved, balance?.available], [1, 1, 0]);
216
361
  const reservations = await app.scope('commerce-trade').model.stockReservation.select({
217
- where: { skuId },
362
+ where: { skuId: fixture.skuId },
363
+ });
364
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
365
+ where: { skuId: fixture.skuId },
218
366
  });
219
- const audits = await app
220
- .scope('commerce-trade')
221
- .model.stockAudit.select({ where: { skuId } });
222
367
  assert.equal(reservations.length, 1);
223
368
  assert.deepEqual(
224
369
  audits.map(audit => audit.operation),
225
370
  ['adjust', 'reserve'],
226
371
  );
227
- } finally {
228
- await app.bean.passport.signout();
229
- }
230
- });
372
+ assert.equal(audits.filter(audit => audit.operation === 'reserve').length, 1);
373
+ assert.equal(
374
+ audits.some(audit => audit.correlationId === `reservation-${suffix}-first`) ||
375
+ audits.some(audit => audit.correlationId === `reservation-${suffix}-second`),
376
+ true,
377
+ );
378
+ });
379
+ } finally {
380
+ await app.bean.executor.mockCtx(async () => {
381
+ await dropStockFixtures(fixtures);
382
+ });
383
+ }
231
384
  });
232
385
 
233
- it('rolls back reservation and balance when audit persistence fails', async () => {
386
+ it('rolls back reservation, balance, and audit writes in one transaction', async () => {
234
387
  await app.bean.executor.mockCtx(async () => {
235
- const suffix = `${Date.now()}`;
388
+ const fixtures: IStockFixturePartial[] = [];
236
389
  await app.bean.passport.signinMock();
237
390
  try {
238
- const skuId = await prepareStock(suffix);
239
- const modelStockAudit = app.scope('commerce-trade').model.stockAudit;
240
- const insert = modelStockAudit.insert.bind(modelStockAudit);
241
- (modelStockAudit as any).insert = async () => {
242
- throw new Error('reservation audit insert failure');
243
- };
244
- try {
245
- const [_, err] = await catchError(() => reserve(skuId, suffix));
246
- assert.match(err?.message ?? '', /reservation audit insert failure/);
247
- } finally {
248
- (modelStockAudit as any).insert = insert;
249
- }
391
+ const suffix = `${Date.now()}`;
392
+ const fixture = await prepareStock(suffix, fixtures);
393
+ const balance = await app.scope('commerce-trade').model.stockBalance.get({
394
+ skuId: fixture.skuId,
395
+ });
396
+ const db = app.ctx.db;
397
+ const [_, err] = await catchError(async () => {
398
+ await db.transaction.begin(async () => {
399
+ const modelStockBalance = app
400
+ .scope('commerce-trade')
401
+ .model.stockBalance.newInstance(db);
402
+ const modelStockReservation = app
403
+ .scope('commerce-trade')
404
+ .model.stockReservation.newInstance(db);
405
+ const modelStockAudit = app.scope('commerce-trade').model.stockAudit.newInstance(db);
406
+ const reservation = await modelStockReservation.insert({
407
+ stockBalanceId: balance!.id,
408
+ skuId: fixture.skuId,
409
+ quantity: 2,
410
+ state: 'reserved',
411
+ correlationId: `reservation-${suffix}-rollback`,
412
+ });
413
+ await modelStockBalance.updateById(balance!.id, {
414
+ onHand: 3,
415
+ reserved: 2,
416
+ available: 1,
417
+ });
418
+ await modelStockAudit.insert({
419
+ stockBalanceId: balance!.id,
420
+ skuId: fixture.skuId,
421
+ stockReservationId: reservation.id,
422
+ actorId: app.bean.passport.currentUser!.id,
423
+ operation: 'reserve',
424
+ delta: -2,
425
+ reason: 'transaction rollback proof',
426
+ correlationId: reservation.correlationId,
427
+ priorOnHand: 3,
428
+ priorReserved: 0,
429
+ priorAvailable: 3,
430
+ onHand: 3,
431
+ reserved: 2,
432
+ available: 1,
433
+ });
434
+ throw new Error('transaction rollback proof');
435
+ });
436
+ });
437
+ assert.match(err?.message ?? '', /transaction rollback proof/);
250
438
 
251
- const balance = await app.scope('commerce-trade').model.stockBalance.get({ skuId });
252
- assert.deepEqual([balance?.onHand, balance?.reserved, balance?.available], [3, 0, 3]);
439
+ const unchanged = await app
440
+ .scope('commerce-trade')
441
+ .model.stockBalance.get({ skuId: fixture.skuId });
442
+ assert.deepEqual([unchanged?.onHand, unchanged?.reserved, unchanged?.available], [3, 0, 3]);
253
443
  assert.equal(
254
- await app.scope('commerce-trade').model.stockReservation.get({ skuId }),
444
+ await app.scope('commerce-trade').model.stockReservation.get({ skuId: fixture.skuId }),
255
445
  undefined,
256
446
  );
447
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
448
+ where: { skuId: fixture.skuId },
449
+ });
450
+ assert.equal(audits.length, 1);
257
451
  } finally {
452
+ await dropStockFixtures(fixtures);
258
453
  await app.bean.passport.signout();
259
454
  }
260
455
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-startup",
3
- "version": "5.1.14",
3
+ "version": "5.1.15",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -52,12 +52,12 @@ export class ServiceStartup extends BeanBase {
52
52
  }
53
53
  }
54
54
 
55
- // version test
55
+ // version seed
56
56
  if (this.app.meta.isTest) {
57
57
  const instanceName = '';
58
58
  await this.bean.executor.newCtx(
59
59
  async () => {
60
- await this.$scope.version.service.version.__instanceTest(instanceName);
60
+ await this.$scope.version.service.version.__instanceSeed(instanceName);
61
61
  },
62
62
  {
63
63
  dbInfo: { level: 1 },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-version",
3
- "version": "5.1.14",
3
+ "version": "5.1.15",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona-module-a-version",
6
6
  "keywords": [
@@ -12,7 +12,7 @@ import type {
12
12
  IMetaVersionInit,
13
13
  IMetaVersionOptions,
14
14
  IMetaVersionOptionsInner,
15
- IMetaVersionTest,
15
+ IMetaVersionSeed,
16
16
  IMetaVersionUpdate,
17
17
  } from '../types/version.ts';
18
18
 
@@ -61,11 +61,11 @@ export class ServiceVersion extends BeanBase {
61
61
  }
62
62
  }
63
63
 
64
- async __instanceTest(instanceName: keyof IInstanceRecord) {
65
- await this.__check({ scene: 'test', instanceName });
64
+ async __instanceSeed(instanceName: keyof IInstanceRecord) {
65
+ await this.__check({ scene: 'seed', instanceName });
66
66
  }
67
67
 
68
- // scene: null/init/test
68
+ // scene: update/init/seed
69
69
  async __check(options: IMetaVersionOptionsInner) {
70
70
  options.result = {};
71
71
 
@@ -158,11 +158,11 @@ export class ServiceVersion extends BeanBase {
158
158
  }
159
159
  }
160
160
 
161
- if (options.scene === 'test') {
162
- // test module
161
+ if (options.scene === 'seed') {
162
+ // seed module
163
163
  await this.bean.executor.newCtx(
164
164
  async () => {
165
- await this.__testModuleTransaction(module, fileVersionNew, options);
165
+ await this.__seedModuleTransaction(module, fileVersionNew, options);
166
166
  },
167
167
  {
168
168
  instanceName: options.instanceName,
@@ -260,13 +260,13 @@ export class ServiceVersion extends BeanBase {
260
260
  }
261
261
  }
262
262
 
263
- // test module
264
- async __testModuleTransaction(module, version, options) {
263
+ // seed module
264
+ async __seedModuleTransaction(module, version, options) {
265
265
  // bean
266
- const beanVersion = this.__getBeanVersion<IMetaVersionTest>(module.info.relativeName, false);
266
+ const beanVersion = this.__getBeanVersion<IMetaVersionSeed>(module.info.relativeName, false);
267
267
  // execute
268
- if (beanVersion && beanVersion.test) {
269
- await beanVersion.test({ ...options, version });
268
+ if (beanVersion && beanVersion.seed) {
269
+ await beanVersion.seed({ ...options, version });
270
270
  }
271
271
  }
272
272
 
@@ -2,7 +2,7 @@ import type { IInstanceRecord } from 'vona';
2
2
  import type { ConfigInstanceBase } from 'vona-module-a-instance';
3
3
 
4
4
  export interface IMetaVersionOptions {
5
- scene: 'update' | 'init' | 'test';
5
+ scene: 'update' | 'init' | 'seed';
6
6
  instanceName?: keyof IInstanceRecord;
7
7
  }
8
8
 
@@ -19,7 +19,7 @@ export interface IMetaVersionInitOptions extends ConfigInstanceBase {
19
19
  version: number;
20
20
  }
21
21
 
22
- export interface IMetaVersionTestOptions {
22
+ export interface IMetaVersionSeedOptions {
23
23
  version: number;
24
24
  instanceName: string;
25
25
  }
@@ -32,8 +32,8 @@ export interface IMetaVersionInit {
32
32
  init: (options: IMetaVersionInitOptions) => Promise<void>;
33
33
  }
34
34
 
35
- export interface IMetaVersionTest {
36
- test: (options: IMetaVersionTestOptions) => Promise<void>;
35
+ export interface IMetaVersionSeed {
36
+ seed: (options: IMetaVersionSeedOptions) => Promise<void>;
37
37
  }
38
38
 
39
39
  declare module 'vona' {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.70",
3
+ "version": "5.1.71",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",