cabloy 5.1.124 → 5.1.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
  3. package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
  4. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
  5. package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
  6. package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
  7. package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
  8. package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
  9. package/.github/workflows/vona-test-pg.yml +3 -0
  10. package/.github/workflows/vona-test-sqlite3.yml +3 -0
  11. package/CHANGELOG.md +21 -0
  12. package/CLAUDE.md +1 -1
  13. package/cabloy-docs/.vitepress/config.mjs +2 -0
  14. package/cabloy-docs/ai/playbook-technical-blog-authoring.md +158 -0
  15. package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
  16. package/cabloy-docs/backend/dto-guide.md +10 -7
  17. package/cabloy-docs/backend/dto-infer-generation.md +127 -7
  18. package/cabloy-docs/backend/foundation.md +1 -1
  19. package/cabloy-docs/backend/introduction.md +1 -0
  20. package/cabloy-docs/backend/module-dependencies.md +104 -0
  21. package/cabloy-docs/backend/serialization-guide.md +6 -0
  22. package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
  23. package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
  24. package/cabloy-docs/reference/package-map.md +1 -0
  25. package/e2e/specs/a-commerce/commerce.spec.ts +89 -0
  26. package/package.json +2 -1
  27. package/scripts/init.ts +7 -12
  28. package/scripts/initAppName.test.ts +72 -0
  29. package/scripts/initAppName.ts +50 -0
  30. package/vona/packages-vona/vona/package.json +1 -1
  31. package/vona/pnpm-lock.yaml +48 -146
  32. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
  33. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
  34. package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
  35. package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
  36. package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
  37. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
  38. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
  39. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
  40. package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
  41. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
  42. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
  43. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
  44. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
  45. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
  46. package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
  47. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
  49. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
  50. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +121 -11
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
  56. package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
  57. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
  58. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
  59. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
  60. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
  61. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
  62. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
  63. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  64. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
  65. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
  66. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
  67. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
  68. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
  69. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
  70. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
  71. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
  72. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
  73. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
  74. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
  75. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
  76. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
  77. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
  78. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
  79. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  80. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
  81. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
  82. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
  83. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
  84. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
  85. package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
  86. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
  87. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
  88. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  89. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  90. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
  92. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  93. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +54 -12
  94. package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +625 -64
  95. package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
  96. package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
  97. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
  98. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
  99. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +111 -26
  100. package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
  101. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
  102. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
  103. package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
  104. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
  105. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
  106. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +0 -209
  107. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +0 -153
  108. package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +0 -278
  109. package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +0 -141
@@ -16,7 +16,7 @@ import { BeanBase, useApp } from 'vona';
16
16
  import { PayProvider } from 'vona-module-a-pay';
17
17
  import { z } from 'zod';
18
18
 
