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.
Files changed (185) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +9 -0
  3. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +24 -1
  4. package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +12 -0
  5. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +28 -0
  6. package/.github/workflows/playwright-e2e.yml +2 -2
  7. package/.github/workflows/vona-test-pg.yml +1 -1
  8. package/.github/workflows/vona-test-sqlite3.yml +1 -1
  9. package/CHANGELOG.md +38 -0
  10. package/CLAUDE.md +1 -0
  11. package/cabloy-docs/.vitepress/config.mjs +7 -0
  12. package/cabloy-docs/backend/markdown-guide.md +189 -0
  13. package/cabloy-docs/frontend/component-guide.md +6 -0
  14. package/cabloy-docs/frontend/css-in-js-guide.md +6 -0
  15. package/cabloy-docs/frontend/markdown-guide.md +258 -0
  16. package/cabloy-docs/frontend/page-guide.md +48 -0
  17. package/cabloy-docs/frontend/page-params-guide.md +4 -2
  18. package/cabloy-docs/frontend/page-route-guide.md +2 -2
  19. package/cabloy-docs/frontend/zova-router-under-the-hood.md +2 -0
  20. package/cabloy-docs/frontend/zova-source-reading-map.md +53 -9
  21. package/cabloy-docs/fullstack/a-pay-payment-suite.md +333 -0
  22. package/cabloy-docs/fullstack/payment-sandbox-configuration.md +285 -0
  23. package/e2e/specs/a-commerce/commerce.spec.ts +503 -47
  24. package/e2e/specs/cabloy-basic/basic.spec.ts +30 -0
  25. package/package.json +1 -1
  26. package/vona/env/.env +2 -0
  27. package/vona/packages-cli/cli/package.json +1 -1
  28. package/vona/packages-cli/cli-set-api/cli/templates/init/error/boilerplate/config/errors.ts +6 -1
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
  31. package/vona/packages-cli/cli-set-api/package.json +1 -1
  32. package/vona/packages-utils/localeutil/package.json +1 -1
  33. package/vona/packages-utils/localeutil/src/util.ts +1 -1
  34. package/vona/packages-vona/vona/package.json +1 -1
  35. package/vona/packages-vona/vona-core/package.json +1 -1
  36. package/vona/packages-vona/vona-mock/package.json +1 -1
  37. package/vona/patches/{zova-core@5.1.82.patch → zova-core@5.1.84.patch} +2 -2
  38. package/vona/pnpm-lock.yaml +826 -270
  39. package/vona/pnpm-workspace.yaml +1 -1
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +1 -1
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +99 -1
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.index.ts +1 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +19 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/product.ts +3 -3
  47. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentUpdate.tsx +14 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentView.tsx +19 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +48 -3
  50. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicDetail.tsx +15 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +48 -3
  52. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +52 -3
  53. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/productContent.tsx +36 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +6 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/productContent.ts +22 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +82 -6
  57. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productContent.test.ts +271 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +81 -2
  59. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
  60. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +32 -15
  61. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +8 -2
  62. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +8 -2
  63. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/locales.ts +18 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +3 -1
  65. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/en-us.ts +3 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/zh-cn.ts +3 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +13 -2
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/providerReference.ts +18 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +43 -8
  71. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +74 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-trade/test/providerReference.test.ts +33 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +73 -0
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +2 -0
  75. package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +30 -0
  76. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +7 -2
  77. package/vona/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +4 -1
  78. package/vona/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +4 -1
  79. package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +4 -4
  80. package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +2 -2
  81. package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +8 -1
  82. package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +6 -2
  83. package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +1 -0
  84. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +4 -4
  85. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +64 -0
  86. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +54 -0
  87. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/this.ts +2 -0
  88. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +137 -0
  89. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/index.ts +1 -0
  90. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.build.json +11 -0
  91. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.json +7 -0
  92. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  93. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/bean.ssr.ts +17 -0
  94. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +11 -3
  95. package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
  96. package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  98. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +1 -0
  99. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +8 -1
  100. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
  101. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/providerOperationFailure.ts +8 -0
  102. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +187 -29
  103. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +4 -0
  104. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  105. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  106. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +0 -6
  107. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
  108. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +2 -1
  109. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +458 -22
  110. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
  111. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/lib/stripeGateway.ts +85 -0
  112. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +2 -0
  113. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  114. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -0
  117. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  118. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +14 -0
  119. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.passport.ts +4 -0
  120. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -2
  121. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +4 -0
  122. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  123. package/zova/packages-utils/logger/package.json +2 -2
  124. package/zova/packages-utils/zova-jsx/package.json +2 -2
  125. package/zova/packages-zova/zova/package.json +3 -3
  126. package/zova/packages-zova/zova-core/package.json +3 -3
  127. package/zova/packages-zova/zova-core/src/core/sys/resource.ts +1 -1
  128. package/zova/packages-zova/zova-core/src/core/sys/util.ts +8 -0
  129. package/zova/pnpm-lock.yaml +570 -11
  130. package/zova/src/front/config/config/config.cabloyCommerce.ts +0 -6
  131. package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +3 -1
  132. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/schemas.ts +236 -36
  133. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/types.ts +4962 -2402
  134. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
  135. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
  136. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +8 -3
  137. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +6 -7
  138. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +1 -3
  139. package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +2 -3
  140. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +13 -20
  141. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +3 -9
  142. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +9 -11
  143. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +0 -1
  144. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +6 -4
  145. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +78 -17
  146. package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +10 -13
  147. package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +3 -3
  148. package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +31 -1
  149. package/zova/src/suite/a-home/modules/home-api/src/api/homeBaseSiteCatalog.ts +24 -0
  150. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +329 -17
  151. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +5866 -1829
  152. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBaseSiteCatalog.ts +13 -0
  153. package/zova/src/suite/a-home/modules/home-base/src/component/page/controller.tsx +1 -0
  154. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +0 -1
  155. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +1 -0
  156. package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +61 -0
  157. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/formFieldMarkdown.ts +36 -0
  158. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/markdownHtml.ts +36 -0
  159. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +147 -0
  160. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/this.ts +2 -0
  161. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +111 -0
  162. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +40 -0
  163. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/style.ts +13 -0
  164. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/controller.tsx +17 -0
  165. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/render.tsx +9 -0
  166. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/style.ts +13 -0
  167. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
  168. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +142 -0
  169. package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.build.json +13 -0
  170. package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.json +5 -0
  171. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +25 -3
  172. package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +1 -0
  173. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/pageEntry.ts +1 -0
  174. package/zova/src/suite/cabloy-basic/package.json +1 -0
  175. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  176. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +7 -0
  177. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  178. package/zova/src/suite-vendor/a-zova/modules/a-logger/package.json +2 -2
  179. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  180. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +3 -2
  181. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  182. package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrProfile.test.ts +88 -0
  183. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  184. package/zova/src/suite-vendor/a-zova/package.json +5 -5
  185. package/.claude/scheduled_tasks.lock +0 -1
