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.
Files changed (141) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +4 -0
  3. package/.github/workflows/vona-test-mysql.yml +12 -0
  4. package/.github/workflows/vona-test-pg.yml +5 -1
  5. package/CHANGELOG.md +20 -0
  6. package/CLAUDE.md +4 -0
  7. package/cabloy-docs/backend/internal-aop-guide.md +35 -0
  8. package/cabloy-docs/backend/unit-testing.md +72 -1
  9. package/package.json +1 -1
  10. package/vona/packages-cli/cli/package.json +1 -1
  11. package/vona/packages-cli/cli-set-api/package.json +1 -1
  12. package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +26 -13
  13. package/vona/packages-utils/compose/package.json +1 -1
  14. package/vona/packages-utils/compose/src/index.ts +19 -11
  15. package/vona/packages-vona/vona/package.json +1 -1
  16. package/vona/packages-vona/vona-core/package.json +1 -1
  17. package/vona/packages-vona/vona-core/src/lib/bean/beanContainer.ts +15 -3
  18. package/vona/packages-vona/vona-mock/package.json +1 -1
  19. package/vona/pnpm-lock.yaml +143 -12
  20. package/vona/pnpm-workspace.yaml +1 -1
  21. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +8 -0
  22. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +1 -0
  23. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  24. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  25. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuAttribute.tsx +17 -0
  26. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +10 -1
  27. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +1 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +6 -1
  29. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  30. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +228 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +15 -0
  32. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +35 -0
  33. package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +26 -0
  34. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +34 -0
  35. package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAttempt.ts +10 -0
  36. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +41 -0
  37. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +33 -0
  38. package/vona/src/suite/a-commerce/modules/commerce-promotion/package.json +4 -2
  39. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +533 -2
  40. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/locales.ts +18 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +24 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.version.ts +137 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/ssrMenu.couponTemplate.ts +30 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +37 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +37 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/coupon.ts +37 -0
  47. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/couponTemplate.ts +65 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +22 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponMineItem.tsx +32 -0
  50. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +43 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectReq.tsx +28 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectRes.tsx +11 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectResItem.tsx +39 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +19 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +25 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponAudit.tsx +73 -0
  57. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponGrant.tsx +83 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +82 -0
  59. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponAudit.ts +10 -0
  61. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponGrant.ts +10 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponTemplate.ts +10 -0
  63. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +397 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/couponTemplate.ts +52 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +162 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +8 -10
  67. package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
  68. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +277 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/bean.scheduleOrderExpiry.ts +26 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +9 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +75 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +11 -2
  73. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +11 -2
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +19 -0
  75. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +19 -0
  76. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +32 -0
  77. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAddressSnapshot.tsx +33 -0
  78. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderCouponSnapshot.tsx +35 -0
  79. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderLineSkuAttributeSnapshot.tsx +15 -0
  80. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +90 -0
  81. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +36 -0
  82. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderLine.tsx +52 -0
  83. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockReservation.tsx +4 -1
  84. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -0
  85. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderAudit.ts +10 -0
  86. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderLine.ts +10 -0
  87. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +556 -0
  88. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +15 -20
  89. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +8 -3
  90. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +158 -0
  91. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +203 -0
  92. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +163 -0
  93. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +11 -6
  94. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +11 -6
  95. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  96. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +10 -7
  97. package/vona/src/suite-vendor/a-image/package.json +1 -1
  98. package/vona/src/suite-vendor/a-vona/modules/a-aspect/package.json +1 -1
  99. package/vona/src/suite-vendor/a-vona/modules/a-aspect/src/types/aopMethod.ts +16 -3
  100. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +4 -2
  101. package/vona/src/suite-vendor/a-vona/modules/a-core/src/bean/bean.core.ts +6 -0
  102. package/vona/src/suite-vendor/a-vona/modules/a-core/src/index.ts +1 -0
  103. package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +6 -0
  104. package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/fetch.ts +7 -0
  105. package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/index.ts +1 -0
  106. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  107. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/lib/utils.ts +5 -3
  108. package/vona/src/suite-vendor/a-vona/modules/a-retryable/package.json +50 -0
  109. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/index.ts +55 -0
  110. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/this.ts +2 -0
  111. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/bean/aopMethod.retryable.ts +45 -0
  112. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/index.ts +1 -0
  113. package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.build.json +11 -0
  114. package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.json +7 -0
  115. package/vona/src/suite-vendor/a-vona/modules/a-vona/package.json +2 -1
  116. package/vona/src/suite-vendor/a-vona/package.json +2 -1
  117. package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
  118. package/zova/packages-utils/zova-jsx/package.json +3 -3
  119. package/zova/packages-zova/zova/package.json +3 -3
  120. package/zova/packages-zova/zova-core/package.json +2 -2
  121. package/zova/pnpm-lock.yaml +31 -31
  122. package/zova/src/suite/a-commerce/modules/commerce-promotion/cli/openapi.config.ts +9 -0
  123. package/zova/src/suite/a-commerce/modules/commerce-promotion/package.json +2 -1
  124. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +115 -1
  125. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/commercePromotionCoupon.ts +24 -0
  126. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/baseURL.ts +5 -0
  127. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/index.ts +3 -0
  128. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +478 -0
  129. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +8187 -0
  130. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/apiSchema/commercePromotionCoupon.ts +13 -0
  131. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +19 -0
  132. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  133. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +30 -0
  134. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +27 -0
  135. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +92 -52
  136. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +1389 -848
  137. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +13 -0
  138. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +13 -0
  139. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +3 -3
  140. package/zova/src/suite-vendor/a-zova/package.json +2 -2
  141. /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(`${Date.now()}`);
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(`${Date.now()}`);
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(`${Date.now()}`);
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(`${Date.now()}`);
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 = `${Date.now()}`;
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 = `${Date.now()}`;
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 = `${Date.now()}`;
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 = `${Date.now()}`;
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 = `${Date.now()}`;
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 = `${Date.now()}`;
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-image-cloudflare",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"