cabloy 5.1.126 → 5.1.128
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/CHANGELOG.md +37 -0
- package/cabloy-docs/backend/error-guide.md +17 -0
- package/cabloy-docs/backend/i18n-guide.md +86 -7
- package/e2e/specs/a-commerce/commerce.spec.ts +222 -6
- package/package.json +1 -1
- package/vona/env/.env +2 -0
- package/vona/env/.env.prod +6 -2
- package/vona/packages-cli/cabloy-cli/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-utils/module-glob/package.json +1 -1
- package/vona/packages-utils/utils/package.json +1 -1
- package/vona/packages-utils/utils/src/vona.ts +8 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorClass.ts +21 -4
- package/vona/packages-vona/vona-core/src/lib/bean/resource/error/errorObject.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/bean/type.ts +3 -1
- package/vona/packages-vona/vona-core/src/types/interface/module.ts +6 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +15 -44
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +37 -2
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categorySelectResItem.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/categoryView.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productSelectResItem.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/category.tsx +21 -2
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/product.tsx +23 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/category.ts +11 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/category.ts +7 -2
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +7 -2
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/categoryPresentation.test.ts +218 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +214 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +39 -3
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +14 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/paymentAttempt.ts +5 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +17 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutCreate.tsx +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutPaymentMethod.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/checkoutPaymentMethods.tsx +17 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundResult.tsx +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +53 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +111 -3
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +94 -55
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +65 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/paymentCallback.ts +32 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/controller/paymentSession.ts +26 -8
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentCallback.ts +99 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +26 -3
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +214 -82
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +27 -2
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +9 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +5 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +7 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +395 -84
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/index.ts +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/paypalGateway.ts +129 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/types/index.ts +2 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +7 -0
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +3 -3
- package/zova/packages-cli/cli-set-front/package.json +4 -4
- package/zova/packages-utils/zova-jsx/package.json +3 -3
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +2 -2
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +16 -4
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +240 -19
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeCheckout.ts +16 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +8 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +184 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeCheckout.ts +12 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +11 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +25 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +27 -4
- package/zova/src/suite/cabloy-basic/modules/basic-select/src/bean/tableCell.select.tsx +12 -6
- package/zova/src/suite/cabloy-basic/modules/basic-select/src/component/formFieldSelect/controller.tsx +6 -3
- package/zova/src/suite-vendor/a-pay/modules/a-pay/cli/openapi.config.ts +1 -1
- package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/schemas.ts +20 -8
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/openapi/types.ts +292 -32
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/api/paymentSession.ts +26 -4
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/apiSchema/paymentSession.ts +5 -0
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/component/paymentNextAction/controller.tsx +2 -1
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +12 -0
- package/zova/src/suite-vendor/a-pay/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +4 -4
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
2
|
+
|
|
3
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
4
|
+
import { Dto } from 'vona-module-a-web';
|
|
5
|
+
|
|
6
|
+
import { DtoCheckoutPaymentMethod } from './checkoutPaymentMethod.tsx';
|
|
7
|
+
|
|
8
|
+
export interface IDtoOptionsCheckoutPaymentMethods extends IDecoratorDtoOptions {}
|
|
9
|
+
|
|
10
|
+
@Dto<IDtoOptionsCheckoutPaymentMethods>()
|
|
11
|
+
export class DtoCheckoutPaymentMethods {
|
|
12
|
+
@Api.field(v.array(DtoCheckoutPaymentMethod))
|
|
13
|
+
items: DtoCheckoutPaymentMethod[];
|
|
14
|
+
|
|
15
|
+
@Api.field(v.required(), v.min(1), v.max(100))
|
|
16
|
+
defaultKey: string;
|
|
17
|
+
}
|
|
@@ -18,6 +18,9 @@ export class DtoRefundResult {
|
|
|
18
18
|
@Api.field(v.optional(), v.tableIdentity())
|
|
19
19
|
refundAttemptId?: TableIdentity;
|
|
20
20
|
|
|
21
|
+
@Api.field(v.optional(), v.tableIdentity())
|
|
22
|
+
refundOperationId?: TableIdentity;
|
|
23
|
+
|
|
21
24
|
@Api.field(z.enum(['paid', 'refund_requested', 'refund_approved', 'refund_rejected', 'refunded']))
|
|
22
25
|
orderState: 'paid' | 'refund_requested' | 'refund_approved' | 'refund_rejected' | 'refunded';
|
|
23
26
|
|
|
@@ -322,6 +322,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
322
322
|
currency: order.currency,
|
|
323
323
|
amountCents: order.payableTotalCents,
|
|
324
324
|
correlationId: `${command.correlationId}:payment`,
|
|
325
|
+
providerCandidateKey: command.providerCandidateKey,
|
|
325
326
|
});
|
|
326
327
|
await onStage?.('afterPaymentAttempt');
|
|
327
328
|
await this._appendAudit({
|
|
@@ -376,6 +377,13 @@ export class ServiceOrder extends BeanBase {
|
|
|
376
377
|
providerEventId: event.eventId,
|
|
377
378
|
});
|
|
378
379
|
if (existingAudit) return this._paymentOutcomeResult(order, lockedAttempt);
|
|
380
|
+
if (
|
|
381
|
+
order.state === 'expired' &&
|
|
382
|
+
lockedAttempt.state === 'cancelled' &&
|
|
383
|
+
event.state === 'succeeded'
|
|
384
|
+
) {
|
|
385
|
+
return await this._scheduleLateCaptureCompensation(order, lockedAttempt, event);
|
|
386
|
+
}
|
|
379
387
|
if (order.state !== 'awaiting_payment' || lockedAttempt.state !== 'created') {
|
|
380
388
|
this.app.throw(409, 'payment attempt is no longer available');
|
|
381
389
|
}
|
|
@@ -1157,7 +1165,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
1157
1165
|
private _refundResult(
|
|
1158
1166
|
order: Pick<EntityOrder, 'id' | 'state' | 'currency'>,
|
|
1159
1167
|
refundRequest: Pick<EntityRefundRequest, 'id' | 'state' | 'amountCents'>,
|
|
1160
|
-
refundAttempt?: Pick<EntityRefundAttempt, 'id' | 'state'>,
|
|
1168
|
+
refundAttempt?: Pick<EntityRefundAttempt, 'id' | 'state' | 'refundOperationId'>,
|
|
1161
1169
|
): DtoRefundResult {
|
|
1162
1170
|
if (
|
|
1163
1171
|
order.state !== 'paid' &&
|
|
@@ -1172,6 +1180,7 @@ export class ServiceOrder extends BeanBase {
|
|
|
1172
1180
|
orderId: order.id,
|
|
1173
1181
|
refundRequestId: refundRequest.id,
|
|
1174
1182
|
refundAttemptId: refundAttempt?.id,
|
|
1183
|
+
refundOperationId: refundAttempt?.refundOperationId,
|
|
1175
1184
|
orderState: order.state,
|
|
1176
1185
|
refundState: refundRequest.state,
|
|
1177
1186
|
refundAttemptState: refundAttempt?.state,
|
|
@@ -1216,6 +1225,49 @@ export class ServiceOrder extends BeanBase {
|
|
|
1216
1225
|
});
|
|
1217
1226
|
}
|
|
1218
1227
|
|
|
1228
|
+
private async _scheduleLateCaptureCompensation(
|
|
1229
|
+
order: EntityOrder,
|
|
1230
|
+
attempt: EntityPaymentAttempt,
|
|
1231
|
+
event: IPaymentOutcomeEvent,
|
|
1232
|
+
) {
|
|
1233
|
+
if (!event.providerCaptureId)
|
|
1234
|
+
this.app.throw(409, 'late payment outcome has no provider capture');
|
|
1235
|
+
await this.$scope.commercePayment.model.paymentAttempt.updateById(attempt.id, {
|
|
1236
|
+
providerCaptureId: event.providerCaptureId,
|
|
1237
|
+
});
|
|
1238
|
+
const correlationId = `${order.correlationId}:late-capture:${event.eventId}`;
|
|
1239
|
+
const existingAudit = await this.$scope.commercePayment.model.paymentAudit.get({
|
|
1240
|
+
paymentAttemptId: attempt.id,
|
|
1241
|
+
providerEventId: event.eventId,
|
|
1242
|
+
});
|
|
1243
|
+
if (existingAudit) return this._paymentOutcomeResult(order, attempt);
|
|
1244
|
+
await this.$scope.commercePayment.model.paymentAudit.insert({
|
|
1245
|
+
paymentAttemptId: attempt.id,
|
|
1246
|
+
orderId: order.id,
|
|
1247
|
+
userId: order.userId,
|
|
1248
|
+
provider: event.providerName,
|
|
1249
|
+
providerEventId: event.eventId,
|
|
1250
|
+
outcome: 'succeeded',
|
|
1251
|
+
fromAttemptState: attempt.state,
|
|
1252
|
+
toOrderState: 'expired',
|
|
1253
|
+
idempotencyKey: event.eventId.slice(0, 100),
|
|
1254
|
+
correlationId,
|
|
1255
|
+
reason: 'late provider capture requires automatic compensation',
|
|
1256
|
+
processedAt: new Date(),
|
|
1257
|
+
});
|
|
1258
|
+
const session = await this.$scope.pay.model.paymentSession.getById(event.paymentSessionId);
|
|
1259
|
+
if (!session) this.app.throw(404, 'payment session not found');
|
|
1260
|
+
await this.$scope.pay.service.refundOperation.create({
|
|
1261
|
+
paymentSessionId: session.id,
|
|
1262
|
+
businessReference: `late-capture:${attempt.id}`,
|
|
1263
|
+
amountMinor: event.amountMinor,
|
|
1264
|
+
currency: event.currency,
|
|
1265
|
+
idempotencyKey: `${correlationId}:refund`,
|
|
1266
|
+
correlationId,
|
|
1267
|
+
});
|
|
1268
|
+
return this._paymentOutcomeResult(order, attempt);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1219
1271
|
private async _expireLockedOrder(order: EntityOrder) {
|
|
1220
1272
|
const correlationId = `${order.correlationId}:expiry`;
|
|
1221
1273
|
const reason = 'unpaid order expired';
|
|
@@ -550,6 +550,7 @@ describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, ()
|
|
|
550
550
|
|
|
551
551
|
it('denies anonymous customer payment and order actions', async () => {
|
|
552
552
|
await app.bean.executor.mockCtx(async () => {
|
|
553
|
+
await app.bean.passport.signout();
|
|
553
554
|
const requests = [
|
|
554
555
|
() =>
|
|
555
556
|
app.bean.executor.performAction('get', '/commerce/trade/order/mine', {
|
|
@@ -564,7 +565,7 @@ describe('paymentOutcome.test.ts', { concurrency: false, sequential: true }, ()
|
|
|
564
565
|
innerAccess: false,
|
|
565
566
|
}),
|
|
566
567
|
() =>
|
|
567
|
-
app.bean.executor.performAction('get', '/payment-session/1', {
|
|
568
|
+
app.bean.executor.performAction('get', '/pay/payment-session/1', {
|
|
568
569
|
innerAccess: false,
|
|
569
570
|
}),
|
|
570
571
|
];
|
|
@@ -16,6 +16,7 @@ interface IFixture {
|
|
|
16
16
|
customerName?: string;
|
|
17
17
|
orderId?: number;
|
|
18
18
|
paymentAttemptId?: number;
|
|
19
|
+
paymentSessionId?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async function cleanup(fixture: IFixture) {
|
|
@@ -24,6 +25,21 @@ async function cleanup(fixture: IFixture) {
|
|
|
24
25
|
const promotion = app.scope('commerce-promotion');
|
|
25
26
|
const catalog = app.scope('commerce-catalog');
|
|
26
27
|
const member = app.scope('commerce-member');
|
|
28
|
+
const pay = app.scope('a-pay');
|
|
29
|
+
if (fixture.paymentSessionId !== undefined) {
|
|
30
|
+
const refundOperations = await pay.model.refundOperation.select({
|
|
31
|
+
where: { paymentSessionId: fixture.paymentSessionId },
|
|
32
|
+
});
|
|
33
|
+
for (const refundOperation of refundOperations) {
|
|
34
|
+
await pay.model.providerOperation.delete({ refundOperationId: refundOperation.id });
|
|
35
|
+
await pay.model.outboxEvent.delete({ refundOperationId: refundOperation.id });
|
|
36
|
+
await pay.model.webhookInbox.delete({ refundOperationId: refundOperation.id });
|
|
37
|
+
await pay.model.refundOperation.delete({ id: refundOperation.id });
|
|
38
|
+
}
|
|
39
|
+
await pay.model.outboxEvent.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
40
|
+
await pay.model.webhookInbox.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
41
|
+
await pay.model.providerOperation.delete({ paymentSessionId: fixture.paymentSessionId });
|
|
42
|
+
}
|
|
27
43
|
if (fixture.orderId !== undefined) {
|
|
28
44
|
const requests = await payment.model.refundRequest.select({
|
|
29
45
|
where: { orderId: fixture.orderId },
|
|
@@ -39,6 +55,8 @@ async function cleanup(fixture: IFixture) {
|
|
|
39
55
|
await payment.model.paymentAudit.delete({ paymentAttemptId: fixture.paymentAttemptId });
|
|
40
56
|
await payment.model.paymentAttempt.delete({ id: fixture.paymentAttemptId });
|
|
41
57
|
}
|
|
58
|
+
if (fixture.paymentSessionId !== undefined)
|
|
59
|
+
await pay.model.paymentSession.delete({ id: fixture.paymentSessionId });
|
|
42
60
|
if (fixture.orderId !== undefined)
|
|
43
61
|
await trade.model.orderAudit.delete({ orderId: fixture.orderId });
|
|
44
62
|
if (fixture.skuId !== undefined) await trade.model.stockAudit.delete({ skuId: fixture.skuId });
|
|
@@ -152,10 +170,17 @@ async function createPaidOrder(suffix: string): Promise<IFixture> {
|
|
|
152
170
|
});
|
|
153
171
|
fixture.orderId = checkout.orderId as number;
|
|
154
172
|
fixture.paymentAttemptId = checkout.paymentAttemptId as number;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
fixture.paymentSessionId = checkout.paymentSessionId as number;
|
|
174
|
+
const pay = app.scope('a-pay');
|
|
175
|
+
await pay.service.paymentSession.start(fixture.paymentSessionId);
|
|
176
|
+
await app
|
|
177
|
+
.scope('pay-mock')
|
|
178
|
+
.service.payMock.completePaymentSession(fixture.paymentSessionId, 'succeeded');
|
|
179
|
+
const outbox = await pay.model.outboxEvent.get({
|
|
180
|
+
paymentSessionId: fixture.paymentSessionId,
|
|
181
|
+
eventType: 'payment.outcome.v1',
|
|
158
182
|
});
|
|
183
|
+
await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox!.id });
|
|
159
184
|
return fixture;
|
|
160
185
|
}
|
|
161
186
|
|
|
@@ -310,6 +335,89 @@ describe('refundLifecycle.test.ts', { concurrency: false, sequential: true }, ()
|
|
|
310
335
|
});
|
|
311
336
|
});
|
|
312
337
|
|
|
338
|
+
it('settles an executed mock-provider refund through the durable outbox', async () => {
|
|
339
|
+
await app.bean.executor.mockCtx(async () => {
|
|
340
|
+
const fixture: IFixture = {};
|
|
341
|
+
try {
|
|
342
|
+
Object.assign(fixture, await createPaidOrder(randomUUID().slice(0, 12)));
|
|
343
|
+
const order = app.scope('commerce-trade').service.order;
|
|
344
|
+
await order.requestRefund(fixture.orderId!, {
|
|
345
|
+
reason: 'provider-backed refund',
|
|
346
|
+
idempotencyKey: 'provider-backed-request-1',
|
|
347
|
+
});
|
|
348
|
+
await app.bean.passport.signout();
|
|
349
|
+
await app.bean.passport.signinMock();
|
|
350
|
+
await order.approveRefund(fixture.orderId!, {
|
|
351
|
+
reason: 'provider-backed approval',
|
|
352
|
+
idempotencyKey: 'provider-backed-approve-1',
|
|
353
|
+
});
|
|
354
|
+
const executed = await order.executeRefund(fixture.orderId!);
|
|
355
|
+
assert.ok(executed.refundAttemptId);
|
|
356
|
+
assert.ok(executed.refundOperationId);
|
|
357
|
+
assert.deepEqual(
|
|
358
|
+
[executed.orderState, executed.refundState, executed.refundAttemptState],
|
|
359
|
+
['refund_approved', 'approved', 'created'],
|
|
360
|
+
);
|
|
361
|
+
const pay = app.scope('a-pay');
|
|
362
|
+
const refundOperation = await pay.model.refundOperation.getById(executed.refundOperationId);
|
|
363
|
+
const refundAttempt = await app
|
|
364
|
+
.scope('commerce-payment')
|
|
365
|
+
.model.refundAttempt.getById(executed.refundAttemptId);
|
|
366
|
+
assert.equal(String(refundAttempt?.refundOperationId), String(executed.refundOperationId));
|
|
367
|
+
assert.equal(refundOperation?.state, 'pending');
|
|
368
|
+
const receipt = await app
|
|
369
|
+
.scope('pay-mock')
|
|
370
|
+
.service.payMock.completeRefundOperation(executed.refundOperationId, 'succeeded');
|
|
371
|
+
assert.deepEqual(receipt, {
|
|
372
|
+
refundOperationId: executed.refundOperationId,
|
|
373
|
+
accepted: true,
|
|
374
|
+
});
|
|
375
|
+
const outbox = await pay.model.outboxEvent.get({
|
|
376
|
+
refundOperationId: executed.refundOperationId,
|
|
377
|
+
eventType: 'refund.outcome.v1',
|
|
378
|
+
});
|
|
379
|
+
assert.ok(outbox);
|
|
380
|
+
await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox.id });
|
|
381
|
+
await pay.queue.outboxDispatch.pushAsync({ outboxEventId: outbox.id });
|
|
382
|
+
const [
|
|
383
|
+
dispatched,
|
|
384
|
+
settledOrder,
|
|
385
|
+
settledRequest,
|
|
386
|
+
settledAttempt,
|
|
387
|
+
line,
|
|
388
|
+
balance,
|
|
389
|
+
grant,
|
|
390
|
+
audits,
|
|
391
|
+
] = await Promise.all([
|
|
392
|
+
pay.model.outboxEvent.getById(outbox.id),
|
|
393
|
+
app.scope('commerce-trade').model.order.getById(fixture.orderId!),
|
|
394
|
+
app.scope('commerce-payment').model.refundRequest.getById(executed.refundRequestId),
|
|
395
|
+
app.scope('commerce-payment').model.refundAttempt.getById(executed.refundAttemptId),
|
|
396
|
+
app.scope('commerce-trade').model.orderLine.get({ orderId: fixture.orderId }),
|
|
397
|
+
app.scope('commerce-trade').model.stockBalance.getById(fixture.balanceId!),
|
|
398
|
+
app.scope('commerce-promotion').model.couponGrant.getById(fixture.grantId!),
|
|
399
|
+
app
|
|
400
|
+
.scope('commerce-payment')
|
|
401
|
+
.model.refundAudit.select({ where: { orderId: fixture.orderId } }),
|
|
402
|
+
]);
|
|
403
|
+
const reservation = await app
|
|
404
|
+
.scope('commerce-trade')
|
|
405
|
+
.model.stockReservation.get({ orderLineId: line?.id });
|
|
406
|
+
assert.deepEqual([dispatched?.state, dispatched?.attemptCount], ['dispatched', 1]);
|
|
407
|
+
assert.deepEqual(
|
|
408
|
+
[settledOrder?.state, settledRequest?.state, settledAttempt?.state, reservation?.state],
|
|
409
|
+
['refunded', 'refunded', 'succeeded', 'restored'],
|
|
410
|
+
);
|
|
411
|
+
assert.deepEqual([balance?.onHand, balance?.reserved, balance?.available], [1, 0, 1]);
|
|
412
|
+
assert.equal(grant?.state, 'redeemed');
|
|
413
|
+
assert.equal(audits.filter(audit => audit.toRefundState === 'refunded').length, 1);
|
|
414
|
+
} finally {
|
|
415
|
+
await app.bean.passport.signout();
|
|
416
|
+
await cleanup(fixture);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
313
421
|
it('preserves rejected and failed cycles while permitting a fresh request', async () => {
|
|
314
422
|
await app.bean.executor.mockCtx(async () => {
|
|
315
423
|
const fixture: IFixture = {};
|