Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d87b93d
(SP: 1) [Shop] Fix checkout redirect 404 by removing duplicate locale…
liudmylasovetovs Jan 22, 2026
6d4398f
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 22, 2026
e5e9b0e
(SP: 1) [Shop] Fix locale cart page and orderid page
liudmylasovetovs Jan 22, 2026
2c76b66
(SP: 1) [Frontend] Changin hero headline on shop main page
liudmylasovetovs Jan 23, 2026
4d20881
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 23, 2026
2ae7774
(SP: 1) [Frontend] Fix styles shop home page, buttons
liudmylasovetovs Jan 24, 2026
368850a
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 24, 2026
3bbfab2
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 24, 2026
f0f5d6a
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 28, 2026
e369d3e
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 29, 2026
5676416
(SP:2) [BD][Backend]add monobank scaffolding (db constraints, env gat…
liudmylasovetovs Jan 29, 2026
c197c00
(SP:1) [shop/monobank]: UAH-only checkout invariants, fail-closed PSP…
liudmylasovetovs Jan 29, 2026
306422e
(SP:2) [Backend] Monobank DB foundations + invariants + B3 verificati…
liudmylasovetovs Jan 29, 2026
5b7ec1b
(SP: 3) [Shop API] Monobank: env contract + flags gating + URL base h…
liudmylasovetovs Jan 30, 2026
daf779c
(SP: 2) [Backend] Monobank PSP adapter: API methods + contract tests
liudmylasovetovs Jan 30, 2026
dd793d0
move: shop tests to folder shop
liudmylasovetovs Jan 30, 2026
c6b0f7d
(SP: 3) [backend | monobank] webhook apply exactly-once (persist-firs…
liudmylasovetovs Jan 30, 2026
6a0a365
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 30, 2026
e43c3f0
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 31, 2026
c0a17fd
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 31, 2026
c946150
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Jan 31, 2026
f2c7ff6
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 1, 2026
5005927
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 1, 2026
120cfcd
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 1, 2026
82586dc
(SP 1) [FIX] conflicts in router and schema
liudmylasovetovs Feb 1, 2026
a6689f0
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 2, 2026
094bda2
(SP 1) [FIX] remove duplicates and conflicts
liudmylasovetovs Feb 2, 2026
4ee0b8e
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 2, 2026
848687c
(SP 1) [FIX] Route Monobank webhook paymentStatus transitions through…
liudmylasovetovs Feb 3, 2026
02f81e1
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 3, 2026
fe59985
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 3, 2026
b190f4a
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 3, 2026
d075229
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 4, 2026
cda1095
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 4, 2026
aa839c4
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 5, 2026
457854d
(SP 1) [Backend] Block checkout when PAYMENTS_ENABLED=false to preven…
liudmylasovetovs Feb 5, 2026
d6ad63b
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 6, 2026
1047b46
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 8, 2026
a0ae043
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 9, 2026
e4415f5
(SP 1) [Backend] strip client money fields for Monobank payloads + tests
liudmylasovetovs Feb 9, 2026
3e5d0a3
(SP 1) [Backend] Wire Monobank checkout branch with deterministic err…
liudmylasovetovs Feb 9, 2026
5963489
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 9, 2026
2ca78b0
(SP: 1) [Backend] Add isolated Monobank webhook route with X-Sign ver…
liudmylasovetovs Feb 10, 2026
2c86302
(SP: 1) [Backend] hardening(monobank-refund): retry requested, servic…
liudmylasovetovs Feb 10, 2026
a49c323
(SP: 1) [Backend] add monobank unpaid cancel-payment + finalize refun…
liudmylasovetovs Feb 10, 2026
add4b83
(SP: 1) [fix] remove comments
liudmylasovetovs Feb 10, 2026
e3de26f
(SP: 1) [fix] install packages
liudmylasovetovs Feb 10, 2026
d7f2abd
(SP: 1) [fix] install package-lock
liudmylasovetovs Feb 10, 2026
65cfc5a
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 10, 2026
269addf
(SP: 1) [fix] fix(p1-tests-docs): stabilize monobank tests, correct e…
liudmylasovetovs Feb 10, 2026
ee1adb2
(SP: 1) [fix] harden Monobank cancel/apply/metadata + Stripe flag fal…
liudmylasovetovs Feb 10, 2026
6e27496
(SP: 1) [fix] nitpik fix monobank and monowebhook
liudmylasovetovs Feb 10, 2026
24c295c
(SP: 1) [fix] chore(monobank-webhook): defer webhook refactor/typing …
liudmylasovetovs Feb 10, 2026
40ea2ea
Merge branch 'develop' of https://github.com/DevLoversTeam/devlovers.…
liudmylasovetovs Feb 10, 2026
ae72ec7
(SP: 1) [Backend]shop(monobank): harden webhook apply outcome typing
liudmylasovetovs Feb 10, 2026
87c1314
(SP: 1) [Backend] test(shop/monobank): archive test product instead o…
liudmylasovetovs Feb 10, 2026
b9a58a6
(SP: 1) [Backend] harden monobank webhook atomic updates, audit trail…
liudmylasovetovs Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,298 changes: 765 additions & 533 deletions frontend/lib/services/orders/monobank-webhook.ts

Large diffs are not rendered by default.

38 changes: 21 additions & 17 deletions frontend/lib/services/orders/monobank.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'server-only';

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

import { db } from '@/db';
import { orderItems, orders, paymentAttempts } from '@/db/schema';
Expand Down Expand Up @@ -49,7 +49,7 @@ async function getActiveAttempt(
and(
eq(paymentAttempts.orderId, orderId),
eq(paymentAttempts.provider, 'monobank'),
sql`${paymentAttempts.status} in ('creating','active')`
inArray(paymentAttempts.status, ['creating', 'active'])
)
)
.limit(1);
Expand Down Expand Up @@ -230,7 +230,7 @@ async function cancelOrderAndRelease(orderId: string, reason: string) {
and(
eq(orders.id, orderId),
eq(orders.paymentProvider, 'monobank'),
sql`${orders.paymentStatus} in ('pending','requires_payment')`
inArray(orders.paymentStatus, ['pending', 'requires_payment'])
)
)
.returning({ id: orders.id });
Expand Down Expand Up @@ -458,6 +458,7 @@ async function createMonoAttemptAndInvoiceImpl(
}
): Promise<{
attemptId: string;
attemptNumber: number;
invoiceId: string;
pageUrl: string;
currency: 'UAH';
Expand All @@ -473,6 +474,7 @@ async function createMonoAttemptAndInvoiceImpl(
invoiceId: existing.providerPaymentIntentId,
pageUrl,
attemptId: existing.id,
attemptNumber: existing.attemptNumber,
currency: MONO_CURRENCY,
totalAmountMinor: snapshot.amountMinor,
};
Expand Down Expand Up @@ -535,6 +537,7 @@ async function createMonoAttemptAndInvoiceImpl(
invoiceId: reused.providerPaymentIntentId,
pageUrl,
attemptId: reused.id,
attemptNumber: reused.attemptNumber,
currency: MONO_CURRENCY,
totalAmountMinor: snapshot.amountMinor,
};
Expand Down Expand Up @@ -579,10 +582,18 @@ async function createMonoAttemptAndInvoiceImpl(
});
}

