cabloy 5.1.115 → 5.1.117
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-contract-loop/SKILL.md +8 -2
- package/.claude/skills/cabloy-contract-loop/evals/evals.json +6 -0
- package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +13 -0
- package/.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +16 -3
- package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +12 -0
- package/.github/workflows/vona-test-mysql.yml +6 -10
- package/.github/workflows/vona-test-pg.yml +9 -10
- package/.github/workflows/vona-test-sqlite3.yml +3 -0
- package/CHANGELOG.md +35 -0
- package/CLAUDE.md +2 -0
- package/cabloy-docs/.vitepress/config.mjs +6 -0
- package/cabloy-docs/ai/docs-skills-rules-mapping.md +7 -0
- package/cabloy-docs/backend/aop-overview.md +16 -24
- package/cabloy-docs/backend/backend-source-reading-roadmap.md +26 -2
- package/cabloy-docs/backend/controller-aop-guide.md +38 -14
- package/cabloy-docs/backend/logger-guide.md +1 -0
- package/cabloy-docs/backend/menu-guide.md +1 -1
- package/cabloy-docs/backend/orm-mutation-guide.md +36 -10
- package/cabloy-docs/backend/orm-select-guide.md +15 -10
- package/cabloy-docs/backend/rate-limit-guide.md +97 -0
- package/cabloy-docs/backend/telemetry-guide.md +93 -0
- package/cabloy-docs/backend/vona-source-reading-map.md +44 -1
- package/cabloy-docs/frontend/model-resource-owner-pattern.md +2 -0
- package/cabloy-docs/frontend/ssr-overview.md +2 -0
- package/cabloy-docs/fullstack/admin-resource-and-web-self-service.md +329 -0
- package/cabloy-docs/fullstack/contract-loop-playbook.md +1 -1
- package/cabloy-docs/fullstack/introduction.md +1 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +479 -1
- package/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoBaseName%>.tsx_ +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoMutateName%>.tsx_ +2 -2
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoResItemName%>.tsx_ +10 -10
- package/vona/packages-cli/cli-set-api/cli/templates/tools/masterDetail/boilerplate/dto/<%=argv.detailDtoViewName%>.tsx_ +2 -2
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +13 -4
- 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/core/logger/utils.ts +12 -3
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +237 -164
- package/vona/src/backend/config/config/config.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +81 -25
- package/vona/src/suite/a-commerce/modules/commerce-member/src/bean/ssrMenu.address.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/controller/address.ts +42 -20
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +35 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +39 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineReq.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineView.tsx +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +1 -7
- package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +43 -42
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +168 -127
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +76 -3
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/dto/paymentAttemptView.tsx +2 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAudit.tsx +54 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/model/paymentAudit.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/mockPaymentAdapter.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +22 -7
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponIssue.tsx +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +480 -8
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/ssrMenu.test.ts +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +309 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.index.ts +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/meta.version.ts +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/bean/ssrMenu.order.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +69 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/payment.ts +23 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +4 -4
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderDetail.tsx +72 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderMineReq.tsx +21 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderMineRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectReq.tsx +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectRes.tsx +11 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +38 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderShip.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +26 -0
- package/vona/src/suite/a-commerce/modules/{commerce-member/src/dto/addressCreate.tsx → commerce-trade/src/dto/orderView.tsx} +5 -17
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/paymentOutcomeCreate.tsx +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/paymentOutcomeResult.tsx +29 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/orderAudit.tsx +13 -7
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/shipment.tsx +28 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/shipment.ts +10 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/cart.ts +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +436 -34
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +4 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +160 -80
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +773 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +212 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +37 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +29 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +714 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +282 -9
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipment.test.ts +260 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +15 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts +20 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/testLock.ts +11 -0
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +21 -0
- package/vona/src/suite/a-home/modules/home-user/src/controller/passportTest.ts +21 -0
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +77 -0
- package/vona/src/suite-vendor/a-vona/modules/a-broadcast/package.json +2 -1
- package/vona/src/suite-vendor/a-vona/modules/a-broadcast/src/service/broadcast.ts +84 -23
- package/vona/src/suite-vendor/a-vona/modules/a-broadcast/src/types/broadcast.ts +7 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/src/lib/core.ts +8 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +4 -2
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +68 -53
- package/vona/src/suite-vendor/a-vona/modules/a-orm/cli/model/metadata/generate.ts +10 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_cache.ts +12 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +3 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_utils.ts +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +4 -2
- package/vona/src/suite-vendor/a-vona/modules/a-queue/src/service/queue.ts +103 -54
- package/vona/src/suite-vendor/a-vona/modules/a-queue/src/types/queue.ts +5 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +48 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/.metadata/index.ts +100 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/bean/interceptor.rateLimit.ts +72 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/index.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/main.ts +28 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/service/rateLimit.ts +75 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/rateLimit.ts +19 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redis.ts +1 -2
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/types/redis.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +63 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/.metadata/index.ts +132 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +71 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +73 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/config/config.ts +83 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/index.ts +2 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/ingress.ts +34 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/monkey.ts +22 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +201 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/types/index.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/types/telemetry.ts +22 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/test/telemetry.test.ts +339 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-vona/package.json +3 -1
- package/vona/src/suite-vendor/a-vona/tsconfig.json +3 -0
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +3 -2
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +87 -127
- package/zova/src/front/config/config/config.cabloyCommerce.ts +4 -0
- package/zova/src/suite/a-commerce/modules/commerce-member/cli/openapi.config.ts +9 -1
- package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +16 -0
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/commerceMemberAddress.ts +94 -50
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +162 -52
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +2656 -1298
- package/zova/src/suite/a-commerce/modules/commerce-member/src/apiSchema/commerceMemberAddress.ts +23 -13
- package/zova/src/suite/a-commerce/modules/commerce-member/src/model/addressMine.ts +67 -0
- package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +21 -11
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +40 -32
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +638 -633
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +6 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/component/tableCellActionShip.ts +31 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +248 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/checkout.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/order.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/orders.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/page/payment.ts +19 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +114 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradePayment.ts +36 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +75 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +762 -46
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +35 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradePayment.ts +13 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/bean/tableCell.actionShip.tsx +41 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionShip/controller.tsx +66 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +2 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +2 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +38 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/orderMine.ts +31 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/payment.ts +25 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +14 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +124 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +105 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +95 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +88 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +28 -0
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/command.delete.tsx +3 -2
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionUpdate.tsx +2 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionView.tsx +1 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +4 -0
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressUpdate.tsx +0 -39
|
@@ -4,32 +4,107 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { describe, it } from 'node:test';
|
|
5
5
|
import { app } from 'vona-mock';
|
|
6
6
|
|
|
7
|
+
import type { EntityCouponGrant } from '../src/entity/couponGrant.tsx';
|
|
8
|
+
import type { EntityCouponTemplate } from '../src/entity/couponTemplate.tsx';
|
|
9
|
+
|
|
7
10
|
interface IFixture {
|
|
8
11
|
templateId?: number;
|
|
9
12
|
grantId?: number;
|
|
13
|
+
userId?: number;
|
|
14
|
+
templateIds?: number[];
|
|
15
|
+
grantIds?: number[];
|
|
16
|
+
userIds?: number[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createTestId() {
|
|
20
|
+
return randomUUID().slice(0, 12);
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
async function cleanup(fixture: IFixture) {
|
|
13
24
|
const promotion = app.scope('commerce-promotion');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
const grantIds = [...new Set([fixture.grantId, ...(fixture.grantIds ?? [])].filter(Boolean))];
|
|
26
|
+
const templateIds = [
|
|
27
|
+
...new Set([fixture.templateId, ...(fixture.templateIds ?? [])].filter(Boolean)),
|
|
28
|
+
];
|
|
29
|
+
const userIds = [...new Set([fixture.userId, ...(fixture.userIds ?? [])].filter(Boolean))];
|
|
30
|
+
for (const grantId of grantIds) {
|
|
31
|
+
await promotion.model.couponAudit.delete({ couponGrantId: grantId });
|
|
32
|
+
await promotion.model.couponGrant.delete({ id: grantId });
|
|
17
33
|
}
|
|
18
|
-
|
|
19
|
-
await promotion.model.couponTemplate.delete({ id:
|
|
34
|
+
for (const templateId of templateIds) {
|
|
35
|
+
await promotion.model.couponTemplate.delete({ id: templateId });
|
|
20
36
|
}
|
|
37
|
+
for (const userId of userIds) {
|
|
38
|
+
await app.scope('home-user').model.roleUser.delete({ userId });
|
|
39
|
+
await app.bean.user.removeById(userId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function createTemplate(
|
|
44
|
+
fixture: IFixture,
|
|
45
|
+
suffix: string,
|
|
46
|
+
overrides: Partial<EntityCouponTemplate> = {},
|
|
47
|
+
): Promise<EntityCouponTemplate> {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const template = await app.scope('commerce-promotion').model.couponTemplate.insert({
|
|
50
|
+
name: `Coupon ${suffix}`,
|
|
51
|
+
state: 'active',
|
|
52
|
+
currency: 'USD',
|
|
53
|
+
discountCents: 500,
|
|
54
|
+
minSpendCents: 1_000,
|
|
55
|
+
validFrom: new Date(now - 60_000),
|
|
56
|
+
validUntil: new Date(now + 60_000),
|
|
57
|
+
issuedCount: 0,
|
|
58
|
+
redeemedCount: 0,
|
|
59
|
+
...overrides,
|
|
60
|
+
});
|
|
61
|
+
(fixture.templateIds ??= []).push(template.id as number);
|
|
62
|
+
return template;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function createGrant(
|
|
66
|
+
fixture: IFixture,
|
|
67
|
+
template: EntityCouponTemplate,
|
|
68
|
+
userId: number,
|
|
69
|
+
suffix: string,
|
|
70
|
+
overrides: Partial<EntityCouponGrant> = {},
|
|
71
|
+
): Promise<EntityCouponGrant> {
|
|
72
|
+
const grant = await app.scope('commerce-promotion').model.couponGrant.insert({
|
|
73
|
+
templateId: template.id,
|
|
74
|
+
userId,
|
|
75
|
+
couponCode: `grant-${suffix}`,
|
|
76
|
+
state: 'available',
|
|
77
|
+
templateNameSnapshot: template.name,
|
|
78
|
+
currencySnapshot: template.currency,
|
|
79
|
+
discountCentsSnapshot: template.discountCents,
|
|
80
|
+
minSpendCentsSnapshot: template.minSpendCents,
|
|
81
|
+
validFromSnapshot: template.validFrom,
|
|
82
|
+
validUntilSnapshot: template.validUntil,
|
|
83
|
+
...overrides,
|
|
84
|
+
});
|
|
85
|
+
(fixture.grantIds ??= []).push(grant.id as number);
|
|
86
|
+
return grant;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function registerCustomer(fixture: IFixture, name: string) {
|
|
90
|
+
await app.bean.user.register({ name }, true);
|
|
91
|
+
await app.bean.passport.signinMock(name as any);
|
|
92
|
+
const customer = app.bean.passport.currentUser!;
|
|
93
|
+
(fixture.userIds ??= []).push(customer.id as number);
|
|
94
|
+
return customer;
|
|
21
95
|
}
|
|
22
96
|
|
|
23
97
|
describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
24
98
|
it('enforces fixed-discount eligibility and exact-once transitions', async () => {
|
|
25
99
|
await app.bean.executor.mockCtx(async () => {
|
|
26
100
|
const fixture: IFixture = {};
|
|
27
|
-
const suffix =
|
|
101
|
+
const suffix = createTestId();
|
|
28
102
|
try {
|
|
29
103
|
const customerName = `coupon-customer-${suffix}`;
|
|
30
104
|
await app.bean.user.register({ name: customerName }, true);
|
|
31
105
|
await app.bean.passport.signinMock(customerName as any);
|
|
32
106
|
const customer = app.bean.passport.currentUser!;
|
|
107
|
+
fixture.userId = customer.id as number;
|
|
33
108
|
fixture.templateId = (
|
|
34
109
|
await app.scope('commerce-promotion').model.couponTemplate.insert({
|
|
35
110
|
name: `Coupon ${suffix}`,
|
|
@@ -39,14 +114,29 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
|
39
114
|
minSpendCents: 1000,
|
|
40
115
|
validFrom: new Date(Date.now() - 1_000),
|
|
41
116
|
validUntil: new Date(Date.now() + 60_000),
|
|
42
|
-
totalIssueLimit:
|
|
117
|
+
totalIssueLimit: 2,
|
|
43
118
|
totalUsageLimit: 1,
|
|
44
|
-
perCustomerIssueLimit:
|
|
119
|
+
perCustomerIssueLimit: 3,
|
|
45
120
|
issuedCount: 0,
|
|
46
121
|
redeemedCount: 0,
|
|
47
122
|
})
|
|
48
123
|
).id as number;
|
|
49
124
|
const coupon = app.scope('commerce-promotion').service.coupon;
|
|
125
|
+
const [, issuanceLengthError] = await catchError(() =>
|
|
126
|
+
coupon.issue({
|
|
127
|
+
templateId: fixture.templateId!,
|
|
128
|
+
userId: customer.id,
|
|
129
|
+
correlationId: 'x'.repeat(81),
|
|
130
|
+
reason: 'test overlong issue correlation',
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
assert.equal(issuanceLengthError?.code, 400);
|
|
134
|
+
assert.equal(
|
|
135
|
+
await app
|
|
136
|
+
.scope('commerce-promotion')
|
|
137
|
+
.model.couponGrant.get({ couponCode: 'x'.repeat(81) }),
|
|
138
|
+
undefined,
|
|
139
|
+
);
|
|
50
140
|
const grant = await coupon.issue({
|
|
51
141
|
templateId: fixture.templateId,
|
|
52
142
|
userId: customer.id,
|
|
@@ -56,6 +146,20 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
|
56
146
|
fixture.grantId = grant.id as number;
|
|
57
147
|
assert.equal(grant.state, 'available');
|
|
58
148
|
assert.equal(grant.discountCentsSnapshot, 500);
|
|
149
|
+
const exactLimitGrant = await coupon.issue({
|
|
150
|
+
templateId: fixture.templateId,
|
|
151
|
+
userId: customer.id,
|
|
152
|
+
correlationId: 'a'.repeat(80),
|
|
153
|
+
reason: 'test exact issue correlation limit',
|
|
154
|
+
});
|
|
155
|
+
assert.equal(exactLimitGrant.couponCode.length, 80);
|
|
156
|
+
await app.scope('commerce-promotion').model.couponAudit.delete({
|
|
157
|
+
couponGrantId: exactLimitGrant.id,
|
|
158
|
+
});
|
|
159
|
+
await app.scope('commerce-promotion').model.couponGrant.delete({ id: exactLimitGrant.id });
|
|
160
|
+
await app.scope('commerce-promotion').model.couponTemplate.updateById(fixture.templateId, {
|
|
161
|
+
issuedCount: 1,
|
|
162
|
+
});
|
|
59
163
|
|
|
60
164
|
const [_, belowMinimumError] = await catchError(() =>
|
|
61
165
|
coupon.reserve({
|
|
@@ -95,6 +199,25 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
|
95
199
|
reason: 'test reserve',
|
|
96
200
|
});
|
|
97
201
|
assert.equal(replay.couponGrant.id, grant.id);
|
|
202
|
+
const [, wrongCustomerError] = await catchError(() =>
|
|
203
|
+
coupon.reserve({
|
|
204
|
+
couponGrantId: grant.id,
|
|
205
|
+
userId: customer.id + 10_000,
|
|
206
|
+
orderId: 100_001,
|
|
207
|
+
eligibleSubtotalCents: 1_200,
|
|
208
|
+
currency: 'USD',
|
|
209
|
+
correlationId: `reserve-wrong-customer-${suffix}`,
|
|
210
|
+
reason: 'test wrong customer',
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
assert.equal(wrongCustomerError?.code, 404);
|
|
214
|
+
const reserveAuditsBeforeRelease = await app
|
|
215
|
+
.scope('commerce-promotion')
|
|
216
|
+
.model.couponAudit.select({ where: { couponGrantId: grant.id } });
|
|
217
|
+
assert.deepEqual(
|
|
218
|
+
reserveAuditsBeforeRelease.map(item => item.operation),
|
|
219
|
+
['issue', 'reserve'],
|
|
220
|
+
);
|
|
98
221
|
|
|
99
222
|
const released = await coupon.release({
|
|
100
223
|
couponGrantId: grant.id,
|
|
@@ -103,6 +226,12 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
|
103
226
|
reason: 'test release',
|
|
104
227
|
});
|
|
105
228
|
assert.equal(released.state, 'available');
|
|
229
|
+
const releasedPersisted = await app
|
|
230
|
+
.scope('commerce-promotion')
|
|
231
|
+
.model.couponGrant.getById(grant.id);
|
|
232
|
+
assert.equal(releasedPersisted?.reservationOrderId, null);
|
|
233
|
+
assert.equal(releasedPersisted?.reservationCorrelationId, null);
|
|
234
|
+
assert.equal(releasedPersisted?.reservedAt, null);
|
|
106
235
|
const releaseReplay = await coupon.release({
|
|
107
236
|
couponGrantId: grant.id,
|
|
108
237
|
orderId: 100_001,
|
|
@@ -124,6 +253,35 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
|
124
253
|
reason: 'test reserve again',
|
|
125
254
|
});
|
|
126
255
|
assert.equal(reservedAgain.discountCents, 500);
|
|
256
|
+
const anotherGrant = await coupon.issue({
|
|
257
|
+
templateId: fixture.templateId,
|
|
258
|
+
userId: customer.id,
|
|
259
|
+
correlationId: `issue-another-${suffix}`,
|
|
260
|
+
reason: 'test second grant',
|
|
261
|
+
});
|
|
262
|
+
const [, secondCouponError] = await catchError(() =>
|
|
263
|
+
coupon.reserve({
|
|
264
|
+
couponGrantId: anotherGrant.id,
|
|
265
|
+
userId: customer.id,
|
|
266
|
+
orderId: 100_002,
|
|
267
|
+
eligibleSubtotalCents: 1_200,
|
|
268
|
+
currency: 'USD',
|
|
269
|
+
correlationId: `reserve-another-${suffix}`,
|
|
270
|
+
reason: 'test second coupon',
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
assert.equal(secondCouponError?.code, 409);
|
|
274
|
+
assert.equal(
|
|
275
|
+
(await app.scope('commerce-promotion').model.couponGrant.getById(anotherGrant.id))?.state,
|
|
276
|
+
'available',
|
|
277
|
+
);
|
|
278
|
+
await app.scope('commerce-promotion').model.couponAudit.delete({
|
|
279
|
+
couponGrantId: anotherGrant.id,
|
|
280
|
+
});
|
|
281
|
+
await app.scope('commerce-promotion').model.couponGrant.delete({ id: anotherGrant.id });
|
|
282
|
+
await app.scope('commerce-promotion').model.couponTemplate.updateById(fixture.templateId, {
|
|
283
|
+
issuedCount: 1,
|
|
284
|
+
});
|
|
127
285
|
const redeemed = await coupon.redeem({
|
|
128
286
|
couponGrantId: grant.id,
|
|
129
287
|
orderId: 100_002,
|
|
@@ -154,9 +312,323 @@ describe('couponReservation.test.ts', { concurrency: false }, () => {
|
|
|
154
312
|
['issue', 'reserve', 'release', 'reserve', 'redeem'],
|
|
155
313
|
);
|
|
156
314
|
} finally {
|
|
315
|
+
await app.bean.passport.signout();
|
|
157
316
|
await cleanup(fixture);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('rejects invalid, expired, and exhausted issuance and reservation without writes', async () => {
|
|
322
|
+
await app.bean.executor.mockCtx(async () => {
|
|
323
|
+
const fixture: IFixture = {};
|
|
324
|
+
const suffix = createTestId();
|
|
325
|
+
try {
|
|
326
|
+
const customer = await registerCustomer(fixture, `coupon-boundary-${suffix}`);
|
|
327
|
+
const coupon = app.scope('commerce-promotion').service.coupon;
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
const disabledTemplate = await createTemplate(fixture, `disabled-${suffix}`, {
|
|
330
|
+
state: 'disabled',
|
|
331
|
+
});
|
|
332
|
+
const expiredTemplate = await createTemplate(fixture, `expired-${suffix}`, {
|
|
333
|
+
validFrom: new Date(now - 120_000),
|
|
334
|
+
validUntil: new Date(now - 60_000),
|
|
335
|
+
});
|
|
336
|
+
const issueExhaustedTemplate = await createTemplate(fixture, `issue-exhausted-${suffix}`, {
|
|
337
|
+
totalIssueLimit: 1,
|
|
338
|
+
issuedCount: 1,
|
|
339
|
+
});
|
|
340
|
+
const perCustomerLimitedTemplate = await createTemplate(fixture, `per-customer-${suffix}`, {
|
|
341
|
+
perCustomerIssueLimit: 1,
|
|
342
|
+
});
|
|
343
|
+
const perCustomerGrant = await coupon.issue({
|
|
344
|
+
templateId: perCustomerLimitedTemplate.id,
|
|
345
|
+
userId: customer.id,
|
|
346
|
+
correlationId: `issue-per-customer-first-${suffix}`,
|
|
347
|
+
reason: 'test per-customer first issuance',
|
|
348
|
+
});
|
|
349
|
+
(fixture.grantIds ??= []).push(perCustomerGrant.id as number);
|
|
350
|
+
const [__, perCustomerError] = await catchError(() =>
|
|
351
|
+
coupon.issue({
|
|
352
|
+
templateId: perCustomerLimitedTemplate.id,
|
|
353
|
+
userId: customer.id,
|
|
354
|
+
correlationId: `issue-per-customer-second-${suffix}`,
|
|
355
|
+
reason: 'test per-customer exhausted issuance',
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
assert.equal(perCustomerError?.code, 409);
|
|
359
|
+
assert.equal(
|
|
360
|
+
await app
|
|
361
|
+
.scope('commerce-promotion')
|
|
362
|
+
.model.couponGrant.get({ couponCode: `issue-per-customer-second-${suffix}` }),
|
|
363
|
+
undefined,
|
|
364
|
+
);
|
|
365
|
+
assert.equal(
|
|
366
|
+
(
|
|
367
|
+
await app
|
|
368
|
+
.scope('commerce-promotion')
|
|
369
|
+
.model.couponTemplate.getById(perCustomerLimitedTemplate.id)
|
|
370
|
+
)?.issuedCount,
|
|
371
|
+
1,
|
|
372
|
+
);
|
|
373
|
+
for (const [template, label] of [
|
|
374
|
+
[disabledTemplate, 'disabled'],
|
|
375
|
+
[expiredTemplate, 'expired'],
|
|
376
|
+
[issueExhaustedTemplate, 'issue-exhausted'],
|
|
377
|
+
] as const) {
|
|
378
|
+
const correlationId = `issue-${label}-${suffix}`;
|
|
379
|
+
const [_, error] = await catchError(() =>
|
|
380
|
+
coupon.issue({
|
|
381
|
+
templateId: template.id,
|
|
382
|
+
userId: customer.id,
|
|
383
|
+
correlationId,
|
|
384
|
+
reason: `test ${label} issuance`,
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
assert.equal(error?.code, 409);
|
|
388
|
+
assert.equal(
|
|
389
|
+
await app
|
|
390
|
+
.scope('commerce-promotion')
|
|
391
|
+
.model.couponGrant.get({ couponCode: correlationId }),
|
|
392
|
+
undefined,
|
|
393
|
+
);
|
|
394
|
+
assert.equal(
|
|
395
|
+
(await app.scope('commerce-promotion').model.couponTemplate.getById(template.id))
|
|
396
|
+
?.issuedCount,
|
|
397
|
+
template.issuedCount,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const disabledReservationTemplate = await createTemplate(
|
|
402
|
+
fixture,
|
|
403
|
+
`reserve-disabled-${suffix}`,
|
|
404
|
+
{ state: 'disabled' },
|
|
405
|
+
);
|
|
406
|
+
const expiredReservationTemplate = await createTemplate(
|
|
407
|
+
fixture,
|
|
408
|
+
`reserve-expired-${suffix}`,
|
|
409
|
+
);
|
|
410
|
+
const usageExhaustedTemplate = await createTemplate(
|
|
411
|
+
fixture,
|
|
412
|
+
`reserve-exhausted-${suffix}`,
|
|
413
|
+
{ totalUsageLimit: 1, redeemedCount: 1 },
|
|
414
|
+
);
|
|
415
|
+
const disabledGrant = await createGrant(
|
|
416
|
+
fixture,
|
|
417
|
+
disabledReservationTemplate,
|
|
418
|
+
customer.id as number,
|
|
419
|
+
`reserve-disabled-${suffix}`,
|
|
420
|
+
);
|
|
421
|
+
const expiredGrant = await createGrant(
|
|
422
|
+
fixture,
|
|
423
|
+
expiredReservationTemplate,
|
|
424
|
+
customer.id as number,
|
|
425
|
+
`reserve-expired-${suffix}`,
|
|
426
|
+
{
|
|
427
|
+
validFromSnapshot: new Date(now - 120_000),
|
|
428
|
+
validUntilSnapshot: new Date(now - 60_000),
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
const usageExhaustedGrant = await createGrant(
|
|
432
|
+
fixture,
|
|
433
|
+
usageExhaustedTemplate,
|
|
434
|
+
customer.id as number,
|
|
435
|
+
`reserve-exhausted-${suffix}`,
|
|
436
|
+
);
|
|
437
|
+
for (const [grant, label] of [
|
|
438
|
+
[disabledGrant, 'disabled'],
|
|
439
|
+
[expiredGrant, 'expired'],
|
|
440
|
+
[usageExhaustedGrant, 'usage-exhausted'],
|
|
441
|
+
] as const) {
|
|
442
|
+
const [_, error] = await catchError(() =>
|
|
443
|
+
coupon.reserve({
|
|
444
|
+
couponGrantId: grant.id,
|
|
445
|
+
userId: customer.id,
|
|
446
|
+
orderId: 200_000 + grant.id,
|
|
447
|
+
eligibleSubtotalCents: 1_000,
|
|
448
|
+
currency: 'USD',
|
|
449
|
+
correlationId: `reserve-${label}-${suffix}`,
|
|
450
|
+
reason: `test ${label} reservation`,
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
assert.equal(error?.code, 409);
|
|
454
|
+
assert.equal(
|
|
455
|
+
(await app.scope('commerce-promotion').model.couponGrant.getById(grant.id))?.state,
|
|
456
|
+
'available',
|
|
457
|
+
);
|
|
458
|
+
assert.deepEqual(
|
|
459
|
+
await app
|
|
460
|
+
.scope('commerce-promotion')
|
|
461
|
+
.model.couponAudit.select({ where: { couponGrantId: grant.id } }),
|
|
462
|
+
[],
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
158
466
|
await app.bean.passport.signout();
|
|
467
|
+
await cleanup(fixture);
|
|
159
468
|
}
|
|
160
469
|
});
|
|
161
470
|
});
|
|
471
|
+
|
|
472
|
+
it('rejects a direct issue action for a recipient from another instance', async () => {
|
|
473
|
+
const defaultFixture: IFixture = {};
|
|
474
|
+
const foreignFixture: IFixture = {};
|
|
475
|
+
const suffix = createTestId();
|
|
476
|
+
let template!: EntityCouponTemplate;
|
|
477
|
+
let foreignUserId!: number;
|
|
478
|
+
try {
|
|
479
|
+
await app.bean.executor.mockCtx(async () => {
|
|
480
|
+
template = await createTemplate(defaultFixture, `foreign-recipient-${suffix}`);
|
|
481
|
+
});
|
|
482
|
+
await app.bean.executor.mockCtx(
|
|
483
|
+
async () => {
|
|
484
|
+
const customer = await registerCustomer(foreignFixture, `coupon-foreign-${suffix}`);
|
|
485
|
+
foreignUserId = customer.id as number;
|
|
486
|
+
await app.bean.passport.signout();
|
|
487
|
+
},
|
|
488
|
+
{ instanceName: 'shareTest' as any },
|
|
489
|
+
);
|
|
490
|
+
await app.bean.executor.mockCtx(async () => {
|
|
491
|
+
await app.bean.passport.signinMock();
|
|
492
|
+
try {
|
|
493
|
+
const [_, error] = await catchError(() =>
|
|
494
|
+
app.bean.executor.performAction('post', '/commerce/promotion/coupon/issue', {
|
|
495
|
+
body: {
|
|
496
|
+
templateId: template.id,
|
|
497
|
+
userId: foreignUserId,
|
|
498
|
+
correlationId: `foreign-recipient-${suffix}`,
|
|
499
|
+
reason: 'test foreign recipient issue action',
|
|
500
|
+
},
|
|
501
|
+
}),
|
|
502
|
+
);
|
|
503
|
+
assert.equal(error?.code, 404);
|
|
504
|
+
assert.equal(
|
|
505
|
+
await app
|
|
506
|
+
.scope('commerce-promotion')
|
|
507
|
+
.model.couponGrant.get({ couponCode: `foreign-recipient-${suffix}` }),
|
|
508
|
+
undefined,
|
|
509
|
+
);
|
|
510
|
+
assert.equal(
|
|
511
|
+
(await app.scope('commerce-promotion').model.couponTemplate.getById(template.id))
|
|
512
|
+
?.issuedCount,
|
|
513
|
+
0,
|
|
514
|
+
);
|
|
515
|
+
} finally {
|
|
516
|
+
await app.bean.passport.signout();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
} finally {
|
|
520
|
+
await app.bean.executor.mockCtx(
|
|
521
|
+
async () => {
|
|
522
|
+
await cleanup(foreignFixture);
|
|
523
|
+
},
|
|
524
|
+
{ instanceName: 'shareTest' as any },
|
|
525
|
+
);
|
|
526
|
+
await app.bean.executor.mockCtx(async () => {
|
|
527
|
+
await cleanup(defaultFixture);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('allows exactly one PostgreSQL reservation when a coupon has one total usage', async t => {
|
|
533
|
+
if (process.env.DATABASE_DEFAULT_CLIENT !== 'pg') {
|
|
534
|
+
t.skip('requires PostgreSQL row-lock contention');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const fixture: IFixture = {};
|
|
538
|
+
const suffix = createTestId();
|
|
539
|
+
let customerId!: number;
|
|
540
|
+
let customerName!: string;
|
|
541
|
+
let grantIds!: number[];
|
|
542
|
+
try {
|
|
543
|
+
await app.bean.executor.mockCtx(async () => {
|
|
544
|
+
customerName = `coupon-concurrent-${suffix}`;
|
|
545
|
+
const customer = await registerCustomer(fixture, customerName);
|
|
546
|
+
customerId = customer.id as number;
|
|
547
|
+
const template = await createTemplate(fixture, `concurrent-${suffix}`, {
|
|
548
|
+
totalIssueLimit: 2,
|
|
549
|
+
totalUsageLimit: 1,
|
|
550
|
+
});
|
|
551
|
+
const coupon = app.scope('commerce-promotion').service.coupon;
|
|
552
|
+
grantIds = [
|
|
553
|
+
(
|
|
554
|
+
await coupon.issue({
|
|
555
|
+
templateId: template.id,
|
|
556
|
+
userId: customerId,
|
|
557
|
+
correlationId: `concurrent-first-${suffix}`,
|
|
558
|
+
reason: 'test concurrent first issue',
|
|
559
|
+
})
|
|
560
|
+
).id as number,
|
|
561
|
+
(
|
|
562
|
+
await coupon.issue({
|
|
563
|
+
templateId: template.id,
|
|
564
|
+
userId: customerId,
|
|
565
|
+
correlationId: `concurrent-second-${suffix}`,
|
|
566
|
+
reason: 'test concurrent second issue',
|
|
567
|
+
})
|
|
568
|
+
).id as number,
|
|
569
|
+
];
|
|
570
|
+
fixture.grantIds = grantIds;
|
|
571
|
+
await app.bean.passport.signout();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const reserveInContext = async (grantId: number, label: string) => {
|
|
575
|
+
return await app.bean.executor.mockCtx(async () => {
|
|
576
|
+
await app.bean.passport.signinMock(customerName as any);
|
|
577
|
+
try {
|
|
578
|
+
return await app.scope('commerce-promotion').service.coupon.reserve({
|
|
579
|
+
couponGrantId: grantId,
|
|
580
|
+
userId: customerId,
|
|
581
|
+
orderId: 300_000 + grantId,
|
|
582
|
+
eligibleSubtotalCents: 1_000,
|
|
583
|
+
currency: 'USD',
|
|
584
|
+
correlationId: `concurrent-${label}-${suffix}`,
|
|
585
|
+
reason: `test concurrent ${label} reservation`,
|
|
586
|
+
});
|
|
587
|
+
} finally {
|
|
588
|
+
await app.bean.passport.signout();
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
const results = await Promise.allSettled([
|
|
593
|
+
reserveInContext(grantIds[0], 'first'),
|
|
594
|
+
reserveInContext(grantIds[1], 'second'),
|
|
595
|
+
]);
|
|
596
|
+
assert.equal(
|
|
597
|
+
results.filter(result => result.status === 'fulfilled').length,
|
|
598
|
+
1,
|
|
599
|
+
JSON.stringify(results),
|
|
600
|
+
);
|
|
601
|
+
assert.equal(
|
|
602
|
+
results.filter(result => result.status === 'rejected').length,
|
|
603
|
+
1,
|
|
604
|
+
JSON.stringify(results),
|
|
605
|
+
);
|
|
606
|
+
const rejected = results.find(result => result.status === 'rejected');
|
|
607
|
+
assert.equal(
|
|
608
|
+
(rejected as PromiseRejectedResult | undefined)?.reason?.code,
|
|
609
|
+
409,
|
|
610
|
+
String((rejected as PromiseRejectedResult | undefined)?.reason),
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
await app.bean.executor.mockCtx(async () => {
|
|
614
|
+
const grants = await app.scope('commerce-promotion').model.couponGrant.select({
|
|
615
|
+
where: { templateId: fixture.templateIds![0] },
|
|
616
|
+
});
|
|
617
|
+
const audits = await app.scope('commerce-promotion').model.couponAudit.select({
|
|
618
|
+
where: { templateId: fixture.templateIds![0] },
|
|
619
|
+
orders: [['id', 'asc']],
|
|
620
|
+
});
|
|
621
|
+
assert.equal(grants.filter(grant => grant.state === 'reserved').length, 1);
|
|
622
|
+
assert.equal(grants.filter(grant => grant.state === 'available').length, 1);
|
|
623
|
+
assert.deepEqual(
|
|
624
|
+
audits.map(audit => audit.operation),
|
|
625
|
+
['issue', 'issue', 'reserve'],
|
|
626
|
+
);
|
|
627
|
+
});
|
|
628
|
+
} finally {
|
|
629
|
+
await app.bean.executor.mockCtx(async () => {
|
|
630
|
+
await cleanup(fixture);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
});
|
|
162
634
|
});
|
|
@@ -6,8 +6,10 @@ const menuNames = [
|
|
|
6
6
|
'commerce-catalog:category#category',
|
|
7
7
|
'commerce-catalog:product#product',
|
|
8
8
|
'commerce-catalog:sku#sku',
|
|
9
|
+
'commerce-member:address#address',
|
|
9
10
|
'commerce-trade:stockBalance#stockBalance',
|
|
10
11
|
'commerce-trade:stockAudit#stockAudit',
|
|
12
|
+
'commerce-trade:order#order',
|
|
11
13
|
];
|
|
12
14
|
|
|
13
15
|
async function retrieveMenus() {
|
|
@@ -53,6 +55,12 @@ describe('ssrMenu.test.ts', () => {
|
|
|
53
55
|
assert.equal(menuSku?.group, catalog.name);
|
|
54
56
|
assert.equal(menuSku?.meta?.params?.resource, 'commerce-catalog:sku');
|
|
55
57
|
|
|
58
|
+
const menuAddress = menusSystemAdmin.menus?.find(
|
|
59
|
+
menu => menu.name === 'commerce-member:address#address',
|
|
60
|
+
);
|
|
61
|
+
assert.equal(menuAddress?.group, catalog.name);
|
|
62
|
+
assert.equal(menuAddress?.meta?.params?.resource, 'commerce-member:address');
|
|
63
|
+
|
|
56
64
|
const inventory = menusSystemAdmin.groups?.find(
|
|
57
65
|
group => group.name === 'commerce-siteadmin:inventory',
|
|
58
66
|
);
|
|
@@ -69,6 +77,12 @@ describe('ssrMenu.test.ts', () => {
|
|
|
69
77
|
);
|
|
70
78
|
assert.equal(menuStockAudit?.group, inventory.name);
|
|
71
79
|
assert.equal(menuStockAudit?.meta?.params?.resource, 'commerce-trade:stockAudit');
|
|
80
|
+
|
|
81
|
+
const menuOrder = menusSystemAdmin.menus?.find(
|
|
82
|
+
menu => menu.name === 'commerce-trade:order#order',
|
|
83
|
+
);
|
|
84
|
+
assert.equal(menuOrder?.group, inventory.name);
|
|
85
|
+
assert.equal(menuOrder?.meta?.params?.resource, 'commerce-trade:order');
|
|
72
86
|
} finally {
|
|
73
87
|
await app.bean.passport.signout();
|
|
74
88
|
}
|