cabloy 5.1.112 → 5.1.114

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 (157) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +2 -0
  3. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -0
  4. package/.github/workflows/playwright-e2e.yml +34 -2
  5. package/.github/workflows/vona-test-mysql.yml +12 -0
  6. package/.github/workflows/vona-test-pg.yml +12 -0
  7. package/CHANGELOG.md +27 -0
  8. package/CLAUDE.md +6 -0
  9. package/cabloy-docs/backend/internal-aop-guide.md +35 -0
  10. package/cabloy-docs/backend/migration-and-changes.md +13 -9
  11. package/cabloy-docs/backend/unit-testing.md +97 -1
  12. package/e2e/scripts/startE2eVona.ts +51 -13
  13. package/e2e/specs/a-commerce/commerce.spec.ts +1 -0
  14. package/e2e/specs/cabloy-basic/basic.spec.ts +12 -7
  15. package/package.json +1 -1
  16. package/vona/packages-cli/cli/package.json +1 -1
  17. package/vona/packages-cli/cli-set-api/package.json +1 -1
  18. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.test.ts +6 -9
  19. package/vona/packages-cli/cli-set-api/toolsIsolate/test.ts +74 -32
  20. package/vona/packages-utils/compose/package.json +1 -1
  21. package/vona/packages-utils/compose/src/index.ts +19 -11
  22. package/vona/packages-vona/vona/package.json +1 -1
  23. package/vona/packages-vona/vona-core/package.json +1 -1
  24. package/vona/packages-vona/vona-core/src/lib/bean/beanContainer.ts +15 -3
  25. package/vona/packages-vona/vona-mock/package.json +1 -1
  26. package/vona/pnpm-lock.yaml +82 -67
  27. package/vona/src/backend/config/config/config.test.ts +1 -1
  28. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +8 -0
  29. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +1 -0
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuAttribute.tsx +17 -0
  33. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +10 -1
  34. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/catalog.test.ts +234 -0
  35. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +6 -1
  36. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  37. package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +228 -0
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +15 -0
  39. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +35 -0
  40. package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +26 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +34 -0
  42. package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAttempt.ts +10 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +41 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +33 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-promotion/package.json +4 -2
  46. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +533 -2
  47. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/locales.ts +18 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.index.ts +24 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/meta.version.ts +137 -0
  50. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/bean/ssrMenu.couponTemplate.ts +30 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +37 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +37 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/coupon.ts +37 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/controller/couponTemplate.ts +65 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +22 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponMineItem.tsx +32 -0
  57. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +43 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectReq.tsx +28 -0
  59. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectRes.tsx +11 -0
  60. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateSelectResItem.tsx +39 -0
  61. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +19 -0
  62. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +25 -0
  63. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponAudit.tsx +73 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponGrant.tsx +83 -0
  65. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +82 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/index.ts +1 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponAudit.ts +10 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponGrant.ts +10 -0
  69. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/model/couponTemplate.ts +10 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +397 -0
  71. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/couponTemplate.ts +52 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +162 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-seed/src/bean/meta.version.ts +11 -13
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
  75. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +277 -0
  76. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/bean.scheduleOrderExpiry.ts +26 -0
  77. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +9 -0
  78. package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +79 -0
  79. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +14 -1
  80. package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +14 -1
  81. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +19 -0
  82. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +19 -0
  83. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +32 -0
  84. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAddressSnapshot.tsx +33 -0
  85. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderCouponSnapshot.tsx +35 -0
  86. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderLineSkuAttributeSnapshot.tsx +15 -0
  87. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +90 -0
  88. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +36 -0
  89. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderLine.tsx +52 -0
  90. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockAudit.tsx +39 -7
  91. package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/stockReservation.tsx +4 -1
  92. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -0
  93. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderAudit.ts +10 -0
  94. package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/orderLine.ts +10 -0
  95. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +556 -0
  96. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +32 -0
  97. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +8 -3
  98. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +158 -0
  99. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +203 -0
  100. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +163 -0
  101. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +274 -66
  102. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +282 -82
  103. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  104. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +10 -7
  105. package/vona/src/suite-vendor/a-image/package.json +1 -1
  106. package/vona/src/suite-vendor/a-vona/modules/a-aspect/package.json +1 -1
  107. package/vona/src/suite-vendor/a-vona/modules/a-aspect/src/types/aopMethod.ts +16 -3
  108. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +4 -2
  109. package/vona/src/suite-vendor/a-vona/modules/a-core/src/bean/bean.core.ts +6 -0
  110. package/vona/src/suite-vendor/a-vona/modules/a-core/src/index.ts +1 -0
  111. package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +6 -0
  112. package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/fetch.ts +7 -0
  113. package/vona/src/suite-vendor/a-vona/modules/a-core/src/types/index.ts +1 -0
  114. package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-executor/src/lib/utils.ts +5 -3
  116. package/vona/src/suite-vendor/a-vona/modules/a-retryable/package.json +50 -0
  117. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/index.ts +55 -0
  118. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/.metadata/this.ts +2 -0
  119. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/bean/aopMethod.retryable.ts +45 -0
  120. package/vona/src/suite-vendor/a-vona/modules/a-retryable/src/index.ts +1 -0
  121. package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.build.json +11 -0
  122. package/vona/src/suite-vendor/a-vona/modules/a-retryable/tsconfig.json +7 -0
  123. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  124. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +2 -2
  125. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  126. package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/version.ts +12 -12
  127. package/vona/src/suite-vendor/a-vona/modules/a-version/src/types/version.ts +4 -4
  128. package/vona/src/suite-vendor/a-vona/modules/a-vona/package.json +2 -1
  129. package/vona/src/suite-vendor/a-vona/package.json +2 -1
  130. package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
  131. package/zova/packages-utils/zova-jsx/package.json +3 -3
  132. package/zova/packages-zova/zova/package.json +3 -3
  133. package/zova/packages-zova/zova-core/package.json +2 -2
  134. package/zova/pnpm-lock.yaml +14 -14
  135. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +0 -1
  136. package/zova/src/suite/a-commerce/modules/commerce-promotion/cli/openapi.config.ts +9 -0
  137. package/zova/src/suite/a-commerce/modules/commerce-promotion/package.json +2 -1
  138. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +115 -1
  139. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/commercePromotionCoupon.ts +24 -0
  140. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/baseURL.ts +5 -0
  141. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/index.ts +3 -0
  142. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +478 -0
  143. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +8187 -0
  144. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/apiSchema/commercePromotionCoupon.ts +13 -0
  145. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +19 -0
  146. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
  147. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +30 -0
  148. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +27 -0
  149. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +92 -52
  150. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +1389 -848
  151. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +13 -0
  152. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +14 -0
  153. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +17 -7
  154. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.header.tsx +1 -1
  155. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +17 -7
  156. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +3 -3
  157. package/zova/src/suite-vendor/a-zova/package.json +2 -2