await deps.cancelOrderAndRelease(
args.orderId,
'Monobank snapshot validation failed.'
);
try {
await deps.cancelOrderAndRelease(
args.orderId,
'Monobank snapshot validation failed.'
);
} catch (cancelErr) {
logError('monobank_cancel_order_failed', cancelErr, {
orderId: args.orderId,
attemptId: attempt.id,
requestId: args.requestId,
});
}

throw error;
}
Expand Down Expand Up @@ -673,6 +684,7 @@ async function createMonoAttemptAndInvoiceImpl(
invoiceId: invoice.invoiceId,
pageUrl: invoice.pageUrl,
attemptId: attempt.id,
attemptNumber: attempt.attemptNumber,
currency: MONO_CURRENCY,
totalAmountMinor: snapshot.amountMinor,
};
Expand All @@ -686,6 +698,7 @@ export async function createMonoAttemptAndInvoice(args: {
maxAttempts?: number;
}): Promise<{
attemptId: string;
attemptNumber: number;
invoiceId: string;
pageUrl: string;
currency: 'UAH';
Expand Down Expand Up @@ -738,14 +751,5 @@ export async function createMonobankAttemptAndInvoice(args: {
maxAttempts: args.maxAttempts,
});

const [row] = await db
.select({ attemptNumber: paymentAttempts.attemptNumber })
.from(paymentAttempts)
.where(eq(paymentAttempts.id, result.attemptId))
.limit(1);

return {
...result,
attemptNumber: row?.attemptNumber ?? 1,
};
return result;
}
50 changes: 37 additions & 13 deletions frontend/lib/tests/shop/monobank-psp-unavailable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { orders, paymentAttempts, productPrices, products } from '@/db/schema';
import { resetEnvCache } from '@/lib/env';
import { toDbMoney } from '@/lib/shop/money';
import { deriveTestIpFromIdemKey } from '@/lib/tests/helpers/ip';
import { isUuidV1toV5 } from '@/lib/utils/uuid';

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

