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
@@ -48,31 +48,63 @@ export class EntityStockAudit extends EntityBase {
48
48
  )
49
49
  stockReservationId?: TableIdentity;
50
50
 
51
+ @Api.field(v.title($locale('ActorId')), v.optional(), v.tableIdentity(), ZovaRender.order(4))
52
+ actorId?: TableIdentity;
53
+
51
54
  @Api.field(
52
55
  v.title($locale('StockOperation')),
53
56
  v.required(),
54
57
  z.enum(['adjust', 'reserve', 'consume', 'release', 'restore']),
55
- ZovaRender.order(4),
58
+ ZovaRender.order(5),
56
59
  )
57
60
  operation: 'adjust' | 'reserve' | 'consume' | 'release' | 'restore';
58
61
 
59
- @Api.field(v.title($locale('Delta')), v.required(), z.int(), ZovaRender.order(5))
62
+ @Api.field(v.title($locale('Delta')), v.required(), z.int(), ZovaRender.order(6))
60
63
  delta: number;
61
64
 
62
- @Api.field(v.title($locale('Reason')), v.required(), v.min(1), ZovaRender.order(6))
65
+ @Api.field(v.title($locale('Reason')), v.required(), v.min(1), ZovaRender.order(7))
63
66
  reason: string;
64
67
 
65
- @Api.field(v.title($locale('CorrelationId')), v.required(), v.min(1), ZovaRender.order(7))
68
+ @Api.field(v.title($locale('CorrelationId')), v.required(), v.min(1), ZovaRender.order(8))
66
69
  correlationId: string;
67
70
 
68
- @Api.field(v.title($locale('OnHand')), v.required(), z.number().int().min(0), ZovaRender.order(8))
71
+ @Api.field(
72
+ v.title($locale('PriorOnHand')),
73
+ v.required(),
74
+ z.number().int().min(0),
75
+ ZovaRender.order(9),
76
+ )
77
+ priorOnHand: number;
78
+
79
+ @Api.field(
80
+ v.title($locale('PriorReserved')),
81
+ v.required(),
82
+ z.number().int().min(0),
83
+ ZovaRender.order(10),
84
+ )
85
+ priorReserved: number;
86
+
87
+ @Api.field(
88
+ v.title($locale('PriorAvailable')),
89
+ v.required(),
90
+ z.number().int().min(0),
91
+ ZovaRender.order(11),
92
+ )
93
+ priorAvailable: number;
94
+
95
+ @Api.field(
96
+ v.title($locale('OnHand')),
97
+ v.required(),
98
+ z.number().int().min(0),
99
+ ZovaRender.order(12),
100
+ )
69
101
  onHand: number;
70
102
 
71
103
  @Api.field(
72
104
  v.title($locale('Reserved')),
73
105
  v.required(),
74
106
  z.number().int().min(0),
75
- ZovaRender.order(9),
107
+ ZovaRender.order(13),
76
108
  )
77
109
  reserved: number;
78
110
 
@@ -80,7 +112,7 @@ export class EntityStockAudit extends EntityBase {
80
112
  v.title($locale('Available')),
81
113
  v.required(),
82
114
  z.number().int().min(0),
83
- ZovaRender.order(10),
115
+ ZovaRender.order(14),
84
116
  )
85
117
  available: number;
86
118
  }