@@ -2,11 +2,24 @@ 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
 
8
9
  const actionPath = '/commerce/trade/stockBalance/adjustStock';
9
10
 
11
+ interface IStockFixture {
12
+ categoryId: number;
13
+ productId: number;
14
+ skuId: number;
15
+ }
16
+
17
+ type IStockFixturePartial = Partial<IStockFixture>;
18
+
19
+ function createTestId() {
20
+ return randomUUID().slice(0, 12);
21
+ }
22
+
10
23
  function stockAdjust(skuId: number, delta: number, suffix: string): DtoStockAdjust {
11
24
  return {
12
25
  skuId,
@@ -16,57 +29,101 @@ function stockAdjust(skuId: number, delta: number, suffix: string): DtoStockAdju
16
29
  };
17
30
  }
18
31
 
19
- async function createSku(suffix: string): Promise<number> {
20
- const categoryId = await app.bean.executor.performAction('post', '/commerce/catalog/category', {
21
- body: { name: `stock-category-${suffix}`, published: true },
22
- });
23
- const productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
24
- body: { categoryId, title: `stock-product-${suffix}`, published: true },
25
- });
26
- return await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
27
- body: {
28
- productId,
29
- code: `stock-sku-${suffix}`,
30
- priceCents: 100,
31
- lifecycle: 'active',
32
- },
33
- });
32
+ async function createSku(suffix: string): Promise<IStockFixture> {
33
+ const fixture: IStockFixturePartial = {};
34
+ try {
35
+ fixture.categoryId = await app.bean.executor.performAction(
36
+ 'post',
37
+ '/commerce/catalog/category',
38
+ {
39
+ body: { name: `stock-category-${suffix}`, published: true },
40
+ },
41
+ );
42
+ fixture.productId = await app.bean.executor.performAction('post', '/commerce/catalog/product', {
43
+ body: { categoryId: fixture.categoryId, title: `stock-product-${suffix}`, published: true },
44
+ });
45
+ fixture.skuId = await app.bean.executor.performAction('post', '/commerce/catalog/sku', {
46
+ body: {
47
+ productId: fixture.productId,
48
+ code: `stock-sku-${suffix}`,
49
+ priceCents: 100,
50
+ lifecycle: 'active',
51
+ },
52
+ });
53
+ return fixture as IStockFixture;
54
+ } catch (error) {
55
+ await dropStockFixture(fixture);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ async function dropStockFixture(fixture: IStockFixturePartial | undefined) {
61
+ if (!fixture) return;
62
+ const scopeTrade = app.scope('commerce-trade');
63
+ const scopeCatalog = app.scope('commerce-catalog');
64
+ if (fixture.skuId !== undefined) {
65
+ await scopeTrade.model.stockAudit.delete({ skuId: fixture.skuId });
66
+ await scopeTrade.model.stockReservation.delete({ skuId: fixture.skuId });
67
+ await scopeTrade.model.stockBalance.delete({ skuId: fixture.skuId });
68
+ await scopeCatalog.model.sku.delete({ id: fixture.skuId });
69
+ }
70
+ if (fixture.productId !== undefined) {
71
+ await scopeCatalog.model.product.delete({ id: fixture.productId });
72
+ }
73
+ if (fixture.categoryId !== undefined) {
74
+ await scopeCatalog.model.category.delete({ id: fixture.categoryId });
75
+ }
34
76
  }
35
77
 
36
78
  async function adjustStock(stockAdjust: DtoStockAdjust): Promise<EntityStockBalance> {
37
79
  return await app.bean.executor.performAction('post', actionPath, { body: stockAdjust });
38
80
  }
39
81
 
40
- describe('stockBalance.test.ts', () => {
82
+ describe('stockBalance.test.ts', { concurrency: false }, () => {
41
83
  it('action:stockBalance:adjustStock appends an audit with server-scoped balance', async () => {
42
84
  await app.bean.executor.mockCtx(async () => {
85
+ let fixture: IStockFixture | undefined;
43
86
  await app.bean.passport.signinMock();
44
87
  try {
45
- const skuId = await createSku(`${Date.now()}`);
46
- const balance = await adjustStock(stockAdjust(skuId, 8, `${skuId}-initial`));
88
+ fixture = await createSku(createTestId());
89
+ const balance = await adjustStock(
90
+ stockAdjust(fixture.skuId, 8, `${fixture.skuId}-initial`),
91
+ );
47
92
  assert.equal(balance.onHand, 8);
48
93
  assert.equal(balance.reserved, 0);
49
94
  assert.equal(balance.available, 8);
50
95
 
51
- const adjusted = await adjustStock(stockAdjust(skuId, -3, `${skuId}-decrement`));
96
+ const adjusted = await adjustStock(
97
+ stockAdjust(fixture.skuId, -3, `${fixture.skuId}-decrement`),
98
+ );
52
99
  assert.equal(adjusted.id, balance.id);
53
100
  assert.equal(adjusted.onHand, 5);
54
101
  assert.equal(adjusted.reserved, 0);
55
102
  assert.equal(adjusted.available, 5);
56
103
 
57
104
  const audits = await app.scope('commerce-trade').model.stockAudit.select({
58
- where: { skuId },
105
+ where: { skuId: fixture.skuId },
59
106
  });
60
107
  assert.equal(audits.length, 2);
61
108
  assert.deepEqual(
62
- audits.map(item => [item.delta, item.onHand, item.reserved, item.available]),
109
+ audits.map(item => [
110
+ item.delta,
111
+ item.priorOnHand,
112
+ item.priorReserved,
113
+ item.priorAvailable,
114
+ item.onHand,
115
+ item.reserved,
116
+ item.available,
117
+ ]),
63
118
  [
64
- [8, 8, 0, 8],
65
- [-3, 5, 0, 5],
119
+ [8, 0, 0, 0, 8, 0, 8],
120
+ [-3, 8, 0, 8, 5, 0, 5],
66
121
  ],
67
122
  );
123
+ assert.equal(String(audits[0].actorId), String(app.bean.passport.currentUser!.id));
68
124
  assert.equal(audits[1].stockBalanceId, balance.id);
69
125
  } finally {
126
+ await dropStockFixture(fixture);
70
127
  await app.bean.passport.signout();
71
128
  }
72
129
  });
@@ -74,12 +131,15 @@ describe('stockBalance.test.ts', () => {
74
131
 
75
132
  it('action:stockBalance:adjustStock rejects negative stock without an audit', async () => {
76
133
  await app.bean.executor.mockCtx(async () => {
134
+ let fixture: IStockFixture | undefined;
77
135
  await app.bean.passport.signinMock();
78
136
  try {
79
- const skuId = await createSku(`${Date.now()}`);
80
- const balance = await adjustStock(stockAdjust(skuId, 2, `${skuId}-initial`));
137
+ fixture = await createSku(createTestId());
138
+ const balance = await adjustStock(
139
+ stockAdjust(fixture.skuId, 2, `${fixture.skuId}-initial`),
140
+ );
81
141
  const [_, err] = await catchError(() =>
82
- adjustStock(stockAdjust(skuId, -3, `${skuId}-reject`)),
142
+ adjustStock(stockAdjust(fixture.skuId, -3, `${fixture.skuId}-reject`)),
83
143
  );
84
144
  assert.match(err?.message ?? '', /negative/);
85
145
 
@@ -87,39 +147,66 @@ describe('stockBalance.test.ts', () => {
87
147
  assert.equal(unchanged?.onHand, 2);
88
148
  assert.equal(unchanged?.available, 2);
89
149
  const audits = await app.scope('commerce-trade').model.stockAudit.select({
90
- where: { skuId },
150
+ where: { skuId: fixture.skuId },
91
151
  });
92
152
  assert.equal(audits.length, 1);
93
153
  } finally {
154
+ await dropStockFixture(fixture);
94
155
  await app.bean.passport.signout();
95
156
  }
96
157
  });
97
158
  });
98
159
 
99
- it('action:stockBalance:adjustStock rolls back the balance when audit insertion fails', async () => {
160
+ it('rolls back balance and audit writes in one transaction', async () => {
100
161
  await app.bean.executor.mockCtx(async () => {
162
+ let fixture: IStockFixture | undefined;
101
163
  await app.bean.passport.signinMock();
102
164
  try {
103
- const skuId = await createSku(`${Date.now()}`);
104
- const balance = await adjustStock(stockAdjust(skuId, 4, `${skuId}-initial`));
105
- const modelStockAudit = app.scope('commerce-trade').model.stockAudit;
106
- const insert = modelStockAudit.insert.bind(modelStockAudit);
107
- (modelStockAudit as any).insert = async () => {
108
- throw new Error('audit insert failure');
109
- };
110
- try {
111
- const [_, err] = await catchError(() =>
112
- adjustStock(stockAdjust(skuId, 1, `${skuId}-rollback`)),
113
- );
114
- assert.match(err?.message ?? '', /audit insert failure/);
115
- } finally {
116
- (modelStockAudit as any).insert = insert;
117
- }
165
+ fixture = await createSku(createTestId());
166
+ const balance = await adjustStock(
167
+ stockAdjust(fixture.skuId, 4, `${fixture.skuId}-initial`),
168
+ );
169
+ const db = app.ctx.db;
170
+ const [_, err] = await catchError(async () => {
171
+ await db.transaction.begin(async () => {
172
+ const modelStockBalance = app
173
+ .scope('commerce-trade')
174
+ .model.stockBalance.newInstance(db);
175
+ const modelStockAudit = app.scope('commerce-trade').model.stockAudit.newInstance(db);
176
+ await modelStockBalance.updateById(balance.id, {
177
+ onHand: 5,
178
+ reserved: 0,
179
+ available: 5,
180
+ });
181
+ await modelStockAudit.insert({
182
+ stockBalanceId: balance.id,
183
+ skuId: fixture!.skuId,
184
+ actorId: app.bean.passport.currentUser!.id,
185
+ operation: 'adjust',
186
+ delta: 1,
187
+ reason: 'transaction rollback proof',
188
+ correlationId: `stock-test-${fixture!.skuId}-rollback`,
189
+ priorOnHand: 4,
190
+ priorReserved: 0,
191
+ priorAvailable: 4,
192
+ onHand: 5,
193
+ reserved: 0,
194
+ available: 5,
195
+ });
196
+ throw new Error('transaction rollback proof');
197
+ });
198
+ });
199
+ assert.match(err?.message ?? '', /transaction rollback proof/);
118
200
 
119
201
  const unchanged = await app.scope('commerce-trade').model.stockBalance.getById(balance.id);
120
202
  assert.equal(unchanged?.onHand, 4);
121
203
  assert.equal(unchanged?.available, 4);
204
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
205
+ where: { skuId: fixture.skuId },
206
+ });
207
+ assert.equal(audits.length, 1);
122
208
  } finally {
209
+ await dropStockFixture(fixture);
123
210
  await app.bean.passport.signout();
124
211
  }
125
212
  });
@@ -149,38 +236,159 @@ describe('stockBalance.test.ts', () => {
149
236
  });
150
237
 
151
238
  it('action:stockBalance:adjustStock treats a cross-instance SKU as not found', async () => {
152
- let skuId!: number;
153
- await app.bean.executor.mockCtx(async () => {
154
- await app.bean.passport.signinMock();
155
- try {
156
- skuId = await createSku(`${Date.now()}`);
157
- await adjustStock(stockAdjust(skuId, 5, `${skuId}-default`));
158
- } finally {
159
- await app.bean.passport.signout();
160
- }
161
- });
239
+ let fixture: IStockFixture | undefined;
240
+ try {
241
+ await app.bean.executor.mockCtx(async () => {
242
+ await app.bean.passport.signinMock();
243
+ try {
244
+ fixture = await createSku(createTestId());
245
+ await adjustStock(stockAdjust(fixture.skuId, 5, `${fixture.skuId}-default`));
246
+ } finally {
247
+ await app.bean.passport.signout();
248
+ }
249
+ });
162
250
 
163
- await app.bean.executor.mockCtx(
164
- async () => {
251
+ await app.bean.executor.mockCtx(
252
+ async () => {
253
+ await app.bean.passport.signinMock();
254
+ try {
255
+ const [_, err] = await catchError(() =>
256
+ adjustStock(stockAdjust(fixture!.skuId, 1, `${fixture!.skuId}-share`)),
257
+ );
258
+ assert.equal(err?.code, 404);
259
+ assert.equal(
260
+ await app.scope('commerce-trade').model.stockBalance.get({ skuId: fixture!.skuId }),
261
+ undefined,
262
+ );
263
+ assert.deepEqual(
264
+ await app.scope('commerce-trade').model.stockAudit.select({
265
+ where: { skuId: fixture!.skuId },
266
+ }),
267
+ [],
268
+ );
269
+ } finally {
270
+ await app.bean.passport.signout();
271
+ }
272
+ },
273
+ { instanceName: 'shareTest' as any },
274
+ );
275
+ } finally {
276
+ await app.bean.executor.mockCtx(async () => {
277
+ await dropStockFixture(fixture);
278
+ });
279
+ }
280
+ });
281
+
282
+ it('hides foreign stock balances and audits from operator read APIs', async () => {
283
+ const suffix = createTestId();
284
+ let fixture: IStockFixture | undefined;
285
+ let balanceId!: number;
286
+ let auditId!: number;
287
+ const reason = `stock read isolation ${suffix}`;
288
+ const correlationId = `stock-read-isolation-${suffix}`;
289
+ try {
290
+ await app.bean.executor.mockCtx(async () => {
165
291
  await app.bean.passport.signinMock();
166
292
  try {
167
- const [_, err] = await catchError(() =>
168
- adjustStock(stockAdjust(skuId, 1, `${skuId}-share`)),
293
+ fixture = await createSku(suffix);
294
+ const balance = await adjustStock({
295
+ skuId: fixture.skuId,
296
+ delta: 5,
297
+ reason,
298
+ correlationId,
299
+ });
300
+ balanceId = balance.id;
301
+ const audits = await app.scope('commerce-trade').model.stockAudit.select({
302
+ where: { skuId: fixture.skuId },
303
+ });
304
+ assert.equal(audits.length, 1);
305
+ auditId = audits[0].id;
306
+ } finally {
307
+ await app.bean.passport.signout();
308
+ }
309
+ });
310
+
311
+ await app.bean.executor.mockCtx(
312
+ async () => {
313
+ await app.bean.passport.signinMock();
314
+ try {
315
+ assert.equal(
316
+ await app.bean.executor.performAction('get', '/commerce/trade/stockBalance/:id', {
317
+ params: { id: balanceId },
318
+ }),
319
+ undefined,
320
+ );
321
+ const balances = await app.bean.executor.performAction(
322
+ 'get',
323
+ '/commerce/trade/stockBalance',
324
+ );
325
+ assert.equal(
326
+ balances.list.some(
327
+ (balance: { id: unknown }) => String(balance.id) === String(balanceId),
328
+ ),
329
+ false,
330
+ );
331
+ assert.equal(
332
+ await app.bean.executor.performAction('get', '/commerce/trade/stockAudit/:id', {
333
+ params: { id: auditId },
334
+ }),
335
+ undefined,
336
+ );
337
+ const audits = await app.bean.executor.performAction(
338
+ 'get',
339
+ '/commerce/trade/stockAudit',
340
+ );
341
+ assert.equal(
342
+ audits.list.some((audit: { id: unknown }) => String(audit.id) === String(auditId)),
343
+ false,
344
+ );
345
+ } finally {
346
+ await app.bean.passport.signout();
347
+ }
348
+ },
349
+ { instanceName: 'shareTest' as any },
350
+ );
351
+
352
+ await app.bean.executor.mockCtx(async () => {
353
+ await app.bean.passport.signinMock();
354
+ try {
355
+ const balance = await app.bean.executor.performAction(
356
+ 'get',
357
+ '/commerce/trade/stockBalance/:id',
358
+ { params: { id: balanceId } },
359
+ );
360
+ assert.deepEqual(
361
+ [balance.id, balance.skuId, balance.onHand, balance.reserved, balance.available],
362
+ [balanceId, fixture!.skuId, 5, 0, 5],
169
363
  );
170
- assert.equal(err?.code, 404);
171
- assert.equal(
172
- await app.scope('commerce-trade').model.stockBalance.get({ skuId }),
173
- undefined,
364
+ const audit = await app.bean.executor.performAction(
365
+ 'get',
366
+ '/commerce/trade/stockAudit/:id',
367
+ { params: { id: auditId } },
174
368
  );
175
369
  assert.deepEqual(
176
- await app.scope('commerce-trade').model.stockAudit.select({ where: { skuId } }),
177
- [],
370
+ [
371
+ audit.id,
372
+ audit.stockBalanceId,
373
+ audit.skuId,
374
+ audit.operation,
375
+ audit.delta,
376
+ audit.reason,
377
+ audit.correlationId,
378
+ audit.onHand,
379
+ audit.reserved,
380
+ audit.available,
381
+ ],
382
+ [auditId, balanceId, fixture!.skuId, 'adjust', 5, reason, correlationId, 5, 0, 5],
178
383
  );
179
384
  } finally {
180
385
  await app.bean.passport.signout();
181
386
  }
182
- },
183
- { instanceName: 'shareTest' as any },
184
- );
387
+ });
388
+ } finally {
389
+ await app.bean.executor.mockCtx(async () => {
390
+ await dropStockFixture(fixture);
391
+ });
392
+ }
185
393
  });
186
394
  });