Skip to content

Commit dd79c1b

Browse files
Merge pull request #305 from DevLoversTeam/lso/feat/shop
2 parents b1543d6 + b9a58a6 commit dd79c1b

8 files changed

Lines changed: 1592 additions & 730 deletions

frontend/lib/services/orders/monobank-webhook.ts

Lines changed: 765 additions & 533 deletions
Large diffs are not rendered by default.

frontend/lib/services/orders/monobank.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'server-only';
22

3-
import { and, eq, sql } from 'drizzle-orm';
3+
import { and, eq, inArray, sql } from 'drizzle-orm';
44

55
import { db } from '@/db';
66
import { orderItems, orders, paymentAttempts } from '@/db/schema';
@@ -49,7 +49,7 @@ async function getActiveAttempt(
4949
and(
5050
eq(paymentAttempts.orderId, orderId),
5151
eq(paymentAttempts.provider, 'monobank'),
52-
sql`${paymentAttempts.status} in ('creating','active')`
52+
inArray(paymentAttempts.status, ['creating', 'active'])
5353
)
5454
)
5555
.limit(1);
@@ -230,7 +230,7 @@ async function cancelOrderAndRelease(orderId: string, reason: string) {
230230
and(
231231
eq(orders.id, orderId),
232232
eq(orders.paymentProvider, 'monobank'),
233-
sql`${orders.paymentStatus} in ('pending','requires_payment')`
233+
inArray(orders.paymentStatus, ['pending', 'requires_payment'])
234234
)
235235
)
236236
.returning({ id: orders.id });
@@ -458,6 +458,7 @@ async function createMonoAttemptAndInvoiceImpl(
458458
}
459459
): Promise<{
460460
attemptId: string;
461+
attemptNumber: number;
461462
invoiceId: string;
462463
pageUrl: string;
463464
currency: 'UAH';
@@ -473,6 +474,7 @@ async function createMonoAttemptAndInvoiceImpl(
473474
invoiceId: existing.providerPaymentIntentId,
474475
pageUrl,
475476
attemptId: existing.id,
477+
attemptNumber: existing.attemptNumber,
476478
currency: MONO_CURRENCY,
477479
totalAmountMinor: snapshot.amountMinor,
478480
};
@@ -535,6 +537,7 @@ async function createMonoAttemptAndInvoiceImpl(
535537
invoiceId: reused.providerPaymentIntentId,
536538
pageUrl,
537539
attemptId: reused.id,
540+
attemptNumber: reused.attemptNumber,
538541
currency: MONO_CURRENCY,
539542
totalAmountMinor: snapshot.amountMinor,
540543
};
@@ -579,10 +582,18 @@ async function createMonoAttemptAndInvoiceImpl(
579582
});
580583
}
581584

582-
await deps.cancelOrderAndRelease(
583-
args.orderId,
584-
'Monobank snapshot validation failed.'
585-
);
585+
try {
586+
await deps.cancelOrderAndRelease(
587+
args.orderId,
588+
'Monobank snapshot validation failed.'
589+
);
590+
} catch (cancelErr) {
591+
logError('monobank_cancel_order_failed', cancelErr, {
592+
orderId: args.orderId,
593+
attemptId: attempt.id,
594+
requestId: args.requestId,
595+
});
596+
}
586597

587598
throw error;
588599
}
@@ -673,6 +684,7 @@ async function createMonoAttemptAndInvoiceImpl(
673684
invoiceId: invoice.invoiceId,
674685
pageUrl: invoice.pageUrl,
675686
attemptId: attempt.id,
687+
attemptNumber: attempt.attemptNumber,
676688
currency: MONO_CURRENCY,
677689
totalAmountMinor: snapshot.amountMinor,
678690
};
@@ -686,6 +698,7 @@ export async function createMonoAttemptAndInvoice(args: {
686698
maxAttempts?: number;
687699
}): Promise<{
688700
attemptId: string;
701+
attemptNumber: number;
689702
invoiceId: string;
690703
pageUrl: string;
691704
currency: 'UAH';
@@ -738,14 +751,5 @@ export async function createMonobankAttemptAndInvoice(args: {
738751
maxAttempts: args.maxAttempts,
739752
});
740753

741-
const [row] = await db
742-
.select({ attemptNumber: paymentAttempts.attemptNumber })
743-
.from(paymentAttempts)
744-
.where(eq(paymentAttempts.id, result.attemptId))
745-
.limit(1);
746-
747-
return {
748-
...result,
749-
attemptNumber: row?.attemptNumber ?? 1,
750-
};
754+
return result;
751755
}

