feat(subscription): separate selection from payment and add issue feedback
Docker Image / Build and Push Docker Image (push) Successful in 2m2s

(cherry picked from commit fe9d31146b)
This commit is contained in:
Codex
2026-07-27 11:35:28 +08:00
parent 9602fdd94d
commit b34d3a3a67
15 changed files with 1339 additions and 74 deletions
+81 -1
View File
@@ -70,6 +70,7 @@ const idrGiftCatalog = {
async function registerIndonesiaPaymentMocks(page: Page) {
const orderStatuses = new Map<string, "pending" | "paid">();
const orderPlans = new Map<string, { planId: string; orderType: string }>();
let createOrderCount = 0;
await page.route("**/api/user/profile", async (route) => {
await route.fulfill({
@@ -83,6 +84,7 @@ async function registerIndonesiaPaymentMocks(page: Page) {
await route.fulfill({ json: apiEnvelope(idrGiftCatalog) });
});
await page.route("**/api/payment/create-order", async (route) => {
createOrderCount += 1;
const body = route.request().postDataJSON() as { planId: string };
const plan =
idrPlans.plans.find((item) => item.planId === body.planId) ??
@@ -138,6 +140,9 @@ async function registerIndonesiaPaymentMocks(page: Page) {
});
return {
getCreateOrderCount() {
return createOrderCount;
},
markPaid(orderId: string) {
orderStatuses.set(orderId, "paid");
},
@@ -180,6 +185,20 @@ async function expectQrisOrder(
return `order_qris_${planId}`;
}
async function expectCheckoutButtonLayout(page: Page) {
const viewport = page.viewportSize();
const checkoutBox = await page
.getByRole("button", { name: "Pay and Top Up" })
.boundingBox();
const stripeBox = await page.getByRole("button", { name: "Stripe" }).boundingBox();
expect(viewport).not.toBeNull();
expect(checkoutBox).not.toBeNull();
expect(stripeBox).not.toBeNull();
if (!viewport || !checkoutBox || !stripeBox) return;
expect(checkoutBox.y + checkoutBox.height).toBeLessThanOrEqual(viewport.height);
expect(stripeBox.y + stripeBox.height).toBeLessThanOrEqual(checkoutBox.y);
}
test.beforeEach(async ({ baseURL, context, page }) => {
await clearBrowserState(context, page, baseURL);
await mockCoreApis(page);
@@ -196,12 +215,29 @@ test("Indonesia VIP defaults to QRIS and completes after status polling", async
"aria-pressed",
"true",
);
const checkoutButton = page.getByRole("button", { name: "Pay and Top Up" });
await expect(checkoutButton).toBeDisabled();
await expectCheckoutButtonLayout(page);
await page.getByRole("button", { name: /Monthly, 195900 IDR/i }).click();
const renewalDialog = page.getByRole("dialog", {
name: "Automatic Renewal Confirmation",
});
await expect(renewalDialog).toBeVisible();
expect(payment.getCreateOrderCount()).toBe(0);
await renewalDialog.getByRole("button", { name: "Confirm" }).click();
await expect(renewalDialog).toBeHidden();
await expect(checkoutButton).toBeEnabled();
expect(payment.getCreateOrderCount()).toBe(0);
const orderId = await expectQrisOrder(
page,
/Pay and Activ/i,
/Pay and Top Up/i,
"vip_monthly",
/Rp\s*195[.,]900/,
);
expect(payment.getCreateOrderCount()).toBe(1);
payment.markPaid(orderId);
const success = page.getByRole("alertdialog", { name: "Payment successful" });
@@ -215,6 +251,7 @@ 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");
await expectCheckoutButtonLayout(page);
const orderId = await expectQrisOrder(
page,
@@ -229,6 +266,49 @@ test("Indonesia credit top-up uses QRIS display cents", async ({ page }) => {
).toBeVisible({ timeout: 10_000 });
});
test("payment issue submits Other to the feedback API without creating an order", async ({
page,
}) => {
const payment = await registerIndonesiaPaymentMocks(page);
await prepareIndonesiaUser(page);
let feedbackBody = "";
let idempotencyKey = "";
await page.route("**/api/feedback", async (route) => {
feedbackBody = route.request().postData() ?? "";
idempotencyKey = route.request().headers()["idempotency-key"] ?? "";
await route.fulfill({
json: apiEnvelope({ feedbackId: "feedback-payment-1", duplicate: false }),
});
});
await page.goto("/subscription?type=topup&character=elio");
await page.getByRole("button", { name: "Payment issue?" }).click();
const dialog = page.getByRole("dialog", {
name: "What problem did you encounter?",
});
await expect(dialog).toBeVisible();
await dialog.getByRole("radio", { name: "Other" }).check();
await dialog
.getByLabel("Please describe the issue")
.fill("QRIS did not open correctly.");
await dialog.getByRole("button", { name: "Submit" }).click();
await expect(page.getByRole("status")).toHaveText(
"Thanks. Your payment issue has been submitted.",
);
await expect(dialog).toBeHidden();
expect(payment.getCreateOrderCount()).toBe(0);
expect(idempotencyKey).toMatch(/^payment_feedback_[A-Za-z0-9_-]+$/);
expect(feedbackBody).toContain('name="category"');
expect(feedbackBody).toContain("payment");
expect(feedbackBody).toContain(
"Payment issue: Other\r\nDetails: QRIS did not open correctly.",
);
expect(feedbackBody).toContain('name="context"');
expect(feedbackBody).toContain('"paymentIssueReason":"other"');
expect(feedbackBody).toContain('"characterId":"elio"');
});
test("Indonesia gift checkout keeps IDR price through QRIS and thank-you flow", async ({
page,
}) => {