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.
- package/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +2 -0
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +9 -0
- package/.github/workflows/playwright-e2e.yml +34 -2
- package/.github/workflows/vona-test-pg.yml +8 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/cabloy-docs/backend/migration-and-changes.md +13 -9
- package/cabloy-docs/backend/unit-testing.md +25 -0
- package/e2e/scripts/startE2eVona.ts +51 -13
- package/e2e/specs/a-commerce/commerce.spec.ts +1 -0
- package/e2e/specs/cabloy-basic/basic.spec.ts +12 -7
- package/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +6 -9
- package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +50 -21
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +67 -67
- package/vona/src/backend/config/config/config.test.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +234 -0
- package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +3 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +4 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +4 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +4 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockAudit.tsx +39 -7
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +38 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +268 -65
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +276 -81
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/version.ts +12 -12
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/types/version.ts +4 -4
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/pnpm-lock.yaml +14 -14
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +0 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +1 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +17 -7
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.header.tsx +1 -1
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
68
|
+
@Api.field(v.title($locale('CorrelationId')), v.required(), v.min(1), ZovaRender.order(8))
|
|
66
69
|
correlationId: string;
|
|
67
70
|
|
|
68
|
-
@Api.field(
|
|
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(
|
|
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(
|
|
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<
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
46
|
-
const balance = await adjustStock(
|
|
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(
|
|
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 => [
|
|
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
|
-
|
|
80
|
-
const balance = await adjustStock(
|
|
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('
|
|
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
|
-
|
|
104
|
-
const balance = await adjustStock(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
await app.bean.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
382
|
+
});
|
|
383
|
+
} finally {
|
|
384
|
+
await app.bean.executor.mockCtx(async () => {
|
|
385
|
+
await dropStockFixture(fixture);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
185
388
|
});
|
|
186
389
|
});
|