diff --git a/.env.example b/.env.example index 4e97545..ab7ddff 100644 --- a/.env.example +++ b/.env.example @@ -1,35 +1,29 @@ -# Required +# --- Required --- DATABASE_URL="postgresql://user:password@localhost:5432/amph_v2?schema=public" JWT_SECRET="generate-with-openssl-rand-base64-32" -# Required for payments (PayMongo) -# Test keys start with sk_test_ / pk_test_; live keys start with sk_live_ / pk_live_ -PAYMONGO_SECRET_KEY="sk_test_..." -PAYMONGO_PUBLIC_KEY="pk_test_..." -PAYMONGO_WEBHOOK_SECRET="whsec_..." +# Required for `pnpm prisma db seed` (creates the admin account) +ADMIN_EMAIL="you@example.com" +ADMIN_PASSWORD="ChangeMe123!" -# Required for email -RESEND_API_KEY="re_..." -RESEND_FROM_EMAIL="noreply@projectamazonph.com" +# Strongly recommended in production — used to build absolute URLs +# (account-claim links shown in /admin/enroll, certificate verification). +NEXT_PUBLIC_APP_URL="http://localhost:3000" -# Required for file storage +# --- Optional --- +# Required for file storage (used by the db-backup cron script only — +# no app code touches Vercel Blob in this stripped build) BLOB_READ_WRITE_TOKEN="vercel_blob_..." -# Required for cron jobs (Vercel Cron sends this as `Authorization: Bearer `) -# Used by /api/cron/expire-checkouts to authenticate scheduled invocations. -CRON_SECRET="generate-with-openssl-rand-base64-32" - -# Required for error tracking -SENTRY_DSN="https://...@sentry.io/..." -NEXT_PUBLIC_SENTRY_DSN="https://...@sentry.io/..." - -# Sprint 11 / STORY-048 — Sentry source-map upload (set in CI + deploy only) -SENTRY_AUTH_TOKEN="sntrys_..." +# Error tracking (Sentry no-ops when unset) +SENTRY_DSN="" +NEXT_PUBLIC_SENTRY_DSN="" +# Sentry source-map upload (CI/deploy only) +SENTRY_AUTH_TOKEN="" SENTRY_ORG="projectamazonph" SENTRY_PROJECT="amph-v2" -SENTRY_HOST="https://sentry.io" # self-hosted? override here -# Sprint 11 / STORY-052 — Sentry → Slack error alerting +# Sentry → Slack error alerting # Required: API token with project:read scope; Slack Incoming Webhook URL SENTRY_API_TOKEN="sntrys_..." SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T.../B.../..." @@ -41,7 +35,9 @@ WINDOW_MINUTES="60" # how many minutes back to aggregate # to "true" to promote it to an enforcing Content-Security-Policy header. CSP_ENFORCE="false" -# Optional -NEXT_PUBLIC_APP_URL="http://localhost:3000" NODE_ENV="development" -LOG_LEVEL="debug" \ No newline at end of file +LOG_LEVEL="debug" + +# NOTE (stripped launch build): PayMongo and Resend were removed — payments +# are collected manually (GCash/bank transfer) and enrollments granted via +# /admin/enroll. See docs/LAUNCH-DEPLOY.md. diff --git a/docs/LAUNCH-DEPLOY.md b/docs/LAUNCH-DEPLOY.md new file mode 100644 index 0000000..9132e6e --- /dev/null +++ b/docs/LAUNCH-DEPLOY.md @@ -0,0 +1,71 @@ +# Launch deploy — stripped build (manual enrollments) + +This branch is the **minimum-friction launch build**: PayMongo (payments) and +Resend (email) are stripped out entirely. You collect payments yourself +(GCash / bank transfer / Messenger) and grant course access from the admin +panel. Everything else — courses, lessons, the five practice tools, +gamification, certificates, live classes, admin — is intact. + +## What was removed + +- **PayMongo**: checkout flow (`/pricing` buy buttons, `/checkout/complete`), + webhook route, refunds (student request flow + admin review pages), + receipts/invoice PDFs, the `paymongo` npm package. +- **Resend**: all email sending. `src/lib/email.ts` is now a no-op stub that + logs instead of sending; the `resend` npm package and webhook route are gone. +- Dashboard "Payments" page and nav entry. + +The Prisma schema was **not** changed — `Payment`, `CheckoutSession`, +`RefundRequest` tables still exist (empty), so no migration surgery is needed +and the payment stack can be restored later from the main repo's history. + +## What was added + +- **`/admin/enroll`** — enter a student email + pricing tier; the student is + enrolled in every course on that tier. + - Existing account → access appears on their dashboard immediately. + - New email → a placeholder account is created and you get a **one-time + claim link** (shown once, expires in 7 days). Send it to the student + yourself (Messenger/email); they use it to set their password. +- Admin dashboard now shows recent enrollments instead of payments, with an + "Enroll Student" quick action. Each user's admin detail page links to a + pre-filled enroll form. +- `/pricing` keeps the tier cards but the CTA is now "create an account + + message us to pay" instead of a checkout button. + +## Deploy steps (Vercel + hosted Postgres) + +1. **Database**: create a Postgres instance (Neon, Supabase, or Vercel + Postgres). Copy the connection string. +2. **Env vars** (Vercel → Project → Settings → Environment Variables): + - `DATABASE_URL` — from step 1 + - `JWT_SECRET` — `openssl rand -base64 32` + - `NEXT_PUBLIC_APP_URL` — your production URL (needed for correct claim + links from /admin/enroll) + - Optional: `SENTRY_DSN` / `NEXT_PUBLIC_SENTRY_DSN` +3. **Migrate + import + seed** (locally, pointing at the production DB) — + order matters: the curriculum import must run before the seed's + course-dependent steps (live classes) can complete: + ```bash + DATABASE_URL="..." ADMIN_EMAIL="you@..." ADMIN_PASSWORD="..." ./scripts/setup-db.sh + ``` + or step by step: + ```bash + DATABASE_URL="..." pnpm prisma:deploy # schema + DATABASE_URL="..." pnpm tsx scripts/import-amph-content.ts # courses/modules/lessons/quizzes + DATABASE_URL="..." ADMIN_EMAIL="you@..." ADMIN_PASSWORD="..." pnpm prisma db seed # admin, tiers, badges, live classes + ``` +4. **Deploy**: import the repo in Vercel, pick this branch, deploy. No other + configuration is required. +5. **Smoke test**: sign in at `/auth/signin` with the admin account → + `/admin/enroll` → enroll a test email → open the claim link in an + incognito window → set a password → dashboard shows the courses. + +## Day-to-day enrollment workflow + +1. Student pays you via GCash/bank transfer and messages you their email. +2. Admin → **Enroll Student** → paste email, pick tier, submit. +3. New student? Copy the claim link and send it to them. Existing student? + Tell them to refresh their dashboard. +4. Refunds/disputes: handle in chat; suspend the user or leave the + enrollment — there is no in-app refund flow in this build. diff --git a/package.json b/package.json index 70107ae..e60c440 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,10 @@ "jose": "^6.2.3", "marked": "^18.0.6", "next": "^16.0.0", - "paymongo": "^1.3.2", "pg": "^8.22.0", "pino": "^10.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "resend": "^6.17.2", "server-only": "^0.0.1", "zod": "^4.4.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90cefc4..1134106 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: next: specifier: ^16.0.0 version: 16.2.10(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - paymongo: - specifier: ^1.3.2 - version: 1.3.2 pg: specifier: ^8.22.0 version: 8.22.0 @@ -50,9 +47,6 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.7(react@19.2.7) - resend: - specifier: ^6.17.2 - version: 6.17.2(@react-email/render@1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) server-only: specifier: ^0.0.1 version: 0.0.1 @@ -933,13 +927,6 @@ packages: '@types/react': optional: true - '@react-email/render@1.1.2': - resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-pdf/fns@3.1.3': resolution: {integrity: sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w==} @@ -1240,9 +1227,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@selderee/plugin-htmlparser2@0.11.0': - resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@sentry/babel-plugin-component-annotate@5.3.0': resolution: {integrity: sha512-p4q8gn8wcFqZGP/s2MnJCAAd8fTikaU6A0mM97RDHQgStcrYiaS0Sc5zUNfb1V+UOLPuvdEdL6MwyxfzjYJQTA==} engines: {node: '>= 18'} @@ -1462,9 +1446,6 @@ packages: peerDependencies: webpack: '>=5.0.0' - '@stablelib/base64@1.0.1': - resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1975,9 +1956,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - bent@7.3.12: - resolution: {integrity: sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==} - better-result@2.9.2: resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} @@ -2009,9 +1987,6 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bytesish@0.4.4: - resolution: {integrity: sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==} - c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -2039,9 +2014,6 @@ packages: caniuse-lite@1.0.30001802: resolution: {integrity: sha512-vmv8ub2xwTNmljSKf82mtCk5JH7hC+YgzLj3P5zotvA0tPQ9016tdNNOG8WRca1IxOnhSsivB+J0z5FeE5LOUw==} - caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2169,10 +2141,6 @@ packages: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2212,19 +2180,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2263,10 +2218,6 @@ packages: resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} engines: {node: '>=10.13.0'} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2482,9 +2433,6 @@ packages: fast-copy@4.0.4: resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} - fast-deep-equal@2.0.1: - resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2501,9 +2449,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - fast-uri@3.1.3: resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} @@ -2695,13 +2640,6 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-to-text@9.0.5: - resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} - engines: {node: '>=14'} - - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-status-codes@2.3.0: resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} @@ -2845,10 +2783,6 @@ packages: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -2969,9 +2903,6 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} - leac@0.6.0: - resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3355,9 +3286,6 @@ packages: parse-svg-path@0.1.2: resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} - parseley@0.12.1: - resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3376,12 +3304,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - paymongo@1.3.2: - resolution: {integrity: sha512-QkqtkaXf7l/n/9YffsxsTUF00mzmRwwZLv3DxL59XI9KZuanRnyGmnobrY5X3JztpfHwm6+i1gex2xGwuAeqCA==} - - peberminta@0.9.0: - resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} @@ -3464,9 +3386,6 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postal-mime@2.7.4: - resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -3577,9 +3496,6 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - react-promise-suspense@0.3.4: - resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} - react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -3618,15 +3534,6 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} - resend@6.17.2: - resolution: {integrity: sha512-hbaXEORFIFfT2Bh03NsA/akTTTKkD1hiuJ88ke64c5dWVV6DyoLxzFve3OiZqVOQ+JKLJ5uVkPR6hlUslpJFoA==} - engines: {node: '>=20'} - peerDependencies: - '@react-email/render': '*' - peerDependenciesMeta: - '@react-email/render': - optional: true - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3705,9 +3612,6 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} - selderee@0.11.0: - resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} - semifies@1.0.0: resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} @@ -3816,9 +3720,6 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} - standardwebhooks@1.0.0: - resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} - std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -3946,9 +3847,6 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4952,15 +4850,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@react-email/render@1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - html-to-text: 9.0.5 - prettier: 3.9.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-promise-suspense: 0.3.4 - optional: true - '@react-pdf/fns@3.1.3': {} '@react-pdf/font@4.0.8': @@ -5213,12 +5102,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@selderee/plugin-htmlparser2@0.11.0': - dependencies: - domhandler: 5.0.3 - selderee: 0.11.0 - optional: true - '@sentry/babel-plugin-component-annotate@5.3.0': {} '@sentry/browser-utils@10.66.0': @@ -5467,8 +5350,6 @@ snapshots: - rollup - supports-color - '@stablelib/base64@1.0.1': {} - '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': @@ -6012,12 +5893,6 @@ snapshots: baseline-browser-mapping@2.10.42: {} - bent@7.3.12: - dependencies: - bytesish: 0.4.4 - caseless: 0.12.0 - is-stream: 2.0.1 - better-result@2.9.2: {} bidi-js@1.0.3: @@ -6055,8 +5930,6 @@ snapshots: buffer-from@1.1.2: {} - bytesish@0.4.4: {} - c12@3.3.4(magicast@0.5.3): dependencies: chokidar: 5.0.0 @@ -6095,8 +5968,6 @@ snapshots: caniuse-lite@1.0.30001802: {} - caseless@0.12.0: {} - chai@6.2.2: {} chalk@4.1.2: @@ -6199,9 +6070,6 @@ snapshots: deepmerge-ts@7.1.5: {} - deepmerge@4.3.1: - optional: true - define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -6234,28 +6102,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - optional: true - - domelementtype@2.3.0: - optional: true - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - optional: true - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - optional: true - dotenv@16.6.1: {} dotenv@17.4.2: {} @@ -6290,9 +6136,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 - entities@4.5.0: - optional: true - env-paths@3.0.0: {} environment@1.1.0: {} @@ -6675,9 +6518,6 @@ snapshots: fast-copy@4.0.4: {} - fast-deep-equal@2.0.1: - optional: true - fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -6694,8 +6534,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-sha256@1.3.0: {} - fast-uri@3.1.3: {} fastq@1.20.1: @@ -6884,23 +6722,6 @@ snapshots: html-escaper@2.0.2: {} - html-to-text@9.0.5: - dependencies: - '@selderee/plugin-htmlparser2': 0.11.0 - deepmerge: 4.3.1 - dom-serializer: 2.0.0 - htmlparser2: 8.0.2 - selderee: 0.11.0 - optional: true - - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - optional: true - http-status-codes@2.3.0: {} https-proxy-agent@5.0.1: @@ -7045,8 +6866,6 @@ snapshots: dependencies: call-bound: 1.0.4 - is-stream@2.0.1: {} - is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -7160,9 +6979,6 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 - leac@0.6.0: - optional: true - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -7490,12 +7306,6 @@ snapshots: parse-svg-path@0.1.2: {} - parseley@0.12.1: - dependencies: - leac: 0.6.0 - peberminta: 0.9.0 - optional: true - path-exists@4.0.0: {} path-key@3.1.1: {} @@ -7509,15 +7319,6 @@ snapshots: pathe@2.0.3: {} - paymongo@1.3.2: - dependencies: - bent: 7.3.12 - path-parse: 1.0.7 - tmpl: 1.0.5 - - peberminta@0.9.0: - optional: true - perfect-debounce@2.1.0: {} pg-cloudflare@1.4.0: @@ -7617,8 +7418,6 @@ snapshots: possible-typed-array-names@1.1.0: {} - postal-mime@2.7.4: {} - postcss-value-parser@4.2.0: {} postcss@8.4.31: @@ -7723,11 +7522,6 @@ snapshots: react-is@17.0.2: {} - react-promise-suspense@0.3.4: - dependencies: - fast-deep-equal: 2.0.1 - optional: true - react@19.2.7: {} readdirp@5.0.0: {} @@ -7772,13 +7566,6 @@ snapshots: transitivePeerDependencies: - supports-color - resend@6.17.2(@react-email/render@1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7)): - dependencies: - postal-mime: 2.7.4 - standardwebhooks: 1.0.0 - optionalDependencies: - '@react-email/render': 1.1.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -7899,11 +7686,6 @@ snapshots: secure-json-parse@4.1.0: {} - selderee@0.11.0: - dependencies: - parseley: 0.12.1 - optional: true - semifies@1.0.0: {} semver@6.3.1: {} @@ -8043,11 +7825,6 @@ snapshots: dependencies: type-fest: 0.7.1 - standardwebhooks@1.0.0: - dependencies: - '@stablelib/base64': 1.0.1 - fast-sha256: 1.3.0 - std-env@3.10.0: {} std-env@4.2.0: {} @@ -8184,8 +7961,6 @@ snapshots: tinyrainbow@3.1.0: {} - tmpl@1.0.5: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 diff --git a/scripts/setup-db.sh b/scripts/setup-db.sh new file mode 100755 index 0000000..fb4ef50 --- /dev/null +++ b/scripts/setup-db.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# One-command database wiring for the stripped launch build. +# +# Usage: +# DATABASE_URL=... ADMIN_EMAIL=... ADMIN_PASSWORD=... ./scripts/setup-db.sh +# +# Runs, in order: schema migrations, curriculum content import (courses, +# modules, lessons, quizzes), then the seed (admin account, pricing tiers, +# badges, live classes). The import must precede the seed — the seed's +# live-class step needs a published course to attach to. Everything is +# idempotent; safe to re-run. +set -euo pipefail + +: "${DATABASE_URL:?DATABASE_URL is required}" +: "${ADMIN_EMAIL:?ADMIN_EMAIL is required}" +: "${ADMIN_PASSWORD:?ADMIN_PASSWORD is required}" + +echo "==> prisma migrate deploy" +pnpm prisma:deploy + +echo "==> importing curriculum content" +pnpm tsx scripts/import-amph-content.ts + +echo "==> seeding admin, tiers, badges, live classes" +pnpm prisma db seed + +echo "==> done. Sign in at /auth/signin as ${ADMIN_EMAIL}." diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index f875b85..ac5b61e 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -9,7 +9,6 @@ const STUDENT_NAV_ITEMS: NavItem[] = [ { href: '/tools', label: 'Tools', icon: 'Gear' }, { href: '/live-classes', label: 'Live classes', icon: 'Video' }, { href: '/certificates', label: 'Certificates', icon: 'GraduationCap' }, - { href: '/payments', label: 'Payments', icon: 'CreditCard' }, ]; export default async function DashboardLayout({ diff --git a/src/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsx b/src/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsx deleted file mode 100644 index a21a4c1..0000000 --- a/src/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { useState, useTransition } from 'react'; -import { useRouter } from 'next/navigation'; -import { Card, CardHeader, CardTitle, CardDescription, Button } from '@/components/ui'; -import { Toast } from '@/components/ui/Toast'; -import { createRefundRequestAction } from '@/app/actions/refunds'; -import styles from './request-refund.module.css'; - -interface FormProps { - paymentId: string; - amountPhp: number; -} - -export function RequestRefundForm({ paymentId, amountPhp }: FormProps) { - const router = useRouter(); - const [isPending, startTransition] = useTransition(); - const [error, setError] = useState(null); - const { toast } = Toast.useToast(); - - async function handleSubmit(formData: FormData) { - setError(null); - const result = await createRefundRequestAction({ - paymentId, - reason: formData.get('reason') as string, - }); - if (result.success) { - toast('Refund request sent', 'success'); - router.push('/dashboard/payments?refund=requested'); - router.refresh(); - return; - } - setError(result.error); - } - - return ( - - - Reason for refund - - Minimum 10 characters. Be specific — "wrong tier for me" is more - helpful than "changed mind". - - -
startTransition(() => handleSubmit(formData))} - className={styles.form} - > - {error && ( -
- {error} -
- )} -
- Refund amount - - {new Intl.NumberFormat('en-PH', { - style: 'currency', - currency: 'PHP', - maximumFractionDigits: 2, - }).format(amountPhp / 100)} - - Full refund only at this stage. -
-
- -