async function cleanupOrder(orderId: string) {
await db.execute(sql`delete from inventory_moves where order_id = ${orderId}::uuid`);
await db.execute(sql`delete from order_items where order_id = ${orderId}::uuid`);
await db.delete(paymentAttempts).where(eq(paymentAttempts.orderId, orderId));
await db.delete(orders).where(eq(orders.id, orderId));
if (!isUuidV1toV5(orderId))
throw new Error(`cleanupOrder: invalid uuid: ${orderId}`);

await db.execute(
sql`delete from inventory_moves where order_id = ${orderId}::uuid`
);
await db.execute(
sql`delete from order_items where order_id = ${orderId}::uuid`
);
await db.execute(
sql`delete from payment_attempts where order_id = ${orderId}::uuid`
);
await db.execute(sql`delete from orders where id = ${orderId}::uuid`);
}

async function cleanupProduct(productId: string) {
await db.execute(sql`delete from inventory_moves where product_id = ${productId}::uuid`);
await db.execute(sql`delete from order_items where product_id = ${productId}::uuid`);
await db.delete(productPrices).where(eq(productPrices.productId, productId));
await db.delete(products).where(eq(products.id, productId));
async function archiveProduct(productId: string) {
if (!isUuidV1toV5(productId))
throw new Error(`archiveProduct: invalid uuid: ${productId}`);
const TEST_ARCHIVE_PREFIX = '[TEST-ARCHIVED] ';
await db
.update(products)
.set({
isActive: false,
stock: 0,
title: sql<string>`
case
when ${products.title} like ${TEST_ARCHIVE_PREFIX + '%'} then ${products.title}
else ${TEST_ARCHIVE_PREFIX} || ${products.title}
end
`,
updatedAt: new Date(),
} as any)
.where(eq(products.id, productId));
}


async function postCheckout(idemKey: string, productId: string) {
const mod = (await import('@/app/api/shop/checkout/route')) as unknown as {
POST: (req: NextRequest) => Promise<Response>;
Expand Down Expand Up @@ -237,12 +259,14 @@ describe.sequential('monobank PSP_UNAVAILABLE invariant', () => {
if (orderId) {
await cleanupOrder(orderId);
}
} catch (e) { warnCleanup('cleanupOrder', e); }
} catch (e) {
warnCleanup('cleanupOrder', e);
}

try {
await cleanupProduct(productId);
await archiveProduct(productId);
} catch (e) {
warnCleanup('cleanupProduct', e);
warnCleanup('archiveProduct', e);
}
}
}, 20_000);
Expand Down
Loading