import { expect, test, type Page } from "@playwright/test"; import { mockCoreApis } from "@e2e/fixtures/api-mocks"; import { apiEnvelope } from "@e2e/fixtures/data/common"; import { e2eEmailUser } from "@e2e/fixtures/data/user"; import { clearBrowserState, seedEmailSession, } from "@e2e/fixtures/test-helpers"; const idrPlans = { plans: [ { planId: "vip_monthly", planName: "Monthly", orderType: "vip_monthly", amountCents: 19_590_000, originalAmountCents: 19_590_000, dailyPriceCents: 653_000, currency: "IDR", vipDays: 30, dolAmount: null, creditBalance: 3_000, }, { planId: "dol_1000", planName: "1,000 Credits", orderType: "dol", amountCents: 8_890_000, originalAmountCents: null, dailyPriceCents: null, currency: "IDR", vipDays: null, dolAmount: 1_000, creditBalance: 1_000, }, ], }; const idrGiftCatalog = { characterId: "elio", categories: [ { category: "coffee", name: "Coffee", productCount: 1, imageUrl: null, }, ], plans: [ { planId: "tip_coffee_usd_4_99", planName: "Velvet Espresso", orderType: "tip", tipType: "coffee_small", category: "coffee", characterId: "elio", description: "Buy Elio a Velvet Espresso", imageUrl: null, amountCents: 8_929_900, currency: "IDR", autoRenew: false, isFirstRechargeOffer: false, firstRechargeDiscountPercent: 0, promotionType: null, }, ], }; async function registerIndonesiaPaymentMocks(page: Page) { const orderStatuses = new Map(); const orderPlans = new Map(); await page.route("**/api/user/profile", async (route) => { await route.fulfill({ json: apiEnvelope({ ...e2eEmailUser, countryCode: "ID" }), }); }); await page.route("**/api/payment/plans**", async (route) => { await route.fulfill({ json: apiEnvelope(idrPlans) }); }); await page.route("**/api/payment/gift-products**", async (route) => { await route.fulfill({ json: apiEnvelope(idrGiftCatalog) }); }); await page.route("**/api/payment/create-order", async (route) => { const body = route.request().postDataJSON() as { planId: string }; const plan = idrPlans.plans.find((item) => item.planId === body.planId) ?? idrGiftCatalog.plans.find((item) => item.planId === body.planId); const orderId = `order_qris_${body.planId}`; orderStatuses.set(orderId, "pending"); orderPlans.set(orderId, { planId: body.planId, orderType: plan?.orderType ?? "dol", }); await route.fulfill({ json: apiEnvelope({ orderId, payParams: { provider: "ezpay", countryCode: "ID", channelCode: "ID_QRIS_DYNAMIC_QR", channelType: "QR", payData: "00020101021226670016COM.NOBUBANK.WWW", firstChargeAmountCents: plan?.amountCents ?? 0, currency: "IDR", }, }), }); }); await page.route("**/api/payment/order-status**", async (route) => { const url = new URL(route.request().url()); const orderId = url.searchParams.get("order_id") ?? ""; const plan = orderPlans.get(orderId); await route.fulfill({ json: apiEnvelope({ orderId, status: orderStatuses.get(orderId) ?? "pending", orderType: plan?.orderType ?? "dol", planId: plan?.planId ?? null, creditsAdded: 0, }), }); }); await page.route("**/api/payment/tip-message", async (route) => { const body = route.request().postDataJSON() as { orderId: string }; await route.fulfill({ json: apiEnvelope({ orderId: body.orderId, characterId: "elio", planId: "tip_coffee_usd_4_99", productName: "Velvet Espresso", tipCount: 1, poolIndex: 0, message: "Thank you. Your gift made me smile.", }), }); }); return { markPaid(orderId: string) { orderStatuses.set(orderId, "paid"); }, }; } async function prepareIndonesiaUser(page: Page) { await seedEmailSession(page); await page.evaluate(() => { const rawUser = localStorage.getItem("cozsweet:user"); const user = rawUser ? JSON.parse(rawUser) : {}; localStorage.setItem( "cozsweet:user", JSON.stringify({ ...user, countryCode: "ID" }), ); }); } async function expectQrisOrder( page: Page, buttonName: RegExp, planId: string, formattedAmount: RegExp, ) { const requestPromise = page.waitForRequest("**/api/payment/create-order"); await page.getByRole("button", { name: buttonName }).click(); const request = await requestPromise; expect(request.postDataJSON()).toMatchObject({ planId, payChannel: "ezpay", }); const dialog = page.getByRole("dialog", { name: "Scan to pay with QRIS" }); await expect(dialog).toBeVisible(); await expect(dialog).toContainText(formattedAmount); await expect(dialog).toContainText("Waiting for payment"); await expect( dialog.getByRole("img", { name: "QRIS payment QR code" }), ).toBeVisible(); return `order_qris_${planId}`; } test.beforeEach(async ({ baseURL, context, page }) => { await clearBrowserState(context, page, baseURL); await mockCoreApis(page); }); test("Indonesia VIP defaults to QRIS and completes after status polling", async ({ page, }) => { const payment = await registerIndonesiaPaymentMocks(page); await prepareIndonesiaUser(page); await page.goto("/subscription?type=vip"); await expect(page.getByRole("button", { name: "QRIS" })).toHaveAttribute( "aria-pressed", "true", ); const orderId = await expectQrisOrder( page, /Pay and Activ/i, "vip_monthly", /Rp\s*195[.,]900/, ); payment.markPaid(orderId); const success = page.getByRole("alertdialog", { name: "Payment successful" }); await expect(success).toBeVisible({ timeout: 10_000 }); await expect( page.getByRole("dialog", { name: "Scan to pay with QRIS" }), ).toBeHidden(); }); test("Indonesia credit top-up uses QRIS display cents", async ({ page }) => { const payment = await registerIndonesiaPaymentMocks(page); await prepareIndonesiaUser(page); await page.goto("/subscription?type=topup"); const orderId = await expectQrisOrder( page, /Pay and Top Up/i, "dol_1000", /Rp\s*88[.,]900/, ); payment.markPaid(orderId); await expect( page.getByRole("alertdialog", { name: "Payment successful" }), ).toBeVisible({ timeout: 10_000 }); }); test("Indonesia gift checkout keeps IDR price through QRIS and thank-you flow", async ({ page, }) => { const payment = await registerIndonesiaPaymentMocks(page); await prepareIndonesiaUser(page); await page.goto("/characters/elio/tip"); await expect(page.getByText(/IDR\s*89[.,]299/)).toBeVisible(); const orderId = await expectQrisOrder( page, /Order and Buy/i, "tip_coffee_usd_4_99", /Rp\s*89[.,]299/, ); payment.markPaid(orderId); await expect(page.getByText("Gift received")).toBeVisible({ timeout: 10_000 }); await expect(page.getByRole("button", { name: "Send another gift" })).toBeVisible(); await expect( page.getByRole("dialog", { name: "Scan to pay with QRIS" }), ).toBeHidden(); });