@@ -47,6 +47,11 @@ export class ServiceStockBalance extends BeanBase {
47
47
  let stockBalance = await this.scope.model.stockBalance.getForUpdate({
48
48
  skuId: stockAdjust.skuId,
49
49
  });
50
+ const priorBalance = stockBalance ?? {
51
+ onHand: 0,
52
+ reserved: 0,
53
+ available: 0,
54
+ };
50
55
  if (!stockBalance) {
51
56
  if (stockAdjust.delta < 0) {
52
57
  throw new Error('stock adjustment would make balance negative');
@@ -79,6 +84,7 @@ export class ServiceStockBalance extends BeanBase {
79
84
 
80
85
  await this._appendAudit({
81
86
  stockBalance,
87
+ priorBalance,
82
88
  delta: stockAdjust.delta,
83
89
  operation: 'adjust',
84
90
  reason: stockAdjust.reason,
@@ -87,8 +93,23 @@ export class ServiceStockBalance extends BeanBase {
87
93
  return stockBalance;
88
94
  }
89
95
 
90
- @Core.transaction({ isolationLevel: 'SERIALIZABLE' })
91
96
  async reserve(command: IStockReservationCommand): Promise<EntityStockReservation> {
97
+ const transaction = this.bean.database.current.transaction;
98
+ if (transaction.inTransaction) {
99
+ return await this._reserve(command);
100
+ }
101
+ for (let attempt = 0; ; attempt++) {
102
+ try {
103
+ return await transaction.begin(() => this._reserve(command), {
104
+ isolationLevel: 'SERIALIZABLE',
105
+ });
106
+ } catch (error) {
107
+ if (!this._isSerializationFailure(error) || attempt >= 1) throw error;
108
+ }
109
+ }
110
+ }
111
+
112
+ private async _reserve(command: IStockReservationCommand): Promise<EntityStockReservation> {
92
113
  this._assertReservationCommand(command);
93
114
  const sku = await this.$scope.commerceCatalog.model.sku.getById(command.skuId, {
94
115
  include: {
@@ -133,12 +154,14 @@ export class ServiceStockBalance extends BeanBase {
133
154
  state: 'reserved',
134
155
  correlationId: command.correlationId,
135
156
  });
157
+ const priorBalance = stockBalance;
136
158
  const updatedBalance = await this._updateBalance(stockBalance, {
137
159
  onHand: stockBalance.onHand,
138
160
  reserved: stockBalance.reserved + command.quantity,
139
161
  });
140
162
  await this._appendAudit({
141
163
  stockBalance: updatedBalance,
164
+ priorBalance,
142
165
  stockReservationId: reservation.id,
143
166
  delta: -command.quantity,
144
167
  operation: 'reserve',
@@ -218,6 +241,7 @@ export class ServiceStockBalance extends BeanBase {
218
241
  if (!stockBalance) {
219
242
  this.app.throw(404, 'stock balance not found');
220
243
  }
244
+ const priorBalance = stockBalance;
221
245
  const updatedBalance = await this._updateBalance(
222
246
  stockBalance,
223
247
  calculateBalance(stockBalance, reservation),
@@ -228,6 +252,7 @@ export class ServiceStockBalance extends BeanBase {
228
252
  targetState === 'consumed' ? 'consume' : targetState === 'released' ? 'release' : 'restore';
229
253
  await this._appendAudit({
230
254
  stockBalance: updatedBalance,
255
+ priorBalance,
231
256
  stockReservationId: reservation.id,
232
257
  delta:
233
258
  operation === 'consume'
@@ -264,6 +289,7 @@ export class ServiceStockBalance extends BeanBase {
264
289
 
265
290
  private async _appendAudit({
266
291
  stockBalance,
292
+ priorBalance,
267
293
  stockReservationId,
268
294
  delta,
269
295
  operation,
@@ -271,6 +297,7 @@ export class ServiceStockBalance extends BeanBase {
271
297
  correlationId,
272
298
  }: {
273
299
  stockBalance: EntityStockBalance;
300
+ priorBalance: Pick<EntityStockBalance, 'onHand' | 'reserved' | 'available'>;
274
301
  stockReservationId?: TableIdentity;
275
302
  delta: number;
276
303
  operation: 'adjust' | 'reserve' | 'consume' | 'release' | 'restore';
@@ -281,16 +308,26 @@ export class ServiceStockBalance extends BeanBase {
281
308
  stockBalanceId: stockBalance.id,
282
309
  skuId: stockBalance.skuId,
283
310
  stockReservationId,
311
+ actorId: this.bean.passport.currentUser?.anonymous
312
+ ? undefined
313
+ : this.bean.passport.currentUser?.id,
284
314
  operation,
285
315
  delta,
286
316
  reason,
287
317
  correlationId,
318
+ priorOnHand: priorBalance.onHand,
319
+ priorReserved: priorBalance.reserved,
320
+ priorAvailable: priorBalance.available,
288
321
  onHand: stockBalance.onHand,
289
322
  reserved: stockBalance.reserved,
290
323
  available: stockBalance.available,
291
324
  });
292
325
  }
293
326
 
327
+ private _isSerializationFailure(error: unknown) {
328
+ return (error as { code?: unknown })?.code === '40001';
329
+ }
330
+
294
331
  private _assertReservationCommand(command: IStockReservationCommand) {
295
332
  if (!Number.isInteger(command.quantity) || command.quantity <= 0) {
296
333
  this.app.throw(400, 'stock reservation quantity must be a positive integer');
@@ -7,6 +7,14 @@ import { app } from 'vona-mock';
7
7
 
8
8
  const actionPath = '/commerce/trade/stockBalance/adjustStock';
9
9
 
10
+ interface IStockFixture {
11
+ categoryId: number;
12
+ productId: number;
13
+ skuId: number;
14
+ }
15
+
16
+ type IStockFixturePartial = Partial<IStockFixture>;
17
+
10
18
  function stockAdjust(skuId: number, delta: number, suffix: string): DtoStockAdjust {
11
19
  return {
12
20
  skuId,
@@ -16,21 +24,50 @@ function stockAdjust(skuId: number, delta: number, suffix: string): DtoStockAdju
16
24
  };
17
25
  }
18
26
 
19
- async function createSku(suffix: string): Promise<number> {
20
- const categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
21
- body: { name: `stock-category-${suffix}`, published: true },
22
- });
23
- const productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
24
- body: { categoryId, title: `stock-product-${suffix}`, published: true },
25
- });
26
- return await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
27
- body: {
28
- productId,
29
- code: `stock-sku-${suffix}`,
30
- priceCents: 100,
31
- lifecycle: 'active',
32
- },
33
- });
27
+ async function createSku(suffix: string): Promise<IStockFixture> {
28
+ const fixture: IStockFixturePartial = {};
29
+ try {
30
+ fixture.categoryId = await app.bean.executor.performAction(
31
+ 'post',
32
+ '/commerce/catalog/category',
33
+ {
34
+ body: { name: `stock-category-${suffix}`, published: true },
35
+ },
36
+ );
37
+ fixture.productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
38
+ body: { categoryId: fixture.categoryId, title: `stock-product-${suffix}`, published: true },
39
+ });
40
+ fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
41
+ body: {
42
+ productId: fixture.productId,
43
+ code: `stock-sku-${suffix}`,
44
+ priceCents: 100,
45
+ lifecycle: 'active',
46
+ },
47
+ });
48
+ return fixture as IStockFixture;
49
+ } catch (error) {
50
+ await dropStockFixture(fixture);
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ async function dropStockFixture(fixture: IStockFixturePartial | undefined) {
56
+ if (!fixture) return;
57
+ const scopeTrade = app.scope('commerce-trade');
58
+ const scopeCatalog = app.scope('commerce-catalog');
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
+ }
34
71
  }
35
72
 
36
73
  async function adjustStock(stockAdjust: DtoStockAdjust): Promise<EntityStockBalance> {
@@ -40,33 +77,48 @@ async function adjustStock(stockAdjust: DtoStockAdjust): Promise<EntityStockBala
40
77
  describe('stockBalance.test.ts', () => {
41
78
  it('action:stockBalance:adjustStock appends an audit with server-scoped balance', async () => {
42
79
  await app.bean.executor.mockCtx(async () => {
80
+ let fixture: IStockFixture | undefined;
43
81
  await app.bean.passport.signinMock();
44
82
  try {
45
- const skuId = await createSku(`${Date.now()}`);
46
- const balance = await adjustStock(stockAdjust(skuId, 8, `${skuId}-initial`));
83
+ fixture = await createSku(`${Date.now()}`);
84
+ const balance = await adjustStock(
85
+ stockAdjust(fixture.skuId, 8, `${fixture.skuId}-initial`),
86
+ );
47
87
  assert.equal(balance.onHand, 8);
48
88
  assert.equal(balance.reserved, 0);
49
89
  assert.equal(balance.available, 8);
50
90
 
51
- const adjusted = await adjustStock(stockAdjust(skuId, -3, `${skuId}-decrement`));
91
+ const adjusted = await adjustStock(
92
+ stockAdjust(fixture.skuId, -3, `${fixture.skuId}-decrement`),
93
+ );
52
94
  assert.equal(adjusted.id, balance.id);
53
95
  assert.equal(adjusted.onHand, 5);
54
96
  assert.equal(adjusted.reserved, 0);
55
97
  assert.equal(adjusted.available, 5);
56
98
 
57
99
  const audits = await app.scope('commerce-trade').model.stockAudit.select({
58
- where: { skuId },
100
+ where: { skuId: fixture.skuId },
59
101
  });
60
102
  assert.equal(audits.length, 2);
61
103
  assert.deepEqual(
62
- audits.map(item => [item.delta, item.onHand, item.reserved, item.available]),
104
+ audits.map(item => [
105
+ item.delta,
106
+ item.priorOnHand,
107
+ item.priorReserved,
108
+ item.priorAvailable,
109
+ item.onHand,
110
+ item.reserved,
111
+ item.available,
112
+ ]),
63
113
  [
64
- [8, 8, 0, 8],
65
- [-3, 5, 0, 5],
114
+ [8, 0, 0, 0, 8, 0, 8],
115
+ [-3, 8, 0, 8, 5, 0, 5],
66
116
  ],
67
117
  );
118
+ assert.equal(String(audits[0].actorId), String(app.bean.passport.currentUser!.id));
68
119
  assert.equal(audits[1].stockBalanceId, balance.id);
69
120
  } finally {
121
+ await dropStockFixture(fixture);
70
122
  await app.bean.passport.signout();
71
123
  }
72
124
  });
@@ -74,12 +126,15 @@ describe('stockBalance.test.ts', () => {
74
126
 
75
127
  it('action:stockBalance:adjustStock rejects negative stock without an audit', async () => {
76
128
  await app.bean.executor.mockCtx(async () => {
129
+ let fixture: IStockFixture | undefined;
77
130
  await app.bean.passport.signinMock();
78
131
  try {
79
- const skuId = await createSku(`${Date.now()}`);
80
- const balance = await adjustStock(stockAdjust(skuId, 2, `${skuId}-initial`));
132
+ fixture = await createSku(`${Date.now()}`);
133
+ const balance = await adjustStock(
134
+ stockAdjust(fixture.skuId, 2, `${fixture.skuId}-initial`),
135
+ );
81
136
  const [_, err] = await catchError(() =>
82
- adjustStock(stockAdjust(skuId, -3, `${skuId}-reject`)),
137
+ adjustStock(stockAdjust(fixture.skuId, -3, `${fixture.skuId}-reject`)),
83
138
  );
84
139
  assert.match(err?.message ?? '', /negative/);
85
140
 
@@ -87,39 +142,66 @@ describe('stockBalance.test.ts', () => {
87
142
  assert.equal(unchanged?.onHand, 2);
88
143
  assert.equal(unchanged?.available, 2);
89
144
  const audits = await app.scope('commerce-trade').model.stockAudit.select({
90
- where: { skuId },
145
+ where: { skuId: fixture.skuId },
91
146
  });
92
147
  assert.equal(audits.length, 1);
93
148
  } finally {
149
+ await dropStockFixture(fixture);
94
150
  await app.bean.passport.signout();
95
151
  }
96
152
  });
97
153
  });
98
154
 
99
- it('action:stockBalance:adjustStock rolls back the balance when audit insertion fails', async () => {
155
+ it('rolls back balance and audit writes in one transaction', async () => {
100
156
  await app.bean.executor.mockCtx(async () => {
157
+ let fixture: IStockFixture | undefined;
101
158
  await app.bean.passport.signinMock();
102
159
  try {
103
- const skuId = await createSku(`${Date.now()}`);
104
- const balance = await adjustStock(stockAdjust(skuId, 4, `${skuId}-initial`));
105
- const modelStockAudit = app.scope('commerce-trade').model.stockAudit;
106
- const insert = modelStockAudit.insert.bind(modelStockAudit);
107
- (modelStockAudit as any).insert = async () => {
108
- throw new Error('audit insert failure');
109
- };
110
- try {
111
- const [_, err] = await catchError(() =>
112
- adjustStock(stockAdjust(skuId, 1, `${skuId}-rollback`)),
113
- );
114
- assert.match(err?.message ?? '', /audit insert failure/);
115
- } finally {
116
- (modelStockAudit as any).insert = insert;
117
- }
160
+ fixture = await createSku(`${Date.now()}`);
161
+ const balance = await adjustStock(
162
+ stockAdjust(fixture.skuId, 4, `${fixture.skuId}-initial`),
163
+ );
164
+ const db = app.ctx.db;
165
+ const [_, err] = await catchError(async () => {
166
+ await db.transaction.begin(async () => {
167
+ const modelStockBalance = app
168
+ .scope('commerce-trade')
169
+ .model.stockBalance.newInstance(db);
170
+ const modelStockAudit = app.scope('commerce-trade').model.stockAudit.newInstance(db);
171
+ await modelStockBalance.updateById(balance.id, {
172
+ onHand: 5,
173
+ reserved: 0,
174
+ available: 5,
175
+ });
176
+ await modelStockAudit.insert({
177
+ stockBalanceId: balance.id,
178
+ skuId: fixture!.skuId,
179
+ actorId: app.bean.passport.currentUser!.id,
180
+ operation: 'adjust',
181
+ delta: 1,
182
+ reason: 'transaction rollback proof',
183
+ correlationId: `stock-test-${fixture!.skuId}-rollback`,
184
+ priorOnHand: 4,
185
+ priorReserved: 0,
186
+ priorAvailable: 4,
187
+ onHand: 5,
188
+ reserved: 0,
189
+ available: 5,
190
+ });
191
+ throw new Error('transaction rollback proof');
192
+ });
193
+ });
194
+ assert.match(err?.message ?? '', /transaction rollback proof/);
118
195
 
119
196
  const unchanged = await app.scope('commerce-trade').model.stockBalance.getById(balance.id);
120
197
  assert.equal(unchanged?.onHand, 4);
121
198
  assert.equal(unchanged?.available, 4);
199
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
200
+ where: { skuId: fixture.skuId },
201
+ });
202
+ assert.equal(audits.length, 1);
122
203
  } finally {
204
+ await dropStockFixture(fixture);
123
205
  await app.bean.passport.signout();
124
206
  }
125
207
  });
@@ -149,38 +231,159 @@ describe('stockBalance.test.ts', () => {
149
231
  });
150
232
 
151
233
  it('action:stockBalance:adjustStock treats a cross-instance SKU as not found', async () => {
152
- let skuId!: number;
153
- await app.bean.executor.mockCtx(async () => {
154
- await app.bean.passport.signinMock();
155
- try {
156
- skuId = await createSku(`${Date.now()}`);
157
- await adjustStock(stockAdjust(skuId, 5, `${skuId}-default`));
158
- } finally {
159
- await app.bean.passport.signout();
160
- }
161
- });
234
+ let fixture: IStockFixture | undefined;
235
+ try {
236
+ await app.bean.executor.mockCtx(async () => {
237
+ await app.bean.passport.signinMock();
238
+ try {
239
+ fixture = await createSku(`${Date.now()}`);
240
+ await adjustStock(stockAdjust(fixture.skuId, 5, `${fixture.skuId}-default`));
241
+ } finally {
242
+ await app.bean.passport.signout();
243
+ }
244
+ });
162
245
 
163
- await app.bean.executor.mockCtx(
164
- async () => {
246
+ await app.bean.executor.mockCtx(
247
+ async () => {
248
+ await app.bean.passport.signinMock();
249
+ try {
250
+ const [_, err] = await catchError(() =>
251
+ adjustStock(stockAdjust(fixture!.skuId, 1, `${fixture!.skuId}-share`)),
252
+ );
253
+ assert.equal(err?.code, 404);
254
+ assert.equal(
255
+ await app.scope('commerce-trade').model.stockBalance.get({ skuId: fixture!.skuId }),
256
+ undefined,
257
+ );
258
+ assert.deepEqual(
259
+ await app.scope('commerce-trade').model.stockAudit.select({
260
+ where: { skuId: fixture!.skuId },
261
+ }),
262
+ [],
263
+ );
264
+ } finally {
265
+ await app.bean.passport.signout();
266
+ }
267
+ },
268
+ { instanceName: 'shareTest' as any },
269
+ );
270
+ } finally {
271
+ await app.bean.executor.mockCtx(async () => {
272
+ await dropStockFixture(fixture);
273
+ });
274
+ }
275
+ });
276
+
277
+ it('hides foreign stock balances and audits from operator read APIs', async () => {
278
+ const suffix = `${Date.now()}`;
279
+ let fixture: IStockFixture | undefined;
280
+ let balanceId!: number;
281
+ let auditId!: number;
282
+ const reason = `stock read isolation ${suffix}`;
283
+ const correlationId = `stock-read-isolation-${suffix}`;
284
+ try {
285
+ await app.bean.executor.mockCtx(async () => {
286
+ await app.bean.passport.signinMock();
287
+ try {
288
+ fixture = await createSku(suffix);
289
+ const balance = await adjustStock({
290
+ skuId: fixture.skuId,
291
+ delta: 5,
292
+ reason,
293
+ correlationId,
294
+ });
295
+ balanceId = balance.id;
296
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
297
+ where: { skuId: fixture.skuId },
298
+ });
299
+ assert.equal(audits.length, 1);
300
+ auditId = audits[0].id;
301
+ } finally {
302
+ await app.bean.passport.signout();
303
+ }
304
+ });
305
+
306
+ await app.bean.executor.mockCtx(
307
+ async () => {
308
+ await app.bean.passport.signinMock();
309
+ try {
310
+ assert.equal(
311
+ await app.bean.executor.performAction('get', '/commerce/trade/stockBalance/:id', {
312
+ params: { id: balanceId },
313
+ }),
314
+ undefined,
315
+ );
316
+ const balances = await app.bean.executor.performAction(
317
+ 'get',
318
+ '/commerce/trade/stockBalance',
319
+ );
320
+ assert.equal(
321
+ balances.list.some(
322
+ (balance: { id: unknown }) => String(balance.id) === String(balanceId),
323
+ ),
324
+ false,
325
+ );
326
+ assert.equal(
327
+ await app.bean.executor.performAction('get', '/commerce/trade/stockAudit/:id', {
328
+ params: { id: auditId },
329
+ }),
330
+ undefined,
331
+ );
332
+ const audits = await app.bean.executor.performAction(
333
+ 'get',
334
+ '/commerce/trade/stockAudit',
335
+ );
336
+ assert.equal(
337
+ audits.list.some((audit: { id: unknown }) => String(audit.id) === String(auditId)),
338
+ false,
339
+ );
340
+ } finally {
341
+ await app.bean.passport.signout();
342
+ }
343
+ },
344
+ { instanceName: 'shareTest' as any },
345
+ );
346
+
347
+ await app.bean.executor.mockCtx(async () => {
165
348
  await app.bean.passport.signinMock();
166
349
  try {
167
- const [_, err] = await catchError(() =>
168
- adjustStock(stockAdjust(skuId, 1, `${skuId}-share`)),
350
+ const balance = await app.bean.executor.performAction(
351
+ 'get',
352
+ '/commerce/trade/stockBalance/:id',
353
+ { params: { id: balanceId } },
354
+ );
355
+ assert.deepEqual(
356
+ [balance.id, balance.skuId, balance.onHand, balance.reserved, balance.available],
357
+ [balanceId, fixture!.skuId, 5, 0, 5],
169
358
  );
170
- assert.equal(err?.code, 404);
171
- assert.equal(
172
- await app.scope('commerce-trade').model.stockBalance.get({ skuId }),
173
- undefined,
359
+ const audit = await app.bean.executor.performAction(
360
+ 'get',
361
+ '/commerce/trade/stockAudit/:id',
362
+ { params: { id: auditId } },
174
363
  );
175
364
  assert.deepEqual(
176
- await app.scope('commerce-trade').model.stockAudit.select({ where: { skuId } }),
177
- [],
365
+ [
366
+ audit.id,
367
+ audit.stockBalanceId,
368
+ audit.skuId,
369
+ audit.operation,
370
+ audit.delta,
371
+ audit.reason,
372
+ audit.correlationId,
373
+ audit.onHand,
374
+ audit.reserved,
375
+ audit.available,
376
+ ],
377
+ [auditId, balanceId, fixture!.skuId, 'adjust', 5, reason, correlationId, 5, 0, 5],
178
378
  );
179
379
  } finally {
180
380
  await app.bean.passport.signout();
181
381
  }
182
- },
183
- { instanceName: 'shareTest' as any },
184
- );
382
+ });
383
+ } finally {
384
+ await app.bean.executor.mockCtx(async () => {
385
+ await dropStockFixture(fixture);
386
+ });
387
+ }
185
388
  });
186
389
  });