cabloy 5.1.202 → 5.1.203
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 +11 -0
- package/CONTRIBUTING.md +18 -17
- package/README.md +2 -2
- package/package.json +2 -3
- package/repo-agent-governance/managed-assets.json +12 -12
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
- package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
- package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
- package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
- package/repo-docs/demo.md +14 -0
- package/repo-docs/frontend/scripts.md +10 -18
- package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
- package/repo-docs/index.md +2 -2
- package/repo-docs/reference/repo-scripts.md +15 -31
- package/repo-e2e/config/playwright.config.ts +14 -19
- package/repo-e2e/scripts/e2e.test.ts +17 -0
- package/repo-e2e/scripts/e2e.ts +5 -1
- package/repo-e2e/scripts/runE2e.ts +3 -15
- package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
- package/repo-e2e/scripts/runE2eArgs.ts +16 -29
- package/repo-e2e/scripts/startE2eVona.ts +20 -9
- package/repo-e2e/specs/a-commerce.spec.ts +232 -31
- package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
- package/repo-e2e/specs/home-user-account.spec.ts +9 -2
- package/scripts/upgrade.test.ts +147 -0
- package/scripts/upgrade.ts +49 -27
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
- package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
- package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
|
@@ -1063,11 +1063,13 @@ declare module 'vona-module-commerce-trade' {
|
|
|
1063
1063
|
/** controller: begin */
|
|
1064
1064
|
export * from '../controller/cart.ts';
|
|
1065
1065
|
export * from '../controller/checkout.ts';
|
|
1066
|
+
export * from '../controller/e2eFixture.ts';
|
|
1066
1067
|
export * from '../controller/order.ts';
|
|
1067
1068
|
export * from '../controller/stockAudit.ts';
|
|
1068
1069
|
export * from '../controller/stockBalance.ts';
|
|
1069
1070
|
import type { IControllerOptionsCart } from '../controller/cart.ts';
|
|
1070
1071
|
import type { IControllerOptionsCheckout } from '../controller/checkout.ts';
|
|
1072
|
+
import type { IControllerOptionsE2eFixture } from '../controller/e2eFixture.ts';
|
|
1071
1073
|
import type { IControllerOptionsOrder } from '../controller/order.ts';
|
|
1072
1074
|
import type { IControllerOptionsStockAudit } from '../controller/stockAudit.ts';
|
|
1073
1075
|
import type { IControllerOptionsStockBalance } from '../controller/stockBalance.ts';
|
|
@@ -1077,6 +1079,7 @@ declare module 'vona-module-a-web' {
|
|
|
1077
1079
|
export interface IControllerRecord {
|
|
1078
1080
|
'commerce-trade:cart': IControllerOptionsCart;
|
|
1079
1081
|
'commerce-trade:checkout': IControllerOptionsCheckout;
|
|
1082
|
+
'commerce-trade:e2eFixture': IControllerOptionsE2eFixture;
|
|
1080
1083
|
'commerce-trade:order': IControllerOptionsOrder;
|
|
1081
1084
|
'commerce-trade:stockAudit': IControllerOptionsStockAudit;
|
|
1082
1085
|
'commerce-trade:stockBalance': IControllerOptionsStockBalance;
|
|
@@ -1108,6 +1111,17 @@ declare module 'vona-module-commerce-trade' {
|
|
|
1108
1111
|
get $onionOptions(): IControllerOptionsCheckout;
|
|
1109
1112
|
}
|
|
1110
1113
|
|
|
1114
|
+
export interface ControllerE2eFixture {
|
|
1115
|
+
/** @internal */
|
|
1116
|
+
get scope(): ScopeModuleCommerceTrade;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
export interface ControllerE2eFixture {
|
|
1120
|
+
get $beanFullName(): 'commerce-trade.controller.e2eFixture';
|
|
1121
|
+
get $onionName(): 'commerce-trade:e2eFixture';
|
|
1122
|
+
get $onionOptions(): IControllerOptionsE2eFixture;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1111
1125
|
export interface ControllerOrder {
|
|
1112
1126
|
/** @internal */
|
|
1113
1127
|
get scope(): ScopeModuleCommerceTrade;
|
|
@@ -1148,6 +1162,8 @@ import type { ControllerCart } from '../controller/cart.ts';
|
|
|
1148
1162
|
// @ts-ignore ignore
|
|
1149
1163
|
import type { ControllerCheckout } from '../controller/checkout.ts';
|
|
1150
1164
|
// @ts-ignore ignore
|
|
1165
|
+
import type { ControllerE2eFixture } from '../controller/e2eFixture.ts';
|
|
1166
|
+
// @ts-ignore ignore
|
|
1151
1167
|
import type { ControllerOrder } from '../controller/order.ts';
|
|
1152
1168
|
// @ts-ignore ignore
|
|
1153
1169
|
import type { ControllerStockAudit } from '../controller/stockAudit.ts';
|
|
@@ -1163,6 +1179,10 @@ declare module 'vona-module-commerce-trade' {
|
|
|
1163
1179
|
actions?: TypeControllerOptionsActions<ControllerCheckout>;
|
|
1164
1180
|
}
|
|
1165
1181
|
|
|
1182
|
+
export interface IControllerOptionsE2eFixture {
|
|
1183
|
+
actions?: TypeControllerOptionsActions<ControllerE2eFixture>;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1166
1186
|
export interface IControllerOptionsOrder {
|
|
1167
1187
|
actions?: TypeControllerOptionsActions<ControllerOrder>;
|
|
1168
1188
|
}
|
|
@@ -1192,6 +1212,9 @@ declare module 'vona-module-a-web' {
|
|
|
1192
1212
|
export interface IApiPathPostRecord{
|
|
1193
1213
|
'/commerce/trade/cart/items': undefined;
|
|
1194
1214
|
'/commerce/trade/checkout': undefined;
|
|
1215
|
+
'/commerce/trade/e2eFixture/catalogue': undefined;
|
|
1216
|
+
'/commerce/trade/e2eFixture/order/:orderId/payment/:attemptId/dispatch': undefined;
|
|
1217
|
+
'/commerce/trade/e2eFixture/order/:orderId/refund/:attemptId/dispatch': undefined;
|
|
1195
1218
|
'/commerce/trade/order/:id/requestRefund': undefined;
|
|
1196
1219
|
'/commerce/trade/order/:id/approveRefund': undefined;
|
|
1197
1220
|
'/commerce/trade/order/:id/rejectRefund': undefined;
|
|
@@ -1207,6 +1230,9 @@ export interface IApiPathPatchRecord{
|
|
|
1207
1230
|
export interface IApiPathDeleteRecord{
|
|
1208
1231
|
'/commerce/trade/cart/items/:id': undefined;
|
|
1209
1232
|
'/commerce/trade/cart/items': undefined;
|
|
1233
|
+
'/commerce/trade/e2eFixture/order/:orderId': undefined;
|
|
1234
|
+
'/commerce/trade/e2eFixture/customer': undefined;
|
|
1235
|
+
'/commerce/trade/e2eFixture/catalogue/:categoryId': undefined;
|
|
1210
1236
|
}
|
|
1211
1237
|
|
|
1212
1238
|
}
|
|
@@ -1247,6 +1273,7 @@ declare module 'vona' {
|
|
|
1247
1273
|
/** bean: end */
|
|
1248
1274
|
/** service: begin */
|
|
1249
1275
|
export * from '../service/cart.ts';
|
|
1276
|
+
export * from '../service/e2eFixture.ts';
|
|
1250
1277
|
export * from '../service/order.ts';
|
|
1251
1278
|
export * from '../service/stockAudit.ts';
|
|
1252
1279
|
export * from '../service/stockBalance.ts';
|
|
@@ -1256,6 +1283,7 @@ declare module 'vona-module-a-bean' {
|
|
|
1256
1283
|
|
|
1257
1284
|
export interface IServiceRecord {
|
|
1258
1285
|
'commerce-trade:cart': never;
|
|
1286
|
+
'commerce-trade:e2eFixture': never;
|
|
1259
1287
|
'commerce-trade:order': never;
|
|
1260
1288
|
'commerce-trade:stockAudit': never;
|
|
1261
1289
|
'commerce-trade:stockBalance': never;
|
|
@@ -1275,6 +1303,16 @@ declare module 'vona-module-commerce-trade' {
|
|
|
1275
1303
|
get $onionName(): 'commerce-trade:cart';
|
|
1276
1304
|
}
|
|
1277
1305
|
|
|
1306
|
+
export interface ServiceE2eFixture {
|
|
1307
|
+
/** @internal */
|
|
1308
|
+
get scope(): ScopeModuleCommerceTrade;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
export interface ServiceE2eFixture {
|
|
1312
|
+
get $beanFullName(): 'commerce-trade.service.e2eFixture';
|
|
1313
|
+
get $onionName(): 'commerce-trade:e2eFixture';
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1278
1316
|
export interface ServiceOrder {
|
|
1279
1317
|
/** @internal */
|
|
1280
1318
|
get scope(): ScopeModuleCommerceTrade;
|
|
@@ -1308,11 +1346,13 @@ declare module 'vona-module-commerce-trade' {
|
|
|
1308
1346
|
/** service: end */
|
|
1309
1347
|
/** service: begin */
|
|
1310
1348
|
import type { ServiceCart } from '../service/cart.ts';
|
|
1349
|
+
import type { ServiceE2eFixture } from '../service/e2eFixture.ts';
|
|
1311
1350
|
import type { ServiceOrder } from '../service/order.ts';
|
|
1312
1351
|
import type { ServiceStockAudit } from '../service/stockAudit.ts';
|
|
1313
1352
|
import type { ServiceStockBalance } from '../service/stockBalance.ts';
|
|
1314
1353
|
export interface IModuleService {
|
|
1315
1354
|
'cart': ServiceCart;
|
|
1355
|
+
'e2eFixture': ServiceE2eFixture;
|
|
1316
1356
|
'order': ServiceOrder;
|
|
1317
1357
|
'stockAudit': ServiceStockAudit;
|
|
1318
1358
|
'stockBalance': ServiceStockBalance;
|
|
@@ -1324,6 +1364,7 @@ import 'vona';
|
|
|
1324
1364
|
declare module 'vona' {
|
|
1325
1365
|
export interface IBeanRecordGeneral {
|
|
1326
1366
|
'commerce-trade.service.cart': ServiceCart;
|
|
1367
|
+
'commerce-trade.service.e2eFixture': ServiceE2eFixture;
|
|
1327
1368
|
'commerce-trade.service.order': ServiceOrder;
|
|
1328
1369
|
'commerce-trade.service.stockAudit': ServiceStockAudit;
|
|
1329
1370
|
'commerce-trade.service.stockBalance': ServiceStockBalance;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
import type { IDecoratorControllerOptions } from 'vona-module-a-web';
|
|
3
|
+
|
|
4
|
+
import { BeanBase } from 'vona';
|
|
5
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
6
|
+
import { Arg, Controller, Web } from 'vona-module-a-web';
|
|
7
|
+
|
|
8
|
+
export interface IControllerOptionsE2eFixture extends IDecoratorControllerOptions {}
|
|
9
|
+
|
|
10
|
+
// Not part of the application contract: this exists solely for owned, local E2E data.
|
|
11
|
+
@Controller<IControllerOptionsE2eFixture>({
|
|
12
|
+
path: 'e2eFixture',
|
|
13
|
+
meta: { mode: ['dev', 'test'] },
|
|
14
|
+
})
|
|
15
|
+
@Api.exclude()
|
|
16
|
+
export class ControllerE2eFixture extends BeanBase {
|
|
17
|
+
@Web.post('catalogue')
|
|
18
|
+
async createCatalogue() {
|
|
19
|
+
return await this.scope.service.e2eFixture.createCatalogue();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Web.post('order/:orderId/payment/:attemptId/dispatch')
|
|
23
|
+
async dispatchPayment(
|
|
24
|
+
@Arg.param('orderId', v.tableIdentity()) orderId: TableIdentity,
|
|
25
|
+
@Arg.param('attemptId', v.tableIdentity()) attemptId: TableIdentity,
|
|
26
|
+
) {
|
|
27
|
+
return await this.scope.service.e2eFixture.dispatchPayment(orderId, attemptId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Web.post('order/:orderId/refund/:attemptId/dispatch')
|
|
31
|
+
async dispatchRefund(
|
|
32
|
+
@Arg.param('orderId', v.tableIdentity()) orderId: TableIdentity,
|
|
33
|
+
@Arg.param('attemptId', v.tableIdentity()) attemptId: TableIdentity,
|
|
34
|
+
) {
|
|
35
|
+
return await this.scope.service.e2eFixture.dispatchRefund(orderId, attemptId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Web.delete('order/:orderId')
|
|
39
|
+
async removeOrder(@Arg.param('orderId', v.tableIdentity()) orderId: TableIdentity) {
|
|
40
|
+
await this.scope.service.e2eFixture.removeOrder(orderId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Web.delete('customer')
|
|
44
|
+
async removeCustomer() {
|
|
45
|
+
await this.scope.service.e2eFixture.removeCustomer();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Web.delete('catalogue/:categoryId')
|
|
49
|
+
async removeCatalogue(@Arg.param('categoryId', v.tableIdentity()) categoryId: TableIdentity) {
|
|
50
|
+
await this.scope.service.e2eFixture.removeCatalogue(categoryId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { IPaymentOutcomeEvent, IRefundOutcomeEvent } from 'vona-module-a-pay';
|
|
2
|
+
import type { EntityOutboxEvent, EntityWebhookInbox } from 'vona-module-a-pay';
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import type { TypeOrderState } from '../entity/order.tsx';
|
|
7
|
+
import type { EntityOrderAudit, TypeOrderAuditOperation } from '../entity/orderAudit.tsx';
|
|
8
|
+
import type { EntityStockBalance } from '../entity/stockBalance.tsx';
|
|
9
|
+
import type { EntityStockReservation } from '../entity/stockReservation.tsx';
|
|
10
|
+
|
|
11
|
+
export const fixturePrefix = 'e2e-fixture-';
|
|
12
|
+
|
|
13
|
+
export function createOwnedFixtureName(userId: string | number) {
|
|
14
|
+
return `${fixturePrefix}${userId}-${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
15
|
+
}
|
|
16
|
+
const mockProvider = 'pay-mock:mock';
|
|
17
|
+
|
|
18
|
+
export function isOwnedFixtureName(name: string, userId: string | number) {
|
|
19
|
+
return new RegExp(`^${fixturePrefix}${userId}-[0-9a-f]{12}$`).test(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isFixtureCustomer<T extends { name: string; anonymous?: boolean }>(
|
|
23
|
+
user: T | undefined,
|
|
24
|
+
): user is T {
|
|
25
|
+
return !!user && !user.anonymous && user.name.startsWith(fixturePrefix);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fixtureReservationMatchesLine(
|
|
29
|
+
reservation: Pick<
|
|
30
|
+
EntityStockReservation,
|
|
31
|
+
'skuId' | 'orderLineId' | 'stockBalanceId' | 'quantity'
|
|
32
|
+
>,
|
|
33
|
+
line: { id: string | number; skuId: string | number; quantity: number },
|
|
34
|
+
balance: Pick<EntityStockBalance, 'id' | 'skuId'>,
|
|
35
|
+
) {
|
|
36
|
+
return (
|
|
37
|
+
String(reservation.orderLineId) === String(line.id) &&
|
|
38
|
+
String(reservation.skuId) === String(line.skuId) &&
|
|
39
|
+
String(reservation.stockBalanceId) === String(balance.id) &&
|
|
40
|
+
String(balance.skuId) === String(line.skuId) &&
|
|
41
|
+
reservation.quantity === line.quantity
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fixtureAdvancedPaymentAuditMatches(
|
|
46
|
+
audits: Pick<
|
|
47
|
+
EntityOrderAudit,
|
|
48
|
+
'id' | 'orderId' | 'operation' | 'fromState' | 'toState' | 'correlationId'
|
|
49
|
+
>[],
|
|
50
|
+
orderId: string | number,
|
|
51
|
+
state: Extract<TypeOrderState, 'shipped' | 'refunded'>,
|
|
52
|
+
paymentCorrelationId: string,
|
|
53
|
+
shipmentCorrelationId?: string,
|
|
54
|
+
refundCorrelationId?: string,
|
|
55
|
+
) {
|
|
56
|
+
const expected: Array<{
|
|
57
|
+
operation: TypeOrderAuditOperation;
|
|
58
|
+
fromState: TypeOrderState;
|
|
59
|
+
toState: TypeOrderState;
|
|
60
|
+
}> = [
|
|
61
|
+
{ operation: 'paid', fromState: 'awaiting_payment', toState: 'paid' },
|
|
62
|
+
...(state === 'shipped'
|
|
63
|
+
? [{ operation: 'shipped' as const, fromState: 'paid' as const, toState: 'shipped' as const }]
|
|
64
|
+
: [
|
|
65
|
+
{
|
|
66
|
+
operation: 'refund_requested' as const,
|
|
67
|
+
fromState: 'paid' as const,
|
|
68
|
+
toState: 'refund_requested' as const,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
operation: 'refund_approved' as const,
|
|
72
|
+
fromState: 'refund_requested' as const,
|
|
73
|
+
toState: 'refund_approved' as const,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
operation: 'refunded' as const,
|
|
77
|
+
fromState: 'refund_approved' as const,
|
|
78
|
+
toState: 'refunded' as const,
|
|
79
|
+
},
|
|
80
|
+
]),
|
|
81
|
+
];
|
|
82
|
+
const steps = audits.filter(audit => audit.operation !== 'created');
|
|
83
|
+
return (
|
|
84
|
+
steps.length === expected.length &&
|
|
85
|
+
steps.every(
|
|
86
|
+
(audit, index) =>
|
|
87
|
+
String(audit.orderId) === String(orderId) &&
|
|
88
|
+
audit.operation === expected[index].operation &&
|
|
89
|
+
audit.fromState === expected[index].fromState &&
|
|
90
|
+
audit.toState === expected[index].toState &&
|
|
91
|
+
(index !== 0 || audit.correlationId === paymentCorrelationId) &&
|
|
92
|
+
(state !== 'shipped' || index !== 1 || audit.correlationId === shipmentCorrelationId) &&
|
|
93
|
+
(state !== 'refunded' || index !== 3 || audit.correlationId === refundCorrelationId),
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function fixtureEventNeedsDispatch(event: EntityOutboxEvent, now = new Date()) {
|
|
99
|
+
if (event.state === 'dispatched') {
|
|
100
|
+
if (
|
|
101
|
+
!event.dispatchedAt ||
|
|
102
|
+
event.claimToken ||
|
|
103
|
+
event.claimedAt ||
|
|
104
|
+
event.claimExpiresAt ||
|
|
105
|
+
event.errorSummary ||
|
|
106
|
+
event.attemptCount < 1
|
|
107
|
+
) {
|
|
108
|
+
throw new Error('fixture event has inconsistent dispatched state');
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
event.state !== 'pending' ||
|
|
114
|
+
event.attemptCount !== 0 ||
|
|
115
|
+
event.claimToken ||
|
|
116
|
+
event.claimedAt ||
|
|
117
|
+
event.claimExpiresAt ||
|
|
118
|
+
event.dispatchedAt ||
|
|
119
|
+
event.errorSummary ||
|
|
120
|
+
(event.nextAttemptAt && event.nextAttemptAt > now)
|
|
121
|
+
) {
|
|
122
|
+
throw new Error('fixture event is not eligible for dispatch');
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function selectFixtureEvent(
|
|
128
|
+
events: EntityOutboxEvent[],
|
|
129
|
+
inboxes: EntityWebhookInbox[],
|
|
130
|
+
expected: {
|
|
131
|
+
kind: 'payment' | 'refund';
|
|
132
|
+
paymentSessionId: string | number;
|
|
133
|
+
businessReference: string;
|
|
134
|
+
amountMinor: number;
|
|
135
|
+
currency: string;
|
|
136
|
+
refundOperationId?: string | number;
|
|
137
|
+
clientName: string;
|
|
138
|
+
environment: 'sandbox' | 'live';
|
|
139
|
+
},
|
|
140
|
+
) {
|
|
141
|
+
const eventType = `${expected.kind}.outcome.v1`;
|
|
142
|
+
const candidates = events.filter(
|
|
143
|
+
event =>
|
|
144
|
+
event.eventType === eventType &&
|
|
145
|
+
String(event.paymentSessionId) === String(expected.paymentSessionId) &&
|
|
146
|
+
(expected.kind === 'payment'
|
|
147
|
+
? !event.refundOperationId
|
|
148
|
+
: String(event.refundOperationId) === String(expected.refundOperationId)),
|
|
149
|
+
);
|
|
150
|
+
if (candidates.length !== 1) throw new Error('fixture outbox event is missing or ambiguous');
|
|
151
|
+
const event = candidates[0];
|
|
152
|
+
const payload = event.payload as IPaymentOutcomeEvent | IRefundOutcomeEvent;
|
|
153
|
+
if (
|
|
154
|
+
typeof payload?.eventId !== 'string' ||
|
|
155
|
+
!payload.eventId.startsWith(`mock-${expected.kind}-`) ||
|
|
156
|
+
String(event.paymentSessionId) !== String(payload.paymentSessionId) ||
|
|
157
|
+
(expected.kind === 'payment'
|
|
158
|
+
? !!event.refundOperationId
|
|
159
|
+
: String(event.refundOperationId) !==
|
|
160
|
+
String((payload as IRefundOutcomeEvent).refundOperationId)) ||
|
|
161
|
+
payload.providerName !== mockProvider ||
|
|
162
|
+
payload.businessReference !== expected.businessReference ||
|
|
163
|
+
String(payload.paymentSessionId) !== String(expected.paymentSessionId) ||
|
|
164
|
+
payload.amountMinor !== expected.amountMinor ||
|
|
165
|
+
payload.currency !== expected.currency ||
|
|
166
|
+
!['succeeded', 'failed', 'cancelled'].includes(payload.state) ||
|
|
167
|
+
(expected.kind === 'refund' &&
|
|
168
|
+
String((payload as IRefundOutcomeEvent).refundOperationId) !==
|
|
169
|
+
String(expected.refundOperationId))
|
|
170
|
+
) {
|
|
171
|
+
throw new Error('fixture outbox payload conflicts with the mock operation');
|
|
172
|
+
}
|
|
173
|
+
const matchingInboxes = inboxes.filter(
|
|
174
|
+
inbox =>
|
|
175
|
+
inbox.providerEventId === payload.eventId &&
|
|
176
|
+
inbox.providerName === mockProvider &&
|
|
177
|
+
inbox.clientName === expected.clientName &&
|
|
178
|
+
inbox.environment === expected.environment,
|
|
179
|
+
);
|
|
180
|
+
if (matchingInboxes.length !== 1)
|
|
181
|
+
throw new Error('fixture webhook inbox is missing or ambiguous');
|
|
182
|
+
const inbox = matchingInboxes[0];
|
|
183
|
+
if (
|
|
184
|
+
inbox.state !== 'processed' ||
|
|
185
|
+
!inbox.processedAt ||
|
|
186
|
+
inbox.errorSummary ||
|
|
187
|
+
inbox.eventType !== `${expected.kind}.${payload.state}` ||
|
|
188
|
+
String(inbox.paymentSessionId) !== String(expected.paymentSessionId) ||
|
|
189
|
+
(expected.kind === 'payment'
|
|
190
|
+
? !!inbox.refundOperationId || inbox.paymentState !== payload.state
|
|
191
|
+
: String(inbox.refundOperationId) !== String(expected.refundOperationId) ||
|
|
192
|
+
inbox.refundState !== payload.state) ||
|
|
193
|
+
inbox.amountMinor !== expected.amountMinor ||
|
|
194
|
+
inbox.currency !== expected.currency ||
|
|
195
|
+
(expected.kind === 'payment' &&
|
|
196
|
+
(inbox.providerCaptureId ?? undefined) !==
|
|
197
|
+
((payload as IPaymentOutcomeEvent).providerCaptureId ?? undefined)) ||
|
|
198
|
+
(expected.kind === 'refund' &&
|
|
199
|
+
inbox.providerRefundId !== (payload as IRefundOutcomeEvent).providerRefundId)
|
|
200
|
+
) {
|
|
201
|
+
throw new Error('fixture webhook inbox is not a processed matching terminal outcome');
|
|
202
|
+
}
|
|
203
|
+
return { event, payload };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function selectPaymentFixtureEvent(
|
|
207
|
+
events: EntityOutboxEvent[],
|
|
208
|
+
inboxes: EntityWebhookInbox[],
|
|
209
|
+
expected: Omit<Parameters<typeof selectFixtureEvent>[2], 'kind' | 'refundOperationId'>,
|
|
210
|
+
) {
|
|
211
|
+
const result = selectFixtureEvent(events, inboxes, { ...expected, kind: 'payment' });
|
|
212
|
+
return { ...result, payload: result.payload as IPaymentOutcomeEvent };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function selectRefundFixtureEvent(
|
|
216
|
+
events: EntityOutboxEvent[],
|
|
217
|
+
inboxes: EntityWebhookInbox[],
|
|
218
|
+
expected: Omit<Parameters<typeof selectFixtureEvent>[2], 'kind' | 'refundOperationId'> & {
|
|
219
|
+
refundOperationId: string | number;
|
|
220
|
+
},
|
|
221
|
+
) {
|
|
222
|
+
const result = selectFixtureEvent(events, inboxes, { ...expected, kind: 'refund' });
|
|
223
|
+
return { ...result, payload: result.payload as IRefundOutcomeEvent };
|
|
224
|
+
}
|