vona-module-test-pay 5.0.2 → 5.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-test-pay",
3
- "version": "5.0.2",
3
+ "version": "5.0.3",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -30,8 +30,8 @@
30
30
  "postpack": "clean-package restore"
31
31
  },
32
32
  "dependencies": {
33
- "vona-module-a-pay": "5.0.3",
34
- "vona-module-pay-mock": "5.0.3"
33
+ "vona-module-a-pay": "5.0.4",
34
+ "vona-module-pay-mock": "5.0.4"
35
35
  },
36
36
  "title": "test-pay",
37
37
  "vonaModule": {
@@ -51,5 +51,5 @@
51
51
  }
52
52
  }
53
53
  },
54
- "gitHead": "e86b8ee51b837417b7b28b2c67862877d4092371"
54
+ "gitHead": "c408492ce774644ac8a2530702db916986f52151"
55
55
  }
@@ -79,6 +79,31 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
79
79
  });
80
80
  });
81
81
 
82
+ it('rejects an unavailable provider candidate instead of falling back', async () => {
83
+ await app.bean.executor.mockCtx(async () => {
84
+ const scope = app.scope('a-pay');
85
+ const suffix = randomUUID().slice(0, 12);
86
+ const user = await app.bean.user.register({ name: `payment-candidate-${suffix}` }, true);
87
+ const fixture: IFixture = { userId: user.id as number };
88
+ try {
89
+ await assert.rejects(
90
+ scope.service.paymentSession.create({
91
+ userId: user.id,
92
+ payScene: 'commerce-payment:commerceOrder',
93
+ businessReference: `business-${suffix}`,
94
+ amountMinor: 1299,
95
+ currency: 'USD',
96
+ correlationId: `payment-${suffix}`,
97
+ providerCandidateKey: 'stripe',
98
+ }),
99
+ { status: 422 },
100
+ );
101
+ } finally {
102
+ await cleanup(fixture);
103
+ }
104
+ });
105
+ });
106
+
82
107
  it('rejects a currency outside the payment scene policy', async () => {
83
108
  await app.bean.executor.mockCtx(async () => {
84
109
  const scope = app.scope('a-pay');
@@ -142,6 +167,83 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
142
167
  });
143
168
  });
144
169
 
