cabloy 5.1.134 → 5.1.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +9 -0
- package/.claude/skills/cabloy-zova-source-reading/SKILL.md +24 -1
- package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +12 -0
- package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +28 -0
- package/.github/workflows/playwright-e2e.yml +2 -2
- package/.github/workflows/vona-test-pg.yml +1 -1
- package/.github/workflows/vona-test-sqlite3.yml +1 -1
- package/CHANGELOG.md +38 -0
- package/CLAUDE.md +1 -0
- package/cabloy-docs/.vitepress/config.mjs +7 -0
- package/cabloy-docs/backend/markdown-guide.md +189 -0
- package/cabloy-docs/frontend/component-guide.md +6 -0
- package/cabloy-docs/frontend/css-in-js-guide.md +6 -0
- package/cabloy-docs/frontend/markdown-guide.md +258 -0
- package/cabloy-docs/frontend/page-guide.md +48 -0
- package/cabloy-docs/frontend/page-params-guide.md +4 -2
- package/cabloy-docs/frontend/page-route-guide.md +2 -2
- package/cabloy-docs/frontend/zova-router-under-the-hood.md +2 -0
- package/cabloy-docs/frontend/zova-source-reading-map.md +53 -9
- package/cabloy-docs/fullstack/a-pay-payment-suite.md +333 -0
- package/cabloy-docs/fullstack/payment-sandbox-configuration.md +285 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +503 -47
- package/e2e/specs/cabloy-basic/basic.spec.ts +30 -0
- package/package.json +1 -1
- package/vona/env/.env +2 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/init/error/boilerplate/config/errors.ts +6 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-utils/localeutil/package.json +1 -1
- package/vona/packages-utils/localeutil/src/util.ts +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/patches/{zova-core@5.1.82.patch → zova-core@5.1.84.patch} +2 -2
- package/vona/pnpm-lock.yaml +826 -270
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +99 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/product.ts +3 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentUpdate.tsx +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentView.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +48 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicDetail.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +48 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +52 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/productContent.tsx +36 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +6 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/productContent.ts +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +82 -6
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productContent.test.ts +271 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +81 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +32 -15
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +8 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +8 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +13 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/providerReference.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +43 -8
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +74 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/providerReference.test.ts +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +73 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +2 -0
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +30 -0
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +7 -2
- package/vona/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +4 -1
- package/vona/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +4 -1
- package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +4 -4
- package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +2 -2
- package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +8 -1
- package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +6 -2
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +4 -4
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +64 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +54 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +137 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/bean.ssr.ts +17 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +11 -3
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
- package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +8 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/providerOperationFailure.ts +8 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +187 -29
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +4 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +0 -6
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +458 -22
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/lib/stripeGateway.ts +85 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +2 -0
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +14 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.passport.ts +4 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -2
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +4 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-utils/logger/package.json +2 -2
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +3 -3
- package/zova/packages-zova/zova-core/src/core/sys/resource.ts +1 -1
- package/zova/packages-zova/zova-core/src/core/sys/util.ts +8 -0
- package/zova/pnpm-lock.yaml +570 -11
- package/zova/src/front/config/config/config.cabloyCommerce.ts +0 -6
- package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +3 -1
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/schemas.ts +236 -36
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/types.ts +4962 -2402
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +8 -3
- package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +6 -7
- package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +1 -3
- package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +2 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +13 -20
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +3 -9
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +9 -11
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +0 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +6 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +78 -17
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +10 -13
- package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +3 -3
- package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +31 -1
- package/zova/src/suite/a-home/modules/home-api/src/api/homeBaseSiteCatalog.ts +24 -0
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +329 -17
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +5866 -1829
- package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBaseSiteCatalog.ts +13 -0
- package/zova/src/suite/a-home/modules/home-base/src/component/page/controller.tsx +1 -0
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +0 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +61 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/formFieldMarkdown.ts +36 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/markdownHtml.ts +36 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +147 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/this.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +111 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +40 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/style.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/controller.tsx +17 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/render.tsx +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/style.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +142 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.build.json +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.json +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +25 -3
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/pageEntry.ts +1 -0
- package/zova/src/suite/cabloy-basic/package.json +1 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +7 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-logger/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +3 -2
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrProfile.test.ts +88 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
- package/.claude/scheduled_tasks.lock +0 -1
package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts
CHANGED
|
@@ -3,36 +3,42 @@ import type {
|
|
|
3
3
|
IPaymentOutcomeEvent,
|
|
4
4
|
IRefundOutcomeEvent,
|
|
5
5
|
} from 'vona-module-a-pay';
|
|
6
|
+
import type { IPayProviderPaypalClientOptions } from 'vona-module-pay-paypal';
|
|
7
|
+
import type { IPayProviderStripeClientOptions } from 'vona-module-pay-stripe';
|
|
6
8
|
|
|
7
9
|
import { BeanBase } from 'vona';
|
|
8
10
|
import { PayScene } from 'vona-module-a-pay';
|
|
11
|
+
import 'vona-module-a-ssr';
|
|
9
12
|
|
|
10
13
|
@PayScene({
|
|
11
14
|
providers: [
|
|
12
15
|
{ key: 'mock', providerName: 'pay-mock:mock', clientName: 'default' },
|
|
13
16
|
{ key: 'paypal', providerName: 'pay-paypal:paypal', clientName: 'default' },
|
|
17
|
+
{ key: 'stripe', providerName: 'pay-stripe:stripe', clientName: 'default' },
|
|
14
18
|
],
|
|
15
19
|
isProviderAvailable: (_ctx, input) => {
|
|
16
20
|
if (input.candidate.key === 'mock') return true;
|
|
17
|
-
if (input.candidate.key !== 'paypal') return false;
|
|
18
21
|
const { clientOptions } = _ctx.bean.payProvider.resolveByName(
|
|
19
22
|
input.candidate.providerName,
|
|
20
23
|
input.candidate.clientName,
|
|
21
24
|
);
|
|
22
|
-
const options = clientOptions as {
|
|
23
|
-
secretCredential?: { clientId?: string; clientSecret?: string };
|
|
24
|
-
webhookId?: string;
|
|
25
|
-
merchantReference?: string;
|
|
26
|
-
};
|
|
27
25
|
const server = _ctx.app.config.server.serve;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
options
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const hasPublicServer = !!(server.protocol && server.host);
|
|
27
|
+
if (input.candidate.key === 'paypal') {
|
|
28
|
+
const options = clientOptions as IPayProviderPaypalClientOptions;
|
|
29
|
+
return !!(
|
|
30
|
+
options.secretCredential?.clientId &&
|
|
31
|
+
options.secretCredential.clientSecret &&
|
|
32
|
+
options.webhookId &&
|
|
33
|
+
options.merchantReference &&
|
|
34
|
+
hasPublicServer
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (input.candidate.key === 'stripe') {
|
|
38
|
+
const options = clientOptions as IPayProviderStripeClientOptions;
|
|
39
|
+
return !!(options.secretCredential && options.secretWebhook && hasPublicServer);
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
36
42
|
},
|
|
37
43
|
resolveProvider: (_ctx, input) => {
|
|
38
44
|
const mock = input.providers.find(item => item.key === 'mock');
|
|
@@ -51,7 +57,18 @@ export class PaySceneCommerceOrder extends BeanBase {
|
|
|
51
57
|
async getPaymentCallbackPath(session: EntityPaymentSession): Promise<string> {
|
|
52
58
|
const attempt = await this.scope.model.paymentAttempt.get({ paymentSessionId: session.id });
|
|
53
59
|
if (!attempt) this.app.throw(404, 'commerce payment attempt not found');
|
|
54
|
-
|
|
60
|
+
const pagePath = this.bean.ssr.getPagePath(
|
|
61
|
+
'commerce-siteweb:commerce',
|
|
62
|
+
'/commerce/trade/payment/:paymentSessionId/:orderId',
|
|
63
|
+
{
|
|
64
|
+
params: {
|
|
65
|
+
paymentSessionId: session.id,
|
|
66
|
+
orderId: attempt.orderId,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
if (!pagePath) this.app.throw(500, 'commerce SSR Site is unavailable');
|
|
71
|
+
return pagePath;
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
async onPaymentOutcome(event: IPaymentOutcomeEvent): Promise<void> {
|
|
@@ -42,8 +42,14 @@ describe('paymentAttempt.test.ts', { concurrency: false }, () => {
|
|
|
42
42
|
paymentSessionId = created.paymentSessionId as number;
|
|
43
43
|
assert.equal(created.state, 'created');
|
|
44
44
|
const session = await app.scope('a-pay').model.paymentSession.getById(paymentSessionId);
|
|
45
|
-
assert.
|
|
46
|
-
assert.equal(session
|
|
45
|
+
assert.ok(session);
|
|
46
|
+
assert.equal(session.payScene, 'commerce-payment:commerceOrder');
|
|
47
|
+
assert.equal(session.providerName, 'pay-mock:mock');
|
|
48
|
+
const payScene = app.bean.payScene.get('commerce-payment:commerceOrder');
|
|
49
|
+
assert.equal(
|
|
50
|
+
await payScene.getPaymentCallbackPath(session),
|
|
51
|
+
`/commerce/commerce/trade/payment/${paymentSessionId}/${created.orderId}`,
|
|
52
|
+
);
|
|
47
53
|
assert.equal(session?.clientName, 'default');
|
|
48
54
|
assert.equal(session?.environment, 'sandbox');
|
|
49
55
|
assert.match(
|
|
@@ -51,8 +51,11 @@ declare module 'vona-module-commerce-siteweb' {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
/** ssrMenu: end */
|
|
54
|
+
/** locale: begin */
|
|
55
|
+
import { locales } from './locales.ts';
|
|
56
|
+
/** locale: end */
|
|
54
57
|
/** scope: begin */
|
|
55
|
-
import { BeanScopeBase, type BeanScopeUtil } from 'vona';
|
|
58
|
+
import { BeanScopeBase, type BeanScopeUtil, type TypeModuleLocales, type TypeLocaleBase } from 'vona';
|
|
56
59
|
import { Scope } from 'vona-module-a-bean';
|
|
57
60
|
|
|
58
61
|
@Scope()
|
|
@@ -60,6 +63,7 @@ export class ScopeModuleCommerceSiteweb extends BeanScopeBase {}
|
|
|
60
63
|
|
|
61
64
|
export interface ScopeModuleCommerceSiteweb {
|
|
62
65
|
util: BeanScopeUtil;
|
|
66
|
+
locale: TypeModuleLocales<(typeof locales)[TypeLocaleBase]>;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
import 'vona';
|
|
@@ -74,7 +78,9 @@ declare module 'vona' {
|
|
|
74
78
|
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
export interface IBeanScopeLocale {
|
|
82
|
+
'commerce-siteweb': (typeof locales)[TypeLocaleBase];
|
|
83
|
+
}
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TypeLocaleBase } from 'vona';
|
|
2
|
+
|
|
3
|
+
import { $makeLocaleMagic } from 'vona';
|
|
4
|
+
|
|
5
|
+
import locale_en_us from '../config/locale/en-us.ts';
|
|
6
|
+
import locale_zh_cn from '../config/locale/zh-cn.ts';
|
|
7
|
+
|
|
8
|
+
export const locales = {
|
|
9
|
+
'en-us': locale_en_us,
|
|
10
|
+
'zh-cn': locale_zh_cn,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function $locale<K extends keyof (typeof locales)[TypeLocaleBase]>(
|
|
14
|
+
key: K,
|
|
15
|
+
...args: any[]
|
|
16
|
+
): any {
|
|
17
|
+
return $makeLocaleMagic(`commerce-siteweb::${key}`, ...args);
|
|
18
|
+
}
|
|
@@ -6,11 +6,13 @@ import { SsrMenu } from 'vona-module-a-ssr';
|
|
|
6
6
|
|
|
7
7
|
import type { ISsrSiteOptionsCommerce } from './ssrSite.commerce.ts';
|
|
8
8
|
|
|
9
|
+
import { $locale } from '../.metadata/locales.ts';
|
|
10
|
+
|
|
9
11
|
export interface ISsrMenuOptionsHome extends IDecoratorSsrMenuOptions<ISsrSiteOptionsCommerce> {}
|
|
10
12
|
|
|
11
13
|
@SsrMenu<ISsrMenuOptionsHome>({
|
|
12
14
|
item: {
|
|
13
|
-
title: 'Commerce',
|
|
15
|
+
title: $locale('Commerce'),
|
|
14
16
|
order: $order(1),
|
|
15
17
|
icon: undefined,
|
|
16
18
|
link: '/:locale' as never,
|
|
@@ -11,6 +11,17 @@ import { DtoCheckoutResult } from '../dto/checkoutResult.tsx';
|
|
|
11
11
|
|
|
12
12
|
export interface IControllerOptionsCheckout extends IDecoratorControllerOptions {}
|
|
13
13
|
|
|
14
|
+
function getPaymentMethodLabel(key: string): string {
|
|
15
|
+
switch (key) {
|
|
16
|
+
case 'paypal':
|
|
17
|
+
return 'PayPal';
|
|
18
|
+
case 'stripe':
|
|
19
|
+
return 'Stripe';
|
|
20
|
+
default:
|
|
21
|
+
return 'Mock payment';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
@Controller<IControllerOptionsCheckout>('checkout')
|
|
15
26
|
export class ControllerCheckout extends BeanBase {
|
|
16
27
|
@Web.get('payment-methods')
|
|
@@ -22,8 +33,8 @@ export class ControllerCheckout extends BeanBase {
|
|
|
22
33
|
);
|
|
23
34
|
const items = candidates.map(candidate => ({
|
|
24
35
|
key: candidate.key,
|
|
25
|
-
label: candidate.key
|
|
26
|
-
interaction: candidate.key === '
|
|
36
|
+
label: getPaymentMethodLabel(candidate.key),
|
|
37
|
+
interaction: candidate.key === 'mock' ? ('embedded' as const) : ('redirect' as const),
|
|
27
38
|
}));
|
|
28
39
|
const defaultKey = items.find(item => item.key === 'mock')?.key ?? items[0]!.key;
|
|
29
40
|
return { items, defaultKey };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createHash } from 'vona';
|
|
2
|
+
|
|
3
|
+
const hashEncoding = 'base64url';
|
|
4
|
+
|
|
5
|
+
export function compactProviderReference(
|
|
6
|
+
purpose: string,
|
|
7
|
+
source: string,
|
|
8
|
+
maxLength: number,
|
|
9
|
+
): string {
|
|
10
|
+
if (source.length <= maxLength) return source;
|
|
11
|
+
|
|
12
|
+
const digest = createHash(`${purpose}\0${source}`, hashEncoding, 'sha256');
|
|
13
|
+
const reference = `${purpose}:${digest}`;
|
|
14
|
+
if (reference.length > maxLength) {
|
|
15
|
+
throw new RangeError('provider reference maximum length is too small');
|
|
16
|
+
}
|
|
17
|
+
return reference;
|
|
18
|
+
}
|
|
@@ -38,8 +38,12 @@ import type { EntityShipment } from '../entity/shipment.tsx';
|
|
|
38
38
|
import type { ModelOrder } from '../model/order.ts';
|
|
39
39
|
|
|
40
40
|
import { orderSummaryColumns } from '../lib/order.ts';
|
|
41
|
+
import { compactProviderReference } from '../lib/providerReference.ts';
|
|
41
42
|
|
|
42
43
|
const maxOrderCents = 2_147_483_647;
|
|
44
|
+
const maxProviderReferenceLength = 100;
|
|
45
|
+
const couponCorrelationSuffix = ':coupon';
|
|
46
|
+
const lateCaptureRefundSuffix = ':refund';
|
|
43
47
|
const customerVisibleOrderStates: EntityOrder['state'][] = [
|
|
44
48
|
'awaiting_payment',
|
|
45
49
|
'paid',
|
|
@@ -396,7 +400,16 @@ export class ServiceOrder extends BeanBase {
|
|
|
396
400
|
if (order.state !== 'awaiting_payment' || lockedAttempt.state !== 'created') {
|
|
397
401
|
this.app.throw(409, 'payment attempt is no longer available');
|
|
398
402
|
}
|
|
399
|
-
const
|
|
403
|
+
const idempotencyKey = compactProviderReference(
|
|
404
|
+
'payment-event',
|
|
405
|
+
event.eventId,
|
|
406
|
+
maxProviderReferenceLength,
|
|
407
|
+
);
|
|
408
|
+
const correlationId = compactProviderReference(
|
|
409
|
+
'payment-correlation',
|
|
410
|
+
`${order.correlationId}:provider:${event.eventId}`,
|
|
411
|
+
maxProviderReferenceLength - couponCorrelationSuffix.length,
|
|
412
|
+
);
|
|
400
413
|
const orderState = event.state === 'succeeded' ? 'paid' : 'cancelled';
|
|
401
414
|
const reason = `provider payment ${event.state}`;
|
|
402
415
|
await this.scope.model.order.updateById(order.id, { state: orderState });
|
|
@@ -423,7 +436,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
423
436
|
outcome: event.state,
|
|
424
437
|
fromAttemptState: 'created',
|
|
425
438
|
toOrderState: orderState,
|
|
426
|
-
idempotencyKey
|
|
439
|
+
idempotencyKey,
|
|
427
440
|
correlationId,
|
|
428
441
|
reason,
|
|
429
442
|
processedAt: new Date(),
|
|
@@ -464,9 +477,14 @@ export class ServiceOrder extends BeanBase {
|
|
|
464
477
|
) {
|
|
465
478
|
this.app.throw(409, 'provider event conflicts with the commerce refund attempt');
|
|
466
479
|
}
|
|
480
|
+
const idempotencyKey = compactProviderReference(
|
|
481
|
+
'refund-event',
|
|
482
|
+
event.eventId,
|
|
483
|
+
maxProviderReferenceLength,
|
|
484
|
+
);
|
|
467
485
|
const existingAudit = await this.$scope.commercePayment.model.refundAudit.get({
|
|
468
486
|
refundAttemptId: lockedAttempt.id,
|
|
469
|
-
idempotencyKey
|
|
487
|
+
idempotencyKey,
|
|
470
488
|
});
|
|
471
489
|
if (existingAudit) return this._refundResult(order, request, lockedAttempt);
|
|
472
490
|
if (
|
|
@@ -482,7 +500,11 @@ export class ServiceOrder extends BeanBase {
|
|
|
482
500
|
const attemptState = isSucceeded ? 'succeeded' : 'failed';
|
|
483
501
|
const refundState = isSucceeded ? 'refunded' : 'failed';
|
|
484
502
|
const orderState = isSucceeded ? 'refunded' : 'paid';
|
|
485
|
-
const correlationId =
|
|
503
|
+
const correlationId = compactProviderReference(
|
|
504
|
+
'refund-correlation',
|
|
505
|
+
`${order.correlationId}:provider-refund:${event.eventId}`,
|
|
506
|
+
maxProviderReferenceLength,
|
|
507
|
+
);
|
|
486
508
|
const reason = `provider refund ${event.state}`;
|
|
487
509
|
await this.$scope.commercePayment.model.refundAttempt.updateById(lockedAttempt.id, {
|
|
488
510
|
state: attemptState,
|
|
@@ -509,7 +531,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
509
531
|
order,
|
|
510
532
|
toRefundState: refundState,
|
|
511
533
|
attemptState,
|
|
512
|
-
idempotencyKey
|
|
534
|
+
idempotencyKey,
|
|
513
535
|
correlationId,
|
|
514
536
|
reason,
|
|
515
537
|
});
|
|
@@ -1447,7 +1469,16 @@ export class ServiceOrder extends BeanBase {
|
|
|
1447
1469
|
await this.$scope.commercePayment.model.paymentAttempt.updateById(attempt.id, {
|
|
1448
1470
|
providerCaptureId: event.providerCaptureId,
|
|
1449
1471
|
});
|
|
1450
|
-
const
|
|
1472
|
+
const idempotencyKey = compactProviderReference(
|
|
1473
|
+
'payment-event',
|
|
1474
|
+
event.eventId,
|
|
1475
|
+
maxProviderReferenceLength,
|
|
1476
|
+
);
|
|
1477
|
+
const correlationId = compactProviderReference(
|
|
1478
|
+
'late-capture-correlation',
|
|
1479
|
+
`${order.correlationId}:late-capture:${event.eventId}`,
|
|
1480
|
+
maxProviderReferenceLength - lateCaptureRefundSuffix.length,
|
|
1481
|
+
);
|
|
1451
1482
|
const existingAudit = await this.$scope.commercePayment.model.paymentAudit.get({
|
|
1452
1483
|
paymentAttemptId: attempt.id,
|
|
1453
1484
|
providerEventId: event.eventId,
|
|
@@ -1462,7 +1493,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
1462
1493
|
outcome: 'succeeded',
|
|
1463
1494
|
fromAttemptState: attempt.state,
|
|
1464
1495
|
toOrderState: 'expired',
|
|
1465
|
-
idempotencyKey
|
|
1496
|
+
idempotencyKey,
|
|
1466
1497
|
correlationId,
|
|
1467
1498
|
reason: 'late provider capture requires automatic compensation',
|
|
1468
1499
|
processedAt: new Date(),
|
|
@@ -1474,7 +1505,11 @@ export class ServiceOrder extends BeanBase {
|
|
|
1474
1505
|
businessReference: `late-capture:${attempt.id}`,
|
|
1475
1506
|
amountMinor: event.amountMinor,
|
|
1476
1507
|
currency: event.currency,
|
|
1477
|
-
idempotencyKey:
|
|
1508
|
+
idempotencyKey: compactProviderReference(
|
|
1509
|
+
'late-capture-refund',
|
|
1510
|
+
`${order.correlationId}:late-capture:${event.eventId}${lateCaptureRefundSuffix}`,
|
|
1511
|
+
maxProviderReferenceLength,
|
|
1512
|
+
),
|
|
1478
1513
|
correlationId,
|
|
1479
1514
|
});
|
|
1480
1515
|
return this._paymentOutcomeResult(order, attempt);
|
|
@@ -28,9 +28,28 @@ async function cleanup(fixture: IFixture) {
|
|
|
28
28
|
const member = app.scope('commerce-member');
|
|
29
29
|
const pay = app.scope('a-pay');
|
|
30
30
|
if (fixture.paymentSessionId !== undefined) {
|
|
31
|
+
const refundOperations = await pay.model.refundOperation.select({
|
|
32
|
+
where: { paymentSessionId: fixture.paymentSessionId },
|
|
33
|
+
});
|
|
34
|
+
for (const refundOperation of refundOperations) {
|
|
35
|
+
const providerOperation = await pay.model.providerOperation.get({
|
|
36
|
+
refundOperationId: refundOperation.id,
|
|
37
|
+
kind: 'refund',
|
|
38
|
+
});
|
|
39
|
+
if (providerOperation) {
|
|
40
|
+
await pay.model.providerOperationRecoveryAudit.delete({
|
|
41
|
+
providerOperationId: providerOperation.id,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
await pay.model.outboxEvent.delete({ refundOperationId: refundOperation.id });
|
|
45
|
+
await pay.model.webhookInbox.delete({ refundOperationId: refundOperation.id });
|
|
46
|
+
await pay.model.providerOperation.delete({ refundOperationId: refundOperation.id });
|
|
47
|
+
await pay.model.refundOperation.delete({ id: refundOperation.id });
|
|
48
|
+
}
|
|
31
49
|
await pay.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
32
50
|
await pay.model.paymentAudit.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
33
51
|
await pay.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
52
|
+
await pay.model.providerOperation.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
34
53
|
}
|
|
35
54
|
if (fixture.paymentAttemptId !== undefined)
|
|
36
55
|
await payment.model.paymentAudit.delete({ paymentAttemptId: fixture.paymentAttemptId });
|
|
@@ -307,6 +326,61 @@ describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, ()
|
|
|
307
326
|
});
|
|
308
327
|
});
|
|
309
328
|
|
|
329
|
+
it('keeps long provider event references distinct and replays the same event once', async () => {
|
|
330
|
+
const sharedPrefix = 'provider-event-'.padEnd(100, 'x');
|
|
331
|
+
const firstEventId = `${sharedPrefix}A`;
|
|
332
|
+
const secondEventId = `${sharedPrefix}B`;
|
|
333
|
+
const fixtures: IFixture[] = [];
|
|
334
|
+
try {
|
|
335
|
+
for (const [index, eventId] of [firstEventId, secondEventId].entries()) {
|
|
336
|
+
await app.bean.executor.mockCtx(async () => {
|
|
337
|
+
const fixture = await createCheckoutFixture(randomUUID().slice(0, 12));
|
|
338
|
+
fixtures.push(fixture);
|
|
339
|
+
await app.bean.passport.signout();
|
|
340
|
+
const event = {
|
|
341
|
+
eventId,
|
|
342
|
+
paymentSessionId: fixture.paymentSessionId!,
|
|
343
|
+
businessReference: String(fixture.paymentAttemptId),
|
|
344
|
+
providerName: 'pay-mock:mock',
|
|
345
|
+
state: 'succeeded' as const,
|
|
346
|
+
providerCaptureId: `capture-${index}`,
|
|
347
|
+
amountMinor: 799,
|
|
348
|
+
currency: 'USD',
|
|
349
|
+
};
|
|
350
|
+
const order = app.scope('commerce-trade').service.order;
|
|
351
|
+
await order.settlePaymentFromProvider(event);
|
|
352
|
+
await order.settlePaymentFromProvider(event);
|
|
353
|
+
const [audit] = await app.scope('commerce-payment').model.paymentAudit.select({
|
|
354
|
+
where: { paymentAttemptId: fixture.paymentAttemptId },
|
|
355
|
+
});
|
|
356
|
+
assert.equal(audit?.providerEventId, eventId);
|
|
357
|
+
assert.ok(audit?.idempotencyKey.length <= 100);
|
|
358
|
+
assert.ok(audit?.correlationId.length <= 100);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
await app.bean.executor.mockCtx(async () => {
|
|
362
|
+
const audits = await Promise.all(
|
|
363
|
+
fixtures.map(async fixture => {
|
|
364
|
+
return (
|
|
365
|
+
await app.scope('commerce-payment').model.paymentAudit.select({
|
|
366
|
+
where: { paymentAttemptId: fixture.paymentAttemptId },
|
|
367
|
+
})
|
|
368
|
+
)[0];
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
assert.equal(audits.filter(Boolean).length, 2);
|
|
372
|
+
assert.notEqual(audits[0]?.idempotencyKey, audits[1]?.idempotencyKey);
|
|
373
|
+
assert.notEqual(audits[0]?.correlationId, audits[1]?.correlationId);
|
|
374
|
+
});
|
|
375
|
+
} finally {
|
|
376
|
+
for (const fixture of fixtures.reverse()) {
|
|
377
|
+
await app.bean.executor.mockCtx(async () => {
|
|
378
|
+
await cleanup(fixture);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
310
384
|
it('settles a provider event without customer Passport authority', async () => {
|
|
311
385
|
await app.bean.executor.mockCtx(async () => {
|
|
312
386
|
const fixture: IFixture = {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { compactProviderReference } from '../src/lib/providerReference.ts';
|
|
5
|
+
|
|
6
|
+
describe('providerReference.test.ts', () => {
|
|
7
|
+
it('keeps fitting values and compacts long values with a domain-separated digest', () => {
|
|
8
|
+
const maxLength = 100;
|
|
9
|
+
const sharedPrefix = 'provider-event-'.padEnd(100, 'x');
|
|
10
|
+
const first = `${sharedPrefix}A`;
|
|
11
|
+
const second = `${sharedPrefix}B`;
|
|
12
|
+
|
|
13
|
+
assert.equal(compactProviderReference('payment-event', 'event-1', maxLength), 'event-1');
|
|
14
|
+
assert.equal(
|
|
15
|
+
compactProviderReference('payment-event', 'x'.repeat(maxLength), maxLength),
|
|
16
|
+
'x'.repeat(maxLength),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const reference = compactProviderReference('payment-event', first, maxLength);
|
|
20
|
+
assert.equal(reference.length, 'payment-event:'.length + 43);
|
|
21
|
+
assert.equal(reference, compactProviderReference('payment-event', first, maxLength));
|
|
22
|
+
assert.notEqual(reference, compactProviderReference('payment-event', second, maxLength));
|
|
23
|
+
assert.notEqual(reference, compactProviderReference('refund-event', first, maxLength));
|
|
24
|
+
assert.match(reference, /^payment-event:[\w-]{43}$/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects a maximum length that cannot hold its compact reference', () => {
|
|
28
|
+
assert.throws(
|
|
29
|
+
() => compactProviderReference('payment-event', 'x'.repeat(101), 10),
|
|
30
|
+
/maximum length is too small/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -384,6 +384,79 @@ describe('refundLifecycle.test.ts', { concurrency: false, sequential: true }, ()
|
|
|
384
384
|
});
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
+
it('keeps long provider refund event references distinct and replays the same event once', async () => {
|
|
388
|
+
const sharedPrefix = 'provider-refund-event-'.padEnd(100, 'x');
|
|
389
|
+
const eventIds = [`${sharedPrefix}A`, `${sharedPrefix}B`];
|
|
390
|
+
const fixtures: IFixture[] = [];
|
|
391
|
+
try {
|
|
392
|
+
for (const [index, eventId] of eventIds.entries()) {
|
|
393
|
+
await app.bean.executor.mockCtx(async () => {
|
|
394
|
+
const fixture = await createPaidOrder(randomUUID().slice(0, 12));
|
|
395
|
+
fixtures.push(fixture);
|
|
396
|
+
const order = app.scope('commerce-trade').service.order;
|
|
397
|
+
await order.requestRefund(fixture.orderId!, {
|
|
398
|
+
reason: 'long provider event',
|
|
399
|
+
idempotencyKey: `long-event-request-${index}`,
|
|
400
|
+
});
|
|
401
|
+
await app.bean.passport.signout();
|
|
402
|
+
await app.bean.passport.signinMock();
|
|
403
|
+
const approved = await order.approveRefund(fixture.orderId!, {
|
|
404
|
+
reason: 'long provider event approved',
|
|
405
|
+
idempotencyKey: `long-event-approve-${index}`,
|
|
406
|
+
});
|
|
407
|
+
const executed = await order.executeRefund(fixture.orderId!);
|
|
408
|
+
const event = {
|
|
409
|
+
eventId,
|
|
410
|
+
paymentSessionId: fixture.paymentSessionId!,
|
|
411
|
+
refundOperationId: executed.refundOperationId!,
|
|
412
|
+
businessReference: String(executed.refundAttemptId),
|
|
413
|
+
providerName: 'pay-mock:mock',
|
|
414
|
+
state: 'failed' as const,
|
|
415
|
+
providerRefundId: `provider-refund-${index}`,
|
|
416
|
+
amountMinor: 799,
|
|
417
|
+
currency: 'USD',
|
|
418
|
+
};
|
|
419
|
+
const first = await order.settleRefundFromProvider(event);
|
|
420
|
+
const replay = await order.settleRefundFromProvider(event);
|
|
421
|
+
assert.deepEqual(replay, first);
|
|
422
|
+
assert.deepEqual(
|
|
423
|
+
[first.orderState, first.refundState, first.refundAttemptState],
|
|
424
|
+
['paid', 'failed', 'failed'],
|
|
425
|
+
);
|
|
426
|
+
const audits = await app.scope('commerce-payment').model.refundAudit.select({
|
|
427
|
+
where: { refundRequestId: approved.refundRequestId },
|
|
428
|
+
});
|
|
429
|
+
const [audit] = audits.filter(item => item.toRefundState === 'failed');
|
|
430
|
+
assert.equal(audit?.idempotencyKey.length <= 100, true);
|
|
431
|
+
assert.equal(audit?.correlationId.length <= 100, true);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
await app.bean.executor.mockCtx(async () => {
|
|
435
|
+
const failedAudits = await Promise.all(
|
|
436
|
+
fixtures.map(async fixture => {
|
|
437
|
+
const request = await app.scope('commerce-payment').model.refundRequest.get({
|
|
438
|
+
orderId: fixture.orderId,
|
|
439
|
+
});
|
|
440
|
+
return (
|
|
441
|
+
await app.scope('commerce-payment').model.refundAudit.select({
|
|
442
|
+
where: { refundRequestId: request?.id },
|
|
443
|
+
})
|
|
444
|
+
).find(item => item.toRefundState === 'failed');
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
assert.notEqual(failedAudits[0]?.idempotencyKey, failedAudits[1]?.idempotencyKey);
|
|
448
|
+
assert.notEqual(failedAudits[0]?.correlationId, failedAudits[1]?.correlationId);
|
|
449
|
+
});
|
|
450
|
+
} finally {
|
|
451
|
+
for (const fixture of fixtures.reverse()) {
|
|
452
|
+
await app.bean.executor.mockCtx(async () => {
|
|
453
|
+
await app.bean.passport.signout();
|
|
454
|
+
await cleanup(fixture);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
387
460
|
it('settles an executed mock-provider refund through the durable outbox', async () => {
|
|
388
461
|
await app.bean.executor.mockCtx(async () => {
|
|
389
462
|
const fixture: IFixture = {};
|
|
@@ -362,6 +362,7 @@ describe('reservationExpiry.test.ts', { concurrency: false, sequential: true },
|
|
|
362
362
|
.execute();
|
|
363
363
|
const audits = await app.scope('commerce-trade').model.orderAudit.select({
|
|
364
364
|
where: { orderId: defaultFixture!.orderId! },
|
|
365
|
+
orders: [['id', 'asc']],
|
|
365
366
|
});
|
|
366
367
|
assert.deepEqual(
|
|
367
368
|
audits.map(audit => audit.operation),
|
|
@@ -412,6 +413,7 @@ describe('reservationExpiry.test.ts', { concurrency: false, sequential: true },
|
|
|
412
413
|
.execute();
|
|
413
414
|
const audits = await app.scope('commerce-trade').model.orderAudit.select({
|
|
414
415
|
where: { orderId: shareFixture!.orderId! },
|
|
416
|
+
orders: [['id', 'asc']],
|
|
415
417
|
});
|
|
416
418
|
assert.deepEqual(
|
|
417
419
|
audits.map(audit => audit.operation),
|
|
@@ -152,7 +152,17 @@ declare module 'vona-module-home-user' {
|
|
|
152
152
|
[SymbolKeyEntityMeta]: EntityRoleMeta;
|
|
153
153
|
[SymbolKeyModelOptions]: IModelOptionsRole;
|
|
154
154
|
get<T extends IModelGetOptions<EntityRole,ModelRole>>(where: TypeModelWhere<EntityRole>, options?: T): Promise<TypeModelRelationResult<EntityRole, ModelRole, T> | undefined>;
|
|
155
|
+
/**
|
|
156
|
+
* Retrieves one matching primary row with a pessimistic FOR UPDATE lock.
|
|
157
|
+
* Requires an active transaction. The lock is released when that transaction completes.
|
|
158
|
+
* Entity and query caches are bypassed.
|
|
159
|
+
*/
|
|
155
160
|
getForUpdate<T extends IModelGetOptions<EntityRole,ModelRole>>(where: TypeModelWhere<EntityRole>, options?: T): Promise<TypeModelRelationResult<EntityRole, ModelRole, T> | undefined>;
|
|
161
|
+
/**
|
|
162
|
+
* Retrieves a primary row by ID with the same pessimistic FOR UPDATE lock semantics.
|
|
163
|
+
* Requires an active transaction. The lock is released when that transaction completes.
|
|
164
|
+
* Entity and query caches are bypassed.
|
|
165
|
+
*/
|
|
156
166
|
getByIdForUpdate<T extends IModelGetOptions<EntityRole,ModelRole>>(id: TableIdentity, options?: T): Promise<TypeModelRelationResult<EntityRole, ModelRole, T> | undefined>;
|
|
157
167
|
mget<T extends IModelGetOptions<EntityRole,ModelRole>>(ids: TableIdentity[], options?: T): Promise<TypeModelRelationResult<EntityRole, ModelRole, T>[]>;
|
|
158
168
|
selectAndCount<T extends IModelSelectParams<EntityRole,ModelRole,ModelJoins>, ModelJoins extends TypeModelsClassLikeGeneral | undefined = undefined>(params?: T, options?: IModelMethodOptions, modelJoins?: ModelJoins): Promise<TypeModelSelectAndCount<EntityRole, ModelRole, T>>;
|
|
@@ -183,7 +193,17 @@ export interface ModelRoleUser {
|
|
|
183
193
|
[SymbolKeyEntityMeta]: EntityRoleUserMeta;
|
|
184
194
|
[SymbolKeyModelOptions]: IModelOptionsRoleUser;
|
|
185
195
|
get<T extends IModelGetOptions<EntityRoleUser,ModelRoleUser>>(where: TypeModelWhere<EntityRoleUser>, options?: T): Promise<TypeModelRelationResult<EntityRoleUser, ModelRoleUser, T> | undefined>;
|
|
196
|
+
/**
|
|
197
|
+
* Retrieves one matching primary row with a pessimistic FOR UPDATE lock.
|
|
198
|
+
* Requires an active transaction. The lock is released when that transaction completes.
|
|
199
|
+
* Entity and query caches are bypassed.
|
|
200
|
+
*/
|
|
186
201
|
getForUpdate<T extends IModelGetOptions<EntityRoleUser,ModelRoleUser>>(where: TypeModelWhere<EntityRoleUser>, options?: T): Promise<TypeModelRelationResult<EntityRoleUser, ModelRoleUser, T> | undefined>;
|
|
202
|
+
/**
|
|
203
|
+
* Retrieves a primary row by ID with the same pessimistic FOR UPDATE lock semantics.
|
|
204
|
+
* Requires an active transaction. The lock is released when that transaction completes.
|
|
205
|
+
* Entity and query caches are bypassed.
|
|
206
|
+
*/
|
|
187
207
|
getByIdForUpdate<T extends IModelGetOptions<EntityRoleUser,ModelRoleUser>>(id: TableIdentity, options?: T): Promise<TypeModelRelationResult<EntityRoleUser, ModelRoleUser, T> | undefined>;
|
|
188
208
|
mget<T extends IModelGetOptions<EntityRoleUser,ModelRoleUser>>(ids: TableIdentity[], options?: T): Promise<TypeModelRelationResult<EntityRoleUser, ModelRoleUser, T>[]>;
|
|
189
209
|
selectAndCount<T extends IModelSelectParams<EntityRoleUser,ModelRoleUser,ModelJoins>, ModelJoins extends TypeModelsClassLikeGeneral | undefined = undefined>(params?: T, options?: IModelMethodOptions, modelJoins?: ModelJoins): Promise<TypeModelSelectAndCount<EntityRoleUser, ModelRoleUser, T>>;
|
|
@@ -210,7 +230,17 @@ export interface ModelUser {
|
|
|
210
230
|
[SymbolKeyEntityMeta]: EntityUserMeta;
|
|
211
231
|
[SymbolKeyModelOptions]: IModelOptionsUser;
|
|
212
232
|
get<T extends IModelGetOptions<EntityUser,ModelUser>>(where: TypeModelWhere<EntityUser>, options?: T): Promise<TypeModelRelationResult<EntityUser, ModelUser, T> | undefined>;
|
|
233
|
+
/**
|
|
234
|
+
* Retrieves one matching primary row with a pessimistic FOR UPDATE lock.
|
|
235
|
+
* Requires an active transaction. The lock is released when that transaction completes.
|
|
236
|
+
* Entity and query caches are bypassed.
|
|
237
|
+
*/
|
|
213
238
|
getForUpdate<T extends IModelGetOptions<EntityUser,ModelUser>>(where: TypeModelWhere<EntityUser>, options?: T): Promise<TypeModelRelationResult<EntityUser, ModelUser, T> | undefined>;
|
|
239
|
+
/**
|
|
240
|
+
* Retrieves a primary row by ID with the same pessimistic FOR UPDATE lock semantics.
|
|
241
|
+
* Requires an active transaction. The lock is released when that transaction completes.
|
|
242
|
+
* Entity and query caches are bypassed.
|
|
243
|
+
*/
|
|
214
244
|
getByIdForUpdate<T extends IModelGetOptions<EntityUser,ModelUser>>(id: TableIdentity, options?: T): Promise<TypeModelRelationResult<EntityUser, ModelUser, T> | undefined>;
|
|
215
245
|
mget<T extends IModelGetOptions<EntityUser,ModelUser>>(ids: TableIdentity[], options?: T): Promise<TypeModelRelationResult<EntityUser, ModelUser, T>[]>;
|
|
216
246
|
selectAndCount<T extends IModelSelectParams<EntityUser,ModelUser,ModelJoins>, ModelJoins extends TypeModelsClassLikeGeneral | undefined = undefined>(params?: T, options?: IModelMethodOptions, modelJoins?: ModelJoins): Promise<TypeModelSelectAndCount<EntityUser, ModelUser, T>>;
|
|
@@ -19,7 +19,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate, IMetaVe
|
|
|
19
19
|
table.basicFields();
|
|
20
20
|
table.string(entityRole.name, 255).comment(entityRole.$comment.name);
|
|
21
21
|
table.string(entityRole.title, 255).comment(entityRole.$comment.title);
|
|
22
|
-
table.json(entityRole.
|
|
22
|
+
table.json(entityRole.titleLocales).comment(entityRole.$comment.titleLocales);
|
|
23
23
|
table.json(entityRole.siteIds).comment(entityRole.$comment.siteIds);
|
|
24
24
|
});
|
|
25
25
|
// homeUser
|
|
@@ -35,6 +35,11 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate, IMetaVe
|
|
|
35
35
|
.boolean(entityUser.activated)
|
|
36
36
|
.defaultTo(entityUser.$default.activated)
|
|
37
37
|
.comment(entityUser.$comment.activated);
|
|
38
|
+
table
|
|
39
|
+
.string(entityUser.accountStatus, 255)
|
|
40
|
+
.notNullable()
|
|
41
|
+
.defaultTo(entityUser.$default.accountStatus)
|
|
42
|
+
.comment(entityUser.$comment.accountStatus);
|
|
38
43
|
table.string(entityUser.locale, 255).comment(entityUser.$comment.locale);
|
|
39
44
|
table.string(entityUser.tz, 255).comment(entityUser.$comment.tz);
|
|
40
45
|
});
|
|
@@ -55,7 +60,7 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate, IMetaVe
|
|
|
55
60
|
await this.scope.model.role.insert({
|
|
56
61
|
name,
|
|
57
62
|
title: role.title,
|
|
58
|
-
|
|
63
|
+
titleLocales: role.titleLocales ?? {},
|
|
59
64
|
siteIds: role.siteIds,
|
|
60
65
|
});
|
|
61
66
|
}
|
|
@@ -11,7 +11,10 @@ export default {
|
|
|
11
11
|
UserAvatar: 'Avatar',
|
|
12
12
|
UserEmail: 'Email',
|
|
13
13
|
UserMobile: 'Mobile',
|
|
14
|
-
UserActivated: 'Activated',
|
|
14
|
+
UserActivated: 'Identity Activated',
|
|
15
|
+
UserAccountStatus: 'Account Status',
|
|
16
|
+
UserAccountStatusActive: 'Active',
|
|
17
|
+
UserAccountStatusDisabled: 'Disabled',
|
|
15
18
|
UserLocale: 'Language',
|
|
16
19
|
UserTz: 'Timezone',
|
|
17
20
|
UsernameExists: 'Username Exists',
|