254 lines
7.2 KiB
TypeScript
254 lines
7.2 KiB
TypeScript
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<string, "pending" | "paid">();
|
|
const orderPlans = new Map<string, { planId: string; orderType: string }>();
|
|
|
|
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();
|
|
});
|