vona-module-test-pay 5.0.3 → 5.0.5
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 +5 -4
- package/test/paymentSession.test.ts +340 -3
- package/test/paypalLifecycle.test.ts +1266 -0
- package/test/paypalProvider.test.ts +444 -81
- package/test/stripeLifecycle.test.ts +545 -0
- package/test/stripeProvider.test.ts +300 -0
- package/test/webhook.test.ts +25 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona-module-test-pay",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Vona Module"
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
"postpack": "clean-package restore"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"vona-module-a-pay": "5.0.
|
|
34
|
-
"vona-module-pay-mock": "5.0.
|
|
33
|
+
"vona-module-a-pay": "5.0.6",
|
|
34
|
+
"vona-module-pay-mock": "5.0.6",
|
|
35
|
+
"vona-module-pay-stripe": "5.0.6"
|
|
35
36
|
},
|
|
36
37
|
"title": "test-pay",
|
|
37
38
|
"vonaModule": {
|
|
@@ -51,5 +52,5 @@
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "75a09e6a22891e958802ee649ed454db38f90c16"
|
|
55
56
|
}
|
|
@@ -8,6 +8,18 @@ interface IFixture {
|
|
|
8
8
|
paymentSessionId?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function isZodCustomError(error: unknown, path: string[], message: string) {
|
|
12
|
+
assert.equal((error as any).code, 422);
|
|
13
|
+
assert.deepEqual((error as any).message, [
|
|
14
|
+
{
|
|
15
|
+
code: 'custom',
|
|
16
|
+
path,
|
|
17
|
+
message,
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
async function createFixture(expiresAt: Date): Promise<IFixture> {
|
|
12
24
|
const scope = app.scope('a-pay');
|
|
13
25
|
const suffix = randomUUID().slice(0, 12);
|
|
@@ -27,6 +39,20 @@ async function createFixture(expiresAt: Date): Promise<IFixture> {
|
|
|
27
39
|
async function cleanup(fixture: IFixture) {
|
|
28
40
|
const scope = app.scope('a-pay');
|
|
29
41
|
if (fixture.paymentSessionId !== undefined) {
|
|
42
|
+
const refunds = await scope.model.refundOperation.select({
|
|
43
|
+
where: { paymentSessionId: fixture.paymentSessionId },
|
|
44
|
+
});
|
|
45
|
+
for (const refund of refunds) {
|
|
46
|
+
const providerOperation = await scope.model.providerOperation.get({
|
|
47
|
+
refundOperationId: refund.id,
|
|
48
|
+
kind: 'refund',
|
|
49
|
+
});
|
|
50
|
+
if (providerOperation) {
|
|
51
|
+
await scope.model.providerOperationRecoveryAudit.delete({
|
|
52
|
+
providerOperationId: providerOperation.id,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
30
56
|
await scope.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
31
57
|
await scope.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
32
58
|
await scope.model.providerOperation.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
@@ -71,6 +97,18 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
|
|
|
71
97
|
assert.equal(session.providerName, 'pay-mock:mock');
|
|
72
98
|
assert.equal(session.clientName, 'default');
|
|
73
99
|
assert.equal(session.environment, 'sandbox');
|
|
100
|
+
assert.match(
|
|
101
|
+
session.providerInvoiceReference,
|
|
102
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
103
|
+
);
|
|
104
|
+
assert.match(
|
|
105
|
+
session.providerCorrelationReference,
|
|
106
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
107
|
+
);
|
|
108
|
+
assert.notEqual(session.providerInvoiceReference, session.providerCorrelationReference);
|
|
109
|
+
const reloaded = await scope.model.paymentSession.getById(session.id);
|
|
110
|
+
assert.equal(reloaded?.providerInvoiceReference, session.providerInvoiceReference);
|
|
111
|
+
assert.equal(reloaded?.providerCorrelationReference, session.providerCorrelationReference);
|
|
74
112
|
assert.ok(session.expiresAt.getTime() >= createdAt + 30 * 60 * 1000);
|
|
75
113
|
assert.ok(session.expiresAt.getTime() <= Date.now() + 30 * 60 * 1000);
|
|
76
114
|
} finally {
|
|
@@ -94,9 +132,14 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
|
|
|
94
132
|
amountMinor: 1299,
|
|
95
133
|
currency: 'USD',
|
|
96
134
|
correlationId: `payment-${suffix}`,
|
|
97
|
-
providerCandidateKey: '
|
|
135
|
+
providerCandidateKey: 'unavailable-provider',
|
|
98
136
|
}),
|
|
99
|
-
|
|
137
|
+
error =>
|
|
138
|
+
isZodCustomError(
|
|
139
|
+
error,
|
|
140
|
+
['providerCandidateKey'],
|
|
141
|
+
'payment provider candidate is unavailable',
|
|
142
|
+
),
|
|
100
143
|
);
|
|
101
144
|
} finally {
|
|
102
145
|
await cleanup(fixture);
|
|
@@ -120,7 +163,42 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
|
|
|
120
163
|
currency: 'EUR',
|
|
121
164
|
correlationId: `payment-${suffix}`,
|
|
122
165
|
}),
|
|
123
|
-
|
|
166
|
+
error =>
|
|
167
|
+
isZodCustomError(
|
|
168
|
+
error,
|
|
169
|
+
['currency'],
|
|
170
|
+
'payment currency is not allowed by the payment scene',
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
await scope.model.paymentSession.select({
|
|
175
|
+
where: { correlationId: `payment-${suffix}` },
|
|
176
|
+
}),
|
|
177
|
+
[],
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
await cleanup(fixture);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('reports an invalid payment amount as a field error', async () => {
|
|
186
|
+
await app.bean.executor.mockCtx(async () => {
|
|
187
|
+
const scope = app.scope('a-pay');
|
|
188
|
+
const suffix = randomUUID().slice(0, 12);
|
|
189
|
+
const user = await app.bean.user.register({ name: `payment-session-${suffix}` }, true);
|
|
190
|
+
const fixture: IFixture = { userId: user.id as number };
|
|
191
|
+
try {
|
|
192
|
+
await assert.rejects(
|
|
193
|
+
scope.service.paymentSession.create({
|
|
194
|
+
userId: user.id,
|
|
195
|
+
payScene: 'commerce-payment:commerceOrder',
|
|
196
|
+
businessReference: `business-${suffix}`,
|
|
197
|
+
amountMinor: -1,
|
|
198
|
+
currency: 'USD',
|
|
199
|
+
correlationId: `payment-${suffix}`,
|
|
200
|
+
}),
|
|
201
|
+
error => isZodCustomError(error, ['amountMinor'], 'payment amount is invalid'),
|
|
124
202
|
);
|
|
125
203
|
assert.deepEqual(
|
|
126
204
|
await scope.model.paymentSession.select({
|
|
@@ -190,6 +268,58 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
|
|
|
190
268
|
});
|
|
191
269
|
});
|
|
192
270
|
|
|
271
|
+
it('rejects refund commands that conflict with the payment state', async () => {
|
|
272
|
+
await app.bean.executor.mockCtx(async () => {
|
|
273
|
+
const fixture = await createFixture(new Date(Date.now() + 60_000));
|
|
274
|
+
try {
|
|
275
|
+
const scope = app.scope('a-pay');
|
|
276
|
+
await scope.model.paymentSession.updateById(fixture.paymentSessionId!, {
|
|
277
|
+
state: 'succeeded',
|
|
278
|
+
providerCaptureId: `capture-${randomUUID().slice(0, 12)}`,
|
|
279
|
+
finalizedAt: new Date(),
|
|
280
|
+
});
|
|
281
|
+
const command = {
|
|
282
|
+
paymentSessionId: fixture.paymentSessionId!,
|
|
283
|
+
businessReference: `refund-${randomUUID().slice(0, 12)}`,
|
|
284
|
+
currency: 'USD',
|
|
285
|
+
idempotencyKey: `refund-${randomUUID()}`,
|
|
286
|
+
correlationId: `refund-${randomUUID()}`,
|
|
287
|
+
};
|
|
288
|
+
await assert.rejects(scope.service.refundOperation.create({ ...command, amountMinor: 0 }), {
|
|
289
|
+
code: 409,
|
|
290
|
+
status: 409,
|
|
291
|
+
});
|
|
292
|
+
assert.deepEqual(
|
|
293
|
+
await scope.model.refundOperation.select({
|
|
294
|
+
where: { paymentSessionId: fixture.paymentSessionId! },
|
|
295
|
+
}),
|
|
296
|
+
[],
|
|
297
|
+
);
|
|
298
|
+
await scope.service.refundOperation.create({ ...command, amountMinor: 1_000 });
|
|
299
|
+
await assert.rejects(
|
|
300
|
+
scope.service.refundOperation.create({
|
|
301
|
+
...command,
|
|
302
|
+
amountMinor: 300,
|
|
303
|
+
businessReference: `refund-${randomUUID().slice(0, 12)}`,
|
|
304
|
+
idempotencyKey: `refund-${randomUUID()}`,
|
|
305
|
+
correlationId: `refund-${randomUUID()}`,
|
|
306
|
+
}),
|
|
307
|
+
{ code: 409, status: 409 },
|
|
308
|
+
);
|
|
309
|
+
assert.equal(
|
|
310
|
+
(
|
|
311
|
+
await scope.model.refundOperation.select({
|
|
312
|
+
where: { paymentSessionId: fixture.paymentSessionId! },
|
|
313
|
+
})
|
|
314
|
+
).length,
|
|
315
|
+
1,
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
await cleanup(fixture);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
193
323
|
it('schedules a pending refund for durable reconciliation', async () => {
|
|
194
324
|
await app.bean.executor.mockCtx(async () => {
|
|
195
325
|
const fixture = await createFixture(new Date(Date.now() + 60_000));
|
|
@@ -213,6 +343,19 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
|
|
|
213
343
|
kind: 'refund',
|
|
214
344
|
});
|
|
215
345
|
assert.ok(providerOperation);
|
|
346
|
+
assert.match(
|
|
347
|
+
refund.providerInvoiceReference,
|
|
348
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
349
|
+
);
|
|
350
|
+
assert.match(
|
|
351
|
+
refund.providerCorrelationReference,
|
|
352
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
353
|
+
);
|
|
354
|
+
assert.notEqual(refund.providerInvoiceReference, refund.providerCorrelationReference);
|
|
355
|
+
assert.match(
|
|
356
|
+
providerOperation.idempotencyKey,
|
|
357
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
358
|
+
);
|
|
216
359
|
const claimed = await scope.service.providerOperation.claim(providerOperation.id);
|
|
217
360
|
assert.ok(claimed?.claimToken);
|
|
218
361
|
await scope.service.providerOperation.markSubmitted(
|
|
@@ -244,6 +387,200 @@ describe('paymentSession.test.ts', { concurrency: false }, () => {
|
|
|
244
387
|
});
|
|
245
388
|
});
|
|
246
389
|
|
|
390
|
+
it('fences an ambiguous refund submission until audited recovery', async () => {
|
|
391
|
+
await app.bean.executor.mockCtx(async () => {
|
|
392
|
+
const fixture = await createFixture(new Date(Date.now() + 60_000));
|
|
393
|
+
try {
|
|
394
|
+
const scope = app.scope('a-pay');
|
|
395
|
+
await scope.model.paymentSession.updateById(fixture.paymentSessionId!, {
|
|
396
|
+
state: 'succeeded',
|
|
397
|
+
providerCaptureId: `capture-${randomUUID().slice(0, 12)}`,
|
|
398
|
+
finalizedAt: new Date(),
|
|
399
|
+
});
|
|
400
|
+
const refund = await scope.service.refundOperation.create({
|
|
401
|
+
paymentSessionId: fixture.paymentSessionId!,
|
|
402
|
+
businessReference: `refund-${randomUUID().slice(0, 12)}`,
|
|
403
|
+
amountMinor: 1299,
|
|
404
|
+
currency: 'USD',
|
|
405
|
+
idempotencyKey: `refund-${randomUUID()}`,
|
|
406
|
+
correlationId: `refund-${randomUUID()}`,
|
|
407
|
+
});
|
|
408
|
+
const operation = await scope.model.providerOperation.get({
|
|
409
|
+
refundOperationId: refund.id,
|
|
410
|
+
kind: 'refund',
|
|
411
|
+
});
|
|
412
|
+
assert.ok(operation);
|
|
413
|
+
const providerRequestId = operation.idempotencyKey;
|
|
414
|
+
assert.match(
|
|
415
|
+
providerRequestId,
|
|
416
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
417
|
+
);
|
|
418
|
+
const claimed = await scope.service.providerOperation.claim(operation.id);
|
|
419
|
+
assert.ok(claimed?.claimToken);
|
|
420
|
+
await scope.service.providerOperation.markSubmitted(operation.id, claimed.claimToken);
|
|
421
|
+
await scope.service.providerOperation.releaseForReconciliation(
|
|
422
|
+
operation.id,
|
|
423
|
+
claimed.claimToken,
|
|
424
|
+
new Error('provider secret token must never persist'),
|
|
425
|
+
);
|
|
426
|
+
const failed = await scope.model.providerOperation.getById(operation.id);
|
|
427
|
+
assert.equal(failed?.state, 'reconciliation_required');
|
|
428
|
+
assert.equal(failed?.attemptCount, 1);
|
|
429
|
+
assert.equal(failed?.errorCode, 'refund_submission_outcome_unknown');
|
|
430
|
+
assert.equal(failed?.errorSummary, 'Provider refund submission outcome is unknown');
|
|
431
|
+
assert.equal(failed?.providerRequestId, providerRequestId);
|
|
432
|
+
assert.equal(failed?.idempotencyKey, providerRequestId);
|
|
433
|
+
assert.equal(failed?.errorSummary?.includes('secret'), false);
|
|
434
|
+
await scope.model.providerOperation.updateById(operation.id, {
|
|
435
|
+
nextAttemptAt: new Date(0),
|
|
436
|
+
});
|
|
437
|
+
await scope.service.providerOperation.queueDue();
|
|
438
|
+
const fenced = await scope.model.providerOperation.getById(operation.id);
|
|
439
|
+
assert.equal(fenced?.attemptCount, 1);
|
|
440
|
+
await scope.service.providerOperation.reconcileRefund(operation.id, {
|
|
441
|
+
actionIdempotencyKey: randomUUID(),
|
|
442
|
+
reason: 'Provider outcome could not be verified',
|
|
443
|
+
});
|
|
444
|
+
const audits = await scope.model.providerOperationRecoveryAudit.select({
|
|
445
|
+
where: { providerOperationId: operation.id },
|
|
446
|
+
});
|
|
447
|
+
assert.equal(audits.length, 1);
|
|
448
|
+
assert.equal(audits[0].action, 'reconcile');
|
|
449
|
+
assert.equal(audits[0].resolution, 'unresolved');
|
|
450
|
+
await assert.rejects(
|
|
451
|
+
scope.service.providerOperation.retryRefund(operation.id, {
|
|
452
|
+
actionIdempotencyKey: randomUUID(),
|
|
453
|
+
reason: 'Retry without acknowledgement',
|
|
454
|
+
}),
|
|
455
|
+
{ status: 409 },
|
|
456
|
+
);
|
|
457
|
+
const retryKey = randomUUID();
|
|
458
|
+
await scope.service.providerOperation.retryRefund(operation.id, {
|
|
459
|
+
actionIdempotencyKey: retryKey,
|
|
460
|
+
reason: 'Retry the original provider request after reconciliation',
|
|
461
|
+
acknowledgeRetryRisk: true,
|
|
462
|
+
});
|
|
463
|
+
const retried = await scope.model.providerOperation.getById(operation.id);
|
|
464
|
+
assert.equal(retried?.attemptCount, 2);
|
|
465
|
+
assert.equal(retried?.providerRequestId, providerRequestId);
|
|
466
|
+
assert.equal(retried?.idempotencyKey, providerRequestId);
|
|
467
|
+
const retryAudit = await scope.model.providerOperationRecoveryAudit.get({
|
|
468
|
+
providerOperationId: operation.id,
|
|
469
|
+
actionIdempotencyKey: retryKey,
|
|
470
|
+
});
|
|
471
|
+
assert.equal(retryAudit?.action, 'retry');
|
|
472
|
+
assert.equal(retryAudit?.resolution, 'retried');
|
|
473
|
+
await scope.service.providerOperation.retryRefund(operation.id, {
|
|
474
|
+
actionIdempotencyKey: retryKey,
|
|
475
|
+
reason: 'Retry the original provider request after reconciliation',
|
|
476
|
+
acknowledgeRetryRisk: true,
|
|
477
|
+
});
|
|
478
|
+
assert.equal((await scope.model.providerOperation.getById(operation.id))?.attemptCount, 2);
|
|
479
|
+
await assert.rejects(
|
|
480
|
+
scope.service.providerOperation.retryRefund(operation.id, {
|
|
481
|
+
actionIdempotencyKey: randomUUID(),
|
|
482
|
+
reason: 'A second retry must be denied',
|
|
483
|
+
acknowledgeRetryRisk: true,
|
|
484
|
+
}),
|
|
485
|
+
{ status: 409 },
|
|
486
|
+
);
|
|
487
|
+
} finally {
|
|
488
|
+
await cleanup(fixture);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('finalizes an exhausted payment start operation from provider failure', async () => {
|
|
494
|
+
await app.bean.executor.mockCtx(async () => {
|
|
495
|
+
const fixture = await createFixture(new Date(Date.now() + 60_000));
|
|
496
|
+
try {
|
|
497
|
+
const scope = app.scope('a-pay');
|
|
498
|
+
const operation = await scope.service.providerOperation.ensureStart(
|
|
499
|
+
fixture.paymentSessionId!,
|
|
500
|
+
);
|
|
501
|
+
const claimed = await scope.service.providerOperation.claim(operation.id);
|
|
502
|
+
assert.ok(claimed?.claimToken);
|
|
503
|
+
await scope.service.providerOperation.markSubmitted(operation.id, claimed.claimToken);
|
|
504
|
+
await scope.model.providerOperation.updateById(operation.id, { attemptCount: 10 });
|
|
505
|
+
|
|
506
|
+
await scope.service.providerOperation.releaseForReconciliation(
|
|
507
|
+
operation.id,
|
|
508
|
+
claimed.claimToken,
|
|
509
|
+
new Error('provider start failed'),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const [storedSession, storedOperation, audits, outbox] = await Promise.all([
|
|
513
|
+
scope.model.paymentSession.getById(fixture.paymentSessionId!),
|
|
514
|
+
scope.model.providerOperation.getById(operation.id),
|
|
515
|
+
scope.model.paymentAudit.select({
|
|
516
|
+
where: { paymentSessionId: fixture.paymentSessionId! },
|
|
517
|
+
}),
|
|
518
|
+
scope.model.outboxEvent.select({
|
|
519
|
+
where: { paymentSessionId: fixture.paymentSessionId! },
|
|
520
|
+
}),
|
|
521
|
+
]);
|
|
522
|
+
assert.equal(storedSession?.state, 'failed');
|
|
523
|
+
assert.ok(storedSession?.finalizedAt);
|
|
524
|
+
assert.equal(storedOperation?.state, 'failed');
|
|
525
|
+
assert.equal(storedOperation?.claimToken, undefined);
|
|
526
|
+
assert.equal(storedOperation?.nextAttemptAt, undefined);
|
|
527
|
+
assert.equal(storedOperation?.errorCode, 'provider_operation_failed');
|
|
528
|
+
assert.equal(
|
|
529
|
+
storedOperation?.errorSummary,
|
|
530
|
+
'Provider operation failed and will be reconciled',
|
|
531
|
+
);
|
|
532
|
+
assert.equal(audits.length, 2);
|
|
533
|
+
assert.equal(audits[1].providerOperationId, operation.id);
|
|
534
|
+
assert.equal(audits[1].fromState, 'starting');
|
|
535
|
+
assert.equal(audits[1].toState, 'failed');
|
|
536
|
+
assert.equal(audits[1].source, 'providerOperation.start.attemptsExhausted');
|
|
537
|
+
assert.equal(outbox.length, 1);
|
|
538
|
+
assert.equal(outbox[0].eventType, 'payment.outcome.v1');
|
|
539
|
+
assert.deepEqual((outbox[0].payload as any).state, 'failed');
|
|
540
|
+
} finally {
|
|
541
|
+
await cleanup(fixture);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('finalizes an exhausted eligible payment operation only once', async () => {
|
|
547
|
+
await app.bean.executor.mockCtx(async () => {
|
|
548
|
+
const fixture = await createFixture(new Date(Date.now() + 60_000));
|
|
549
|
+
try {
|
|
550
|
+
const scope = app.scope('a-pay');
|
|
551
|
+
const operation = await scope.service.providerOperation.ensureStart(
|
|
552
|
+
fixture.paymentSessionId!,
|
|
553
|
+
);
|
|
554
|
+
await scope.model.providerOperation.updateById(operation.id, {
|
|
555
|
+
attemptCount: 10,
|
|
556
|
+
nextAttemptAt: new Date(0),
|
|
557
|
+
errorCode: 'provider_operation_failed',
|
|
558
|
+
errorSummary: 'Provider operation failed and will be reconciled',
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
assert.equal(await scope.service.providerOperation.claim(operation.id), undefined);
|
|
562
|
+
assert.equal(await scope.service.providerOperation.claim(operation.id), undefined);
|
|
563
|
+
|
|
564
|
+
const [storedSession, storedOperation, audits, outbox] = await Promise.all([
|
|
565
|
+
scope.model.paymentSession.getById(fixture.paymentSessionId!),
|
|
566
|
+
scope.model.providerOperation.getById(operation.id),
|
|
567
|
+
scope.model.paymentAudit.select({
|
|
568
|
+
where: { paymentSessionId: fixture.paymentSessionId! },
|
|
569
|
+
}),
|
|
570
|
+
scope.model.outboxEvent.select({
|
|
571
|
+
where: { paymentSessionId: fixture.paymentSessionId! },
|
|
572
|
+
}),
|
|
573
|
+
]);
|
|
574
|
+
assert.equal(storedSession?.state, 'failed');
|
|
575
|
+
assert.equal(storedOperation?.state, 'failed');
|
|
576
|
+
assert.equal(audits.length, 2);
|
|
577
|
+
assert.equal(outbox.length, 1);
|
|
578
|
+
} finally {
|
|
579
|
+
await cleanup(fixture);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
247
584
|
it('allows only one competing start to call the provider', async () => {
|
|
248
585
|
const fixture = await app.bean.executor.mockCtx(async () => {
|
|
249
586
|
return await createFixture(new Date(Date.now() + 60_000));
|