import { config } from "dotenv"; import { z } from "zod"; config({ path: ".env.local" }); async function main() { const { db } = await import("@/lib/db"); const { createUpgradeRequest, approveUpgradeRequest } = await import( "@/lib/upgrade-requests" ); const { ACTIVATE_STATUSES, ACCOUNT_TYPES, REQUEST_STATUSES, PAID_ACCOUNT_TYPE_VALUES } = await import( "@/lib/constants" ); const { ApiError } = await import("@/lib/errors"); // Mirrors the zod schema in app/api/maestros/[id]/upgrade-requests/route.ts — // this is what the [업그레이드] button's POST body must pass through. const postBodySchema = z.object({ requestedAccountType: z.coerce .number() .int() .refine((v) => (PAID_ACCOUNT_TYPE_VALUES as readonly number[]).includes(v)), }); type Scenario = { label: string; initialActivateStatus: number; initialAccountType: number; requestedAccountType: number; createExpectsError?: number; approveExpectsError?: number; directInsertRequest?: boolean; }; const scenarios: Scenario[] = [ { label: "TRIAL status + paid AccountType(BASIC_20) → PRO_100 (higher tier)", initialActivateStatus: ACTIVATE_STATUSES.TRIAL, initialAccountType: ACCOUNT_TYPES.BASIC_20, requestedAccountType: ACCOUNT_TYPES.PRO_100, }, { label: "TRIAL status + paid AccountType(STANDARD_50) → STANDARD_50 (same-tier conversion)", initialActivateStatus: ACTIVATE_STATUSES.TRIAL, initialAccountType: ACCOUNT_TYPES.STANDARD_50, requestedAccountType: ACCOUNT_TYPES.STANDARD_50, }, { label: "TRIAL status + trial AccountType(MAESTRO_TRIAL=101) → BASIC_20 (via admin create)", initialActivateStatus: ACTIVATE_STATUSES.TRIAL, initialAccountType: ACCOUNT_TYPES.MAESTRO_TRIAL, requestedAccountType: ACCOUNT_TYPES.BASIC_20, }, { label: "TRIAL status + trial AccountType(STUDENT_TRIAL=100) → SCHOOL_1000 (direct-insert bypass, mimicking chocomae flow)", initialActivateStatus: ACTIVATE_STATUSES.TRIAL, initialAccountType: ACCOUNT_TYPES.STUDENT_TRIAL, requestedAccountType: ACCOUNT_TYPES.SCHOOL_1000, directInsertRequest: true, }, ]; let failures = 0; for (const scenario of scenarios) { const stamp = Date.now(); const testName = `__TEST_TRIAL_UPG_${stamp}__`.slice(0, 50); const initialAvailable = new Date(2000, 0, 1); let maestroID: number | null = null; console.log(`\n=== ${scenario.label} ===`); try { const created = await db.maestro.create({ data: { Name: testName, Password: "", Email: `${stamp}@test.invalid`.slice(0, 50), AccountType: scenario.initialAccountType, ActivateStatus: scenario.initialActivateStatus, AvailableActivateDateTime: initialAvailable, PlayerCount: 0, AcceptClausesDateTime: new Date(), AllowEditEnterCode: 0, }, select: { MaestroID: true }, }); maestroID = created.MaestroID; console.log(` seeded maestro MaestroID=${maestroID}`); let maestroUpgradeID: number | null = null; // Step 1: create request (either via admin API or direct insert) if (scenario.directInsertRequest) { const inserted = await db.maestro_upgrade.create({ data: { MaestroID: maestroID, RegisteredActivateStatus: scenario.initialActivateStatus, RegisteredAccountType: scenario.initialAccountType, RequestedAccountType: scenario.requestedAccountType, RequestedDateTime: new Date(), Status: REQUEST_STATUSES.REQUESTED, }, select: { MaestroUpgradeID: true }, }); maestroUpgradeID = inserted.MaestroUpgradeID; console.log(` direct-inserted upgrade request maestroUpgradeID=${maestroUpgradeID}`); } else { // Simulate the [업그레이드] button POST body path: zod-validate first. const parsed = postBodySchema.safeParse({ requestedAccountType: scenario.requestedAccountType, }); console.log( ` zod schema on { requestedAccountType: ${scenario.requestedAccountType} } → ${parsed.success ? "accepted" : "rejected"}` ); if (!parsed.success) { console.log( ` FAIL API zod schema rejected paid-tier target (would produce 400 from API layer)` ); failures += 1; continue; } try { const result = await createUpgradeRequest( maestroID, parsed.data.requestedAccountType ); if (scenario.createExpectsError !== undefined) { console.log(` FAIL createUpgradeRequest expected ApiError(${scenario.createExpectsError}), succeeded`); failures += 1; } else { maestroUpgradeID = result.maestroUpgradeID; console.log(` createUpgradeRequest → maestroUpgradeID=${maestroUpgradeID}, status=${result.status}`); } } catch (error) { if ( scenario.createExpectsError !== undefined && error instanceof ApiError && error.statusCode === scenario.createExpectsError ) { console.log(` PASS createUpgradeRequest correctly threw ApiError(${error.statusCode}): ${error.message}`); continue; } throw error; } } if (maestroUpgradeID === null) continue; // Step 2: approve try { const approveResult = await approveUpgradeRequest(maestroUpgradeID); if (scenario.approveExpectsError !== undefined) { console.log(` FAIL approveUpgradeRequest expected ApiError(${scenario.approveExpectsError}), succeeded`); failures += 1; continue; } console.log(` approveUpgradeRequest → availableActivateDateTime=${approveResult.availableActivateDateTime}`); } catch (error) { if ( scenario.approveExpectsError !== undefined && error instanceof ApiError && error.statusCode === scenario.approveExpectsError ) { console.log(` PASS approveUpgradeRequest correctly threw ApiError(${error.statusCode}): ${error.message}`); // Verify rollback: request row should NOT be APPLIED, and maestro state should be unchanged const stillRequestedRow = await db.maestro_upgrade.findUniqueOrThrow({ where: { MaestroUpgradeID: maestroUpgradeID }, select: { Status: true }, }); const stillTrialMaestro = await db.maestro.findUniqueOrThrow({ where: { MaestroID: maestroID }, select: { ActivateStatus: true, AccountType: true }, }); const rollbackOK = stillRequestedRow.Status === REQUEST_STATUSES.REQUESTED && stillTrialMaestro.ActivateStatus === scenario.initialActivateStatus && stillTrialMaestro.AccountType === scenario.initialAccountType; console.log(` ${rollbackOK ? "PASS" : "FAIL"} transaction rollback preserved state`); if (!rollbackOK) failures += 1; continue; } throw error; } // Step 3: verify post-approval state const afterApprove = await db.maestro.findUniqueOrThrow({ where: { MaestroID: maestroID }, select: { ActivateStatus: true, AvailableActivateDateTime: true, AccountType: true, PlayerCount: true, }, }); const requestRow = await db.maestro_upgrade.findUniqueOrThrow({ where: { MaestroUpgradeID: maestroUpgradeID }, select: { Status: true }, }); const logRows = await db.maestro_log.findMany({ where: { MaestroID: maestroID }, select: { Type: true }, orderBy: { MaestroLogID: "asc" }, }); const nowPlus1yr = new Date(); nowPlus1yr.setFullYear(nowPlus1yr.getFullYear() + 1); const checks: Array<[string, boolean, string]> = [ [ "maestro.ActivateStatus becomes ACTIVE", afterApprove.ActivateStatus === ACTIVATE_STATUSES.ACTIVE, `got ${afterApprove.ActivateStatus}`, ], [ "maestro.AccountType becomes RequestedAccountType", afterApprove.AccountType === scenario.requestedAccountType, `got ${afterApprove.AccountType}, expected ${scenario.requestedAccountType}`, ], [ "maestro.PlayerCount unchanged (upgrade should NOT touch PlayerCount)", afterApprove.PlayerCount === 0, `got ${afterApprove.PlayerCount}`, ], [ "AvailableActivateDateTime ≈ now + 1 year", Math.abs(afterApprove.AvailableActivateDateTime.getTime() - nowPlus1yr.getTime()) < 60_000, `delta=${afterApprove.AvailableActivateDateTime.getTime() - nowPlus1yr.getTime()}ms`, ], [ "upgrade request Status becomes APPLIED", requestRow.Status === REQUEST_STATUSES.APPLIED, `got ${requestRow.Status}`, ], [ "logs include upgrade_maestro", logRows.some((l) => l.Type === "upgrade_maestro"), JSON.stringify(logRows.map((l) => l.Type)), ], ]; for (const [name, passed, detail] of checks) { console.log(` ${passed ? "PASS" : "FAIL"} ${name} (${detail})`); if (!passed) failures += 1; } } catch (error) { failures += 1; console.error(` ERROR:`, error); } finally { if (maestroID !== null) { await db.maestro_upgrade.deleteMany({ where: { MaestroID: maestroID } }); await db.maestro_log.deleteMany({ where: { MaestroID: maestroID } }); await db.maestro.delete({ where: { MaestroID: maestroID } }); console.log(` cleanup: removed rows for MaestroID=${maestroID}`); } } } await db.$disconnect(); if (failures > 0) { console.error(`\n${failures} check(s) failed.`); process.exitCode = 1; } else { console.log(`\nAll checks passed.`); } } main().catch((error) => { console.error("Test run failed"); console.error(error); process.exitCode = 1; });