cabloy 5.1.122 → 5.1.124
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-frontend-scaffold/SKILL.md +13 -3
- package/.claude/skills/cabloy-workflow/SKILL.md +6 -0
- package/.claude/skills/cabloy-workflow/evals/evals.json +6 -0
- package/CHANGELOG.md +24 -0
- package/CLAUDE.md +1 -1
- package/cabloy-docs/.vitepress/config.mjs +4 -0
- package/cabloy-docs/frontend/form-layout-guide.md +29 -7
- package/cabloy-docs/fullstack/parallel-worktree-environment.md +87 -0
- package/e2e/scripts/e2e.ts +13 -1
- package/e2e/scripts/startE2eVona.ts +4 -1
- package/e2e/specs/a-commerce/commerce.spec.ts +129 -32
- package/package.json +2 -1
- package/playwright-report/data/24ac790ec112339c76cfb0c8e2c2539c2f953a0b.zip +0 -0
- package/playwright-report/data/2fddcabcdbd0b695061a565acfff243cb3a4f5e3.zip +0 -0
- package/playwright-report/data/4738038d9e861d63fd25937c9a3570934786a62f.zip +0 -0
- package/playwright-report/data/72f99c7da67c7f6ea53cab7d8681798b70d79a10.md +222 -0
- package/playwright-report/data/7b9684a7c793e9798b898f8ff50177b660a7a733.md +222 -0
- package/playwright-report/data/8b31595f5f1cc9d23863af51a592e6b90dcbc4ce.zip +0 -0
- package/playwright-report/data/95b578407a68806202dda917a61c50c750bd285b.md +222 -0
- package/playwright-report/data/ad960b3a58f224f5b4cad04a24067600064ffb7b.md +222 -0
- package/playwright-report/index.html +90 -0
- package/playwright-report/trace/assets/codeMirrorModule-LEHpjmcn.js +32 -0
- package/playwright-report/trace/assets/defaultSettingsView-BNmKHKpQ.js +264 -0
- package/playwright-report/trace/assets/urlMatch-BYQrIQwR.js +1 -0
- package/playwright-report/trace/codeMirrorModule.DYBRYzYX.css +1 -0
- package/playwright-report/trace/codicon.DCmgc-ay.ttf +0 -0
- package/playwright-report/trace/defaultSettingsView.CjdS-WJx.css +1 -0
- package/playwright-report/trace/index.CzXZzn5A.css +1 -0
- package/playwright-report/trace/index.DMMX1gXU.js +2 -0
- package/playwright-report/trace/index.html +44 -0
- package/playwright-report/trace/manifest.webmanifest +16 -0
- package/playwright-report/trace/playwright-logo.svg +9 -0
- package/playwright-report/trace/snapshot.html +10 -0
- package/playwright-report/trace/snapshot.v8KI4P3m.js +2 -0
- package/playwright-report/trace/sw.bundle.js +5 -0
- package/playwright-report/trace/uiMode.BZQ54Kgt.css +1 -0
- package/playwright-report/trace/uiMode.Ut8wwJNp.js +6 -0
- package/playwright-report/trace/uiMode.html +18 -0
- package/playwright-report/trace/xtermModule.DYP7pi_n.css +32 -0
- package/vona/env/.env +12 -0
- package/vona/env/.env.test +7 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}Create.tsx_ +19 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}Update.tsx_ +19 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}View.tsx_ +19 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +6 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +6 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}Create.tsx_ +19 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}Update.tsx_ +19 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}View.tsx_ +19 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +6 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +6 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/pnpm-lock.yaml +255 -12
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/.metadata/index.ts +45 -3
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/meta.version.ts +25 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +20 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAttempt.tsx +9 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/paymentAudit.tsx +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/entity/refundAttempt.tsx +6 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/index.ts +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/lib/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +54 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +7 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/types/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +21 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/.metadata/index.ts +30 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +4 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +4 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +88 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +44 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateView.tsx +72 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +121 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +0 -21
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutResult.tsx +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +79 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +227 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +121 -13
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +312 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +76 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +842 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payProvider.ts +92 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +105 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +28 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +113 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +31 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.outboxDispatch.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/paymentSession.ts +55 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/webhook.ts +47 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/dto/paymentSessionStart.tsx +8 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/dto/paymentSessionView.tsx +69 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/dto/webhookReceipt.tsx +12 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +46 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentAudit.tsx +36 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentSession.tsx +72 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +54 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/refundOperation.tsx +37 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +59 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/payProvider.ts +7 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/payScene.ts +7 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/outboxEvent.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentAudit.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/providerOperation.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/refundOperation.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/webhookInbox.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +156 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +278 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +5 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +127 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payProvider.ts +74 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +63 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +151 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +209 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +153 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +278 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +55 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +182 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +149 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +25 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockPaymentComplete.tsx +13 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockPaymentReceipt.tsx +16 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +78 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/types/index.ts +10 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +141 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +55 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/index.ts +62 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +91 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/types/index.ts +9 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +55 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/index.ts +62 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +88 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +8 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-pay/package.json +15 -0
- package/vona/src/suite-vendor/a-pay/tsconfig.base.json +4 -0
- package/vona/src/suite-vendor/a-pay/tsconfig.json +19 -0
- package/zova/env/.env +1 -0
- package/zova/openapi.config.ts +5 -1
- package/zova/package.original.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/openapi/config/boilerplate/project/openapi.config.ts +5 -1
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.openapi.generate.ts +8 -0
- package/zova/packages-cli/cli-set-front/src/lib/common/cliCreatePage.ts +7 -24
- package/zova/packages-cli/cli-set-front/src/lib/common/utils.ts +10 -1
- 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 +1 -1
- package/zova/packages-zova/zova-core/src/types/interface/component.ts +5 -0
- package/zova/pnpm-lock.yaml +96 -26
- package/zova/src/front/config/config/config.cabloyCommerce.ts +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/schemas.ts +110 -12
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +1292 -160
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +0 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/package.json +4 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -47
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +23 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +249 -62
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +5 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +152 -39
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +1 -1
- package/zova/src/suite-vendor/a-pay/modules/a-pay/cli/openapi.config.ts +9 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +53 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/component/paymentNextAction.ts +31 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +217 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/this.ts +2 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/baseURL.ts +5 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/index.ts +3 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/schemas.ts +584 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/types.ts +9324 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/paymentSession.ts +52 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/apiSchema/paymentSession.ts +20 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/component/paymentNextAction/controller.tsx +76 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/index.ts +3 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +32 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentCoordinator.ts +13 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/types/index.ts +1 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +25 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/tsconfig.build.json +13 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/tsconfig.json +5 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/cli/openapi.config.ts +9 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/package.json +53 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +140 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/this.ts +2 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/baseURL.ts +5 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/index.ts +3 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/schemas.ts +584 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/openapi/types.ts +9324 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/payMockPayment.ts +34 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/apiSchema/payMockPayment.ts +13 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/index.ts +1 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/model/payMockPayment.ts +20 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.build.json +13 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-mock/tsconfig.json +5 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-paypal/package.json +52 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/index.ts +26 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-paypal/src/.metadata/this.ts +2 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.build.json +13 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-paypal/tsconfig.json +5 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-stripe/package.json +52 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/index.ts +26 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-stripe/src/.metadata/this.ts +2 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.build.json +13 -0
- package/zova/src/suite-vendor/a-pay/modules/pay-stripe/tsconfig.json +5 -0
- package/zova/src/suite-vendor/a-pay/package.json +15 -0
- package/zova/src/suite-vendor/a-pay/tsconfig.base.json +4 -0
- package/zova/src/suite-vendor/a-pay/tsconfig.json +4 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/payment.ts +0 -23
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradePayment.ts +0 -36
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradePayment.ts +0 -13
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/payment.ts +0 -25
- /package/vona/patches/{zova-core@5.1.79.patch → zova-core@5.1.81.patch} +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
import { app } from 'vona-mock';
|
|
5
|
+
|
|
6
|
+
interface IFixture {
|
|
7
|
+
userId?: number;
|
|
8
|
+
paymentSessionId?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function createFixture(suffix: string): Promise<IFixture> {
|
|
12
|
+
const scope = app.scope('a-pay');
|
|
13
|
+
const user = await app.bean.user.register({ name: `pay-webhook-${suffix}` }, true);
|
|
14
|
+
const session = await scope.service.paymentSession.create({
|
|
15
|
+
userId: user.id,
|
|
16
|
+
payScene: 'commerce-payment:commerceOrder',
|
|
17
|
+
businessReference: `business-${suffix}`,
|
|
18
|
+
amountMinor: 1299,
|
|
19
|
+
currency: 'USD',
|
|
20
|
+
correlationId: `payment-${suffix}`,
|
|
21
|
+
});
|
|
22
|
+
return { userId: user.id as number, paymentSessionId: session.id as number };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function cleanup(fixture: IFixture) {
|
|
26
|
+
const scope = app.scope('a-pay');
|
|
27
|
+
if (fixture.paymentSessionId !== undefined) {
|
|
28
|
+
await scope.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
29
|
+
await scope.model.paymentAudit.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
30
|
+
await scope.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
31
|
+
await scope.model.paymentSession.delete({ id: fixture.paymentSessionId });
|
|
32
|
+
}
|
|
33
|
+
if (fixture.userId !== undefined) {
|
|
34
|
+
await app.scope('home-user').model.roleUser.delete({ userId: fixture.userId });
|
|
35
|
+
await app.bean.user.removeById(fixture.userId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createRawBody(
|
|
40
|
+
fixture: IFixture,
|
|
41
|
+
suffix: string,
|
|
42
|
+
state: 'succeeded' | 'failed' | 'cancelled' = 'succeeded',
|
|
43
|
+
options?: { eventId?: string; providerPaymentId?: string; providerCaptureId?: string },
|
|
44
|
+
) {
|
|
45
|
+
return JSON.stringify({
|
|
46
|
+
state,
|
|
47
|
+
amountMinor: 1299,
|
|
48
|
+
eventType: `payment.${state}`,
|
|
49
|
+
paymentSessionId: String(fixture.paymentSessionId),
|
|
50
|
+
eventId: options?.eventId ?? `mock-${suffix}`,
|
|
51
|
+
providerPaymentId: options?.providerPaymentId ?? `payment-${suffix}`,
|
|
52
|
+
providerCaptureId: options?.providerCaptureId ?? `capture-${suffix}`,
|
|
53
|
+
currency: 'USD',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createCommand(rawBody: string) {
|
|
58
|
+
const body = JSON.parse(rawBody);
|
|
59
|
+
return {
|
|
60
|
+
providerName: 'pay-mock:mock',
|
|
61
|
+
clientName: 'default',
|
|
62
|
+
environment: 'sandbox' as const,
|
|
63
|
+
rawBody,
|
|
64
|
+
verified: {
|
|
65
|
+
eventId: body.eventId,
|
|
66
|
+
eventType: body.eventType,
|
|
67
|
+
paymentSessionId: body.paymentSessionId,
|
|
68
|
+
payment: {
|
|
69
|
+
state: body.state,
|
|
70
|
+
providerPaymentId: body.providerPaymentId,
|
|
71
|
+
providerCaptureId: body.providerCaptureId,
|
|
72
|
+
},
|
|
73
|
+
summary: {
|
|
74
|
+
amountMinor: body.amountMinor,
|
|
75
|
+
currency: body.currency,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sign(rawBody: string, clientName: 'default' | 'secondary' = 'default') {
|
|
82
|
+
const options = app.bean.payProvider.getOptions('pay-mock:mock', clientName);
|
|
83
|
+
assert.equal(typeof options.secretWebhook, 'string');
|
|
84
|
+
return createHmac('sha256', options.secretWebhook).update(rawBody).digest('hex');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function webhookUrl(providerName = 'pay-mock:mock', clientName = 'default') {
|
|
88
|
+
const url = app.util.getAbsoluteUrlByApiPath(`/pay/webhook/${providerName}/${clientName}`);
|
|
89
|
+
return url.startsWith('http') ? url : `http://127.0.0.1:${app.config.server.listen.port}${url}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function countFacts(paymentSessionId: number) {
|
|
93
|
+
const scope = app.scope('a-pay');
|
|
94
|
+
const [inboxes, audits, outbox] = await Promise.all([
|
|
95
|
+
scope.model.webhookInbox.select({ where: { paymentSessionId } }),
|
|
96
|
+
scope.model.paymentAudit.select({ where: { paymentSessionId } }),
|
|
97
|
+
scope.model.outboxEvent.select({ where: { paymentSessionId } }),
|
|
98
|
+
]);
|
|
99
|
+
return { inboxes, audits, outbox };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('webhook.test.ts', { concurrency: false, sequential: true }, () => {
|
|
103
|
+
it('accepts a raw-body signed HTTP webhook and rejects an invalid signature without mutation', async () => {
|
|
104
|
+
const fixture: IFixture = {};
|
|
105
|
+
try {
|
|
106
|
+
await app.bean.executor.mockCtx(async () => {
|
|
107
|
+
Object.assign(fixture, await createFixture(randomUUID().slice(0, 12)));
|
|
108
|
+
});
|
|
109
|
+
const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
|
|
110
|
+
const url = webhookUrl();
|
|
111
|
+
const invalid = await fetch(url, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'x-pay-mock-signature': 'invalid',
|
|
116
|
+
},
|
|
117
|
+
body: rawBody,
|
|
118
|
+
});
|
|
119
|
+
assert.equal(invalid.status, 401, await invalid.text());
|
|
120
|
+
await app.bean.executor.mockCtx(async () => {
|
|
121
|
+
assert.deepEqual(await countFacts(fixture.paymentSessionId!), {
|
|
122
|
+
inboxes: [],
|
|
123
|
+
audits: [],
|
|
124
|
+
outbox: [],
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
const valid = await fetch(url, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'x-pay-mock-signature': sign(rawBody),
|
|
132
|
+
},
|
|
133
|
+
body: rawBody,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(valid.status, 200);
|
|
136
|
+
await app.bean.executor.mockCtx(async () => {
|
|
137
|
+
const session = await app
|
|
138
|
+
.scope('a-pay')
|
|
139
|
+
.model.paymentSession.getById(fixture.paymentSessionId!);
|
|
140
|
+
const facts = await countFacts(fixture.paymentSessionId!);
|
|
141
|
+
assert.equal(session?.state, 'succeeded');
|
|
142
|
+
assert.deepEqual(
|
|
143
|
+
[facts.inboxes.length, facts.audits.length, facts.outbox.length],
|
|
144
|
+
[1, 1, 1],
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
} finally {
|
|
148
|
+
await app.bean.executor.mockCtx(async () => await cleanup(fixture));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('uses the route client webhook secret and preserves session client binding', async () => {
|
|
153
|
+
const fixture: IFixture = {};
|
|
154
|
+
try {
|
|
155
|
+
await app.bean.executor.mockCtx(async () => {
|
|
156
|
+
Object.assign(fixture, await createFixture(randomUUID().slice(0, 12)));
|
|
157
|
+
});
|
|
158
|
+
const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
|
|
159
|
+
const url = webhookUrl('pay-mock:mock', 'secondary');
|
|
160
|
+
const wrongClient = await fetch(url, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
'x-pay-mock-signature': sign(rawBody, 'default'),
|
|
165
|
+
},
|
|
166
|
+
body: rawBody,
|
|
167
|
+
});
|
|
168
|
+
assert.equal(wrongClient.status, 401, await wrongClient.text());
|
|
169
|
+
const selectedClient = await fetch(url, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
'x-pay-mock-signature': sign(rawBody, 'secondary'),
|
|
174
|
+
},
|
|
175
|
+
body: rawBody,
|
|
176
|
+
});
|
|
177
|
+
assert.equal(selectedClient.status, 409, await selectedClient.text());
|
|
178
|
+
await app.bean.executor.mockCtx(async () => {
|
|
179
|
+
assert.deepEqual(await countFacts(fixture.paymentSessionId!), {
|
|
180
|
+
inboxes: [],
|
|
181
|
+
audits: [],
|
|
182
|
+
outbox: [],
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
} finally {
|
|
186
|
+
await app.bean.executor.mockCtx(async () => await cleanup(fixture));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('persists one verified payment fact and one logical outbox event across replay', async () => {
|
|
191
|
+
await app.bean.executor.mockCtx(async () => {
|
|
192
|
+
const fixture = await createFixture(randomUUID().slice(0, 12));
|
|
193
|
+
try {
|
|
194
|
+
const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
|
|
195
|
+
const first = await app.scope('a-pay').service.webhook.receive(createCommand(rawBody));
|
|
196
|
+
const replay = await app.scope('a-pay').service.webhook.receive(createCommand(rawBody));
|
|
197
|
+
assert.equal(replay.id, first.id);
|
|
198
|
+
assert.equal(replay.state, 'processed');
|
|
199
|
+
const session = await app
|
|
200
|
+
.scope('a-pay')
|
|
201
|
+
.model.paymentSession.getById(fixture.paymentSessionId!);
|
|
202
|
+
const facts = await countFacts(fixture.paymentSessionId!);
|
|
203
|
+
assert.equal(session?.state, 'succeeded');
|
|
204
|
+
assert.deepEqual(
|
|
205
|
+
[facts.inboxes.length, facts.audits.length, facts.outbox.length],
|
|
206
|
+
[1, 1, 1],
|
|
207
|
+
);
|
|
208
|
+
assert.equal(facts.outbox[0]?.eventType, 'payment.outcome.v1');
|
|
209
|
+
} finally {
|
|
210
|
+
await cleanup(fixture);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('converges concurrent duplicate and out-of-order terminal webhooks on the first terminal state', async () => {
|
|
216
|
+
const fixture: IFixture = {};
|
|
217
|
+
try {
|
|
218
|
+
await app.bean.executor.mockCtx(async () => {
|
|
219
|
+
Object.assign(fixture, await createFixture(randomUUID().slice(0, 12)));
|
|
220
|
+
});
|
|
221
|
+
const rawBody = createRawBody(fixture, randomUUID().slice(0, 12));
|
|
222
|
+
const receive = async () => {
|
|
223
|
+
return await app.bean.executor.mockCtx(async () => {
|
|
224
|
+
return await app.scope('a-pay').service.webhook.receive(createCommand(rawBody));
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
const results = await Promise.allSettled([receive(), receive()]);
|
|
228
|
+
assert.equal(
|
|
229
|
+
results.filter(result => result.status === 'fulfilled').length,
|
|
230
|
+
2,
|
|
231
|
+
JSON.stringify(results),
|
|
232
|
+
);
|
|
233
|
+
await app.bean.executor.mockCtx(async () => {
|
|
234
|
+
const failedRawBody = createRawBody(fixture, randomUUID().slice(0, 12), 'failed');
|
|
235
|
+
const ignored = await app
|
|
236
|
+
.scope('a-pay')
|
|
237
|
+
.service.webhook.receive(createCommand(failedRawBody));
|
|
238
|
+
const session = await app
|
|
239
|
+
.scope('a-pay')
|
|
240
|
+
.model.paymentSession.getById(fixture.paymentSessionId!);
|
|
241
|
+
const facts = await countFacts(fixture.paymentSessionId!);
|
|
242
|
+
assert.equal(session?.state, 'succeeded');
|
|
243
|
+
assert.equal(ignored.state, 'processed');
|
|
244
|
+
assert.match(ignored.errorSummary ?? '', /ignored after succeeded/);
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
[facts.inboxes.length, facts.audits.length, facts.outbox.length],
|
|
247
|
+
[2, 1, 1],
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
} finally {
|
|
251
|
+
await app.bean.executor.mockCtx(async () => await cleanup(fixture));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('rejects a same-state webhook with conflicting provider facts', async () => {
|
|
256
|
+
await app.bean.executor.mockCtx(async () => {
|
|
257
|
+
const fixture = await createFixture(randomUUID().slice(0, 12));
|
|
258
|
+
try {
|
|
259
|
+
const first = createRawBody(fixture, randomUUID().slice(0, 12));
|
|
260
|
+
await app.scope('a-pay').service.webhook.receive(createCommand(first));
|
|
261
|
+
const conflict = createRawBody(fixture, randomUUID().slice(0, 12), 'succeeded', {
|
|
262
|
+
providerCaptureId: `other-capture-${randomUUID().slice(0, 12)}`,
|
|
263
|
+
});
|
|
264
|
+
await assert.rejects(
|
|
265
|
+
app.scope('a-pay').service.webhook.receive(createCommand(conflict)),
|
|
266
|
+
(error: any) => error.code === 409,
|
|
267
|
+
);
|
|
268
|
+
const facts = await countFacts(fixture.paymentSessionId!);
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
[facts.inboxes.length, facts.audits.length, facts.outbox.length],
|
|
271
|
+
[1, 1, 1],
|
|
272
|
+
);
|
|
273
|
+
} finally {
|
|
274
|
+
await cleanup(fixture);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vona-module-pay-mock",
|
|
3
|
+
"version": "5.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Vona Module"
|
|
7
|
+
],
|
|
8
|
+
"author": "",
|
|
9
|
+
"files": [
|
|
10
|
+
"assets",
|
|
11
|
+
"dist",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": [
|
|
18
|
+
"./src/index.ts",
|
|
19
|
+
"./dist/index.d.ts"
|
|
20
|
+
],
|
|
21
|
+
"import": "./src/index.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rimraf dist tsconfig.build.tsbuildinfo",
|
|
28
|
+
"tsc:publish": "npm run clean && node ../../../../../packages-cli/cli/src/bin/vona.ts :bin:buildModule --sourcemap && tsc -p tsconfig.build.json",
|
|
29
|
+
"prepublishOnly": "npm run tsc:publish",
|
|
30
|
+
"prepack": "clean-package",
|
|
31
|
+
"postpack": "clean-package restore"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"vona-module-a-pay": "workspace:*"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"clean-package": "^2.2.0",
|
|
38
|
+
"rimraf": "^6.1.3"
|
|
39
|
+
},
|
|
40
|
+
"clean-package": {
|
|
41
|
+
"indent": 2,
|
|
42
|
+
"remove": [
|
|
43
|
+
"clean-package",
|
|
44
|
+
"devDependencies",
|
|
45
|
+
"exports.\\..import"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"title": "pay-mock",
|
|
49
|
+
"vonaModule": {
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"a-pay": "5.0.0",
|
|
52
|
+
"a-vona": "5.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// eslint-disable
|
|
2
|
+
import type { TypeSymbolKeyFieldsMore } from 'vona-module-a-orm';
|
|
3
|
+
import type { TypeEntityOptionsFields,TypeControllerOptionsActions } from 'vona-module-a-openapi';
|
|
4
|
+
/** service: begin */
|
|
5
|
+
export * from '../service/payMock.ts';
|
|
6
|
+
|
|
7
|
+
import 'vona-module-a-bean';
|
|
8
|
+
declare module 'vona-module-a-bean' {
|
|
9
|
+
|
|
10
|
+
export interface IServiceRecord {
|
|
11
|
+
'pay-mock:payMock': never;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
declare module 'vona-module-pay-mock' {
|
|
17
|
+
|
|
18
|
+
export interface ServicePayMock {
|
|
19
|
+
/** @internal */
|
|
20
|
+
get scope(): ScopeModulePayMock;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ServicePayMock {
|
|
24
|
+
get $beanFullName(): 'pay-mock.service.payMock';
|
|
25
|
+
get $onionName(): 'pay-mock:payMock';
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** service: end */
|
|
30
|
+
/** service: begin */
|
|
31
|
+
import type { ServicePayMock } from '../service/payMock.ts';
|
|
32
|
+
export interface IModuleService {
|
|
33
|
+
'payMock': ServicePayMock;
|
|
34
|
+
}
|
|
35
|
+
/** service: end */
|
|
36
|
+
/** service: begin */
|
|
37
|
+
|
|
38
|
+
import 'vona';
|
|
39
|
+
declare module 'vona' {
|
|
40
|
+
export interface IBeanRecordGeneral {
|
|
41
|
+
'pay-mock.service.payMock': ServicePayMock;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** service: end */
|
|
45
|
+
/** dto: begin */
|
|
46
|
+
export * from '../dto/mockPaymentComplete.tsx';
|
|
47
|
+
export * from '../dto/mockPaymentReceipt.tsx';
|
|
48
|
+
import type { IDtoOptionsMockPaymentComplete } from '../dto/mockPaymentComplete.tsx';
|
|
49
|
+
import type { IDtoOptionsMockPaymentReceipt } from '../dto/mockPaymentReceipt.tsx';
|
|
50
|
+
import 'vona-module-a-web';
|
|
51
|
+
declare module 'vona-module-a-web' {
|
|
52
|
+
|
|
53
|
+
export interface IDtoRecord {
|
|
54
|
+
'pay-mock:mockPaymentComplete': IDtoOptionsMockPaymentComplete;
|
|
55
|
+
'pay-mock:mockPaymentReceipt': IDtoOptionsMockPaymentReceipt;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
declare module 'vona-module-pay-mock' {
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
/** dto: end */
|
|
64
|
+
/** dto: begin */
|
|
65
|
+
import type { DtoMockPaymentComplete } from '../dto/mockPaymentComplete.tsx';
|
|
66
|
+
import type { DtoMockPaymentReceipt } from '../dto/mockPaymentReceipt.tsx';
|
|
67
|
+
declare module 'vona-module-pay-mock' {
|
|
68
|
+
|
|
69
|
+
export interface IDtoOptionsMockPaymentComplete {
|
|
70
|
+
fields?: TypeEntityOptionsFields<DtoMockPaymentComplete, IDtoOptionsMockPaymentComplete[TypeSymbolKeyFieldsMore]>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface IDtoOptionsMockPaymentReceipt {
|
|
74
|
+
fields?: TypeEntityOptionsFields<DtoMockPaymentReceipt, IDtoOptionsMockPaymentReceipt[TypeSymbolKeyFieldsMore]>;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** dto: end */
|
|
78
|
+
/** controller: begin */
|
|
79
|
+
export * from '../controller/mockPayment.ts';
|
|
80
|
+
import type { IControllerOptionsMockPayment } from '../controller/mockPayment.ts';
|
|
81
|
+
import 'vona-module-a-web';
|
|
82
|
+
declare module 'vona-module-a-web' {
|
|
83
|
+
|
|
84
|
+
export interface IControllerRecord {
|
|
85
|
+
'pay-mock:mockPayment': IControllerOptionsMockPayment;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
declare module 'vona-module-pay-mock' {
|
|
91
|
+
|
|
92
|
+
export interface ControllerMockPayment {
|
|
93
|
+
/** @internal */
|
|
94
|
+
get scope(): ScopeModulePayMock;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ControllerMockPayment {
|
|
98
|
+
get $beanFullName(): 'pay-mock.controller.mockPayment';
|
|
99
|
+
get $onionName(): 'pay-mock:mockPayment';
|
|
100
|
+
get $onionOptions(): IControllerOptionsMockPayment;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** controller: end */
|
|
104
|
+
/** controller: begin */
|
|
105
|
+
// @ts-ignore ignore
|
|
106
|
+
import type { ControllerMockPayment } from '../controller/mockPayment.ts';
|
|
107
|
+
declare module 'vona-module-pay-mock' {
|
|
108
|
+
|
|
109
|
+
export interface IControllerOptionsMockPayment {
|
|
110
|
+
actions?: TypeControllerOptionsActions<ControllerMockPayment>;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
declare module 'vona-module-a-web' {
|
|
114
|
+
export interface IApiPathPostRecord{
|
|
115
|
+
'/pay/mock/payment-session/:id/complete': undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** controller: end */
|
|
121
|
+
/** payProvider: begin */
|
|
122
|
+
export * from '../bean/payProvider.mock.ts';
|
|
123
|
+
import type { IPayProviderOptionsMock } from '../bean/payProvider.mock.ts';
|
|
124
|
+
import 'vona-module-a-pay';
|
|
125
|
+
declare module 'vona-module-a-pay' {
|
|
126
|
+
|
|
127
|
+
export interface IPayProviderRecord {
|
|
128
|
+
'pay-mock:mock': IPayProviderOptionsMock;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
declare module 'vona-module-pay-mock' {
|
|
134
|
+
|
|
135
|
+
export interface PayProviderMock {
|
|
136
|
+
/** @internal */
|
|
137
|
+
get scope(): ScopeModulePayMock;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface PayProviderMock {
|
|
141
|
+
get $beanFullName(): 'pay-mock.payProvider.mock';
|
|
142
|
+
get $onionName(): 'pay-mock:mock';
|
|
143
|
+
get $onionOptions(): IPayProviderOptionsMock;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** payProvider: end */
|
|
147
|
+
/** payProvider: begin */
|
|
148
|
+
import type { PayProviderMock } from '../bean/payProvider.mock.ts';
|
|
149
|
+
export interface IModulePayProvider {
|
|
150
|
+
'mock': PayProviderMock;
|
|
151
|
+
}
|
|
152
|
+
/** payProvider: end */
|
|
153
|
+
/** scope: begin */
|
|
154
|
+
import { BeanScopeBase, type BeanScopeUtil } from 'vona';
|
|
155
|
+
import { Scope } from 'vona-module-a-bean';
|
|
156
|
+
|
|
157
|
+
@Scope()
|
|
158
|
+
export class ScopeModulePayMock extends BeanScopeBase {}
|
|
159
|
+
|
|
160
|
+
export interface ScopeModulePayMock {
|
|
161
|
+
util: BeanScopeUtil;
|
|
162
|
+
service: IModuleService;
|
|
163
|
+
payProvider: IModulePayProvider;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
import 'vona';
|
|
167
|
+
declare module 'vona' {
|
|
168
|
+
export interface IBeanScopeRecord {
|
|
169
|
+
'pay-mock': ScopeModulePayMock;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface IBeanScopeContainer {
|
|
173
|
+
payMock: ScopeModulePayMock;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
/** scope: end */
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IDecoratorPayProviderOptions,
|
|
3
|
+
IPayProviderClientOptions,
|
|
4
|
+
IPayProviderClientRecord,
|
|
5
|
+
IPayProviderExecute,
|
|
6
|
+
IPayProviderPaymentInput,
|
|
7
|
+
IPayProviderPaymentSnapshot,
|
|
8
|
+
IPayProviderRefundInput,
|
|
9
|
+
IPayProviderRefundSnapshot,
|
|
10
|
+
IPayProviderVerifiedWebhook,
|
|
11
|
+
IPayProviderWebhookInput,
|
|
12
|
+
} from 'vona-module-a-pay';
|
|
13
|
+
|
|
14
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
15
|
+
import { BeanBase, useApp } from 'vona';
|
|
16
|
+
import { PayProvider } from 'vona-module-a-pay';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
const WebhookBodySchema = z.object({
|
|
20
|
+
eventId: z.string().min(1).max(255),
|
|
21
|
+
eventType: z.enum(['payment.succeeded', 'payment.failed', 'payment.cancelled']),
|
|
22
|
+
paymentSessionId: z.string().min(1),
|
|
23
|
+
state: z.enum(['succeeded', 'failed', 'cancelled']),
|
|
24
|
+
amountMinor: z.number().int().nonnegative(),
|
|
25
|
+
currency: z.string().length(3),
|
|
26
|
+
providerPaymentId: z.string().min(1).max(255).optional(),
|
|
27
|
+
providerCaptureId: z.string().min(1).max(255).optional(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export interface IPayProviderMockClientRecord extends IPayProviderClientRecord {
|
|
31
|
+
secondary: never;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IPayProviderMockClientOptions extends IPayProviderClientOptions {
|
|
35
|
+
secretCredential: string | undefined;
|
|
36
|
+
secretWebhook: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const app = useApp();
|
|
40
|
+
|
|
41
|
+
export interface IPayProviderOptionsMock extends IDecoratorPayProviderOptions<
|
|
42
|
+
IPayProviderMockClientRecord,
|
|
43
|
+
IPayProviderMockClientOptions
|
|
44
|
+
> {}
|
|
45
|
+
|
|
46
|
+
@PayProvider<IPayProviderOptionsMock>({
|
|
47
|
+
base: {
|
|
48
|
+
capabilities: {
|
|
49
|
+
redirectCheckout: false,
|
|
50
|
+
embeddedCheckout: true,
|
|
51
|
+
automaticCapture: true,
|
|
52
|
+
manualCapture: false,
|
|
53
|
+
refunds: true,
|
|
54
|
+
partialRefunds: true,
|
|
55
|
+
webhooks: true,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
clients: {
|
|
59
|
+
default: {
|
|
60
|
+
environment: 'sandbox',
|
|
61
|
+
secretCredential: app.meta.env.PAY_MOCK_DEFAULT_CREDENTIAL,
|
|
62
|
+
secretWebhook: app.meta.env.PAY_MOCK_DEFAULT_WEBHOOK,
|
|
63
|
+
},
|
|
64
|
+
secondary: {
|
|
65
|
+
environment: 'sandbox',
|
|
66
|
+
secretCredential: app.meta.env.PAY_MOCK_SECONDARY_CREDENTIAL,
|
|
67
|
+
secretWebhook: app.meta.env.PAY_MOCK_SECONDARY_WEBHOOK,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
export class PayProviderMock
|
|
72
|
+
extends BeanBase
|
|
73
|
+
implements IPayProviderExecute<IPayProviderMockClientOptions>
|
|
74
|
+
{
|
|
75
|
+
async startPayment(
|
|
76
|
+
input: IPayProviderPaymentInput,
|
|
77
|
+
_clientOptions: IPayProviderMockClientOptions,
|
|
78
|
+
): Promise<IPayProviderPaymentSnapshot> {
|
|
79
|
+
return {
|
|
80
|
+
state: 'requires_action',
|
|
81
|
+
providerPaymentId: `mock-payment-${input.paymentSessionId}`,
|
|
82
|
+
nextAction: { kind: 'pending' },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async queryPayment(
|
|
87
|
+
input: IPayProviderPaymentInput,
|
|
88
|
+
_clientOptions: IPayProviderMockClientOptions,
|
|
89
|
+
): Promise<IPayProviderPaymentSnapshot> {
|
|
90
|
+
return {
|
|
91
|
+
state: 'processing',
|
|
92
|
+
providerPaymentId: `mock-payment-${input.paymentSessionId}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async createRefund(
|
|
97
|
+
input: IPayProviderRefundInput,
|
|
98
|
+
_clientOptions: IPayProviderMockClientOptions,
|
|
99
|
+
): Promise<IPayProviderRefundSnapshot> {
|
|
100
|
+
return {
|
|
101
|
+
state: 'pending',
|
|
102
|
+
providerRefundId: `mock-refund-${input.refundOperationId}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async verifyWebhook(
|
|
107
|
+
input: IPayProviderWebhookInput,
|
|
108
|
+
clientOptions: IPayProviderMockClientOptions,
|
|
109
|
+
): Promise<IPayProviderVerifiedWebhook> {
|
|
110
|
+
const signature = input.headers['x-pay-mock-signature'];
|
|
111
|
+
const actual = Array.isArray(signature) ? signature[0] : signature;
|
|
112
|
+
const secret = clientOptions.secretWebhook;
|
|
113
|
+
const rawBody = input.rawBody;
|
|
114
|
+
const expected =
|
|
115
|
+
typeof secret === 'string' && secret && rawBody !== undefined
|
|
116
|
+
? createHmac('sha256', secret).update(rawBody).digest('hex')
|
|
117
|
+
: undefined;
|
|
118
|
+
if (!expected || !actual || !safeEqual(actual, expected)) {
|
|
119
|
+
this.app.throw(401, 'mock webhook signature is invalid');
|
|
120
|
+
}
|
|
121
|
+
const body = WebhookBodySchema.safeParse(input.body);
|
|
122
|
+
if (!body.success) this.app.throw(400, 'mock webhook is invalid');
|
|
123
|
+
const eventType = `payment.${body.data.state}`;
|
|
124
|
+
if (body.data.eventType !== eventType)
|
|
125
|
+
this.app.throw(400, 'mock webhook event type is invalid');
|
|
126
|
+
return {
|
|
127
|
+
eventId: body.data.eventId,
|
|
128
|
+
eventType,
|
|
129
|
+
paymentSessionId: body.data.paymentSessionId,
|
|
130
|
+
payment: {
|
|
131
|
+
state: body.data.state,
|
|
132
|
+
providerPaymentId: body.data.providerPaymentId,
|
|
133
|
+
providerCaptureId: body.data.providerCaptureId,
|
|
134
|
+
},
|
|
135
|
+
summary: {
|
|
136
|
+
amountMinor: body.data.amountMinor,
|
|
137
|
+
currency: body.data.currency,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function safeEqual(actual: string, expected: string) {
|
|
144
|
+
const actualBuffer = Buffer.from(actual);
|
|
145
|
+
const expectedBuffer = Buffer.from(expected);
|
|
146
|
+
return (
|
|
147
|
+
actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer)
|
|
148
|
+
);
|
|
149
|
+
}
|