19
- const WebhookBodySchema = z.object({
19
+ const PaymentWebhookBodySchema = z.object({
20
20
  eventId: z.string().min(1).max(255),
21
21
  eventType: z.enum(['payment.succeeded', 'payment.failed', 'payment.cancelled']),
22
22
  paymentSessionId: z.string().min(1),
@@ -27,6 +27,16 @@ const WebhookBodySchema = z.object({
27
27
  providerCaptureId: z.string().min(1).max(255).optional(),
28
28
  });
29
29
 
30
+ const RefundWebhookBodySchema = z.object({
31
+ eventId: z.string().min(1).max(255),
32
+ eventType: z.enum(['refund.pending', 'refund.succeeded', 'refund.failed', 'refund.cancelled']),
33
+ refundOperationId: z.string().min(1),
34
+ state: z.enum(['pending', 'succeeded', 'failed', 'cancelled']),
35
+ amountMinor: z.number().int().positive(),
36
+ currency: z.string().length(3),
37
+ providerRefundId: z.string().min(1).max(255).optional(),
38
+ });
39
+
30
40
  export interface IPayProviderMockClientRecord extends IPayProviderClientRecord {
31
41
  secondary: never;
32
42
  }
@@ -118,23 +128,42 @@ export class PayProviderMock
118
128
  if (!expected || !actual || !safeEqual(actual, expected)) {
119
129
  this.app.throw(401, 'mock webhook signature is invalid');
120
130
  }
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)
131
+ const payment = PaymentWebhookBodySchema.safeParse(input.body);
132
+ if (payment.success) {
133
+ const eventType = `payment.${payment.data.state}`;
134
+ if (payment.data.eventType !== eventType)
135
+ this.app.throw(400, 'mock webhook event type is invalid');
136
+ return {
137
+ eventId: payment.data.eventId,
138
+ eventType,
139
+ paymentSessionId: payment.data.paymentSessionId,
140
+ payment: {
141
+ state: payment.data.state,
142
+ providerPaymentId: payment.data.providerPaymentId,
143
+ providerCaptureId: payment.data.providerCaptureId,
144
+ },
145
+ summary: {
146
+ amountMinor: payment.data.amountMinor,
147
+ currency: payment.data.currency,
148
+ },
149
+ };
150
+ }
151
+ const refund = RefundWebhookBodySchema.safeParse(input.body);
152
+ if (!refund.success) this.app.throw(400, 'mock webhook is invalid');
153
+ const eventType = `refund.${refund.data.state}`;
154
+ if (refund.data.eventType !== eventType)
125
155
  this.app.throw(400, 'mock webhook event type is invalid');
126
156
  return {
127
- eventId: body.data.eventId,
157
+ eventId: refund.data.eventId,
128
158
  eventType,
129
- paymentSessionId: body.data.paymentSessionId,
130
- payment: {
131
- state: body.data.state,
132
- providerPaymentId: body.data.providerPaymentId,
133
- providerCaptureId: body.data.providerCaptureId,
159
+ refundOperationId: refund.data.refundOperationId,
160
+ refund: {
161
+ state: refund.data.state,
162
+ providerRefundId: refund.data.providerRefundId,
134
163
  },
135
164
  summary: {
136
- amountMinor: body.data.amountMinor,
137
- currency: body.data.currency,
165
+ amountMinor: refund.data.amountMinor,
166
+ currency: refund.data.currency,
138
167
  },
139
168
  };
140
169
  }
@@ -7,6 +7,8 @@ import { Arg, Controller, Web } from 'vona-module-a-web';
7
7
 
8
8
  import { DtoMockPaymentComplete } from '../dto/mockPaymentComplete.tsx';
9
9
  import { DtoMockPaymentReceipt } from '../dto/mockPaymentReceipt.tsx';
10
+ import { DtoMockRefundComplete } from '../dto/mockRefundComplete.tsx';
11
+ import { DtoMockRefundReceipt } from '../dto/mockRefundReceipt.tsx';
10
12
 
11
13
  export interface IControllerOptionsMockPayment extends IDecoratorControllerOptions {}
12
14
 
@@ -22,4 +24,13 @@ export class ControllerMockPayment extends BeanBase {
22
24
  ): Promise<DtoMockPaymentReceipt> {
23
25
  return await this.scope.service.payMock.completePaymentSession(id, command.outcome);
24
26
  }
27
+
28
+ @Web.post('refund-operation/:id/complete')
29
+ @Api.body(DtoMockRefundReceipt)
30
+ async completeRefund(
31
+ @Arg.param('id', v.tableIdentity()) id: TableIdentity,
32
+ @Arg.body() command: DtoMockRefundComplete,
33
+ ): Promise<DtoMockRefundReceipt> {
34
+ return await this.scope.service.payMock.completeRefundOperation(id, command.outcome);
35
+ }
25
36
  }
@@ -0,0 +1,13 @@
1
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
+
3
+ import { Api } from 'vona-module-a-openapiutils';
4
+ import { Dto } from 'vona-module-a-web';
5
+ import { z } from 'zod';
6
+
7
+ export interface IDtoOptionsMockRefundComplete extends IDecoratorDtoOptions {}
8
+
9
+ @Dto<IDtoOptionsMockRefundComplete>()
10
+ export class DtoMockRefundComplete {
11
+ @Api.field(z.enum(['succeeded', 'failed', 'cancelled']))
12
+ outcome: 'succeeded' | 'failed' | 'cancelled';
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
3
+
4
+ import { Api, v } from 'vona-module-a-openapiutils';
5
+ import { Dto } from 'vona-module-a-web';
6
+
7
+ export interface IDtoOptionsMockRefundReceipt extends IDecoratorDtoOptions {}
8
+
9
+ @Dto<IDtoOptionsMockRefundReceipt>()
10
+ export class DtoMockRefundReceipt {
11
+ @Api.field(v.tableIdentity())
12
+ refundOperationId: TableIdentity;
13
+
14
+ @Api.field(v.required())
15
+ accepted: true;
16
+ }
@@ -5,6 +5,7 @@ import { BeanBase } from 'vona';
5
5
  import { Service } from 'vona-module-a-bean';
6
6
 
7
7
  export type TypeMockPaymentOutcome = 'succeeded' | 'failed' | 'cancelled';
8
+ export type TypeMockRefundOutcome = 'succeeded' | 'failed' | 'cancelled';
8
9
 
9
10
  @Service()
10
11
  export class ServicePayMock extends BeanBase {
@@ -70,6 +71,61 @@ export class ServicePayMock extends BeanBase {
70
71
  return { paymentSessionId: session.id, accepted: true as const };
71
72
  }
72
73
 
74
+ async completeRefundOperation(refundOperationId: TableIdentity, outcome: TypeMockRefundOutcome) {
75
+ this._assertSimulatorEnabled();
76
+ const refund = await this.$scope.pay.model.refundOperation.getById(refundOperationId);
77
+ if (!refund) this.app.throw(404, 'mock refund operation not found');
78
+ const session = await this.$scope.pay.model.paymentSession.getById(refund.paymentSessionId);
79
+ if (!session) this.app.throw(404, 'mock refund operation not found');
80
+ if (!(await this.bean.passport.isSystemAdmin())) {
81
+ this.app.throw(403, 'mock refund completion requires system administrator authority');
82
+ }
83
+ if (
84
+ session.providerName !== 'pay-mock:mock' ||
85
+ session.clientName !== 'default' ||
86
+ session.environment !== 'sandbox' ||
87
+ refund.state !== 'pending'
88
+ ) {
89
+ this.app.throw(409, 'mock refund operation is not actionable');
90
+ }
91
+ const { clientOptions } = this.bean.payProvider.resolveByName(
92
+ session.providerName,
93
+ session.clientName,
94
+ );
95
+ const secret = clientOptions.secretWebhook;
96
+ if (typeof secret !== 'string' || !secret) {
97
+ this.app.throw(500, 'mock webhook secret is not configured');
98
+ }
99
+ const rawBody = JSON.stringify({
100
+ eventId: `mock-refund-${randomUUID()}`,
101
+ eventType: `refund.${outcome}`,
102
+ refundOperationId: String(refund.id),
103
+ state: outcome,
104
+ amountMinor: refund.amountMinor,
105
+ currency: refund.currency,
106
+ providerRefundId: refund.providerRefundId ?? `mock-refund-${refund.id}`,
107
+ });
108
+ const signature = createHmac('sha256', secret).update(rawBody).digest('hex');
109
+ const instanceName = this.ctx.instanceName;
110
+ const instanceHeaderField = this.app.config.instance.headerField;
111
+ const response = await fetch(
112
+ this.app.util.getAbsoluteUrlByApiPath(
113
+ `/pay/webhook/${session.providerName}/${session.clientName}`,
114
+ ),
115
+ {
116
+ method: 'POST',
117
+ headers: {
118
+ 'content-type': 'application/json',
119
+ 'x-pay-mock-signature': signature,
120
+ ...(instanceName && instanceHeaderField ? { [instanceHeaderField]: instanceName } : {}),
121
+ },
122
+ body: rawBody,
123
+ },
124
+ );
125
+ if (!response.ok) this.app.throw(502, 'mock refund webhook was rejected');
126
+ return { refundOperationId: refund.id, accepted: true as const };
127
+ }
128
+
73
129
  private _assertSimulatorEnabled() {
74
130
  if (this.app.meta.env.META_MODE === 'prod') {
75
131
  this.app.throw(404, 'mock payment simulator is unavailable');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-pay-paypal",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
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
+ "@cabloy/paypal-server-sdk": "2.2.0",
34
35
  "vona-module-a-pay": "workspace:*"
35
36
  },
36
37
  "devDependencies": {
@@ -11,6 +11,14 @@ import type {
11
11
  IPayProviderWebhookInput,
12
12
  } from 'vona-module-a-pay';
13
13
 
14
+ import {
15
+ CheckoutPaymentIntent,
16
+ Client,
17
+ Environment,
18
+ OrdersController,
19
+ OrderStatus,
20
+ PaymentsController,
21
+ } from '@cabloy/paypal-server-sdk';
14
22
  import { BeanBase, useApp } from 'vona';
15
23
  import { PayProvider } from 'vona-module-a-pay';
16
24
 
@@ -56,32 +64,133 @@ export class PayProviderPaypal
56
64
  implements IPayProviderExecute<IPayProviderPaypalClientOptions>
57
65
  {
58
66
  async startPayment(
59
- _input: IPayProviderPaymentInput,
60
- _clientOptions: IPayProviderPaypalClientOptions,
67
+ input: IPayProviderPaymentInput,
68
+ clientOptions: IPayProviderPaypalClientOptions,
61
69
  ): Promise<IPayProviderPaymentSnapshot> {
62
- this.app.throw(501, 'PayPal provider is not configured');
70
+ if (input.providerOrderId) return await this.queryPayment(input, clientOptions);
71
+ if (!input.returnUrl || !input.cancelUrl) {
72
+ this.app.throw(422, 'PayPal payment requires return and cancel URLs');
73
+ }
74
+ const orders = new OrdersController(this._createClient(clientOptions));
75
+ const response = await orders.createOrder({
76
+ paypalRequestId: input.idempotencyKey,
77
+ body: {
78
+ intent: CheckoutPaymentIntent.Capture,
79
+ applicationContext: { returnUrl: input.returnUrl, cancelUrl: input.cancelUrl },
80
+ purchaseUnits: [
81
+ {
82
+ customId: String(input.paymentSessionId),
83
+ invoiceId: input.businessReference,
84
+ amount: {
85
+ currencyCode: input.currency,
86
+ value: formatMinorAmount(input.amountMinor, input.currency),
87
+ },
88
+ },
89
+ ],
90
+ },
91
+ } as never);
92
+ const order = response.result;
93
+ const approvalUrl = order?.links?.find(item => item.rel === 'approve')?.href;
94
+ if (!order?.id || !approvalUrl) this.app.throw(502, 'PayPal did not return an approval order');
95
+ return {
96
+ state: 'requires_action',
97
+ providerPaymentId: order.id,
98
+ providerOrderId: order.id,
99
+ nextAction: { kind: 'redirect', url: approvalUrl },
100
+ };
63
101
  }
64
102
 
65
103
  async queryPayment(
66
- _input: IPayProviderPaymentInput,
67
- _clientOptions: IPayProviderPaypalClientOptions,
104
+ input: IPayProviderPaymentInput,
105
+ clientOptions: IPayProviderPaypalClientOptions,
68
106
  ): Promise<IPayProviderPaymentSnapshot> {
69
- this.app.throw(501, 'PayPal provider is not configured');
107
+ if (!input.providerOrderId) return { state: 'requires_action' };
108
+ const orders = new OrdersController(this._createClient(clientOptions));
109
+ const response = await orders.getOrder({ id: input.providerOrderId });
110
+ const order = response.result;
111
+ if (!order?.id) this.app.throw(502, 'PayPal order query returned no order');
112
+ const capture = order.purchaseUnits?.[0]?.payments?.captures?.[0];
113
+ if (capture?.status === 'COMPLETED') {
114
+ return {
115
+ state: 'succeeded',
116
+ providerPaymentId: order.id,
117
+ providerOrderId: order.id,
118
+ providerCaptureId: capture.id,
119
+ nextAction: { kind: 'completed' },
120
+ };
121
+ }
122
+ if (capture?.status === 'FAILED' || capture?.status === 'DECLINED') {
123
+ return { state: 'failed', providerPaymentId: order.id, providerOrderId: order.id };
124
+ }
125
+ if (order.status === OrderStatus.Voided) {
126
+ return { state: 'cancelled', providerPaymentId: order.id, providerOrderId: order.id };
127
+ }
128
+ if (capture?.status === 'PENDING') {
129
+ return { state: 'processing', providerPaymentId: order.id, providerOrderId: order.id };
130
+ }
131
+ return { state: 'requires_action', providerPaymentId: order.id, providerOrderId: order.id };
70
132
  }
71
133
 
72
134
  async createRefund(
135
+ input: IPayProviderRefundInput,
136
+ clientOptions: IPayProviderPaypalClientOptions,
137
+ ): Promise<IPayProviderRefundSnapshot> {
138
+ const payments = new PaymentsController(this._createClient(clientOptions));
139
+ const response = await payments.refundCapturedPayment({
140
+ captureId: input.providerCaptureId,
141
+ paypalRequestId: input.idempotencyKey,
142
+ body: {
143
+ amount: {
144
+ currencyCode: input.currency,
145
+ value: formatMinorAmount(input.amountMinor, input.currency),
146
+ },
147
+ },
148
+ } as never);
149
+ return mapRefund(response.result?.status, response.result?.id);
150
+ }
151
+
152
+ async queryRefund(
73
153
  _input: IPayProviderRefundInput,
74
154
  _clientOptions: IPayProviderPaypalClientOptions,
75
155
  ): Promise<IPayProviderRefundSnapshot> {
76
- this.app.throw(501, 'PayPal provider is not configured');
156
+ this.app.throw(501, 'PayPal refund query requires a persisted provider refund identifier');
77
157
  }
78
158
 
79
159
  async verifyWebhook(
80
160
  _input: IPayProviderWebhookInput,
81
161
  _clientOptions: IPayProviderPaypalClientOptions,
82
162
  ): Promise<IPayProviderVerifiedWebhook> {
83
- this.app.throw(501, 'PayPal webhook verification is not configured');
163
+ this.app.throw(501, 'PayPal webhook verification requires configured webhook verification');
84
164
  }
165
+
166
+ private _createClient(clientOptions: IPayProviderPaypalClientOptions) {
167
+ const { clientId, clientSecret } = clientOptions.secretCredential;
168
+ if (!clientId || !clientSecret)
169
+ this.app.throw(503, 'PayPal client credentials are not configured');
170
+ return new Client({
171
+ clientCredentialsAuthCredentials: {
172
+ oAuthClientId: clientId,
173
+ oAuthClientSecret: clientSecret,
174
+ },
175
+ timeout: 0,
176
+ environment:
177
+ clientOptions.environment === 'live' ? Environment.Production : Environment.Sandbox,
178
+ });
179
+ }
180
+ }
181
+
182
+ function formatMinorAmount(amountMinor: number, currency: string) {
183
+ if (!Number.isSafeInteger(amountMinor) || amountMinor < 0 || currency !== 'USD') {
184
+ throw new Error('PayPal supports only nonnegative USD minor-unit amounts');
185
+ }
186
+ return `${Math.floor(amountMinor / 100)}.${String(amountMinor % 100).padStart(2, '0')}`;
187
+ }
188
+
189
+ function mapRefund(status?: string, providerRefundId?: string): IPayProviderRefundSnapshot {
190
+ if (status === 'COMPLETED') return { state: 'succeeded', providerRefundId };
191
+ if (status === 'FAILED') return { state: 'failed', providerRefundId };
192
+ if (status === 'CANCELLED') return { state: 'cancelled', providerRefundId };
193
+ return { state: 'pending', providerRefundId };
85
194
  }
86
195
 
87
196
  declare module 'vona-module-a-pay' {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-pay-stripe",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "vona-suite-a-pay",
3
- "version": "5.0.1",
3
+ "version": "5.0.3",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "files": [],
7
7
  "type": "module",
8
8
  "dependencies": {
9
- "vona-module-a-pay": "^5.0.1",
10
- "vona-module-pay-mock": "^5.0.1",
11
- "vona-module-pay-paypal": "^5.0.1",
12
- "vona-module-pay-stripe": "^5.0.1"
9
+ "vona-module-a-pay": "^5.0.3",
10
+ "vona-module-pay-mock": "^5.0.3",
11
+ "vona-module-pay-paypal": "^5.0.3",
12
+ "vona-module-pay-stripe": "^5.0.3"
13
13
  },
14
14
  "title": "a-pay"
15
15
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-orm",
3
- "version": "5.1.27",
3
+ "version": "5.1.28",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "db",
6
6
  "keywords": [
@@ -82,7 +82,6 @@ function _DtoGet_relation_handle<TRecord extends {}>(
82
82
  return;
83
83
  }
84
84
  const { type, model, options } = relationReal;
85
- const modelTarget = prepareClassModel(model);
86
85
  const optionsReal = Object.assign({}, options, { include: includeReal, with: withReal });
87
86
  if (mutateTypeTopLevel) {
88
87
  if (type === 'belongsTo') {
@@ -94,7 +93,7 @@ function _DtoGet_relation_handle<TRecord extends {}>(
94
93
  schema = v.array(z.object({ id: v.tableIdentity()(), deleted: z.boolean().optional() }));
95
94
  } else if (type === 'hasOne') {
96
95
  const schemaLazy = _DtoGet_relation_handle_schemaLazy(
97
- modelTarget,
96
+ model,
98
97
  optionsReal,
99
98
  autoload,
100
99
  mutateTypeTopLevel,
@@ -105,7 +104,7 @@ function _DtoGet_relation_handle<TRecord extends {}>(
105
104
  } else {
106
105
  // hasMany
107
106
  const schemaLazy = _DtoGet_relation_handle_schemaLazy(
108
- modelTarget,
107
+ model,
109
108
  optionsReal,
110
109
  autoload,
111
110
  mutateTypeTopLevel,
@@ -116,7 +115,7 @@ function _DtoGet_relation_handle<TRecord extends {}>(
116
115
  Api.field(v.optional(), schema)(entityClass.prototype, relationName);
117
116
  } else {
118
117
  const schemaLazy = _DtoGet_relation_handle_schemaLazy(
119
- modelTarget,
118
+ model,
120
119
  optionsReal,
121
120
  autoload,
122
121
  mutateTypeTopLevel,
@@ -146,13 +145,14 @@ function _DtoGet_relation_handle<TRecord extends {}>(
146
145
  }
147
146
 
148
147
  function _DtoGet_relation_handle_schemaLazy(
149
- modelTarget,
148
+ modelLike,
150
149
  optionsReal,
151
150
  autoload,
152
151
  mutateTypeTopLevel?: TypeDtoMutateType,
153
152
  relation?: IRelationItem,
154
153
  ) {
155
154
  return () => {
155
+ const modelTarget = prepareClassModel(modelLike);
156
156
  if (!autoload) {
157
157
  return _DtoGet_relation_handle_schemaLazy_raw(
158
158
  modelTarget,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.75",
3
+ "version": "5.1.76",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -8,6 +8,9 @@ export type ApiSchemaTestVonaEntityProduct = components['schemas']['test-vona.en
8
8
  export type ApiSchemaTestVonaEntityProductPartial = Partial<ApiSchemaTestVonaEntityProduct>;
9
9
  export type ApiSchemaACaptchaDtoCaptchaData = components['schemas']['a-captcha.dto.captchaData'];
10
10
  export type ApiSchemaACaptchaDtoCaptchaDataPartial = Partial<ApiSchemaACaptchaDtoCaptchaData>;
11
+ export type ApiSchemaAPayDtoPaymentSessionView =
12
+ components['schemas']['a-pay.dto.paymentSessionView'];
13
+ export type ApiSchemaAPayDtoPaymentSessionViewPartial = Partial<ApiSchemaAPayDtoPaymentSessionView>;
11
14
  export type ApiSchemaCommerceMemberDtoAddressMineRes =
12
15
  components['schemas']['commerce-member.dto.addressMineRes'];
13
16
  export type ApiSchemaCommerceMemberDtoAddressMineResPartial =
@@ -92,10 +95,10 @@ export type ApiSchemaCommercePromotionDtoCouponTemplateSelectResItem =
92
95
  components['schemas']['commerce-promotion.dto.couponTemplateSelectResItem'];
93
96
  export type ApiSchemaCommercePromotionDtoCouponTemplateSelectResItemPartial =
94
97
  Partial<ApiSchemaCommercePromotionDtoCouponTemplateSelectResItem>;
95
- export type ApiSchemaCommercePromotionDtoCouponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875 =
96
- components['schemas']['commerce-promotion.dto.couponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875'];
97
- export type ApiSchemaCommercePromotionDtoCouponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875Partial =
98
- Partial<ApiSchemaCommercePromotionDtoCouponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875>;
98
+ export type ApiSchemaCommercePromotionDtoCouponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_91abd8df366c0c4d47021b38af2a356349ee178b =
99
+ components['schemas']['commerce-promotion.dto.couponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_91abd8df366c0c4d47021b38af2a356349ee178b'];
100
+ export type ApiSchemaCommercePromotionDtoCouponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_91abd8df366c0c4d47021b38af2a356349ee178bPartial =
101
+ Partial<ApiSchemaCommercePromotionDtoCouponTemplateView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_91abd8df366c0c4d47021b38af2a356349ee178b>;
99
102
  export type ApiSchemaCommercePromotionDtoCouponTemplateUpdate =
100
103
  components['schemas']['commerce-promotion.dto.couponTemplateUpdate'];
101
104
  export type ApiSchemaCommercePromotionDtoCouponTemplateUpdatePartial =
@@ -142,10 +145,37 @@ export type ApiSchemaCommerceTradeDtoOrderCouponSnapshot_2d063d28bc7243bed02ebd8
142
145
  components['schemas']['commerce-trade.dto.orderCouponSnapshot_2d063d28bc7243bed02ebd8bddf1212a93c6305b'];
143
146
  export type ApiSchemaCommerceTradeDtoOrderCouponSnapshot_2d063d28bc7243bed02ebd8bddf1212a93c6305bPartial =
144
147
  Partial<ApiSchemaCommerceTradeDtoOrderCouponSnapshot_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
148
+ export type ApiSchemaCommerceTradeDtoShipmentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b =
149
+ components['schemas']['commerce-trade.dto.shipmentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b'];
150
+ export type ApiSchemaCommerceTradeDtoShipmentView_2d063d28bc7243bed02ebd8bddf1212a93c6305bPartial =
151
+ Partial<ApiSchemaCommerceTradeDtoShipmentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
145
152
  export type ApiSchemaCommerceTradeDtoOrderLineSkuAttributeSnapshot =
146
153
  components['schemas']['commerce-trade.dto.orderLineSkuAttributeSnapshot'];
147
154
  export type ApiSchemaCommerceTradeDtoOrderLineSkuAttributeSnapshotPartial =
148
155
  Partial<ApiSchemaCommerceTradeDtoOrderLineSkuAttributeSnapshot>;
156
+ export type ApiSchemaCommerceTradeDtoRefundResult =
157
+ components['schemas']['commerce-trade.dto.refundResult'];
158
+ export type ApiSchemaCommerceTradeDtoRefundResultPartial =
159
+ Partial<ApiSchemaCommerceTradeDtoRefundResult>;
160
+ export type ApiSchemaCommerceTradeDtoRefundRequestCreate =
161
+ components['schemas']['commerce-trade.dto.refundRequestCreate'];
162
+ export type ApiSchemaCommerceTradeDtoRefundRequestCreatePartial =
163
+ Partial<ApiSchemaCommerceTradeDtoRefundRequestCreate>;
164
+ export type ApiSchemaCommerceTradeDtoRefundReview =
165
+ components['schemas']['commerce-trade.dto.refundReview'];
166
+ export type ApiSchemaCommerceTradeDtoRefundReviewPartial =
167
+ Partial<ApiSchemaCommerceTradeDtoRefundReview>;
168
+ export type ApiSchemaCommerceTradeDtoRefundOutcomeCreate =
169
+ components['schemas']['commerce-trade.dto.refundOutcomeCreate'];
170
+ export type ApiSchemaCommerceTradeDtoRefundOutcomeCreatePartial =
171
+ Partial<ApiSchemaCommerceTradeDtoRefundOutcomeCreate>;
172
+ export type ApiSchemaCommerceTradeDtoShipmentView =
173
+ components['schemas']['commerce-trade.dto.shipmentView'];
174
+ export type ApiSchemaCommerceTradeDtoShipmentViewPartial =
175
+ Partial<ApiSchemaCommerceTradeDtoShipmentView>;
176
+ export type ApiSchemaCommerceTradeDtoOrderShip =
177
+ components['schemas']['commerce-trade.dto.orderShip'];
178
+ export type ApiSchemaCommerceTradeDtoOrderShipPartial = Partial<ApiSchemaCommerceTradeDtoOrderShip>;
149
179
  export type ApiSchemaCommerceTradeDtoOrderSelectRes =
150
180
  components['schemas']['commerce-trade.dto.orderSelectRes'];
151
181
  export type ApiSchemaCommerceTradeDtoOrderSelectResPartial =
@@ -158,14 +188,6 @@ export type ApiSchemaCommerceTradeDtoOrderView_2d063d28bc7243bed02ebd8bddf1212a9
158
188
  components['schemas']['commerce-trade.dto.orderView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875'];
159
189
  export type ApiSchemaCommerceTradeDtoOrderView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875Partial =
160
190
  Partial<ApiSchemaCommerceTradeDtoOrderView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875>;
161
- export type ApiSchemaCommerceTradeDtoPaymentOutcomeResult =
162
- components['schemas']['commerce-trade.dto.paymentOutcomeResult'];
163
- export type ApiSchemaCommerceTradeDtoPaymentOutcomeResultPartial =
164
- Partial<ApiSchemaCommerceTradeDtoPaymentOutcomeResult>;
165
- export type ApiSchemaCommerceTradeDtoPaymentOutcomeCreate =
166
- components['schemas']['commerce-trade.dto.paymentOutcomeCreate'];
167
- export type ApiSchemaCommerceTradeDtoPaymentOutcomeCreatePartial =
168
- Partial<ApiSchemaCommerceTradeDtoPaymentOutcomeCreate>;
169
191
  export type ApiSchemaCommerceTradeDtoStockAuditSelectRes =
170
192
  components['schemas']['commerce-trade.dto.stockAuditSelectRes'];
171
193
  export type ApiSchemaCommerceTradeDtoStockAuditSelectResPartial =
@@ -351,6 +373,18 @@ export type ApiSchemaTrainingStudentDtoStudentSummary_2d063d28bc7243bed02ebd8bdd
351
373
  components['schemas']['training-student.dto.studentSummary_2d063d28bc7243bed02ebd8bddf1212a93c6305b'];
352
374
  export type ApiSchemaTrainingStudentDtoStudentSummary_2d063d28bc7243bed02ebd8bddf1212a93c6305bPartial =
353
375
  Partial<ApiSchemaTrainingStudentDtoStudentSummary_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
376
+ export type ApiSchemaBasicMetricsDtoMetricsSnapshot =
377
+ components['schemas']['basic-metrics.dto.metricsSnapshot'];
378
+ export type ApiSchemaBasicMetricsDtoMetricsSnapshotPartial =
379
+ Partial<ApiSchemaBasicMetricsDtoMetricsSnapshot>;
380
+ export type ApiSchemaBasicMetricsDtoMetricsRuntime =
381
+ components['schemas']['basic-metrics.dto.metricsRuntime'];
382
+ export type ApiSchemaBasicMetricsDtoMetricsRuntimePartial =
383
+ Partial<ApiSchemaBasicMetricsDtoMetricsRuntime>;
384
+ export type ApiSchemaBasicMetricsDtoMetricsQueue =
385
+ components['schemas']['basic-metrics.dto.metricsQueue'];
386
+ export type ApiSchemaBasicMetricsDtoMetricsQueuePartial =
387
+ Partial<ApiSchemaBasicMetricsDtoMetricsQueue>;
354
388
  export type ApiSchemaAFileDtoFileUploadPolicyResponse =
355
389
  components['schemas']['a-file.dto.fileUploadPolicyResponse'];
356
390
  export type ApiSchemaAFileDtoFileUploadPolicyResponsePartial =
@@ -419,6 +453,14 @@ export type ApiSchemaAImageDtoImageTransformOptions_2d063d28bc7243bed02ebd8bddf1
419
453
  components['schemas']['a-image.dto.imageTransformOptions_2d063d28bc7243bed02ebd8bddf1212a93c6305b'];
420
454
  export type ApiSchemaAImageDtoImageTransformOptions_2d063d28bc7243bed02ebd8bddf1212a93c6305bPartial =
421
455
  Partial<ApiSchemaAImageDtoImageTransformOptions_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
456
+ export type ApiSchemaPayMockDtoMockPaymentReceipt =
457
+ components['schemas']['pay-mock.dto.mockPaymentReceipt'];
458
+ export type ApiSchemaPayMockDtoMockPaymentReceiptPartial =
459
+ Partial<ApiSchemaPayMockDtoMockPaymentReceipt>;
460
+ export type ApiSchemaPayMockDtoMockPaymentComplete =
461
+ components['schemas']['pay-mock.dto.mockPaymentComplete'];
462
+ export type ApiSchemaPayMockDtoMockPaymentCompletePartial =
463
+ Partial<ApiSchemaPayMockDtoMockPaymentComplete>;
422
464
  export type ApiSchemaAPaypalEntityPaypalRecord =
423
465
  components['schemas']['a-paypal.entity.paypalRecord'];
424
466
  export type ApiSchemaAPaypalEntityPaypalRecordPartial = Partial<ApiSchemaAPaypalEntityPaypalRecord>;