170
+ it('returns a terminal payment session when confirmation is replayed', async () => {
171
+ await app.bean.executor.mockCtx(async () => {
172
+ const fixture = await createFixture(new Date(Date.now() + 60_000));
173
+ try {
174
+ const scope = app.scope('a-pay');
175
+ await scope.model.paymentSession.updateById(fixture.paymentSessionId!, {
176
+ state: 'succeeded',
177
+ finalizedAt: new Date(),
178
+ });
179
+ const session = await scope.service.providerOperation.confirm(fixture.paymentSessionId!);
180
+ assert.equal(session.state, 'succeeded');
181
+ assert.deepEqual(
182
+ await scope.model.providerOperation.select({
183
+ where: { paymentSessionId: fixture.paymentSessionId!, kind: 'confirm' },
184
+ }),
185
+ [],
186
+ );
187
+ } finally {
188
+ await cleanup(fixture);
189
+ }
190
+ });
191
+ });
192
+
193
+ it('schedules a pending refund for durable reconciliation', async () => {
194
+ await app.bean.executor.mockCtx(async () => {
195
+ const fixture = await createFixture(new Date(Date.now() + 60_000));
196
+ try {
197
+ const scope = app.scope('a-pay');
198
+ await scope.model.paymentSession.updateById(fixture.paymentSessionId!, {
199
+ state: 'succeeded',
200
+ providerCaptureId: `capture-${randomUUID().slice(0, 12)}`,
201
+ finalizedAt: new Date(),
202
+ });
203
+ const refund = await scope.service.refundOperation.create({
204
+ paymentSessionId: fixture.paymentSessionId!,
205
+ businessReference: `refund-${randomUUID().slice(0, 12)}`,
206
+ amountMinor: 1299,
207
+ currency: 'USD',
208
+ idempotencyKey: `refund-${randomUUID()}`,
209
+ correlationId: `refund-${randomUUID()}`,
210
+ });
211
+ const providerOperation = await scope.model.providerOperation.get({
212
+ refundOperationId: refund.id,
213
+ kind: 'refund',
214
+ });
215
+ assert.ok(providerOperation);
216
+ const claimed = await scope.service.providerOperation.claim(providerOperation.id);
217
+ assert.ok(claimed?.claimToken);
218
+ await scope.service.providerOperation.markSubmitted(
219
+ providerOperation.id,
220
+ claimed.claimToken,
221
+ );
222
+ await scope.service.refundOperation.settleProviderSnapshot(
223
+ providerOperation.id,
224
+ claimed.claimToken,
225
+ { state: 'pending', providerRefundId: `refund-${randomUUID().slice(0, 12)}` },
226
+ );
227
+ const [storedRefund, storedOperation, outbox] = await Promise.all([
228
+ scope.model.refundOperation.getById(refund.id),
229
+ scope.model.providerOperation.getById(providerOperation.id),
230
+ scope.model.outboxEvent.select({
231
+ where: { paymentSessionId: fixture.paymentSessionId! },
232
+ }),
233
+ ]);
234
+ assert.equal(storedRefund?.state, 'pending');
235
+ assert.equal(storedOperation?.state, 'reconciliation_required');
236
+ assert.equal(storedOperation?.providerResourceId, storedRefund?.providerRefundId);
237
+ assert.equal(storedOperation?.claimToken, undefined);
238
+ assert.equal(storedOperation?.claimExpiresAt, undefined);
239
+ assert.ok(storedOperation?.nextAttemptAt && storedOperation.nextAttemptAt > new Date());
240
+ assert.deepEqual(outbox, []);
241
+ } finally {
242
+ await cleanup(fixture);
243
+ }
244
+ });
245
+ });
246
+
145
247
  it('allows only one competing start to call the provider', async () => {
146
248
  const fixture = await app.bean.executor.mockCtx(async () => {
147
249
  return await createFixture(new Date(Date.now() + 60_000));
@@ -0,0 +1,118 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { app } from 'vona-mock';
4
+
5
+ describe('paypalProvider.test.ts', { concurrency: false }, () => {
6
+ it('maps SDK camelCase payment and refund facts through the PayPal gateway', async () => {
7
+ await app.bean.executor.mockCtx(async () => {
8
+ const calls: Array<{ kind: string; input: unknown }> = [];
9
+ const gateway = {
10
+ async createOrder(_options: unknown, input: unknown) {
11
+ calls.push({ kind: 'createOrder', input });
12
+ return {
13
+ id: 'order-1',
14
+ links: [
15
+ { rel: 'approve', href: 'https://www.sandbox.paypal.com/checkoutnow?token=order-1' },
16
+ ],
17
+ };
18
+ },
19
+ async captureOrder() {
20
+ throw new Error('unexpected captureOrder');
21
+ },
22
+ async getOrder(_options: unknown, input: unknown) {
23
+ calls.push({ kind: 'getOrder', input });
24
+ return {
25
+ id: 'order-1',
26
+ purchaseUnits: [
27
+ {
28
+ customId: '101',
29
+ invoiceId: 'business-1',
30
+ amount: { currencyCode: 'USD', value: '12.99' },
31
+ payee: { merchantId: 'merchant-1' },
32
+ payments: {
33
+ captures: [
34
+ {
35
+ id: 'capture-1',
36
+ status: 'COMPLETED',
37
+ amount: { currencyCode: 'USD', value: '12.99' },
38
+ },
39
+ ],
40
+ },
41
+ },
42
+ ],
43
+ };
44
+ },
45
+ async refundCapturedPayment(_options: unknown, input: unknown) {
46
+ calls.push({ kind: 'refundCapturedPayment', input });
47
+ return {
48
+ id: 'refund-1',
49
+ customId: '201',
50
+ invoiceId: 'refund-business-1',
51
+ amount: { currencyCode: 'USD', value: '5.00' },
52
+ status: 'COMPLETED',
53
+ payee: { merchantId: 'merchant-1' },
54
+ };
55
+ },
56
+ async getRefund() {
57
+ throw new Error('unexpected getRefund');
58
+ },
59
+ async verifyWebhookSignature() {
60
+ throw new Error('unexpected verifyWebhookSignature');
61
+ },
62
+ };
63
+ const { provider, clientOptions } = app.bean.payProvider.resolveByName(
64
+ 'pay-paypal:paypal',
65
+ 'default',
66
+ );
67
+ const options = {
68
+ ...clientOptions,
69
+ secretCredential: { clientId: 'client-1', clientSecret: 'secret-1' },
70
+ webhookId: 'webhook-1',
71
+ merchantReference: 'merchant-1',
72
+ gateway,
73
+ };
74
+ const paymentInput = {
75
+ paymentSessionId: 101,
76
+ businessReference: 'business-1',
77
+ idempotencyKey: 'payment-key',
78
+ amountMinor: 1299,
79
+ currency: 'USD',
80
+ providerOrderId: 'order-1',
81
+ };
82
+ const payment = await provider.queryPayment(paymentInput, options as never);
83
+ assert.deepEqual(payment, {
84
+ state: 'succeeded',
85
+ providerPaymentId: 'order-1',
86
+ providerOrderId: 'order-1',
87
+ providerCaptureId: 'capture-1',
88
+ nextAction: { kind: 'completed' },
89
+ });
90
+ const refund = await provider.createRefund(
91
+ {
92
+ paymentSessionId: 101,
93
+ refundOperationId: 201,
94
+ businessReference: 'refund-business-1',
95
+ idempotencyKey: 'refund-key',
96
+ amountMinor: 500,
97
+ currency: 'USD',
98
+ providerCaptureId: 'capture-1',
99
+ },
100
+ options as never,
101
+ );
102
+ assert.deepEqual(refund, { state: 'succeeded', providerRefundId: 'refund-1' });
103
+ assert.deepEqual(
104
+ calls.map(item => item.kind),
105
+ ['getOrder', 'refundCapturedPayment'],
106
+ );
107
+ assert.deepEqual(calls[1]?.input, {
108
+ captureId: 'capture-1',
109
+ paypalRequestId: 'refund-key',
110
+ body: {
111
+ amount: { currencyCode: 'USD', value: '5.00' },
112
+ customId: '201',
113
+ invoiceId: 'refund-business-1',
114
+ },
115
+ });
116
+ });
117
+ });
118
+ });