cabloy 5.1.113 → 5.1.115
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/references/follow-up-checklist.md +4 -0
- package/.github/workflows/vona-test-mysql.yml +12 -0
- package/.github/workflows/vona-test-pg.yml +5 -1
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +4 -0
- package/cabloy-docs/backend/internal-aop-guide.md +35 -0
- package/cabloy-docs/backend/unit-testing.md +72 -1
- 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/toolsIsolate/test.ts +26 -13
- package/vona/packages-utils/compose/package.json +1 -1
- package/vona/packages-utils/compose/src/index.ts +19 -11
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/beanContainer.ts +15 -3
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +143 -12
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +8 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuAttribute.tsx +17 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +10 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +228 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +35 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +34 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAttempt.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +41 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/package.json +4 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +533 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +24 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.version.ts +137 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/ssrMenu.couponTemplate.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/coupon.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/couponTemplate.ts +65 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponMineItem.tsx +32 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +43 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectReq.tsx +28 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectResItem.tsx +39 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponAudit.tsx +73 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponGrant.tsx +83 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +82 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponAudit.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponGrant.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponTemplate.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +397 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/couponTemplate.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +162 -0
- package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +8 -10
- package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +277 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/bean.scheduleOrderExpiry.ts +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +9 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +75 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +32 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAddressSnapshot.tsx +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderCouponSnapshot.tsx +35 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderLineSkuAttributeSnapshot.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +90 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +36 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderLine.tsx +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockReservation.tsx +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderAudit.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderLine.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +556 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +15 -20
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +8 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +158 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +203 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +163 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +11 -6
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +11 -6
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +10 -7
- package/vona/src/suite-vendor/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-aspect/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-aspect/src/types/aopMethod.ts +16 -3
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +4 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/bean/bean.core.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/fetch.ts +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/lib/utils.ts +5 -3
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/package.json +50 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/index.ts +55 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/bean/aopMethod.retryable.ts +45 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-vona/package.json +2 -1
- package/vona/src/suite-vendor/a-vona/package.json +2 -1
- package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
- package/zova/packages-utils/zova-jsx/package.json +3 -3
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/pnpm-lock.yaml +31 -31
- package/zova/src/suite/a-commerce/modules/commerce-promotion/cli/openapi.config.ts +9 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/package.json +2 -1
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +115 -1
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/commercePromotionCoupon.ts +24 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/baseURL.ts +5 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/index.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +478 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +8187 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/apiSchema/commercePromotionCoupon.ts +13 -0
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +30 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +27 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +92 -52
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +1389 -848
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +13 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +13 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +3 -3
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- /package/vona/patches/{zova-core@5.1.78.patch → zova-core@5.1.79.patch} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import { app } from 'vona-mock';
|
|
5
|
+
|
|
6
|
+
interface IFixture {
|
|
7
|
+
categoryId?: number;
|
|
8
|
+
productId?: number;
|
|
9
|
+
skuId?: number;
|
|
10
|
+
balanceId?: number;
|
|
11
|
+
cartId?: number;
|
|
12
|
+
cartItemId?: number;
|
|
13
|
+
addressId?: number;
|
|
14
|
+
orderId?: number;
|
|
15
|
+
orderLineId?: number;
|
|
16
|
+
paymentAttemptId?: number;
|
|
17
|
+
orderAuditId?: number;
|
|
18
|
+
reservationId?: number;
|
|
19
|
+
stockAuditIds?: number[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function cleanup(fixture: IFixture) {
|
|
23
|
+
const trade = app.scope('commerce-trade');
|
|
24
|
+
const payment = app.scope('commerce-payment');
|
|
25
|
+
const member = app.scope('commerce-member');
|
|
26
|
+
const catalog = app.scope('commerce-catalog');
|
|
27
|
+
if (fixture.paymentAttemptId !== undefined)
|
|
28
|
+
await payment.model.paymentAttempt.delete({ id: fixture.paymentAttemptId });
|
|
29
|
+
if (fixture.orderAuditId !== undefined)
|
|
30
|
+
await trade.model.orderAudit.delete({ id: fixture.orderAuditId });
|
|
31
|
+
if (fixture.skuId !== undefined) await trade.model.stockAudit.delete({ skuId: fixture.skuId });
|
|
32
|
+
if (fixture.reservationId !== undefined)
|
|
33
|
+
await trade.model.stockReservation.delete({ id: fixture.reservationId });
|
|
34
|
+
if (fixture.orderLineId !== undefined)
|
|
35
|
+
await trade.model.orderLine.delete({ id: fixture.orderLineId });
|
|
36
|
+
if (fixture.orderId !== undefined) await trade.model.order.delete({ id: fixture.orderId });
|
|
37
|
+
if (fixture.cartItemId !== undefined)
|
|
38
|
+
await trade.model.cartItem.delete({ id: fixture.cartItemId });
|
|
39
|
+
if (fixture.cartId !== undefined) await trade.model.cart.delete({ id: fixture.cartId });
|
|
40
|
+
if (fixture.balanceId !== undefined)
|
|
41
|
+
await trade.model.stockBalance.delete({ id: fixture.balanceId });
|
|
42
|
+
if (fixture.skuId !== undefined) await catalog.model.sku.delete({ id: fixture.skuId });
|
|
43
|
+
if (fixture.productId !== undefined)
|
|
44
|
+
await catalog.model.product.delete({ id: fixture.productId });
|
|
45
|
+
if (fixture.categoryId !== undefined)
|
|
46
|
+
await catalog.model.category.delete({ id: fixture.categoryId });
|
|
47
|
+
if (fixture.addressId !== undefined) await member.model.address.delete({ id: fixture.addressId });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('checkoutReservation.test.ts', { concurrency: false }, () => {
|
|
51
|
+
it('creates an atomic checkout from the persisted cart and replays its correlation', async () => {
|
|
52
|
+
await app.bean.executor.mockCtx(async () => {
|
|
53
|
+
const fixture: IFixture = {};
|
|
54
|
+
const suffix = randomUUID().slice(0, 12);
|
|
55
|
+
try {
|
|
56
|
+
await app.bean.passport.signinMock();
|
|
57
|
+
const userId = app.bean.passport.currentUser!.id;
|
|
58
|
+
fixture.categoryId = await app.bean.executor.performAction(
|
|
59
|
+
'post',
|
|
60
|
+
'/commerce/catalog/category',
|
|
61
|
+
{
|
|
62
|
+
body: { name: `checkout-category-${suffix}`, published: true },
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
fixture.productId = await app.bean.executor.performAction(
|
|
66
|
+
'post',
|
|
67
|
+
'/commerce/catalog/product',
|
|
68
|
+
{
|
|
69
|
+
body: {
|
|
70
|
+
categoryId: fixture.categoryId,
|
|
71
|
+
title: `checkout-product-${suffix}`,
|
|
72
|
+
published: true,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
|
|
77
|
+
body: {
|
|
78
|
+
productId: fixture.productId,
|
|
79
|
+
code: `checkout-sku-${suffix}`,
|
|
80
|
+
priceCents: 1299,
|
|
81
|
+
attributes: [],
|
|
82
|
+
lifecycle: 'active',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const balance = await app.scope('commerce-trade').service.stockBalance.adjustStock({
|
|
86
|
+
skuId: fixture.skuId,
|
|
87
|
+
delta: 1,
|
|
88
|
+
reason: 'checkout fixture',
|
|
89
|
+
correlationId: `checkout-stock-${suffix}`,
|
|
90
|
+
});
|
|
91
|
+
fixture.balanceId = balance.id as number;
|
|
92
|
+
const auditsBefore = await app.scope('commerce-trade').model.stockAudit.select({
|
|
93
|
+
where: { skuId: fixture.skuId },
|
|
94
|
+
});
|
|
95
|
+
fixture.stockAuditIds = auditsBefore.map(item => item.id as number);
|
|
96
|
+
fixture.addressId = await app.bean.executor.performAction(
|
|
97
|
+
'post',
|
|
98
|
+
'/commerce/member/address',
|
|
99
|
+
{
|
|
100
|
+
body: {
|
|
101
|
+
recipientName: 'Checkout Customer',
|
|
102
|
+
phone: '15555550123',
|
|
103
|
+
countryCode: 'US',
|
|
104
|
+
region: 'California',
|
|
105
|
+
city: 'San Francisco',
|
|
106
|
+
postalCode: '94105',
|
|
107
|
+
addressLine1: '1 Market Street',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
const cart = await app.scope('commerce-trade').model.cart.insert({ userId });
|
|
112
|
+
fixture.cartId = cart.id as number;
|
|
113
|
+
const cartItem = await app.scope('commerce-trade').model.cartItem.insert({
|
|
114
|
+
cartId: cart.id,
|
|
115
|
+
skuId: fixture.skuId,
|
|
116
|
+
quantity: 1,
|
|
117
|
+
});
|
|
118
|
+
fixture.cartItemId = cartItem.id as number;
|
|
119
|
+
|
|
120
|
+
const result = await app.scope('commerce-trade').service.order.checkout({
|
|
121
|
+
addressId: fixture.addressId,
|
|
122
|
+
correlationId: `checkout-${suffix}`,
|
|
123
|
+
});
|
|
124
|
+
fixture.orderId = result.orderId as number;
|
|
125
|
+
fixture.paymentAttemptId = result.paymentAttemptId as number;
|
|
126
|
+
assert.equal(result.state, 'awaiting_payment');
|
|
127
|
+
assert.equal(result.payableTotalCents, 1299);
|
|
128
|
+
assert.equal(
|
|
129
|
+
await app.scope('commerce-trade').model.cartItem.getById(cartItem.id),
|
|
130
|
+
undefined,
|
|
131
|
+
);
|
|
132
|
+
const line = await app
|
|
133
|
+
.scope('commerce-trade')
|
|
134
|
+
.model.orderLine.get({ orderId: result.orderId });
|
|
135
|
+
fixture.orderLineId = line?.id as number;
|
|
136
|
+
const reservation = await app
|
|
137
|
+
.scope('commerce-trade')
|
|
138
|
+
.model.stockReservation.get({ orderLineId: line?.id });
|
|
139
|
+
fixture.reservationId = reservation?.id as number;
|
|
140
|
+
assert.equal(reservation?.state, 'reserved');
|
|
141
|
+
const audit = await app
|
|
142
|
+
.scope('commerce-trade')
|
|
143
|
+
.model.orderAudit.get({ orderId: result.orderId });
|
|
144
|
+
fixture.orderAuditId = audit?.id as number;
|
|
145
|
+
assert.equal(audit?.operation, 'created');
|
|
146
|
+
const replay = await app.scope('commerce-trade').service.order.checkout({
|
|
147
|
+
addressId: fixture.addressId,
|
|
148
|
+
correlationId: `checkout-${suffix}`,
|
|
149
|
+
});
|
|
150
|
+
assert.equal(replay.orderId, result.orderId);
|
|
151
|
+
assert.equal(replay.paymentAttemptId, result.paymentAttemptId);
|
|
152
|
+
} finally {
|
|
153
|
+
await cleanup(fixture);
|
|
154
|
+
await app.bean.passport.signout();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { catchError } from '@cabloy/utils';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import { app } from 'vona-mock';
|
|
5
|
+
|
|
6
|
+
interface IFixture {
|
|
7
|
+
categoryId?: number;
|
|
8
|
+
productId?: number;
|
|
9
|
+
skuId?: number;
|
|
10
|
+
addressId?: number;
|
|
11
|
+
orderId?: number;
|
|
12
|
+
couponTemplateId?: number;
|
|
13
|
+
couponGrantId?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function cleanup(fixture: IFixture) {
|
|
17
|
+
const promotion = app.scope('commerce-promotion');
|
|
18
|
+
const trade = app.scope('commerce-trade');
|
|
19
|
+
const catalog = app.scope('commerce-catalog');
|
|
20
|
+
const member = app.scope('commerce-member');
|
|
21
|
+
if (fixture.orderId !== undefined) {
|
|
22
|
+
const lines = await trade.model.orderLine.select({ where: { orderId: fixture.orderId } });
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
await trade.model.stockReservation.delete({ orderLineId: line.id });
|
|
25
|
+
}
|
|
26
|
+
await trade.model.orderLine.delete({ orderId: fixture.orderId });
|
|
27
|
+
await trade.model.order.delete({ id: fixture.orderId });
|
|
28
|
+
}
|
|
29
|
+
if (fixture.couponGrantId !== undefined) {
|
|
30
|
+
await promotion.model.couponAudit.delete({ couponGrantId: fixture.couponGrantId });
|
|
31
|
+
await promotion.model.couponGrant.delete({ id: fixture.couponGrantId });
|
|
32
|
+
}
|
|
33
|
+
if (fixture.couponTemplateId !== undefined) {
|
|
34
|
+
await promotion.model.couponTemplate.delete({ id: fixture.couponTemplateId });
|
|
35
|
+
}
|
|
36
|
+
if (fixture.skuId !== undefined) {
|
|
37
|
+
await trade.model.stockAudit.delete({ skuId: fixture.skuId });
|
|
38
|
+
await trade.model.stockReservation.delete({ skuId: fixture.skuId });
|
|
39
|
+
await trade.model.stockBalance.delete({ skuId: fixture.skuId });
|
|
40
|
+
await catalog.model.sku.delete({ id: fixture.skuId });
|
|
41
|
+
}
|
|
42
|
+
if (fixture.productId !== undefined)
|
|
43
|
+
await catalog.model.product.delete({ id: fixture.productId });
|
|
44
|
+
if (fixture.categoryId !== undefined)
|
|
45
|
+
await catalog.model.category.delete({ id: fixture.categoryId });
|
|
46
|
+
if (fixture.addressId !== undefined) await member.model.address.delete({ id: fixture.addressId });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('orderSnapshot.test.ts', { concurrency: false }, () => {
|
|
50
|
+
it('persists catalog and address facts independently from later source changes', async () => {
|
|
51
|
+
await app.bean.executor.mockCtx(async () => {
|
|
52
|
+
const fixture: IFixture = {};
|
|
53
|
+
const suffix = `${Date.now()}`;
|
|
54
|
+
try {
|
|
55
|
+
await app.bean.passport.signinMock();
|
|
56
|
+
fixture.categoryId = await app.bean.executor.performAction(
|
|
57
|
+
'post',
|
|
58
|
+
'/commerce/catalog/category',
|
|
59
|
+
{
|
|
60
|
+
body: { name: `snapshot category ${suffix}`, published: true },
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
fixture.productId = await app.bean.executor.performAction(
|
|
64
|
+
'post',
|
|
65
|
+
'/commerce/catalog/product',
|
|
66
|
+
{
|
|
67
|
+
body: {
|
|
68
|
+
categoryId: fixture.categoryId,
|
|
69
|
+
title: `snapshot product ${suffix}`,
|
|
70
|
+
published: true,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
|
|
75
|
+
body: {
|
|
76
|
+
productId: fixture.productId,
|
|
77
|
+
code: `snapshot-sku-${suffix}`,
|
|
78
|
+
priceCents: 1299,
|
|
79
|
+
attributes: [{ name: 'Color', value: 'Black' }],
|
|
80
|
+
lifecycle: 'active',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
await app.scope('commerce-trade').service.stockBalance.adjustStock({
|
|
84
|
+
skuId: fixture.skuId,
|
|
85
|
+
delta: 2,
|
|
86
|
+
reason: 'snapshot fixture',
|
|
87
|
+
correlationId: `snapshot-stock-${suffix}`,
|
|
88
|
+
});
|
|
89
|
+
await app.bean.passport.signout();
|
|
90
|
+
await app.bean.passport.signinMock();
|
|
91
|
+
fixture.couponTemplateId = (
|
|
92
|
+
await app.scope('commerce-promotion').model.couponTemplate.insert({
|
|
93
|
+
name: `snapshot coupon ${suffix}`,
|
|
94
|
+
state: 'active',
|
|
95
|
+
currency: 'USD',
|
|
96
|
+
discountCents: 500,
|
|
97
|
+
minSpendCents: 1_000,
|
|
98
|
+
validFrom: new Date(Date.now() - 1_000),
|
|
99
|
+
validUntil: new Date(Date.now() + 60_000),
|
|
100
|
+
issuedCount: 0,
|
|
101
|
+
redeemedCount: 0,
|
|
102
|
+
})
|
|
103
|
+
).id as number;
|
|
104
|
+
const customerId = app.bean.passport.currentUser!.id;
|
|
105
|
+
fixture.couponGrantId = (
|
|
106
|
+
await app.scope('commerce-promotion').service.coupon.issue({
|
|
107
|
+
templateId: fixture.couponTemplateId,
|
|
108
|
+
userId: customerId,
|
|
109
|
+
correlationId: `snapshot-coupon-${suffix}`,
|
|
110
|
+
reason: 'snapshot fixture',
|
|
111
|
+
})
|
|
112
|
+
).id as number;
|
|
113
|
+
fixture.addressId = await app.bean.executor.performAction(
|
|
114
|
+
'post',
|
|
115
|
+
'/commerce/member/address',
|
|
116
|
+
{
|
|
117
|
+
body: {
|
|
118
|
+
recipientName: 'Snapshot Recipient',
|
|
119
|
+
phone: '15555550123',
|
|
120
|
+
countryCode: 'US',
|
|
121
|
+
region: 'California',
|
|
122
|
+
city: 'San Francisco',
|
|
123
|
+
postalCode: '94105',
|
|
124
|
+
addressLine1: '1 Market Street',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const created = await app.scope('commerce-trade').service.order.createSnapshot({
|
|
130
|
+
addressId: fixture.addressId,
|
|
131
|
+
couponGrantId: fixture.couponGrantId,
|
|
132
|
+
correlationId: `snapshot-order-${suffix}`,
|
|
133
|
+
lines: [{ skuId: fixture.skuId, quantity: 2 }],
|
|
134
|
+
});
|
|
135
|
+
fixture.orderId = created.order.id as number;
|
|
136
|
+
const replay = await app.scope('commerce-trade').service.order.createSnapshot({
|
|
137
|
+
addressId: fixture.addressId,
|
|
138
|
+
couponGrantId: fixture.couponGrantId,
|
|
139
|
+
correlationId: `snapshot-order-${suffix}`,
|
|
140
|
+
lines: [{ skuId: fixture.skuId, quantity: 2 }],
|
|
141
|
+
});
|
|
142
|
+
assert.equal(replay.order.id, created.order.id);
|
|
143
|
+
assert.equal(replay.lines[0].id, created.lines[0].id);
|
|
144
|
+
const [_, conflict] = await catchError(() =>
|
|
145
|
+
app.scope('commerce-trade').service.order.createSnapshot({
|
|
146
|
+
addressId: fixture.addressId,
|
|
147
|
+
correlationId: `snapshot-order-${suffix}`,
|
|
148
|
+
lines: [{ skuId: fixture.skuId, quantity: 1 }],
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
assert.equal(conflict?.code, 409);
|
|
152
|
+
assert.equal(created.order.discountCents, 500);
|
|
153
|
+
assert.equal(created.order.payableTotalCents, 2098);
|
|
154
|
+
assert.equal(created.order.couponSnapshot?.couponGrantId, fixture.couponGrantId);
|
|
155
|
+
assert.equal(created.order.couponSnapshot?.couponCode, `snapshot-coupon-${suffix}`);
|
|
156
|
+
assert.equal(created.lines[0].titleSnapshot, `snapshot product ${suffix}`);
|
|
157
|
+
assert.deepEqual(created.lines[0].skuAttributesSnapshot, [
|
|
158
|
+
{ name: 'Color', value: 'Black' },
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
await app.scope('commerce-catalog').model.product.updateById(fixture.productId, {
|
|
162
|
+
title: 'Changed product title',
|
|
163
|
+
published: false,
|
|
164
|
+
});
|
|
165
|
+
await app.scope('commerce-catalog').model.sku.updateById(fixture.skuId, {
|
|
166
|
+
code: 'changed-sku-code',
|
|
167
|
+
priceCents: 9999,
|
|
168
|
+
attributes: [{ name: 'Color', value: 'Blue' }],
|
|
169
|
+
lifecycle: 'inactive',
|
|
170
|
+
});
|
|
171
|
+
await app.scope('commerce-member').model.address.updateById(fixture.addressId, {
|
|
172
|
+
city: 'Oakland',
|
|
173
|
+
addressLine1: 'Changed address',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const snapshot = await app
|
|
177
|
+
.scope('commerce-trade')
|
|
178
|
+
.service.order.viewSnapshot(created.order.id);
|
|
179
|
+
assert.equal(snapshot?.order.addressSnapshot.city, 'San Francisco');
|
|
180
|
+
assert.equal(snapshot?.order.addressSnapshot.addressLine1, '1 Market Street');
|
|
181
|
+
assert.equal(snapshot?.order.discountCents, 500);
|
|
182
|
+
assert.equal(snapshot?.order.payableTotalCents, 2098);
|
|
183
|
+
assert.equal(snapshot?.order.couponSnapshot?.templateName, `snapshot coupon ${suffix}`);
|
|
184
|
+
assert.equal(snapshot?.lines.length, 1);
|
|
185
|
+
assert.equal(snapshot?.lines[0].skuCodeSnapshot, `snapshot-sku-${suffix}`);
|
|
186
|
+
assert.equal(snapshot?.lines[0].titleSnapshot, `snapshot product ${suffix}`);
|
|
187
|
+
assert.deepEqual(snapshot?.lines[0].skuAttributesSnapshot, [
|
|
188
|
+
{ name: 'Color', value: 'Black' },
|
|
189
|
+
]);
|
|
190
|
+
assert.equal(snapshot?.lines[0].unitPriceCents, 1299);
|
|
191
|
+
assert.equal(snapshot?.lines[0].eligibleSubtotalCents, 2598);
|
|
192
|
+
assert.equal(snapshot?.lines[0].lineTotalCents, 2598);
|
|
193
|
+
const reservation = await app.scope('commerce-trade').model.stockReservation.get({
|
|
194
|
+
orderLineId: created.lines[0].id,
|
|
195
|
+
});
|
|
196
|
+
assert.equal(reservation?.state, 'reserved');
|
|
197
|
+
} finally {
|
|
198
|
+
await cleanup(fixture);
|
|
199
|
+
await app.bean.passport.signout();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import { app } from 'vona-mock';
|
|
5
|
+
|
|
6
|
+
interface IFixture {
|
|
7
|
+
categoryId?: number;
|
|
8
|
+
productId?: number;
|
|
9
|
+
skuId?: number;
|
|
10
|
+
balanceId?: number;
|
|
11
|
+
cartId?: number;
|
|
12
|
+
addressId?: number;
|
|
13
|
+
orderId?: number;
|
|
14
|
+
orderLineId?: number;
|
|
15
|
+
reservationId?: number;
|
|
16
|
+
paymentAttemptId?: number;
|
|
17
|
+
orderAuditIds?: number[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function cleanup(fixture: IFixture) {
|
|
21
|
+
const trade = app.scope('commerce-trade');
|
|
22
|
+
const payment = app.scope('commerce-payment');
|
|
23
|
+
const member = app.scope('commerce-member');
|
|
24
|
+
const catalog = app.scope('commerce-catalog');
|
|
25
|
+
if (fixture.paymentAttemptId !== undefined)
|
|
26
|
+
await payment.model.paymentAttempt.delete({ id: fixture.paymentAttemptId });
|
|
27
|
+
if (fixture.orderId !== undefined)
|
|
28
|
+
await trade.model.orderAudit.delete({ orderId: fixture.orderId });
|
|
29
|
+
if (fixture.skuId !== undefined) await trade.model.stockAudit.delete({ skuId: fixture.skuId });
|
|
30
|
+
if (fixture.reservationId !== undefined)
|
|
31
|
+
await trade.model.stockReservation.delete({ id: fixture.reservationId });
|
|
32
|
+
if (fixture.orderLineId !== undefined)
|
|
33
|
+
await trade.model.orderLine.delete({ id: fixture.orderLineId });
|
|
34
|
+
if (fixture.orderId !== undefined) await trade.model.order.delete({ id: fixture.orderId });
|
|
35
|
+
if (fixture.cartId !== undefined) {
|
|
36
|
+
await trade.model.cartItem.delete({ cartId: fixture.cartId });
|
|
37
|
+
await trade.model.cart.delete({ id: fixture.cartId });
|
|
38
|
+
}
|
|
39
|
+
if (fixture.balanceId !== undefined)
|
|
40
|
+
await trade.model.stockBalance.delete({ id: fixture.balanceId });
|
|
41
|
+
if (fixture.skuId !== undefined) await catalog.model.sku.delete({ id: fixture.skuId });
|
|
42
|
+
if (fixture.productId !== undefined)
|
|
43
|
+
await catalog.model.product.delete({ id: fixture.productId });
|
|
44
|
+
if (fixture.categoryId !== undefined)
|
|
45
|
+
await catalog.model.category.delete({ id: fixture.categoryId });
|
|
46
|
+
if (fixture.addressId !== undefined) await member.model.address.delete({ id: fixture.addressId });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('reservationExpiry.test.ts', { concurrency: false }, () => {
|
|
50
|
+
it('expires an unpaid order and releases its reservation exactly once', async () => {
|
|
51
|
+
await app.bean.executor.mockCtx(async () => {
|
|
52
|
+
const fixture: IFixture = {};
|
|
53
|
+
const suffix = randomUUID().slice(0, 12);
|
|
54
|
+
try {
|
|
55
|
+
await app.bean.passport.signinMock();
|
|
56
|
+
const userId = app.bean.passport.currentUser!.id;
|
|
57
|
+
fixture.categoryId = await app.bean.executor.performAction(
|
|
58
|
+
'post',
|
|
59
|
+
'/commerce/catalog/category',
|
|
60
|
+
{
|
|
61
|
+
body: { name: `expiry-category-${suffix}`, published: true },
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
fixture.productId = await app.bean.executor.performAction(
|
|
65
|
+
'post',
|
|
66
|
+
'/commerce/catalog/product',
|
|
67
|
+
{
|
|
68
|
+
body: {
|
|
69
|
+
categoryId: fixture.categoryId,
|
|
70
|
+
title: `expiry-product-${suffix}`,
|
|
71
|
+
published: true,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
|
|
76
|
+
body: {
|
|
77
|
+
productId: fixture.productId,
|
|
78
|
+
code: `expiry-sku-${suffix}`,
|
|
79
|
+
priceCents: 899,
|
|
80
|
+
attributes: [],
|
|
81
|
+
lifecycle: 'active',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const balance = await app.scope('commerce-trade').service.stockBalance.adjustStock({
|
|
85
|
+
skuId: fixture.skuId,
|
|
86
|
+
delta: 1,
|
|
87
|
+
reason: 'expiry fixture',
|
|
88
|
+
correlationId: `expiry-stock-${suffix}`,
|
|
89
|
+
});
|
|
90
|
+
fixture.balanceId = balance.id as number;
|
|
91
|
+
fixture.addressId = await app.bean.executor.performAction(
|
|
92
|
+
'post',
|
|
93
|
+
'/commerce/member/address',
|
|
94
|
+
{
|
|
95
|
+
body: {
|
|
96
|
+
recipientName: 'Expiry Customer',
|
|
97
|
+
phone: '15555550124',
|
|
98
|
+
countryCode: 'US',
|
|
99
|
+
region: 'California',
|
|
100
|
+
city: 'San Francisco',
|
|
101
|
+
postalCode: '94105',
|
|
102
|
+
addressLine1: '2 Market Street',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
const cart = await app.scope('commerce-trade').model.cart.insert({ userId });
|
|
107
|
+
fixture.cartId = cart.id as number;
|
|
108
|
+
await app.scope('commerce-trade').model.cartItem.insert({
|
|
109
|
+
cartId: cart.id,
|
|
110
|
+
skuId: fixture.skuId,
|
|
111
|
+
quantity: 1,
|
|
112
|
+
});
|
|
113
|
+
const created = await app.scope('commerce-trade').service.order.checkout({
|
|
114
|
+
addressId: fixture.addressId,
|
|
115
|
+
correlationId: `expiry-${suffix}`,
|
|
116
|
+
});
|
|
117
|
+
fixture.orderId = created.orderId as number;
|
|
118
|
+
fixture.paymentAttemptId = created.paymentAttemptId as number;
|
|
119
|
+
const line = await app
|
|
120
|
+
.scope('commerce-trade')
|
|
121
|
+
.model.orderLine.get({ orderId: created.orderId });
|
|
122
|
+
fixture.orderLineId = line?.id as number;
|
|
123
|
+
const reservation = await app
|
|
124
|
+
.scope('commerce-trade')
|
|
125
|
+
.model.stockReservation.get({ orderLineId: line?.id });
|
|
126
|
+
fixture.reservationId = reservation?.id as number;
|
|
127
|
+
await app.scope('commerce-trade').model.order.updateById(created.orderId, {
|
|
128
|
+
reservationExpiresAt: new Date(Date.now() - 1),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
assert.equal(
|
|
132
|
+
await app.scope('commerce-trade').service.order.expireIfDue(created.orderId),
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
assert.equal(
|
|
136
|
+
await app.scope('commerce-trade').service.order.expireIfDue(created.orderId),
|
|
137
|
+
false,
|
|
138
|
+
);
|
|
139
|
+
const order = await app.scope('commerce-trade').model.order.getById(created.orderId);
|
|
140
|
+
assert.equal(order?.state, 'expired');
|
|
141
|
+
const released = await app
|
|
142
|
+
.scope('commerce-trade')
|
|
143
|
+
.model.stockReservation.getById(reservation!.id);
|
|
144
|
+
assert.equal(released?.state, 'released');
|
|
145
|
+
const attempt = await app
|
|
146
|
+
.scope('commerce-payment')
|
|
147
|
+
.model.paymentAttempt.getById(created.paymentAttemptId);
|
|
148
|
+
assert.equal(attempt?.state, 'cancelled');
|
|
149
|
+
const audits = await app.scope('commerce-trade').model.orderAudit.select({
|
|
150
|
+
where: { orderId: created.orderId },
|
|
151
|
+
orders: [['id', 'asc']],
|
|
152
|
+
});
|
|
153
|
+
assert.deepEqual(
|
|
154
|
+
audits.map(item => item.operation),
|
|
155
|
+
['created', 'expired'],
|
|
156
|
+
);
|
|
157
|
+
} finally {
|
|
158
|
+
await cleanup(fixture);
|
|
159
|
+
await app.bean.passport.signout();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -2,6 +2,7 @@ import type { DtoStockAdjust, EntityStockBalance } from 'vona-module-commerce-tr
|
|
|
2
2
|
|
|
3
3
|
import { catchError } from '@cabloy/utils';
|
|
4
4
|
import assert from 'node:assert';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { describe, it } from 'node:test';
|
|
6
7
|
import { app } from 'vona-mock';
|
|
7
8
|
|
|
@@ -15,6 +16,10 @@ interface IStockFixture {
|
|
|
15
16
|
|
|
16
17
|
type IStockFixturePartial = Partial<IStockFixture>;
|
|
17
18
|
|
|
19
|
+
function createTestId() {
|
|
20
|
+
return randomUUID().slice(0, 12);
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
function stockAdjust(skuId: number, delta: number, suffix: string): DtoStockAdjust {
|
|
19
24
|
return {
|
|
20
25
|
skuId,
|
|
@@ -74,13 +79,13 @@ async function adjustStock(stockAdjust: DtoStockAdjust): Promise<EntityStockBala
|
|
|
74
79
|
return await app.bean.executor.performAction('post', actionPath, { body: stockAdjust });
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
describe('stockBalance.test.ts', () => {
|
|
82
|
+
describe('stockBalance.test.ts', { concurrency: false }, () => {
|
|
78
83
|
it('action:stockBalance:adjustStock appends an audit with server-scoped balance', async () => {
|
|
79
84
|
await app.bean.executor.mockCtx(async () => {
|
|
80
85
|
let fixture: IStockFixture | undefined;
|
|
81
86
|
await app.bean.passport.signinMock();
|
|
82
87
|
try {
|
|
83
|
-
fixture = await createSku(
|
|
88
|
+
fixture = await createSku(createTestId());
|
|
84
89
|
const balance = await adjustStock(
|
|
85
90
|
stockAdjust(fixture.skuId, 8, `${fixture.skuId}-initial`),
|
|
86
91
|
);
|
|
@@ -129,7 +134,7 @@ describe('stockBalance.test.ts', () => {
|
|
|
129
134
|
let fixture: IStockFixture | undefined;
|
|
130
135
|
await app.bean.passport.signinMock();
|
|
131
136
|
try {
|
|
132
|
-
fixture = await createSku(
|
|
137
|
+
fixture = await createSku(createTestId());
|
|
133
138
|
const balance = await adjustStock(
|
|
134
139
|
stockAdjust(fixture.skuId, 2, `${fixture.skuId}-initial`),
|
|
135
140
|
);
|
|
@@ -157,7 +162,7 @@ describe('stockBalance.test.ts', () => {
|
|
|
157
162
|
let fixture: IStockFixture | undefined;
|
|
158
163
|
await app.bean.passport.signinMock();
|
|
159
164
|
try {
|
|
160
|
-
fixture = await createSku(
|
|
165
|
+
fixture = await createSku(createTestId());
|
|
161
166
|
const balance = await adjustStock(
|
|
162
167
|
stockAdjust(fixture.skuId, 4, `${fixture.skuId}-initial`),
|
|
163
168
|
);
|
|
@@ -236,7 +241,7 @@ describe('stockBalance.test.ts', () => {
|
|
|
236
241
|
await app.bean.executor.mockCtx(async () => {
|
|
237
242
|
await app.bean.passport.signinMock();
|
|
238
243
|
try {
|
|
239
|
-
fixture = await createSku(
|
|
244
|
+
fixture = await createSku(createTestId());
|
|
240
245
|
await adjustStock(stockAdjust(fixture.skuId, 5, `${fixture.skuId}-default`));
|
|
241
246
|
} finally {
|
|
242
247
|
await app.bean.passport.signout();
|
|
@@ -275,7 +280,7 @@ describe('stockBalance.test.ts', () => {
|
|
|
275
280
|
});
|
|
276
281
|
|
|
277
282
|
it('hides foreign stock balances and audits from operator read APIs', async () => {
|
|
278
|
-
const suffix =
|
|
283
|
+
const suffix = createTestId();
|
|
279
284
|
let fixture: IStockFixture | undefined;
|
|
280
285
|
let balanceId!: number;
|
|
281
286
|
let auditId!: number;
|
|
@@ -2,6 +2,7 @@ import type { EntityStockReservation } from 'vona-module-commerce-trade';
|
|
|
2
2
|
|
|
3
3
|
import { catchError } from '@cabloy/utils';
|
|
4
4
|
import assert from 'node:assert';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { describe, it } from 'node:test';
|
|
6
7
|
import { app } from 'vona-mock';
|
|
7
8
|
|
|
@@ -13,6 +14,10 @@ interface IStockFixture {
|
|
|
13
14
|
|
|
14
15
|
type IStockFixturePartial = Partial<IStockFixture>;
|
|
15
16
|
|
|
17
|
+
function createTestId() {
|
|
18
|
+
return randomUUID().slice(0, 12);
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
async function createSku(suffix: string, fixtures: IStockFixturePartial[]): Promise<IStockFixture> {
|
|
17
22
|
const fixture: IStockFixturePartial = {};
|
|
18
23
|
fixtures.push(fixture);
|
|
@@ -97,13 +102,13 @@ async function assertReservationRejected(skuId: number, suffix: string, expected
|
|
|
97
102
|
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
describe('stockReservation.test.ts', () => {
|
|
105
|
+
describe('stockReservation.test.ts', { concurrency: false }, () => {
|
|
101
106
|
it('reserves and consumes stock exactly once with traceable audits', async () => {
|
|
102
107
|
await app.bean.executor.mockCtx(async () => {
|
|
103
108
|
const fixtures: IStockFixturePartial[] = [];
|
|
104
109
|
await app.bean.passport.signinMock();
|
|
105
110
|
try {
|
|
106
|
-
const suffix =
|
|
111
|
+
const suffix = createTestId();
|
|
107
112
|
const fixture = await prepareStock(suffix, fixtures);
|
|
108
113
|
const reservation = await reserve(fixture.skuId, suffix);
|
|
109
114
|
const balanceAfterReserve = await app
|
|
@@ -162,7 +167,7 @@ describe('stockReservation.test.ts', () => {
|
|
|
162
167
|
const fixtures: IStockFixturePartial[] = [];
|
|
163
168
|
await app.bean.passport.signinMock();
|
|
164
169
|
try {
|
|
165
|
-
const suffix =
|
|
170
|
+
const suffix = createTestId();
|
|
166
171
|
const inactiveSku = await prepareStock(`${suffix}-inactive`, fixtures);
|
|
167
172
|
await app.scope('commerce-catalog').model.sku.updateById(inactiveSku.skuId, {
|
|
168
173
|
lifecycle: 'inactive',
|
|
@@ -192,7 +197,7 @@ describe('stockReservation.test.ts', () => {
|
|
|
192
197
|
const fixtures: IStockFixturePartial[] = [];
|
|
193
198
|
await app.bean.passport.signinMock();
|
|
194
199
|
try {
|
|
195
|
-
const suffix =
|
|
200
|
+
const suffix = createTestId();
|
|
196
201
|
const fixture = await prepareStock(suffix, fixtures);
|
|
197
202
|
const releasedReservation = await reserve(fixture.skuId, `${suffix}-release`);
|
|
198
203
|
const released = await app.scope('commerce-trade').service.stockBalance.release({
|
|
@@ -310,7 +315,7 @@ describe('stockReservation.test.ts', () => {
|
|
|
310
315
|
return;
|
|
311
316
|
}
|
|
312
317
|
const fixtures: IStockFixturePartial[] = [];
|
|
313
|
-
const suffix =
|
|
318
|
+
const suffix = createTestId();
|
|
314
319
|
let fixture!: IStockFixture;
|
|
315
320
|
try {
|
|
316
321
|
await app.bean.executor.mockCtx(async () => {
|
|
@@ -388,7 +393,7 @@ describe('stockReservation.test.ts', () => {
|
|
|
388
393
|
const fixtures: IStockFixturePartial[] = [];
|
|
389
394
|
await app.bean.passport.signinMock();
|
|
390
395
|
try {
|
|
391
|
-
const suffix =
|
|
396
|
+
const suffix = createTestId();
|
|
392
397
|
const fixture = await prepareStock(suffix, fixtures);
|
|
393
398
|
const balance = await app.scope('commerce-trade').model.stockBalance.get({
|
|
394
399
|
skuId: fixture.skuId,
|