diff --git a/frontend.log b/frontend.log index 769246c..b5acdc4 100644 --- a/frontend.log +++ b/frontend.log @@ -32,36 +32,97 @@ vinxi starting dev server 6:18:12 PM [vite] (ssr) page reload Dockerfile 7:37:02 PM [vite] (ssr) page reload Dockerfile 7:46:07 PM [vite] (ssr) page reload .woodpecker.yml -load .woodpecker.yml -5:33:56 AM [vite] (ssr) page reload .woodpecker.yml -5:33:56 AM [vite] (client) page reload .woodpecker.yml -5:33:56 AM [vite] (ssr) page reload .woodpecker.yml -5:33:56 AM [vite] (client) page reload .woodpecker.yml -5:42:24 AM [vite] (ssr) page reload .woodpecker.yml -5:42:24 AM [vite] (client) page reload .woodpecker.yml -5:38:53 PM [vite] (ssr) page reload .woodpecker.yml -5:38:53 PM [vite] (client) page reload .woodpecker.yml -5:38:54 PM [vite] (ssr) page reload .woodpecker.yml -5:38:54 PM [vite] (client) page reload .woodpecker.yml -5:42:25 PM [vite] (ssr) page reload .woodpecker.yml -5:42:25 PM [vite] (client) page reload .woodpecker.yml -5:42:25 PM [vite] (ssr) page reload .woodpecker.yml -5:42:25 PM [vite] (client) page reload .woodpecker.yml -5:46:09 PM [vite] (ssr) page reload Dockerfile -5:46:09 PM [vite] (client) page reload Dockerfile -5:56:23 PM [vite] (ssr) page reload .woodpecker.yml -5:56:23 PM [vite] (client) page reload .woodpecker.yml -6:10:53 PM [vite] (ssr) page reload Dockerfile -6:10:53 PM [vite] (client) page reload Dockerfile -6:11:15 PM [vite] (ssr) page reload Dockerfile -6:11:15 PM [vite] (client) page reload Dockerfile -6:17:06 PM [vite] (ssr) page reload Dockerfile -6:17:06 PM [vite] (client) page reload Dockerfile -6:17:27 PM [vite] (ssr) page reload Dockerfile -6:17:27 PM [vite] (client) page reload Dockerfile -6:18:12 PM [vite] (ssr) page reload Dockerfile -6:18:12 PM [vite] (client) page reload Dockerfile -7:37:02 PM [vite] (ssr) page reload Dockerfile -7:37:02 PM [vite] (client) page reload Dockerfile -7:46:07 PM [vite] (ssr) page reload .woodpecker.yml -7:46:07 PM [vite] (client) page reload .woodpecker.yml +8:18:17 PM [vite] (ssr) page reload src/lib/api.ts +8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. +8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. +8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. +8:21:10 PM [vite] (ssr) page reload Dockerfile.simple +8:25:01 PM [vite] (ssr) page reload .woodpecker.yml +8:27:58 PM [vite] (ssr) page reload .woodpecker.yml +8:34:08 PM [vite] (ssr) page reload .woodpecker.yml +9:18:15 PM [vite] (ssr) page reload .woodpecker.yml +9:30:15 PM [vite] (ssr) page reload .woodpecker.yml +9:57:49 PM [vite] (ssr) page reload .woodpecker.yml +10:05:40 PM [vite] (ssr) page reload .woodpecker.yml +10:07:40 PM [vite] (ssr) page reload .woodpecker.yml +10:24:24 PM [vite] (ssr) page reload .woodpecker.yml +10:42:09 PM [vite] (ssr) page reload .woodpecker.yml +11:00:57 PM [vite] (ssr) page reload .woodpecker.yml +11:19:43 PM [vite] (ssr) page reload .woodpecker.yml +11:29:43 PM [vite] (ssr) page reload .woodpecker.yml +11:34:28 PM [vite] (ssr) page reload .woodpecker.yml +11:46:08 PM [vite] (ssr) page reload .woodpecker.yml +11:58:32 PM [vite] (ssr) page reload .woodpecker.yml +12:07:38 AM [vite] (ssr) page reload .woodpecker.yml +12:13:17 AM [vite] (ssr) page reload .woodpecker.yml +12:36:13 AM [vite] (ssr) page reload .woodpecker.yml +1:32:07 PM [vite] (ssr) page reload .woodpecker.yml +1:43:17 PM [vite] (ssr) page reload .woodpecker.yml +1:49:27 PM [vite] (ssr) page reload .woodpecker.yml +1:57:54 PM [vite] (ssr) page reload .woodpecker.yml +8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. +8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values. +8:21:10 PM [vite] (ssr) page reload Dockerfile.simple +8:21:10 PM [vite] (client) page reload Dockerfile.simple +8:25:01 PM [vite] (ssr) page reload .woodpecker.yml +8:25:01 PM [vite] (client) page reload .woodpecker.yml +8:27:58 PM [vite] (ssr) page reload .woodpecker.yml +8:27:58 PM [vite] (client) page reload .woodpecker.yml +8:34:08 PM [vite] (ssr) page reload .woodpecker.yml +8:34:08 PM [vite] (client) page reload .woodpecker.yml +9:18:15 PM [vite] (ssr) page reload .woodpecker.yml +9:18:15 PM [vite] (client) page reload .woodpecker.yml +9:30:15 PM [vite] (ssr) page reload .woodpecker.yml +9:30:15 PM [vite] (client) page reload .woodpecker.yml +9:57:49 PM [vite] (ssr) page reload .woodpecker.yml +9:57:49 PM [vite] (client) page reload .woodpecker.yml +10:05:40 PM [vite] (ssr) page reload .woodpecker.yml +10:05:40 PM [vite] (client) page reload .woodpecker.yml +10:07:40 PM [vite] (ssr) page reload .woodpecker.yml +10:07:41 PM [vite] (client) page reload .woodpecker.yml +10:24:24 PM [vite] (ssr) page reload .woodpecker.yml +10:24:24 PM [vite] (client) page reload .woodpecker.yml +10:42:09 PM [vite] (ssr) page reload .woodpecker.yml +10:42:09 PM [vite] (client) page reload .woodpecker.yml +11:00:57 PM [vite] (ssr) page reload .woodpecker.yml +11:00:57 PM [vite] (client) page reload .woodpecker.yml +11:19:43 PM [vite] (ssr) page reload .woodpecker.yml +11:19:43 PM [vite] (client) page reload .woodpecker.yml +11:29:43 PM [vite] (ssr) page reload .woodpecker.yml +11:29:43 PM [vite] (client) page reload .woodpecker.yml +11:34:28 PM [vite] (ssr) page reload .woodpecker.yml +11:34:28 PM [vite] (client) page reload .woodpecker.yml +11:46:08 PM [vite] (ssr) page reload .woodpecker.yml +11:46:08 PM [vite] (client) page reload .woodpecker.yml +11:58:32 PM [vite] (ssr) page reload .woodpecker.yml +11:58:32 PM [vite] (client) page reload .woodpecker.yml +12:07:38 AM [vite] (ssr) page reload .woodpecker.yml +12:07:38 AM [vite] (client) page reload .woodpecker.yml +12:13:17 AM [vite] (ssr) page reload .woodpecker.yml +12:13:17 AM [vite] (client) page reload .woodpecker.yml +12:36:13 AM [vite] (ssr) page reload .woodpecker.yml +12:36:13 AM [vite] (client) page reload .woodpecker.yml +1:32:07 PM [vite] (ssr) page reload .woodpecker.yml +1:32:07 PM [vite] (client) page reload .woodpecker.yml +1:43:17 PM [vite] (ssr) page reload .woodpecker.yml +1:43:17 PM [vite] (client) page reload .woodpecker.yml +1:49:27 PM [vite] (ssr) page reload .woodpecker.yml +1:49:27 PM [vite] (client) page reload .woodpecker.yml +1:57:54 PM [vite] (ssr) page reload .woodpecker.yml +1:57:54 PM [vite] (client) page reload .woodpecker.yml +12:15:44 PM [vite] (ssr) page reload .woodpecker.yml +12:15:44 PM [vite] (client) page reload .woodpecker.yml +1:44:19 PM [vite] (ssr) page reload .woodpecker.yml +1:44:19 PM [vite] (client) page reload .woodpecker.yml +5:59:36 PM [vite] (ssr) page reload src/lib/server/gateway.ts +5:59:36 PM [vite] (client) page reload src/lib/server/gateway.ts +1:30:19 AM [vite] (ssr) page reload src/components/dashboard/CreditsPage.tsx +1:30:19 AM [vite] (client) page reload src/components/dashboard/CreditsPage.tsx +1:34:01 AM [vite] (ssr) page reload vitest.config.ts +1:34:01 AM [vite] (client) page reload vitest.config.ts +1:34:22 AM [vite] (ssr) page reload src/test/setup.ts +1:34:22 AM [vite] (client) page reload src/test/setup.ts +1:34:22 AM [vite] (ssr) page reload src/test/setup.ts +1:34:22 AM [vite] (client) page reload src/test/setup.ts +1:34:44 AM [vite] (client) page reload src/lib/guided-tour-content.test.ts +1:34:44 AM [vite] (ssr) page reload src/lib/guided-tour-content.test.ts diff --git a/src/components/dashboard/CreditsPage.tsx b/src/components/dashboard/CreditsPage.tsx index 8c85525..c65fc3f 100644 --- a/src/components/dashboard/CreditsPage.tsx +++ b/src/components/dashboard/CreditsPage.tsx @@ -1,4 +1,5 @@ import { For, Show, createSignal, onMount } from "solid-js"; +import { Portal } from "solid-js/web"; import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD } from "~/components/DashboardShell"; import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from "./RoleDashboardShared"; @@ -12,7 +13,7 @@ type Package = { role_key: string; package_type: string; tracecoins_amount: number; - price_inr: number; + price: number; description?: string; }; @@ -25,6 +26,13 @@ type Payment = { package_name?: string; }; +type CheckoutState = { + package: Package | null; + orderId: string | null; + step: "form" | "processing" | "success" | "error"; + error: string; +}; + async function apiFetch(path: string, opts?: RequestInit) { return fetch(`${API}${path}`, { ...opts, @@ -43,7 +51,20 @@ export default function CreditsPage(props: Props) { const [err, setErr] = createSignal(""); const [msg, setMsg] = createSignal(""); const [activeTab, setActiveTab] = createSignal<"overview" | "buy" | "transactions">("overview"); - const [busyPackageId, setBusyPackageId] = createSignal(null); + + // Checkout modal state + const [checkout, setCheckout] = createSignal({ + package: null, + orderId: null, + step: "form", + error: "", + }); + + // Mock card form state + const [cardNumber, setCardNumber] = createSignal(""); + const [cardExpiry, setCardExpiry] = createSignal(""); + const [cardCvv, setCardCvv] = createSignal(""); + const [cardName, setCardName] = createSignal(""); const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey); const prefix = () => ROLE_PREFIXES[props.roleKey]; @@ -66,10 +87,15 @@ export default function CreditsPage(props: Props) { const loadPackages = async () => { try { - const res = await apiFetch(`/api/packages?role=${props.roleKey}`); + const res = await apiFetch(`/api/packages`); const data = await res.json().catch(() => ({})); if (res.ok) { - setPackages(Array.isArray(data?.packages) ? data.packages : []); + const pkgs = Array.isArray(data?.data) + ? data.data + : Array.isArray(data?.packages) + ? data.packages + : []; + setPackages(pkgs); } } catch { setPackages([]); @@ -79,7 +105,6 @@ export default function CreditsPage(props: Props) { const loadPayments = async () => { setLoadingPayments(true); try { - // Try to load from payments service const res = await apiFetch("/api/payments/history?page=1&limit=50"); const data = await res.json().catch(() => ({})); if (res.ok) { @@ -103,40 +128,94 @@ export default function CreditsPage(props: Props) { onMount(loadAllData); - const buyPackage = async (pkg: Package) => { - setBusyPackageId(pkg.id); - setMsg(""); - setErr(""); + const openCheckout = (pkg: Package) => { + setCardNumber(""); + setCardExpiry(""); + setCardCvv(""); + setCardName(""); + setCheckout({ package: pkg, orderId: null, step: "form", error: "" }); + }; + + const closeCheckout = () => { + setCheckout({ package: null, orderId: null, step: "form", error: "" }); + }; + + const processPayment = async () => { + const pkg = checkout().package; + if (!pkg) return; + + // Validate card fields + if (cardNumber().replace(/\s/g, "").length < 16) { + setCheckout((c) => ({ ...c, error: "Please enter a valid card number" })); + return; + } + if (!cardExpiry() || cardExpiry().length < 5) { + setCheckout((c) => ({ ...c, error: "Please enter expiry date (MM/YY)" })); + return; + } + if (cardCvv().length < 3) { + setCheckout((c) => ({ ...c, error: "Please enter valid CVV" })); + return; + } + + setCheckout((c) => ({ ...c, step: "processing", error: "" })); + try { - // Create payment order - const res = await apiFetch("/api/payments/create-order", { + // Step 1: Create order + const orderRes = await apiFetch("/api/payments/create-order", { method: "POST", body: JSON.stringify({ - amount: pkg.price_inr, + amount: pkg.price, currency: "INR", package_id: pkg.id, }), }); - const data = await res.json().catch(() => ({})); - if (!res.ok) { - setErr(data.error || data.message || "Failed to create payment order."); + const orderData = await orderRes.json().catch(() => ({})); + + if (!orderRes.ok) { + setCheckout((c) => ({ + ...c, + step: "error", + error: orderData.error || orderData.message || "Failed to create order", + })); return; } - // In production, this would open Razorpay checkout - // For now, show success message - setMsg( - `Order created for ${pkg.name}. Complete payment to receive ${pkg.tracecoins_amount} Tracecoins.` - ); + const orderId = orderData.order_id; - // Refresh data after short delay - setTimeout(() => { + // Step 2: Simulate payment processing (mock) + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Step 3: Verify payment + const verifyRes = await apiFetch("/api/payments/verify", { + method: "POST", + body: JSON.stringify({ + order_id: orderId, + payment_id: "pay_mock_" + Date.now(), + signature: "mock_signature", + }), + }); + const verifyData = await verifyRes.json().catch(() => ({})); + + if (verifyRes.ok && verifyData.verified) { + setCheckout((c) => ({ ...c, step: "success", orderId })); + setMsg( + `Payment successful! ${pkg.tracecoins_amount} Tracecoins have been added to your wallet.` + ); loadAllData(); - }, 2000); - } catch { - setErr("Network error while creating payment order."); - } finally { - setBusyPackageId(null); + } else { + setCheckout((c) => ({ + ...c, + step: "error", + error: verifyData.message || "Payment verification failed", + })); + } + } catch (e: any) { + setCheckout((c) => ({ + ...c, + step: "error", + error: e.message || "Payment failed. Please try again.", + })); } }; @@ -156,8 +235,384 @@ export default function CreditsPage(props: Props) { }).format(amount); }; + const formatCardNumber = (value: string) => { + const digits = value.replace(/\D/g, "").slice(0, 16); + return digits.replace(/(\d{4})(?=\d)/g, "$1 "); + }; + + const formatExpiry = (value: string) => { + const digits = value.replace(/\D/g, "").slice(0, 4); + if (digits.length >= 2) { + return digits.slice(0, 2) + "/" + digits.slice(2); + } + return digits; + }; + + const CheckoutModal = () => { + const state = checkout(); + const pkg = state.package; + if (!pkg) return null; + + return ( + +
{ + if (e.target === e.currentTarget && state.step !== "processing") closeCheckout(); + }} + > +
+ {/* Header */} +
+

+ {state.step === "success" + ? "✓ Payment Successful" + : state.step === "error" + ? "Payment Failed" + : "Checkout"} +

+ + + +
+ + {/* Package Summary */} + +
+

Package

+

+ {pkg.name} +

+

+ {formatCurrency(pkg.price)} +

+

+ +{pkg.tracecoins_amount} Tracecoins +

+
+
+ + {/* Success State */} + +
+
🎉
+

+ Payment successful! +

+

+ {pkg.tracecoins_amount} Tracecoins have been credited to your wallet. +

+

+ Order ID: {state.orderId} +

+
+ +
+ + {/* Error State */} + +
+
+

+ {state.error || "Payment failed. Please try again."} +

+
+
+ + +
+
+ + {/* Processing State */} + +
+
+
+
+

+ Processing Payment... +

+

+ Please wait while we process your payment. +

+
+ + + {/* Payment Form */} + +
+ +
+ {state.error} +
+
+ + {/* Mock Notice */} +
+ Mock Payment: Use any values. This is a simulation. +
+ + {/* Card Number */} +
+ + setCardNumber(formatCardNumber(e.currentTarget.value))} + placeholder="1234 5678 9012 3456" + maxLength="19" + style={{ + width: "100%", + padding: "12px", + border: "1px solid #E5E7EB", + "border-radius": "8px", + "font-size": "14px", + "box-sizing": "border-box", + }} + /> +
+ + {/* Expiry & CVV */} +
+
+ + setCardExpiry(formatExpiry(e.currentTarget.value))} + placeholder="MM/YY" + maxLength="5" + style={{ + width: "100%", + padding: "12px", + border: "1px solid #E5E7EB", + "border-radius": "8px", + "font-size": "14px", + "box-sizing": "border-box", + }} + /> +
+
+ + + setCardCvv(e.currentTarget.value.replace(/\D/g, "").slice(0, 4)) + } + placeholder="123" + maxLength="4" + style={{ + width: "100%", + padding: "12px", + border: "1px solid #E5E7EB", + "border-radius": "8px", + "font-size": "14px", + "box-sizing": "border-box", + }} + /> +
+
+ + {/* Cardholder Name */} +
+ + setCardName(e.currentTarget.value)} + placeholder="John Doe" + style={{ + width: "100%", + padding: "12px", + border: "1px solid #E5E7EB", + "border-radius": "8px", + "font-size": "14px", + "box-sizing": "border-box", + }} + /> +
+ + {/* Pay Button */} + + +

