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
|
@@ -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
|
|
172
|
-
operation.kind === 'start' &&
|
|
173
|
-
?
|
|
174
|
-
:
|
|
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.
|
|
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 (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona-module-pay-stripe",
|
|
3
|
-
"version": "5.0.
|
|
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": {
|