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
|
@@ -11,14 +11,20 @@ import type {
|
|
|
11
11
|
IPayProviderWebhookInput,
|
|
12
12
|
} from 'vona-module-a-pay';
|
|
13
13
|
|
|
14
|
+
import Stripe from 'stripe';
|
|
14
15
|
import { BeanBase, useApp } from 'vona';
|
|
15
|
-
import { PayProvider } from 'vona-module-a-pay';
|
|
16
|
+
import { PayProvider, ProviderOperationFailure } from 'vona-module-a-pay';
|
|
17
|
+
|
|
18
|
+
import type { IStripeGateway, IStripeGatewayOptions } from '../lib/stripeGateway.ts';
|
|
19
|
+
|
|
20
|
+
import { createStripeGateway, StripeGatewayError } from '../lib/stripeGateway.ts';
|
|
16
21
|
|
|
17
22
|
export interface IPayProviderStripeClientRecord extends IPayProviderClientRecord {}
|
|
18
23
|
|
|
19
24
|
export interface IPayProviderStripeClientOptions extends IPayProviderClientOptions {
|
|
20
25
|
secretCredential: string | undefined;
|
|
21
26
|
secretWebhook: string | undefined;
|
|
27
|
+
gateway?: IStripeGateway;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
const app = useApp();
|
|
@@ -31,8 +37,8 @@ export interface IPayProviderOptionsStripe extends IDecoratorPayProviderOptions<
|
|
|
31
37
|
@PayProvider<IPayProviderOptionsStripe>({
|
|
32
38
|
base: {
|
|
33
39
|
capabilities: {
|
|
34
|
-
redirectCheckout:
|
|
35
|
-
embeddedCheckout:
|
|
40
|
+
redirectCheckout: true,
|
|
41
|
+
embeddedCheckout: false,
|
|
36
42
|
automaticCapture: true,
|
|
37
43
|
manualCapture: false,
|
|
38
44
|
refunds: true,
|
|
@@ -42,9 +48,10 @@ export interface IPayProviderOptionsStripe extends IDecoratorPayProviderOptions<
|
|
|
42
48
|
},
|
|
43
49
|
clients: {
|
|
44
50
|
default: {
|
|
45
|
-
environment: 'sandbox',
|
|
51
|
+
environment: app.meta.env.STRIPE_ENVIRONMENT as 'sandbox' | 'live',
|
|
46
52
|
secretCredential: app.meta.env.STRIPE_SECRET_KEY,
|
|
47
53
|
secretWebhook: app.meta.env.STRIPE_WEBHOOK_SECRET,
|
|
54
|
+
merchantReference: app.meta.env.STRIPE_MERCHANT_REFERENCE,
|
|
48
55
|
},
|
|
49
56
|
},
|
|
50
57
|
})
|
|
@@ -53,43 +60,472 @@ export class PayProviderStripe
|
|
|
53
60
|
implements IPayProviderExecute<IPayProviderStripeClientOptions>
|
|
54
61
|
{
|
|
55
62
|
async startPayment(
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
input: IPayProviderPaymentInput,
|
|
64
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
58
65
|
): Promise<IPayProviderPaymentSnapshot> {
|
|
59
|
-
this.
|
|
66
|
+
if (input.providerOrderId) return await this.queryPayment(input, clientOptions);
|
|
67
|
+
if (!input.returnUrl || !input.cancelUrl) {
|
|
68
|
+
this.app.throw(500, 'Stripe payment requires return and cancel URLs');
|
|
69
|
+
}
|
|
70
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
71
|
+
assertStripeMoney(input.amountMinor, input.currency);
|
|
72
|
+
const body = {
|
|
73
|
+
mode: 'payment',
|
|
74
|
+
success_url: input.returnUrl,
|
|
75
|
+
cancel_url: input.cancelUrl,
|
|
76
|
+
client_reference_id: input.providerCorrelationReference,
|
|
77
|
+
metadata: this._paymentMetadata(input),
|
|
78
|
+
payment_intent_data: { metadata: this._paymentMetadata(input) },
|
|
79
|
+
managed_payments: { enabled: false },
|
|
80
|
+
line_items: [
|
|
81
|
+
{
|
|
82
|
+
quantity: 1,
|
|
83
|
+
price_data: {
|
|
84
|
+
currency: input.currency.toLowerCase(),
|
|
85
|
+
unit_amount: input.amountMinor,
|
|
86
|
+
product_data: { name: `Payment ${input.providerInvoiceReference}` },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
} satisfies Stripe.Checkout.SessionCreateParams;
|
|
91
|
+
let checkoutSession: unknown;
|
|
92
|
+
try {
|
|
93
|
+
checkoutSession = await this._gateway(clientOptions).createCheckoutSession(gatewayOptions, {
|
|
94
|
+
idempotencyKey: input.idempotencyKey,
|
|
95
|
+
body,
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this._throwTerminalPaymentFailure(error);
|
|
99
|
+
}
|
|
100
|
+
return this._mapCheckoutSession(checkoutSession, input, clientOptions, true);
|
|
60
101
|
}
|
|
61
102
|
|
|
62
103
|
async confirmPayment(
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
input: IPayProviderPaymentInput,
|
|
105
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
65
106
|
): Promise<IPayProviderPaymentSnapshot> {
|
|
66
|
-
this.
|
|
107
|
+
return await this.queryPayment(input, clientOptions);
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
async queryPayment(
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
input: IPayProviderPaymentInput,
|
|
112
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
72
113
|
): Promise<IPayProviderPaymentSnapshot> {
|
|
73
|
-
this.
|
|
114
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
115
|
+
if (!input.providerOrderId) this.app.throw(409, 'Stripe payment has no Checkout Session');
|
|
116
|
+
const checkoutSession = await this._gateway(clientOptions).retrieveCheckoutSession(
|
|
117
|
+
gatewayOptions,
|
|
118
|
+
{ id: input.providerOrderId },
|
|
119
|
+
);
|
|
120
|
+
return this._mapCheckoutSession(checkoutSession, input, clientOptions);
|
|
74
121
|
}
|
|
75
122
|
|
|
76
123
|
async createRefund(
|
|
77
|
-
|
|
78
|
-
|
|
124
|
+
input: IPayProviderRefundInput,
|
|
125
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
126
|
+
): Promise<IPayProviderRefundSnapshot> {
|
|
127
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
128
|
+
assertStripeMoney(input.amountMinor, input.currency);
|
|
129
|
+
try {
|
|
130
|
+
const refund = await this._gateway(clientOptions).createRefund(gatewayOptions, {
|
|
131
|
+
idempotencyKey: input.idempotencyKey,
|
|
132
|
+
body: {
|
|
133
|
+
payment_intent: input.providerCaptureId,
|
|
134
|
+
amount: input.amountMinor,
|
|
135
|
+
metadata: this._refundMetadata(input),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
return this._mapRefund(refund, input);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (this._isDefinitiveRefundRejection(error)) return { state: 'failed' };
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async queryRefund(
|
|
146
|
+
input: IPayProviderRefundInput,
|
|
147
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
79
148
|
): Promise<IPayProviderRefundSnapshot> {
|
|
80
|
-
this.
|
|
149
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
150
|
+
if (!input.providerRefundId)
|
|
151
|
+
this.app.throw(409, 'Stripe refund has no provider refund identifier');
|
|
152
|
+
const refund = await this._gateway(clientOptions).retrieveRefund(gatewayOptions, {
|
|
153
|
+
id: input.providerRefundId,
|
|
154
|
+
});
|
|
155
|
+
return this._mapRefund(refund, input);
|
|
81
156
|
}
|
|
82
157
|
|
|
83
158
|
async verifyWebhook(
|
|
84
|
-
|
|
85
|
-
|
|
159
|
+
input: IPayProviderWebhookInput,
|
|
160
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
161
|
+
): Promise<IPayProviderVerifiedWebhook> {
|
|
162
|
+
const gatewayOptions = this._gatewayOptions(clientOptions);
|
|
163
|
+
let event: Record<string, unknown>;
|
|
164
|
+
try {
|
|
165
|
+
event = asRecord(
|
|
166
|
+
await this._gateway(clientOptions).constructWebhookEvent(gatewayOptions, input),
|
|
167
|
+
);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this._throwGatewayError(error);
|
|
170
|
+
}
|
|
171
|
+
const eventId = readString(event.id);
|
|
172
|
+
const eventType = readString(event.type);
|
|
173
|
+
const resource = asRecord(asRecord(event.data).object);
|
|
174
|
+
if (!eventId || !eventType || !Object.keys(resource).length) {
|
|
175
|
+
this.app.throw(400, 'Stripe webhook is invalid');
|
|
176
|
+
}
|
|
177
|
+
if (eventType.startsWith('checkout.session.')) {
|
|
178
|
+
return await this._mapCheckoutWebhook(eventId, eventType, resource, clientOptions);
|
|
179
|
+
}
|
|
180
|
+
if (eventType.startsWith('payment_intent.')) {
|
|
181
|
+
return await this._mapPaymentIntentWebhook(eventId, eventType, resource);
|
|
182
|
+
}
|
|
183
|
+
if (eventType.startsWith('refund.')) {
|
|
184
|
+
return await this._mapRefundWebhook(eventId, eventType, resource);
|
|
185
|
+
}
|
|
186
|
+
return { eventId, eventType, ignored: true, summary: { amountMinor: 0, currency: 'USD' } };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async _mapCheckoutWebhook(
|
|
190
|
+
eventId: string,
|
|
191
|
+
eventType: string,
|
|
192
|
+
checkoutSession: Record<string, unknown>,
|
|
193
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
194
|
+
): Promise<IPayProviderVerifiedWebhook> {
|
|
195
|
+
const input = await this._inputFromCheckoutSession(checkoutSession);
|
|
196
|
+
const payment = await this._mapCheckoutSession(checkoutSession, input, clientOptions);
|
|
197
|
+
if (eventType === 'checkout.session.async_payment_failed') {
|
|
198
|
+
return {
|
|
199
|
+
eventId,
|
|
200
|
+
eventType,
|
|
201
|
+
paymentSessionId: input.paymentSessionId,
|
|
202
|
+
payment: { ...payment, state: 'failed' },
|
|
203
|
+
summary: { amountMinor: input.amountMinor, currency: input.currency },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (!['succeeded', 'failed', 'cancelled'].includes(payment.state)) {
|
|
207
|
+
return {
|
|
208
|
+
eventId,
|
|
209
|
+
eventType,
|
|
210
|
+
ignored: true,
|
|
211
|
+
summary: { amountMinor: input.amountMinor, currency: input.currency },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
eventId,
|
|
216
|
+
eventType,
|
|
217
|
+
paymentSessionId: input.paymentSessionId,
|
|
218
|
+
payment,
|
|
219
|
+
summary: { amountMinor: input.amountMinor, currency: input.currency },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async _mapPaymentIntentWebhook(
|
|
224
|
+
eventId: string,
|
|
225
|
+
eventType: string,
|
|
226
|
+
paymentIntent: Record<string, unknown>,
|
|
86
227
|
): Promise<IPayProviderVerifiedWebhook> {
|
|
87
|
-
|
|
228
|
+
const metadata = asRecord(paymentIntent.metadata);
|
|
229
|
+
const providerCorrelationReference = readString(metadata.providerCorrelationReference);
|
|
230
|
+
const providerInvoiceReference = readString(metadata.providerInvoiceReference);
|
|
231
|
+
if (!providerCorrelationReference || !providerInvoiceReference) {
|
|
232
|
+
this.app.throw(400, 'Stripe PaymentIntent webhook is not correlated');
|
|
233
|
+
}
|
|
234
|
+
const session = await this.$scope.pay.model.paymentSession.get({
|
|
235
|
+
providerCorrelationReference,
|
|
236
|
+
});
|
|
237
|
+
if (!session || session.providerInvoiceReference !== providerInvoiceReference) {
|
|
238
|
+
this.app.throw(400, 'Stripe PaymentIntent webhook is not correlated');
|
|
239
|
+
}
|
|
240
|
+
const payment = this._mapPaymentIntent(paymentIntent, {
|
|
241
|
+
paymentSessionId: session.id,
|
|
242
|
+
businessReference: session.businessReference,
|
|
243
|
+
providerInvoiceReference: session.providerInvoiceReference,
|
|
244
|
+
providerCorrelationReference: session.providerCorrelationReference,
|
|
245
|
+
idempotencyKey: '',
|
|
246
|
+
amountMinor: session.amountMinor,
|
|
247
|
+
currency: session.currency,
|
|
248
|
+
providerOrderId: session.providerOrderId,
|
|
249
|
+
});
|
|
250
|
+
const terminalPayment =
|
|
251
|
+
eventType === 'payment_intent.payment_failed'
|
|
252
|
+
? { ...payment, state: 'failed' as const }
|
|
253
|
+
: payment;
|
|
254
|
+
if (!['succeeded', 'failed', 'cancelled'].includes(terminalPayment.state)) {
|
|
255
|
+
return {
|
|
256
|
+
eventId,
|
|
257
|
+
eventType,
|
|
258
|
+
ignored: true,
|
|
259
|
+
summary: { amountMinor: session.amountMinor, currency: session.currency },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
eventId,
|
|
264
|
+
eventType,
|
|
265
|
+
paymentSessionId: session.id,
|
|
266
|
+
payment: terminalPayment,
|
|
267
|
+
summary: { amountMinor: session.amountMinor, currency: session.currency },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async _mapRefundWebhook(
|
|
272
|
+
eventId: string,
|
|
273
|
+
eventType: string,
|
|
274
|
+
refundResource: Record<string, unknown>,
|
|
275
|
+
): Promise<IPayProviderVerifiedWebhook> {
|
|
276
|
+
const refundId = readString(refundResource.id);
|
|
277
|
+
const metadata = asRecord(refundResource.metadata);
|
|
278
|
+
const providerCorrelationReference = readString(metadata.providerCorrelationReference);
|
|
279
|
+
if (!refundId || !providerCorrelationReference) {
|
|
280
|
+
this.app.throw(400, 'Stripe refund webhook is not correlated');
|
|
281
|
+
}
|
|
282
|
+
const refund = await this.$scope.pay.model.refundOperation.get({
|
|
283
|
+
providerCorrelationReference,
|
|
284
|
+
});
|
|
285
|
+
if (!refund) this.app.throw(400, 'Stripe refund webhook is not correlated');
|
|
286
|
+
const session = await this.$scope.pay.model.paymentSession.getById(refund.paymentSessionId);
|
|
287
|
+
if (!session?.providerCaptureId) this.app.throw(409, 'Stripe refund webhook payment conflicts');
|
|
288
|
+
const snapshot = this._mapRefund(refundResource, {
|
|
289
|
+
paymentSessionId: session.id,
|
|
290
|
+
refundOperationId: refund.id,
|
|
291
|
+
businessReference: refund.businessReference,
|
|
292
|
+
providerInvoiceReference: refund.providerInvoiceReference,
|
|
293
|
+
providerCorrelationReference: refund.providerCorrelationReference,
|
|
294
|
+
idempotencyKey: '',
|
|
295
|
+
amountMinor: refund.amountMinor,
|
|
296
|
+
currency: refund.currency,
|
|
297
|
+
providerCaptureId: session.providerCaptureId,
|
|
298
|
+
providerRefundId: refundId,
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
eventId,
|
|
302
|
+
eventType,
|
|
303
|
+
refundOperationId: refund.id,
|
|
304
|
+
refund: snapshot,
|
|
305
|
+
summary: { amountMinor: refund.amountMinor, currency: refund.currency },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async _inputFromCheckoutSession(
|
|
310
|
+
checkoutSession: Record<string, unknown>,
|
|
311
|
+
): Promise<IPayProviderPaymentInput> {
|
|
312
|
+
const metadata = asRecord(checkoutSession.metadata);
|
|
313
|
+
const providerCorrelationReference = readString(metadata.providerCorrelationReference);
|
|
314
|
+
const providerInvoiceReference = readString(metadata.providerInvoiceReference);
|
|
315
|
+
const providerOrderId = readString(checkoutSession.id);
|
|
316
|
+
if (!providerCorrelationReference || !providerInvoiceReference || !providerOrderId) {
|
|
317
|
+
this.app.throw(400, 'Stripe Checkout Session is not correlated');
|
|
318
|
+
}
|
|
319
|
+
const session = await this.$scope.pay.model.paymentSession.get({
|
|
320
|
+
providerCorrelationReference,
|
|
321
|
+
});
|
|
322
|
+
if (!session || session.providerInvoiceReference !== providerInvoiceReference) {
|
|
323
|
+
this.app.throw(400, 'Stripe Checkout Session is not correlated');
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
paymentSessionId: session.id,
|
|
327
|
+
businessReference: session.businessReference,
|
|
328
|
+
providerInvoiceReference: session.providerInvoiceReference,
|
|
329
|
+
providerCorrelationReference: session.providerCorrelationReference,
|
|
330
|
+
idempotencyKey: '',
|
|
331
|
+
amountMinor: session.amountMinor,
|
|
332
|
+
currency: session.currency,
|
|
333
|
+
providerOrderId,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private _mapCheckoutSession(
|
|
338
|
+
checkoutSession: unknown,
|
|
339
|
+
input: IPayProviderPaymentInput,
|
|
340
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
341
|
+
requireRedirect = false,
|
|
342
|
+
): IPayProviderPaymentSnapshot {
|
|
343
|
+
const record = asRecord(checkoutSession);
|
|
344
|
+
const providerOrderId = readString(record.id);
|
|
345
|
+
const amountMinor = readInteger(record.amount_total);
|
|
346
|
+
const currency = readString(record.currency)?.toUpperCase();
|
|
347
|
+
const metadata = asRecord(record.metadata);
|
|
348
|
+
if (
|
|
349
|
+
!providerOrderId ||
|
|
350
|
+
(input.providerOrderId && input.providerOrderId !== providerOrderId) ||
|
|
351
|
+
amountMinor !== input.amountMinor ||
|
|
352
|
+
currency !== input.currency ||
|
|
353
|
+
readString(record.client_reference_id) !== input.providerCorrelationReference ||
|
|
354
|
+
readString(metadata.providerCorrelationReference) !== input.providerCorrelationReference ||
|
|
355
|
+
readString(metadata.providerInvoiceReference) !== input.providerInvoiceReference ||
|
|
356
|
+
!this._matchesEnvironment(record, clientOptions)
|
|
357
|
+
) {
|
|
358
|
+
this.app.throw(409, 'Stripe Checkout Session facts conflict with the payment session');
|
|
359
|
+
}
|
|
360
|
+
const paymentIntent = asRecord(record.payment_intent);
|
|
361
|
+
const paymentIntentId =
|
|
362
|
+
readString(record.payment_intent) ?? readString(paymentIntent.id) ?? undefined;
|
|
363
|
+
if (paymentIntentId && Object.keys(paymentIntent).length) {
|
|
364
|
+
return this._mapPaymentIntent(paymentIntent, { ...input, providerOrderId }, providerOrderId);
|
|
365
|
+
}
|
|
366
|
+
if (record.status === 'expired') {
|
|
367
|
+
return { state: 'cancelled', providerPaymentId: providerOrderId, providerOrderId };
|
|
368
|
+
}
|
|
369
|
+
if (record.payment_status === 'paid' && paymentIntentId) {
|
|
370
|
+
return {
|
|
371
|
+
state: 'succeeded',
|
|
372
|
+
providerPaymentId: paymentIntentId,
|
|
373
|
+
providerOrderId,
|
|
374
|
+
providerCaptureId: paymentIntentId,
|
|
375
|
+
nextAction: { kind: 'completed' },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const url = readString(record.url);
|
|
379
|
+
if (requireRedirect && !url) this.app.throw(502, 'Stripe did not return a Checkout URL');
|
|
380
|
+
return {
|
|
381
|
+
state: 'requires_action',
|
|
382
|
+
providerPaymentId: providerOrderId,
|
|
383
|
+
providerOrderId,
|
|
384
|
+
...(url && { nextAction: { kind: 'redirect' as const, url } }),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private _mapPaymentIntent(
|
|
389
|
+
paymentIntent: Record<string, unknown>,
|
|
390
|
+
input: IPayProviderPaymentInput,
|
|
391
|
+
providerOrderId = input.providerOrderId,
|
|
392
|
+
): IPayProviderPaymentSnapshot {
|
|
393
|
+
const providerPaymentId = readString(paymentIntent.id);
|
|
394
|
+
const amountMinor = readInteger(paymentIntent.amount);
|
|
395
|
+
const currency = readString(paymentIntent.currency)?.toUpperCase();
|
|
396
|
+
const metadata = asRecord(paymentIntent.metadata);
|
|
397
|
+
if (
|
|
398
|
+
!providerPaymentId ||
|
|
399
|
+
amountMinor !== input.amountMinor ||
|
|
400
|
+
currency !== input.currency ||
|
|
401
|
+
readString(metadata.providerCorrelationReference) !== input.providerCorrelationReference ||
|
|
402
|
+
readString(metadata.providerInvoiceReference) !== input.providerInvoiceReference
|
|
403
|
+
) {
|
|
404
|
+
this.app.throw(409, 'Stripe PaymentIntent facts conflict with the payment session');
|
|
405
|
+
}
|
|
406
|
+
const status = readString(paymentIntent.status);
|
|
407
|
+
if (status === 'succeeded') {
|
|
408
|
+
return {
|
|
409
|
+
state: 'succeeded',
|
|
410
|
+
providerPaymentId,
|
|
411
|
+
providerOrderId,
|
|
412
|
+
providerCaptureId: providerPaymentId,
|
|
413
|
+
nextAction: { kind: 'completed' },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (status === 'processing') return { state: 'processing', providerPaymentId, providerOrderId };
|
|
417
|
+
if (status === 'canceled') return { state: 'cancelled', providerPaymentId, providerOrderId };
|
|
418
|
+
if (status === 'requires_payment_method')
|
|
419
|
+
return { state: 'failed', providerPaymentId, providerOrderId };
|
|
420
|
+
return { state: 'requires_action', providerPaymentId, providerOrderId };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private _mapRefund(refund: unknown, input: IPayProviderRefundInput): IPayProviderRefundSnapshot {
|
|
424
|
+
const record = asRecord(refund);
|
|
425
|
+
const providerRefundId = readString(record.id);
|
|
426
|
+
const amountMinor = readInteger(record.amount);
|
|
427
|
+
const currency = readString(record.currency)?.toUpperCase();
|
|
428
|
+
const metadata = asRecord(record.metadata);
|
|
429
|
+
const paymentIntent = readString(record.payment_intent);
|
|
430
|
+
if (
|
|
431
|
+
!providerRefundId ||
|
|
432
|
+
(input.providerRefundId && input.providerRefundId !== providerRefundId) ||
|
|
433
|
+
amountMinor !== input.amountMinor ||
|
|
434
|
+
currency !== input.currency ||
|
|
435
|
+
paymentIntent !== input.providerCaptureId ||
|
|
436
|
+
readString(metadata.providerCorrelationReference) !== input.providerCorrelationReference ||
|
|
437
|
+
readString(metadata.providerInvoiceReference) !== input.providerInvoiceReference
|
|
438
|
+
) {
|
|
439
|
+
this.app.throw(409, 'Stripe refund facts conflict with the refund operation');
|
|
440
|
+
}
|
|
441
|
+
const status = readString(record.status);
|
|
442
|
+
if (status === 'succeeded') return { state: 'succeeded', providerRefundId };
|
|
443
|
+
if (status === 'failed') return { state: 'failed', providerRefundId };
|
|
444
|
+
if (status === 'canceled') return { state: 'cancelled', providerRefundId };
|
|
445
|
+
return { state: 'pending', providerRefundId };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private _paymentMetadata(input: IPayProviderPaymentInput): Record<string, string> {
|
|
449
|
+
return {
|
|
450
|
+
paymentSessionId: String(input.paymentSessionId),
|
|
451
|
+
providerCorrelationReference: input.providerCorrelationReference,
|
|
452
|
+
providerInvoiceReference: input.providerInvoiceReference,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private _refundMetadata(input: IPayProviderRefundInput): Record<string, string> {
|
|
457
|
+
return {
|
|
458
|
+
paymentSessionId: String(input.paymentSessionId),
|
|
459
|
+
refundOperationId: String(input.refundOperationId),
|
|
460
|
+
providerCorrelationReference: input.providerCorrelationReference,
|
|
461
|
+
providerInvoiceReference: input.providerInvoiceReference,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private _gateway(clientOptions: IPayProviderStripeClientOptions): IStripeGateway {
|
|
466
|
+
return clientOptions.gateway ?? createStripeGateway();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private _gatewayOptions(clientOptions: IPayProviderStripeClientOptions): IStripeGatewayOptions {
|
|
470
|
+
const secretKey = clientOptions.secretCredential;
|
|
471
|
+
const webhookSecret = clientOptions.secretWebhook;
|
|
472
|
+
if (!secretKey || !webhookSecret) this.app.throw(503, 'Stripe is not fully configured');
|
|
473
|
+
return { secretKey, webhookSecret };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private _matchesEnvironment(
|
|
477
|
+
value: Record<string, unknown>,
|
|
478
|
+
clientOptions: IPayProviderStripeClientOptions,
|
|
479
|
+
): boolean {
|
|
480
|
+
const livemode = value.livemode;
|
|
481
|
+
return typeof livemode !== 'boolean' || livemode === (clientOptions.environment === 'live');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private _isDefinitiveRefundRejection(error: unknown): boolean {
|
|
485
|
+
return isStripeDefinitiveRejection(error);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private _throwTerminalPaymentFailure(error: unknown): never {
|
|
489
|
+
if (isStripeDefinitiveRejection(error)) {
|
|
490
|
+
throw new ProviderOperationFailure(
|
|
491
|
+
'stripe_request_rejected',
|
|
492
|
+
'Stripe rejected the payment request',
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private _throwGatewayError(error: unknown): never {
|
|
499
|
+
if (error instanceof StripeGatewayError) this.app.throw(error.status, error.message);
|
|
500
|
+
throw error;
|
|
88
501
|
}
|
|
89
502
|
}
|
|
90
503
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
504
|
+
function isStripeDefinitiveRejection(error: unknown): boolean {
|
|
505
|
+
if (error instanceof StripeGatewayError) return error.status >= 400 && error.status < 500;
|
|
506
|
+
const type = (error as Record<string, unknown> | undefined)?.type;
|
|
507
|
+
return [
|
|
508
|
+
'StripeInvalidRequestError',
|
|
509
|
+
'StripeIdempotencyError',
|
|
510
|
+
'StripeAuthenticationError',
|
|
511
|
+
'StripePermissionError',
|
|
512
|
+
].includes(type as string);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function assertStripeMoney(amountMinor: number, currency: string) {
|
|
516
|
+
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0 || currency !== 'USD') {
|
|
517
|
+
throw new Error('Stripe supports only positive USD minor-unit amounts');
|
|
94
518
|
}
|
|
95
519
|
}
|
|
520
|
+
|
|
521
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
522
|
+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function readString(value: unknown): string | undefined {
|
|
526
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function readInteger(value: unknown): number | undefined {
|
|
530
|
+
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
531
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
2
|
+
|
|
3
|
+
export interface IStripeGatewayOptions {
|
|
4
|
+
secretKey: string;
|
|
5
|
+
webhookSecret: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const StripeRequestTimeoutMilliseconds = 15_000;
|
|
9
|
+
|
|
10
|
+
export interface IStripeGateway {
|
|
11
|
+
createCheckoutSession(options: IStripeGatewayOptions, input: unknown): Promise<unknown>;
|
|
12
|
+
retrieveCheckoutSession(options: IStripeGatewayOptions, input: unknown): Promise<unknown>;
|
|
13
|
+
createRefund(options: IStripeGatewayOptions, input: unknown): Promise<unknown>;
|
|
14
|
+
retrieveRefund(options: IStripeGatewayOptions, input: unknown): Promise<unknown>;
|
|
15
|
+
constructWebhookEvent(
|
|
16
|
+
options: IStripeGatewayOptions,
|
|
17
|
+
input: { rawBody?: string; headers: Record<string, string | string[] | undefined> },
|
|
18
|
+
): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class StripeGatewayError extends Error {
|
|
22
|
+
constructor(
|
|
23
|
+
readonly status: number,
|
|
24
|
+
message: string,
|
|
25
|
+
) {
|
|
26
|
+
super(message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createStripeGateway(): IStripeGateway {
|
|
31
|
+
return {
|
|
32
|
+
async createCheckoutSession(options, input) {
|
|
33
|
+
const request = input as {
|
|
34
|
+
body: Stripe.Checkout.SessionCreateParams;
|
|
35
|
+
idempotencyKey: string;
|
|
36
|
+
};
|
|
37
|
+
return await createClient(options).checkout.sessions.create(request.body, {
|
|
38
|
+
idempotencyKey: request.idempotencyKey,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
async retrieveCheckoutSession(options, input) {
|
|
42
|
+
const request = input as { id: string };
|
|
43
|
+
return await createClient(options).checkout.sessions.retrieve(request.id, {
|
|
44
|
+
expand: ['payment_intent'],
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
async createRefund(options, input) {
|
|
48
|
+
const request = input as { body: Stripe.RefundCreateParams; idempotencyKey: string };
|
|
49
|
+
return await createClient(options).refunds.create(request.body, {
|
|
50
|
+
idempotencyKey: request.idempotencyKey,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
async retrieveRefund(options, input) {
|
|
54
|
+
const request = input as { id: string };
|
|
55
|
+
return await createClient(options).refunds.retrieve(request.id);
|
|
56
|
+
},
|
|
57
|
+
async constructWebhookEvent(options, input) {
|
|
58
|
+
const signature = getHeader(input.headers, 'stripe-signature');
|
|
59
|
+
if (!input.rawBody || !signature) {
|
|
60
|
+
throw new StripeGatewayError(401, 'Stripe webhook signature is invalid');
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return createClient(options).webhooks.constructEvent(
|
|
64
|
+
input.rawBody,
|
|
65
|
+
signature,
|
|
66
|
+
options.webhookSecret,
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new StripeGatewayError(401, 'Stripe webhook signature is invalid');
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createClient(options: IStripeGatewayOptions) {
|
|
76
|
+
return new Stripe(options.secretKey, {
|
|
77
|
+
maxNetworkRetries: 0,
|
|
78
|
+
timeout: StripeRequestTimeoutMilliseconds,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getHeader(headers: Record<string, string | string[] | undefined>, key: string) {
|
|
83
|
+
const value = headers[key];
|
|
84
|
+
return Array.isArray(value) ? value[0] : value;
|
|
85
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona-suite-a-pay",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "",
|
|
6
6
|
"files": [],
|
|
7
7
|
"type": "module",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"vona-module-a-pay": "^5.0.
|
|
10
|
-
"vona-module-pay-mock": "^5.0.
|
|
11
|
-
"vona-module-pay-paypal": "^5.0.
|
|
12
|
-
"vona-module-pay-stripe": "^5.0.
|
|
9
|
+
"vona-module-a-pay": "^5.0.6",
|
|
10
|
+
"vona-module-pay-mock": "^5.0.6",
|
|
11
|
+
"vona-module-pay-paypal": "^5.0.6",
|
|
12
|
+
"vona-module-pay-stripe": "^5.0.6"
|
|
13
13
|
},
|
|
14
14
|
"title": "a-pay"
|
|
15
15
|
}
|
|
@@ -153,6 +153,7 @@ export class BeanPermission extends BeanBase {
|
|
|
153
153
|
const isAuthenticated = this.bean.passport.isAuthenticated;
|
|
154
154
|
if (!options.public && !isAuthenticated) return false;
|
|
155
155
|
if (!isAuthenticated) return true;
|
|
156
|
+
if (!this.bean.passport.isAccountActive) return false;
|
|
156
157
|
if (options.activated === true && !this.bean.passport.isActivated) return false;
|
|
157
158
|
if (options.activated === false && this.bean.passport.isActivated) return false;
|
|
158
159
|
return true;
|