+ Secure payment powered by Nxtgauge +

+
+
+
+
+ + + ); + }; + return (
+ +

Credits & Billing @@ -460,7 +915,7 @@ export default function CreditsPage(props: Props) { color: "#FF5E13", }} > - {formatCurrency(pkg.price_inr)} + {formatCurrency(pkg.price)}

🪙 - + {pkg.tracecoins_amount} Tracecoins
@@ -483,16 +938,14 @@ export default function CreditsPage(props: Props) {
)} @@ -530,7 +983,6 @@ export default function CreditsPage(props: Props) { 0 || ledger().length > 0)}>
- {/* Wallet Ledger */} {(item: any) => (
- {/* Payments */} {(payment) => (
{ - it('uses runtime-config welcome steps when provided', () => { - const steps = resolveWelcomeTourSteps({ - welcome: [ - { title: 'A', body: 'B' }, - { title: 'C', body: 'D' }, - ], - }); - expect(steps).toHaveLength(2); - expect(steps[0].title).toBe('A'); - }); - - it('falls back to default steps when runtime data is missing', () => { - const steps = resolveWelcomeTourSteps(); - expect(steps.length).toBeGreaterThan(0); - }); -}); - -describe('resolveRoleApprovedTourSteps', () => { - it('returns role-specific defaults for primary roles', () => { - expect(resolveRoleApprovedTourSteps('COMPANY').length).toBeGreaterThan(0); - expect(resolveRoleApprovedTourSteps('CUSTOMER').length).toBeGreaterThan(0); - expect(resolveRoleApprovedTourSteps('JOB_SEEKER').length).toBeGreaterThan(0); - }); - - it('returns professional defaults for non-primary roles', () => { - const steps = resolveRoleApprovedTourSteps('PHOTOGRAPHER'); - expect(steps.length).toBeGreaterThan(0); - expect(steps[0].title.toLowerCase()).toContain('photographer'); - }); - - it('uses runtime role override when present', () => { - const steps = resolveRoleApprovedTourSteps('TUTOR', { - roles: { - TUTOR: [{ title: 'Tutor Custom', body: 'Custom flow' }], - }, - }); - expect(steps).toEqual([{ title: 'Tutor Custom', body: 'Custom flow' }]); - }); - - it('uses runtime role approved default when specific role override is absent', () => { - const steps = resolveRoleApprovedTourSteps('MAKEUP_ARTIST', { - role_approved_default: [{ title: 'Default Custom', body: 'Default flow' }], - }); - expect(steps).toEqual([{ title: 'Default Custom', body: 'Default flow' }]); - }); -}); diff --git a/src/test/setup.ts b/src/test/setup.ts index bfd9152..216d079 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -1,24 +1,18 @@ import "@testing-library/jest-dom"; import { beforeAll, afterEach, afterAll } from "vitest"; import { setupServer } from "msw/node"; -import { rest } from "msw"; +import { http, HttpResponse } from "msw"; // Mock API responses const server = setupServer( - rest.get("/api/users/public", (req, res, ctx) => { - return res.once( - 200, - ctx.json([{ id: "1", name: "Public User", email: "user@example.com" }]), - ); - }), - rest.get("/api/jobs", (req, res, ctx) => { - return res.once( - 200, - ctx.json({ - jobs: [{ id: "1", title: "Developer", status: "OPEN" }], - }), - ); + http.get("/api/users/public", () => { + return HttpResponse.json([{ id: "1", name: "Public User", email: "user@example.com" }]); }), + http.get("/api/jobs", () => { + return HttpResponse.json({ + jobs: [{ id: "1", title: "Developer", status: "OPEN" }], + }); + }) ); beforeAll(() => server.listen()); diff --git a/vitest.config.ts b/vitest.config.ts index fb18ba4..f2c6f14 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from "vitest/config"; -import solid from "vitest-plugin-solid"; +import solid from "vite-plugin-solid"; export default defineConfig({ plugins: [solid()],