@@ -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
- return !!(
29
- options.secretCredential?.clientId &&
30
- options.secretCredential.clientSecret &&
31
- options.webhookId &&
32
- options.merchantReference &&
33
- server.protocol &&
34
- server.host
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
- return `/commerce/payment/${session.id}/${attempt.orderId}`;
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.equal(session?.payScene, 'commerce-payment:commerceOrder');
46
- assert.equal(session?.providerName, 'pay-mock:mock');
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,
@@ -0,0 +1,3 @@
1
+ export default {
2
+ Commerce: 'Commerce',
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ Commerce: '商城',
3
+ };
@@ -1 +1,2 @@
1
+ export * from './.metadata/locales.ts';
1
2
  export * from './.metadata/index.ts';
@@ -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 === 'paypal' ? 'PayPal' : 'Mock payment',
26
- interaction: candidate.key === 'paypal' ? ('redirect' as const) : ('embedded' as const),
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 correlationId = `${order.correlationId}:provider:${event.eventId}`;
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: event.eventId.slice(0, 100),
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: event.eventId.slice(0, 100),
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 = `${order.correlationId}:provider-refund:${event.eventId}`;
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: event.eventId.slice(0, 100),
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 correlationId = `${order.correlationId}:late-capture:${event.eventId}`;
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: event.eventId.slice(0, 100),
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: `${correlationId}:refund`,
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.locales).comment(entityRole.$comment.locales);
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
- locales: role.locales ?? {},
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',