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
@@ -8,12 +8,15 @@ import { Core } from 'vona-module-a-core';
8
8
  import type { EntityPaymentSession } from '../entity/paymentSession.tsx';
9
9
  import type { EntityProviderOperation } from '../entity/providerOperation.tsx';
10
10
  import type {
11
+ IPayProviderOperationStartInputSnapshot,
11
12
  IPayProviderPaymentInput,
12
13
  IPayProviderPaymentSnapshot,
13
14
  IPayProviderRefundInput,
14
15
  TypeProviderOperationKind,
15
16
  } from '../types/payment.ts';
16
17
 
18
+ import { ProviderOperationFailure } from '../lib/providerOperationFailure.ts';
19
+
17
20
  const ClaimLeaseMilliseconds = 60_000;
18
21
  const MaxAttempts = 10;
19
22
  const PaypalRefundIdempotencyRetentionMilliseconds = 45 * 24 * 60 * 60 * 1_000;
@@ -73,6 +76,16 @@ export class ServiceProviderOperation extends BeanBase {
73
76
  if (session.expiresAt <= new Date()) this.app.throw(409, 'payment session is expired');
74
77
 
75
78
  const now = new Date();
79
+ const { clientOptions } = this.bean.payProvider.resolveByName(
80
+ session.providerName,
81
+ session.clientName,
82
+ );
83
+ if (clientOptions.environment !== session.environment) {
84
+ this.app.throw(500, 'payment session provider environment is inconsistent');
85
+ }
86
+ const callbackUrls = clientOptions.capabilities.redirectCheckout
87
+ ? await this.scope.service.paymentCallback.createUrls(session)
88
+ : undefined;
76
89
  await this.scope.model.paymentSession.updateById(session.id, { state: 'starting' });
77
90
  await this.scope.model.paymentAudit.insert({
78
91
  paymentSessionId: session.id,
@@ -82,7 +95,7 @@ export class ServiceProviderOperation extends BeanBase {
82
95
  source: 'providerOperation.start',
83
96
  occurredAt: now,
84
97
  });
85
- return await this._insertOperation(session, 'start', now);
98
+ return await this._insertOperation(session, 'start', now, callbackUrls);
86
99
  }
87
100
 
88
101
  @Core.transaction()
@@ -168,21 +181,10 @@ export class ServiceProviderOperation extends BeanBase {
168
181
  provider: ReturnType<typeof this.bean.payProvider.resolveByName>['provider'],
169
182
  clientOptions: ReturnType<typeof this.bean.payProvider.resolveByName>['clientOptions'],
170
183
  ) {
171
- const callbackUrls =
172
- operation.kind === 'start' && clientOptions.capabilities.redirectCheckout
173
- ? await this.scope.service.paymentCallback.createUrls(session)
174
- : undefined;
175
- const input: IPayProviderPaymentInput = {
176
- paymentSessionId: session.id,
177
- businessReference: session.businessReference,
178
- providerInvoiceReference: session.providerInvoiceReference,
179
- providerCorrelationReference: session.providerCorrelationReference,
180
- idempotencyKey: operation.idempotencyKey,
181
- amountMinor: session.amountMinor,
182
- currency: session.currency,
183
- providerOrderId: session.providerOrderId,
184
- ...callbackUrls,
185
- };
184
+ const input =
185
+ operation.kind === 'start' && !session.providerOrderId
186
+ ? this._getStartInputSnapshot(operation, session)
187
+ : this._createPaymentInput(operation, session);
186
188
  if (operation.kind === 'start') {
187
189
  return session.providerOrderId
188
190
  ? await provider.queryPayment(input, clientOptions)
@@ -197,6 +199,53 @@ export class ServiceProviderOperation extends BeanBase {
197
199
  this.app.throw(500, `unsupported payment provider operation: ${operation.kind}`);
198
200
  }
199
201
 
202
+ private _getStartInputSnapshot(
203
+ operation: EntityProviderOperation,
204
+ session: EntityPaymentSession,
205
+ ): IPayProviderPaymentInput {
206
+ const snapshot = operation.startInputSnapshot;
207
+ if (!snapshot) {
208
+ throw new ProviderOperationFailure(
209
+ 'start_input_snapshot_missing',
210
+ 'Payment start request cannot be replayed safely',
211
+ );
212
+ }
213
+ if (
214
+ snapshot.version !== 1 ||
215
+ snapshot.paymentSessionId !== session.id ||
216
+ snapshot.businessReference !== session.businessReference ||
217
+ snapshot.providerInvoiceReference !== session.providerInvoiceReference ||
218
+ snapshot.providerCorrelationReference !== session.providerCorrelationReference ||
219
+ snapshot.idempotencyKey !== operation.idempotencyKey ||
220
+ snapshot.amountMinor !== session.amountMinor ||
221
+ snapshot.currency !== session.currency ||
222
+ snapshot.providerOrderId ||
223
+ Boolean(snapshot.returnUrl) !== Boolean(snapshot.cancelUrl)
224
+ ) {
225
+ throw new ProviderOperationFailure(
226
+ 'start_input_snapshot_invalid',
227
+ 'Payment start request cannot be replayed safely',
228
+ );
229
+ }
230
+ return snapshot;
231
+ }
232
+
233
+ private _createPaymentInput(
234
+ operation: EntityProviderOperation,
235
+ session: EntityPaymentSession,
236
+ ): IPayProviderPaymentInput {
237
+ return {
238
+ paymentSessionId: session.id,
239
+ businessReference: session.businessReference,
240
+ providerInvoiceReference: session.providerInvoiceReference,
241
+ providerCorrelationReference: session.providerCorrelationReference,
242
+ idempotencyKey: operation.idempotencyKey,
243
+ amountMinor: session.amountMinor,
244
+ currency: session.currency,
245
+ providerOrderId: session.providerOrderId,
246
+ };
247
+ }
248
+
200
249
  private async _executeRefund(
201
250
  operation: EntityProviderOperation,
202
251
  session: EntityPaymentSession,
@@ -264,9 +313,11 @@ export class ServiceProviderOperation extends BeanBase {
264
313
  return undefined;
265
314
  }
266
315
  if (operation.attemptCount >= MaxAttempts) {
267
- await this.scope.model.providerOperation.updateById(operation.id, {
268
- state: 'failed',
316
+ await this._finalizeFailedOperation(operation, {
269
317
  finalizedAt: now,
318
+ errorCode: operation.errorCode,
319
+ errorSummary: operation.errorSummary,
320
+ source: 'attemptsExhausted',
270
321
  });
271
322
  return undefined;
272
323
  }
@@ -419,16 +470,17 @@ export class ServiceProviderOperation extends BeanBase {
419
470
  operation.kind === 'refund' &&
420
471
  operation.submittedAt &&
421
472
  failure.code === 'refund_submission_outcome_unknown';
422
- if (operation.attemptCount >= MaxAttempts && !isAmbiguousRefundSubmission) {
423
- await this.scope.model.providerOperation.updateById(operation.id, {
424
- state: 'failed',
425
- claimToken: undefined,
426
- claimExpiresAt: undefined,
473
+ if (
474
+ failure.terminal ||
475
+ (operation.attemptCount >= MaxAttempts && !isAmbiguousRefundSubmission)
476
+ ) {
477
+ await this._finalizeFailedOperation(operation, {
427
478
  finalizedAt: now,
428
479
  errorCode: failure.code,
429
480
  errorSummary: failure.summary,
481
+ source: failure.terminal ? 'providerRejected' : 'attemptsExhausted',
430
482
  });
431
- this._logProviderFailure(operation, failure, 'failed');
483
+ this._logProviderFailure(operation, failure, 'failed', error);
432
484
  return undefined;
433
485
  }
434
486
  const nextAttemptAt = new Date(
@@ -442,7 +494,7 @@ export class ServiceProviderOperation extends BeanBase {
442
494
  errorCode: failure.code,
443
495
  errorSummary: failure.summary,
444
496
  });
445
- this._logProviderFailure(operation, failure, 'reconciliation_required');
497
+ this._logProviderFailure(operation, failure, 'reconciliation_required', error);
446
498
  return {
447
499
  ...operation,
448
500
  state: 'reconciliation_required' as const,
@@ -598,12 +650,63 @@ export class ServiceProviderOperation extends BeanBase {
598
650
  };
599
651
  }
600
652
 
653
+ private async _finalizeFailedOperation(
654
+ operation: EntityProviderOperation,
655
+ options: {
656
+ finalizedAt: Date;
657
+ errorCode?: string;
658
+ errorSummary?: string;
659
+ source: 'attemptsExhausted' | 'providerRejected';
660
+ },
661
+ ) {
662
+ await this.scope.model.providerOperation.updateById(operation.id, {
663
+ state: 'failed',
664
+ claimToken: undefined,
665
+ claimExpiresAt: undefined,
666
+ nextAttemptAt: undefined,
667
+ finalizedAt: options.finalizedAt,
668
+ errorCode: options.errorCode,
669
+ errorSummary: options.errorSummary,
670
+ });
671
+ if (operation.kind === 'refund') return;
672
+
673
+ const session = await this.scope.model.paymentSession.getByIdForUpdate(
674
+ operation.paymentSessionId,
675
+ );
676
+ if (!session || ['succeeded', 'failed', 'cancelled', 'expired'].includes(session.state)) return;
677
+ await this.scope.model.paymentSession.updateById(session.id, {
678
+ state: 'failed',
679
+ nextAction: undefined,
680
+ finalizedAt: options.finalizedAt,
681
+ });
682
+ await this.scope.model.paymentAudit.insert({
683
+ paymentSessionId: session.id,
684
+ providerOperationId: operation.id,
685
+ fromState: session.state,
686
+ toState: 'failed',
687
+ correlationId: session.correlationId,
688
+ source: `providerOperation.${operation.kind}.${options.source}`,
689
+ occurredAt: options.finalizedAt,
690
+ });
691
+ await this.scope.service.outbox.enqueue(session.id, 'payment.outcome.v1', {
692
+ eventId: `${operation.correlationId}:${operation.kind}:${options.source}`,
693
+ paymentSessionId: session.id,
694
+ businessReference: session.businessReference,
695
+ providerName: session.providerName,
696
+ state: 'failed',
697
+ providerCaptureId: session.providerCaptureId,
698
+ amountMinor: session.amountMinor,
699
+ currency: session.currency,
700
+ });
701
+ }
702
+
601
703
  private _logProviderFailure(
602
704
  operation: EntityProviderOperation,
603
705
  failure: { code: string; summary: string },
604
706
  state: 'failed' | 'reconciliation_required',
707
+ error: unknown,
605
708
  ) {
606
- this.$logger.warn({
709
+ this.$logger.warn(failure.summary, {
607
710
  event: 'pay.provider_operation_failed',
608
711
  providerOperationId: operation.id,
609
712
  paymentSessionId: operation.paymentSessionId,
@@ -613,6 +716,7 @@ export class ServiceProviderOperation extends BeanBase {
613
716
  state,
614
717
  errorCode: failure.code,
615
718
  errorSummary: failure.summary,
719
+ ...providerErrorLogFields(error),
616
720
  });
617
721
  }
618
722
 
@@ -620,18 +724,43 @@ export class ServiceProviderOperation extends BeanBase {
620
724
  session: EntityPaymentSession,
621
725
  kind: Exclude<TypeProviderOperationKind, 'refund'>,
622
726
  now: Date,
727
+ callbackUrls?: { returnUrl: string; cancelUrl: string },
623
728
  ) {
729
+ const idempotencyKey = randomUUID();
730
+ const startInputSnapshot =
731
+ kind === 'start'
732
+ ? this._createStartInputSnapshot(session, idempotencyKey, callbackUrls)
733
+ : undefined;
624
734
  return await this.scope.model.providerOperation.insert({
625
735
  paymentSessionId: session.id,
626
736
  kind,
627
737
  state: 'created',
628
- idempotencyKey: randomUUID(),
738
+ idempotencyKey,
629
739
  correlationId: session.correlationId,
740
+ startInputSnapshot,
630
741
  attemptCount: 0,
631
742
  nextAttemptAt: new Date(now.getTime() - 1_000),
632
743
  });
633
744
  }
634
745
 
746
+ private _createStartInputSnapshot(
747
+ session: EntityPaymentSession,
748
+ idempotencyKey: string,
749
+ callbackUrls?: { returnUrl: string; cancelUrl: string },
750
+ ): IPayProviderOperationStartInputSnapshot {
751
+ return {
752
+ version: 1,
753
+ paymentSessionId: session.id,
754
+ businessReference: session.businessReference,
755
+ providerInvoiceReference: session.providerInvoiceReference,
756
+ providerCorrelationReference: session.providerCorrelationReference,
757
+ idempotencyKey,
758
+ amountMinor: session.amountMinor,
759
+ currency: session.currency,
760
+ ...callbackUrls,
761
+ };
762
+ }
763
+
635
764
  private async _getSession(paymentSessionId: TableIdentity) {
636
765
  const session = await this.scope.model.paymentSession.getById(paymentSessionId);
637
766
  if (!session) this.app.throw(404, 'payment session not found');
@@ -670,6 +799,30 @@ export class ServiceProviderOperation extends BeanBase {
670
799
  }
671
800
  }
672
801
 
802
+ function providerErrorLogFields(error: unknown) {
803
+ const value = error as Record<string, unknown> | undefined;
804
+ const message = error instanceof Error ? redactProviderErrorMessage(error.message) : undefined;
805
+ return {
806
+ ...(message && { providerErrorMessage: message }),
807
+ ...(typeof value?.type === 'string' && { providerErrorType: value.type.slice(0, 100) }),
808
+ ...(typeof value?.code === 'string' && { providerErrorCode: value.code.slice(0, 100) }),
809
+ ...(typeof value?.requestId === 'string' && {
810
+ providerRequestId: value.requestId.slice(0, 100),
811
+ }),
812
+ };
813
+ }
814
+
815
+ function redactProviderErrorMessage(message: string) {
816
+ return message
817
+ .slice(0, 500)
818
+ .replace(/\b(?:sk_(?:test|live)_\w+|whsec_\w+)\b/g, '<redacted>')
819
+ .replace(/\b(Bearer\s+)\S+/gi, '$1<redacted>')
820
+ .replace(
821
+ /([?&](?:state|client_secret|api_key|key|token|authorization)=)[^&\s]+/gi,
822
+ '$1<redacted>',
823
+ );
824
+ }
825
+
673
826
  function isTerminalPaymentState(
674
827
  state: IPayProviderPaymentSnapshot['state'],
675
828
  ): state is 'succeeded' | 'failed' | 'cancelled' {
@@ -681,17 +834,22 @@ function retryDelayMilliseconds(attemptCount: number) {
681
834
  }
682
835
 
683
836
  function classifyProviderFailure(
684
- _error: unknown,
837
+ error: unknown,
685
838
  kind: TypeProviderOperationKind,
686
- ): { code: string; summary: string } {
839
+ ): { code: string; summary: string; terminal: boolean } {
687
840
  if (kind === 'refund') {
688
841
  return {
689
842
  code: 'refund_submission_outcome_unknown',
690
843
  summary: 'Provider refund submission outcome is unknown',
844
+ terminal: false,
691
845
  };
692
846
  }
847
+ if (error instanceof ProviderOperationFailure) {
848
+ return { code: error.failureCode, summary: error.summary, terminal: true };
849
+ }
693
850
  return {
694
851
  code: 'provider_operation_failed',
695
852
  summary: 'Provider operation failed and will be reconciled',
853
+ terminal: false,
696
854
  };
697
855
  }
@@ -66,6 +66,10 @@ export interface IPayProviderPaymentInput extends IPayMoney {
66
66
  providerOrderId?: string;
67
67
  }
68
68
 
69
+ export interface IPayProviderOperationStartInputSnapshot extends IPayProviderPaymentInput {
70
+ version: 1;
71
+ }
72
+
69
73
  export interface IPayProviderRefundInput extends IPayMoney {
70
74
  paymentSessionId: TableIdentity;
71
75
  refundOperationId: TableIdentity;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-pay-mock",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-pay-paypal",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -706,9 +706,3 @@ function parsePaypalCurrency(value: unknown) {
706
706
  const currency = readString(readField(amount, 'currencyCode', 'currency_code'));
707
707
  return currency === 'USD' ? currency : undefined;
708
708
  }
709
-
710
- declare module 'vona-module-a-pay' {
711
- export interface IPayProviderRecord {
712
- 'pay-paypal:paypal': IPayProviderOptionsPaypal;
713
- }
714
- }
@@ -1,3 +1,4 @@
1
+ export type { IPayProviderPaypalClientOptions } from './bean/payProvider.paypal.ts';
1
2
  export * from './lib/index.ts';
2
3
  export * from './types/index.ts';
3
4
  export * from './.metadata/index.ts';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-pay-stripe",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -31,6 +31,7 @@
31
31
  "postpack": "clean-package restore"
32
32
  },
33
33
  "dependencies": {
34
+ "stripe": "^22.5.0",
34
35
  "vona-module-a-pay": "workspace:*"
35
36
  },
36
37
  "devDependencies": {