frontend/lib/tests/shop/monobank-psp-unavailable.test.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { orders, paymentAttempts, productPrices, products } from '@/db/schema';
88
import { resetEnvCache } from '@/lib/env';
99
import { toDbMoney } from '@/lib/shop/money';
1010
import { deriveTestIpFromIdemKey } from '@/lib/tests/helpers/ip';
11+
import { isUuidV1toV5 } from '@/lib/utils/uuid';
1112

1213
vi.mock('@/lib/auth', () => ({
1314
getCurrentUser: vi.fn().mockResolvedValue(null),
@@ -120,20 +121,41 @@ async function createIsolatedProduct(stock: number) {
120121
}
121122

122123
async function cleanupOrder(orderId: string) {
123-
await db.execute(sql`delete from inventory_moves where order_id = ${orderId}::uuid`);
124-
await db.execute(sql`delete from order_items where order_id = ${orderId}::uuid`);
125-
await db.delete(paymentAttempts).where(eq(paymentAttempts.orderId, orderId));
126-
await db.delete(orders).where(eq(orders.id, orderId));
124+
if (!isUuidV1toV5(orderId))
125+
throw new Error(`cleanupOrder: invalid uuid: ${orderId}`);
126+
127+
await db.execute(
128+
sql`delete from inventory_moves where order_id = ${orderId}::uuid`
129+
);
130+
await db.execute(
131+
sql`delete from order_items where order_id = ${orderId}::uuid`
132+
);
133+
await db.execute(
134+
sql`delete from payment_attempts where order_id = ${orderId}::uuid`
135+
);
136+
await db.execute(sql`delete from orders where id = ${orderId}::uuid`);
127137
}
128138

129-
async function cleanupProduct(productId: string) {
130-
await db.execute(sql`delete from inventory_moves where product_id = ${productId}::uuid`);
131-
await db.execute(sql`delete from order_items where product_id = ${productId}::uuid`);
132-
await db.delete(productPrices).where(eq(productPrices.productId, productId));
133-
await db.delete(products).where(eq(products.id, productId));
139+
async function archiveProduct(productId: string) {
140+
if (!isUuidV1toV5(productId))
141+
throw new Error(`archiveProduct: invalid uuid: ${productId}`);
142+
const TEST_ARCHIVE_PREFIX = '[TEST-ARCHIVED] ';
143+
await db
144+
.update(products)
145+
.set({
146+
isActive: false,
147+
stock: 0,
148+
title: sql<string>`
149+
case
150+
when ${products.title} like ${TEST_ARCHIVE_PREFIX + '%'} then ${products.title}
151+
else ${TEST_ARCHIVE_PREFIX} || ${products.title}
152+
end
153+
`,
154+
updatedAt: new Date(),
155+
} as any)
156+
.where(eq(products.id, productId));
134157
}
135158

136-
137159
async function postCheckout(idemKey: string, productId: string) {
138160
const mod = (await import('@/app/api/shop/checkout/route')) as unknown as {
139161
POST: (req: NextRequest) => Promise<Response>;
@@ -237,12 +259,14 @@ describe.sequential('monobank PSP_UNAVAILABLE invariant', () => {
237259
if (orderId) {
238260
await cleanupOrder(orderId);
239261
}
240-
} catch (e) { warnCleanup('cleanupOrder', e); }
262+
} catch (e) {
263+
warnCleanup('cleanupOrder', e);
264+
}
241265

242266
try {
243-
await cleanupProduct(productId);
267+
await archiveProduct(productId);
244268
} catch (e) {
245-
warnCleanup('cleanupProduct', e);
269+
warnCleanup('archiveProduct', e);
246270
}
247271
}
248272
}, 20_000);

0 commit comments

Comments
 (0)