Skip to content

Strip PayMongo + Resend for manual-enrollment launch build#61

Merged
projectamazonph merged 5 commits into
mainfrom
claude/deployment-plan-stripped-phwr1r
Jul 20, 2026
Merged

Strip PayMongo + Resend for manual-enrollment launch build#61
projectamazonph merged 5 commits into
mainfrom
claude/deployment-plan-stripped-phwr1r

Conversation

@projectamazonph

@projectamazonph projectamazonph commented Jul 19, 2026

Copy link
Copy Markdown
Owner
  • Remove the whole payment stack: PayMongo lib/SDK dep, checkout action +
    /checkout/complete, webhook routes (/api/paymongo, /api/resend), refunds
    (student flow + admin review), receipts/invoice PDFs, webhook signatures,
    and the dashboard Payments page/nav.
  • Replace Resend email with no-op logging stubs (src/lib/email.ts); drop the
    resend dependency. Callers (live-class reminders) keep working.
  • Add manual enrollment: grantManualEnrollment() enrolls a student in every
    course of a pricing tier inside one transaction, reusing the claim-token
    placeholder-account flow; new /admin/enroll page surfaces the one-time
    claim link for the admin to send manually. Audit-logged.
  • Rework admin dashboard around enrollments (recent enrollments table,
    Enroll Student quick action); link enroll form from user detail page.
  • Pricing page: tier cards keep prices, CTA now signup + message-us-to-pay;
    FAQ updated for the manual workflow.
  • Prisma schema untouched (Payment/CheckoutSession/RefundRequest tables kept
    empty) so the payment stack can be restored from history without migration
    surgery.
  • Docs: LAUNCH-DEPLOY.md deploy runbook; .env.example now needs only
    DATABASE_URL + JWT_SECRET (+ NEXT_PUBLIC_APP_URL for claim links).

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01VtZMXSurdD45BWD7qZXudY

Summary by CodeRabbit

  • New Features

    • Added an admin “Enroll a Student” flow with tier/course assignment and one-time claim links for new accounts.
    • Updated the dashboard to show enrollment-focused activity and quick actions.
    • Added simplified database setup automation for the launch build.
  • Changes

    • Replaced online checkout with manual payment guidance; updated pricing CTAs accordingly.
    • Removed in-app payments, refunds, and receipt/download features; adjusted navigation (including bottom nav) to match.
    • Updated environment templates and launch deployment documentation; disabled outbound email delivery for this build.
  • Tests

    • Narrowed enrollment unit tests; removed payment/refund/webhook-signature test suites.

claude added 2 commits July 19, 2026 21:30
- Remove the whole payment stack: PayMongo lib/SDK dep, checkout action +
  /checkout/complete, webhook routes (/api/paymongo, /api/resend), refunds
  (student flow + admin review), receipts/invoice PDFs, webhook signatures,
  and the dashboard Payments page/nav.
- Replace Resend email with no-op logging stubs (src/lib/email.ts); drop the
  resend dependency. Callers (live-class reminders) keep working.
- Add manual enrollment: grantManualEnrollment() enrolls a student in every
  course of a pricing tier inside one transaction, reusing the claim-token
  placeholder-account flow; new /admin/enroll page surfaces the one-time
  claim link for the admin to send manually. Audit-logged.
- Rework admin dashboard around enrollments (recent enrollments table,
  Enroll Student quick action); link enroll form from user detail page.
- Pricing page: tier cards keep prices, CTA now signup + message-us-to-pay;
  FAQ updated for the manual workflow.
- Prisma schema untouched (Payment/CheckoutSession/RefundRequest tables kept
  empty) so the payment stack can be restored from history without migration
  surgery.
- Docs: LAUNCH-DEPLOY.md deploy runbook; .env.example now needs only
  DATABASE_URL + JWT_SECRET (+ NEXT_PUBLIC_APP_URL for claim links).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtZMXSurdD45BWD7qZXudY
Verified end-to-end against a real Postgres: migrate → content import →
seed, then admin sign-in → /admin/enroll → claim link → student password
set → course visible on student dashboard. The runbook previously omitted
the curriculum import step, without which the seed has no course to attach
live classes to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtZMXSurdD45BWD7qZXudY
Copilot AI review requested due to automatic review settings July 19, 2026 21:48
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 80634412-3312-41e4-b1a3-1b5d6f203981

📥 Commits

Reviewing files that changed from the base of the PR and between e9f15cb and d4d3eda.

📒 Files selected for processing (4)
  • .env.example
  • src/app/actions/checkout.ts
  • src/app/admin/page.tsx
  • src/styles/globals.css
💤 Files with no reviewable changes (2)
  • src/app/actions/checkout.ts
  • src/app/admin/page.tsx

📝 Walkthrough

Walkthrough

The launch build removes PayMongo checkout, refunds, receipts, and Resend sending, replaces pricing CTAs with manual-payment enrollment, and adds admin enrollment with transactional course grants and claim links. Admin dashboards, deployment configuration, setup automation, and enrollment-focused tests are updated accordingly.

Changes

Launch payment and email surface

Layer / File(s) Summary
Manual-payment launch configuration
.env.example, docs/LAUNCH-DEPLOY.md, package.json, src/lib/email.ts, src/app/actions/live-classes.ts
Launch variables and documentation now describe manual payments; PayMongo dependencies and outbound email sending are removed, with live-class reminders reduced to logging.
Public enrollment entry points
src/app/(public)/pricing/*
Checkout and discount handling are removed. Pricing CTAs now link to dashboard or signup, with manual-payment instructions and enrollment button styling.
Student navigation and route matching
src/app/(dashboard)/layout.tsx, src/components/ui/BottomNav.tsx, src/middleware.ts
Payments navigation and middleware matching are removed, and the bottom navigation profile target points to certificates.

Manual enrollment domain

Layer / File(s) Summary
Transactional enrollment logic
src/lib/enrollment.ts
PayMongo webhook contracts and handlers are replaced with pricing-tier validation, placeholder-user creation, claim-token handling, and active enrollment creation for missing courses.
Enrollment validation tests
src/lib/__tests__/enrollment.test.ts
Tests now cover email normalization, placeholder accounts, tier validation, course grants, existing enrollments, and claim-token results.

Admin enrollment interface

Layer / File(s) Summary
Admin enrollment action
src/app/actions/admin-enroll.ts
An authorized server action validates input, grants enrollments, records an audit event, revalidates admin paths, and returns enrollment counts plus an optional claim URL.
Enrollment page and form
src/app/admin/enroll/*
Admins can select a pricing tier, enter student details, view enrollment results, and copy a generated claim link.
Enrollment entry links
src/app/admin/users/[id]/page.tsx, docs/LAUNCH-DEPLOY.md
User details and launch documentation provide prefilled enrollment paths and the manual enrollment workflow.

Admin operations and deployment

Layer / File(s) Summary
Enrollment-focused dashboard
src/app/admin/page.tsx
Dashboard metrics and recent activity switch from payments and refunds to active enrollments, with new enrollment and analytics quick actions.
Database setup workflow
scripts/setup-db.sh, docs/LAUNCH-DEPLOY.md
Database migration, content import, and seeding are automated in order and documented with deployment and enrollment smoke-test steps.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant EnrollForm
  participant manualEnrollAction
  participant Prisma
  Admin->>EnrollForm: enter student and pricing tier
  EnrollForm->>manualEnrollAction: submit enrollment
  manualEnrollAction->>Prisma: grant courses and create claim token
  Prisma-->>manualEnrollAction: enrollment result
  manualEnrollAction-->>EnrollForm: counts and optional claim URL
  EnrollForm-->>Admin: display result and copy link
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: removing PayMongo and Resend for a manual-enrollment launch build.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/deployment-plan-stripped-phwr1r

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strips the PayMongo payments + refunds stack and Resend email sending from the launch build, replacing paid enrollment with an admin-driven manual enrollment flow (including claim-link generation for new placeholder accounts). It also updates navigation and public pricing copy to reflect “sign up, then pay and message us” instead of in-app checkout.

Changes:

  • Removed PayMongo checkout/webhooks/refunds/receipts and the related UI routes, actions, libs, and tests.
  • Replaced Resend-backed email sending with a server-only no-op logger stub so call sites keep working.
  • Added /admin/enroll + manualEnrollAction and updated admin dashboard to focus on enrollments; added deploy/runbook docs and simplified .env.example.

Reviewed changes

Copilot reviewed 47 out of 49 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/types/paymongo.d.ts Removed PayMongo module type shim.
src/middleware.ts Dropped /payments matcher now that the route is removed.
src/lib/webhook-signature.ts Removed PayMongo webhook signature verification helper.
src/lib/refunds.ts Removed refund business logic helpers/queries.
src/lib/receipts.tsx Removed invoice issuance + PDF persistence logic.
src/lib/receipt-pdf.tsx Removed receipt PDF renderer component/types.
src/lib/paymongo.ts Removed PayMongo SDK wrapper client and error type.
src/lib/email.tsx Removed Resend-backed email implementation and templates.
src/lib/email.ts Added server-only no-op email stub (logs instead of sending).
src/lib/tests/webhook-signature.test.ts Removed tests for deleted webhook signature verifier.
src/lib/tests/refunds.test.ts Removed tests for deleted refunds helpers.
src/lib/tests/paymongo.test.ts Removed tests for deleted PayMongo wrapper.
src/lib/tests/enrollment.test.ts Updated enrollment tests to cover manual enrollment flow.
src/components/ui/BottomNav.tsx Updated bottom-nav destination away from removed payments route.
src/app/api/resend/webhook/route.ts Removed Resend webhook receiver route.
src/app/api/paymongo/webhook/route.ts Removed PayMongo webhook receiver route.
src/app/api/invoices/[id]/pdf/route.ts Removed invoice PDF download API route.
src/app/admin/users/[id]/page.tsx Added “enroll in a tier” link from user detail page.
src/app/admin/refunds/refunds.module.css Removed refunds admin page styles.
src/app/admin/refunds/page.tsx Removed refunds admin queue page.
src/app/admin/refunds/[id]/RefundDecisionForm.tsx Removed refunds admin decision form.
src/app/admin/refunds/[id]/page.tsx Removed refunds admin detail page.
src/app/admin/refunds/[id]/detail.module.css Removed refunds admin detail styles.
src/app/admin/page.tsx Switched admin dashboard from payments/refunds to enrollments.
src/app/admin/enroll/page.tsx Added admin “Enroll a Student” page.
src/app/admin/enroll/EnrollForm.tsx Added client enroll form + claim-link copy UI.
src/app/admin/enroll/enroll.module.css Added styles for the enroll page/form.
src/app/actions/refunds.ts Removed refunds server actions (student + admin paths).
src/app/actions/live-classes.ts Updated comment to reflect email stub behavior.
src/app/actions/checkout.ts Removed checkout/payment server actions.
src/app/actions/admin-enroll.ts Added manual enrollment server action + audit logging.
src/app/(public)/pricing/pricing.module.css Added styles for new “enroll” CTA link buttons.
src/app/(public)/pricing/page.tsx Replaced checkout CTA with signup/dashboard link + updated FAQ copy.
src/app/(public)/pricing/CheckoutButton.tsx Removed PayMongo checkout button component.
src/app/(public)/pricing/checkout-button.module.css Removed checkout button styles.
src/app/(public)/checkout/complete/page.tsx Removed PayMongo checkout return/complete page.
src/app/(public)/checkout/complete/complete.module.css Removed checkout complete page styles.
src/app/(dashboard)/payments/payments.module.css Removed student payments page styles.
src/app/(dashboard)/payments/page.tsx Removed student payments history page.
src/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsx Removed refund request form component.
src/app/(dashboard)/payments/[id]/request-refund/request-refund.module.css Removed refund request page styles.
src/app/(dashboard)/payments/[id]/request-refund/page.tsx Removed refund request page.
src/app/(dashboard)/layout.tsx Removed “Payments” nav item from dashboard layout.
scripts/setup-db.sh Added one-command DB setup script (migrate → import → seed).
pnpm-lock.yaml Removed PayMongo/Resend dependencies and transitive packages.
package.json Removed PayMongo and Resend dependencies.
docs/LAUNCH-DEPLOY.md Added stripped-build deploy/runbook documentation.
.env.example Simplified env example for stripped build + added seed vars.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

{ key: 'courses', label: 'Courses', href: '/dashboard/courses', icon: 'BookOpen' },
{ key: 'tools', label: 'Tools', href: '/dashboard/tools', icon: 'Gear' },
{ key: 'profile', label: 'Profile', href: '/dashboard/payments', icon: 'User' },
{ key: 'profile', label: 'Profile', href: '/dashboard/certificates', icon: 'User' },
Comment on lines 132 to +137
<h2 className={styles.cardTitle}>
Enrollments ({user.enrollments.length})
{' — '}
<Link href={`/admin/enroll?email=${encodeURIComponent(user.email)}`}>
enroll in a tier
</Link>
Comment on lines +99 to +102
<option key={tier.id} value={tier.id}>
{tier.name} — {tier.priceLabel} ({tier.courseCount}{' '}
{tier.courseCount === 1 ? 'course' : 'courses'})
</option>
Comment on lines +115 to +119
<p className={styles.resultTitle}>
✓ Enrolled in {result.tierName}
{result.enrolledCount > 0
? ` — ${result.enrolledCount} ${result.enrolledCount === 1 ? 'course' : 'courses'} granted`
: ''}
Comment on lines +90 to +93
<p className={styles.fineprint}>
Pay via GCash or bank transfer — message us on Facebook or email
after signing up and we&apos;ll activate your access within 24 hours.
</p>
Comment on lines +103 to +106
Create a free account, then pay via GCash or bank transfer and
message us on Facebook or email with your account email. We
activate your access manually — usually within 24 hours.
</p>
Comment thread docs/LAUNCH-DEPLOY.md
Comment on lines +5 to +7
(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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(public)/pricing/page.tsx:
- Around line 91-105: Replace all em-dashes in the specified code text: in
src/app/(public)/pricing/page.tsx lines 91-105, use commas in the fineprint and
FAQ strings; in src/app/actions/admin-enroll.ts line 27, replace the JSDoc
em-dash with a comma; in src/app/admin/enroll/page.tsx line 35, replace the
subtitle em-dash with a comma; and in src/app/admin/enroll/EnrollForm.tsx line
128, replace the claim-note em-dash with a comma or parentheses. Preserve the
existing wording and behavior.
- Line 91: Update the user-facing strings near the pricing page entries at lines
91 and 105 to replace em-dashes with an approved punctuation mark such as a
period, comma, or parentheses, while preserving the original meaning.

In `@src/app/actions/admin-enroll.ts`:
- Around line 62-69: Update the claim URL construction in the rawClaimToken
branch of the admin enrollment action to handle a missing NEXT_PUBLIC_APP_URL
explicitly in production instead of silently falling back to localhost; either
fail with a clear error or use the project’s established warning mechanism
before retaining the development fallback for non-production environments.
- Around line 1-88: Add direct unit tests for manualEnrollAction covering
invalid input validation, requireAdmin authorization, successful enrollment with
claim URL and revalidation/audit calls, and grantManualEnrollment failure
handling. Mock requireAdmin, grantManualEnrollment, auditLog, and
revalidatePath, and assert the returned ActionResult for each path without
duplicating lower-level grantManualEnrollment tests.
- Around line 47-58: Wrap the auditLog call in the admin enrollment action with
its own try/catch so an audit failure does not fail the already-committed
enrollment or surface an error to the admin. In the catch block, report the
failure through the existing structured logger, while preserving the current
grantManualEnrollment and successful response flow.

In `@src/app/admin/enroll/EnrollForm.tsx`:
- Around line 64-157: Replace the outer formCard div in EnrollForm with a form
element that handles onSubmit, calling preventDefault before handleSubmit. Move
submission behavior from the Button’s onClick to the form submission flow, while
preserving the existing fields, validation, loading state, and result rendering.

In `@src/app/admin/page.tsx`:
- Around line 29-37: Update both enrollment queries in the admin page: add
deletedAt: null to the where clause of the active enrollment count and to the
findMany query used for Recent Enrollments. Preserve the existing status filter,
relations, ordering, and limit.

In `@src/app/admin/users/`[id]/page.tsx:
- Around line 134-137: Replace the em-dash separator in the user enrollment link
section with a period or comma, preserving the surrounding spacing and existing
Link behavior.

In `@src/lib/enrollment.ts`:
- Around line 108-114: Remove all em-dash characters across
src/lib/enrollment.ts and src/lib/email.ts, replacing them with periods, commas,
or parentheses. In src/lib/enrollment.ts, update the message in the tier course
validation near the tier lookup and the comments around the referenced sections;
in src/lib/email.ts, update the header comment near the file start. Preserve the
existing behavior and wording intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c0ff8bd-aec7-4b6d-9740-99e45b6efba2

📥 Commits

Reviewing files that changed from the base of the PR and between 78f1931 and dc72506.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (48)
  • .env.example
  • docs/LAUNCH-DEPLOY.md
  • package.json
  • scripts/setup-db.sh
  • src/app/(dashboard)/layout.tsx
  • src/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsx
  • src/app/(dashboard)/payments/[id]/request-refund/page.tsx
  • src/app/(dashboard)/payments/[id]/request-refund/request-refund.module.css
  • src/app/(dashboard)/payments/page.tsx
  • src/app/(dashboard)/payments/payments.module.css
  • src/app/(public)/checkout/complete/complete.module.css
  • src/app/(public)/checkout/complete/page.tsx
  • src/app/(public)/pricing/CheckoutButton.tsx
  • src/app/(public)/pricing/checkout-button.module.css
  • src/app/(public)/pricing/page.tsx
  • src/app/(public)/pricing/pricing.module.css
  • src/app/actions/admin-enroll.ts
  • src/app/actions/checkout.ts
  • src/app/actions/live-classes.ts
  • src/app/actions/refunds.ts
  • src/app/admin/enroll/EnrollForm.tsx
  • src/app/admin/enroll/enroll.module.css
  • src/app/admin/enroll/page.tsx
  • src/app/admin/page.tsx
  • src/app/admin/refunds/[id]/RefundDecisionForm.tsx
  • src/app/admin/refunds/[id]/detail.module.css
  • src/app/admin/refunds/[id]/page.tsx
  • src/app/admin/refunds/page.tsx
  • src/app/admin/refunds/refunds.module.css
  • src/app/admin/users/[id]/page.tsx
  • src/app/api/invoices/[id]/pdf/route.ts
  • src/app/api/paymongo/webhook/route.ts
  • src/app/api/resend/webhook/route.ts
  • src/components/ui/BottomNav.tsx
  • src/lib/__tests__/enrollment.test.ts
  • src/lib/__tests__/paymongo.test.ts
  • src/lib/__tests__/refunds.test.ts
  • src/lib/__tests__/webhook-signature.test.ts
  • src/lib/email.ts
  • src/lib/email.tsx
  • src/lib/enrollment.ts
  • src/lib/paymongo.ts
  • src/lib/receipt-pdf.tsx
  • src/lib/receipts.tsx
  • src/lib/refunds.ts
  • src/lib/webhook-signature.ts
  • src/middleware.ts
  • src/types/paymongo.d.ts
💤 Files with no reviewable changes (32)
  • src/app/api/invoices/[id]/pdf/route.ts
  • src/app/admin/refunds/[id]/detail.module.css
  • src/app/(dashboard)/payments/[id]/request-refund/request-refund.module.css
  • package.json
  • src/app/admin/refunds/refunds.module.css
  • src/app/api/resend/webhook/route.ts
  • src/types/paymongo.d.ts
  • src/lib/tests/webhook-signature.test.ts
  • src/lib/tests/paymongo.test.ts
  • src/lib/tests/refunds.test.ts
  • src/lib/webhook-signature.ts
  • src/app/(public)/checkout/complete/complete.module.css
  • src/middleware.ts
  • src/app/admin/refunds/page.tsx
  • src/app/api/paymongo/webhook/route.ts
  • src/lib/paymongo.ts
  • src/app/actions/checkout.ts
  • src/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsx
  • src/lib/receipt-pdf.tsx
  • src/app/(dashboard)/payments/[id]/request-refund/page.tsx
  • src/app/(dashboard)/payments/payments.module.css
  • src/app/(dashboard)/layout.tsx
  • src/app/admin/refunds/[id]/RefundDecisionForm.tsx
  • src/app/(public)/checkout/complete/page.tsx
  • src/lib/refunds.ts
  • src/app/admin/refunds/[id]/page.tsx
  • src/app/(public)/pricing/checkout-button.module.css
  • src/app/(public)/pricing/CheckoutButton.tsx
  • src/lib/email.tsx
  • src/app/(dashboard)/payments/page.tsx
  • src/app/actions/refunds.ts
  • src/lib/receipts.tsx


<p className={styles.fineprint}>Pay via GCash, Maya, credit card, or bank transfer.</p>
<p className={styles.fineprint}>
Pay via GCash or bank transfer — message us on Facebook or email

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Em-dashes in .tsx file violate coding guidelines.

The coding guideline for **/*.{ts,tsx,js,jsx} prohibits em-dashes. Use periods, commas, or parentheses instead. Found on lines 91 and 105 in user-facing strings.

Also applies to: 105-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(public)/pricing/page.tsx at line 91, Update the user-facing strings
near the pricing page entries at lines 91 and 105 to replace em-dashes with an
approved punctuation mark such as a period, comma, or parentheses, while
preserving the original meaning.

Source: Coding guidelines

Comment on lines +91 to +105
Pay via GCash or bank transfer — message us on Facebook or email
after signing up and we&apos;ll activate your access within 24 hours.
</p>
</Card>
))}
</div>

<section className={styles.faqBlock}>
<h2>Questions</h2>
<details>
<summary>How do I enroll and pay?</summary>
<p>
Create a free account, then pay via GCash or bank transfer and
message us on Facebook or email with your account email. We
activate your access manually — usually within 24 hours.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Em-dashes in .ts/.tsx files violate coding guidelines across the PR.

The coding guideline for **/*.{ts,tsx,js,jsx} states: "Do not use em-dashes. Use periods, commas, or parentheses instead." All four sites use em-dashes in user-facing strings or comments within code files. Replace each with a comma or parentheses.

  • src/app/(public)/pricing/page.tsx#L91-L105: Replace em-dashes in the fineprint ("bank transfer, message us") and FAQ ("manually, usually within 24 hours").
  • src/app/actions/admin-enroll.ts#L27-L27: Replace em-dash in the JSDoc comment ("accounts, show once").
  • src/app/admin/enroll/page.tsx#L35-L35: Replace em-dash in the subtitle ("claim link, send it to them").
  • src/app/admin/enroll/EnrollForm.tsx#L128-L128: Replace em-dash in the claim note ("to the student, they use it").
📍 Affects 4 files
  • src/app/(public)/pricing/page.tsx#L91-L105 (this comment)
  • src/app/actions/admin-enroll.ts#L27-L27
  • src/app/admin/enroll/page.tsx#L35-L35
  • src/app/admin/enroll/EnrollForm.tsx#L128-L128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(public)/pricing/page.tsx around lines 91 - 105, Replace all
em-dashes in the specified code text: in src/app/(public)/pricing/page.tsx lines
91-105, use commas in the fineprint and FAQ strings; in
src/app/actions/admin-enroll.ts line 27, replace the JSDoc em-dash with a comma;
in src/app/admin/enroll/page.tsx line 35, replace the subtitle em-dash with a
comma; and in src/app/admin/enroll/EnrollForm.tsx line 128, replace the
claim-note em-dash with a comma or parentheses. Preserve the existing wording
and behavior.

Source: Coding guidelines

Comment on lines +1 to +88
'use server';

/**
* Manual enrollment action — stripped launch build.
*
* Admin enters a student email + pricing tier; we create/find the user and
* enroll them in every course on the tier. For brand-new students the
* one-time claim link is returned so the admin can send it to the student
* over Messenger/email themselves (no automated email in this build).
*/

import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { requireAdmin } from '@/lib/auth';
import { auditLog } from '@/lib/admin-audit';
import { grantManualEnrollment } from '@/lib/enrollment';
import type { ActionResult } from '@/lib/validation';

const manualEnrollSchema = z.object({
email: z.string().trim().toLowerCase().email('Enter a valid email address.'),
name: z.string().trim().max(100).optional(),
pricingTierId: z.string().min(1, 'Pick a pricing tier.'),
});

export interface ManualEnrollActionData {
isNewUser: boolean;
/** Full signup link for new accounts — show once, admin sends it manually. */
claimUrl?: string;
tierName: string;
enrolledCount: number;
alreadyEnrolledCount: number;
}

export async function manualEnrollAction(
input: z.infer<typeof manualEnrollSchema>,
): Promise<ActionResult<ManualEnrollActionData>> {
await requireAdmin();

const parsed = manualEnrollSchema.safeParse(input);
if (!parsed.success) {
return {
success: false,
error: parsed.error.issues[0]?.message ?? 'Invalid input.',
};
}

try {
const result = await grantManualEnrollment({
email: parsed.data.email,
name: parsed.data.name || null,
pricingTierId: parsed.data.pricingTierId,
});

await auditLog({
action: 'MANUAL_ENROLL',
entityType: 'User',
entityId: result.userId,
});
revalidatePath('/admin/users');
revalidatePath(`/admin/users/${result.userId}`);

let claimUrl: string | undefined;
if (result.rawClaimToken) {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
const url = new URL('/auth/signup', appUrl);
url.searchParams.set('claim', result.rawClaimToken);
url.searchParams.set('email', parsed.data.email);
url.searchParams.set('next', '/dashboard');
claimUrl = url.toString();
}

return {
success: true,
data: {
isNewUser: result.isNewUser,
claimUrl,
tierName: result.tierName,
enrolledCount: result.enrolledCourseIds.length,
alreadyEnrolledCount: result.alreadyEnrolledCourseIds.length,
},
};
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : 'Enrollment failed.',
};
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for test files covering admin-enroll.
rg -l 'manualEnrollAction' --glob '*test*' --glob '*spec*'
fd -t f 'admin-enroll' --glob '*test*' --glob '*spec*'

Repository: projectamazonph/amph-v2

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- repo files matching admin-enroll / manualEnrollAction / grantManualEnrollment ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
  -e 'manualEnrollAction' \
  -e 'grantManualEnrollment' \
  -e 'admin-enroll' \
  .

echo '--- test/spec files under src/app/actions and nearby ---'
fd -t f . src/app/actions | rg '(\.test\.|\.spec\.|test|spec)'

echo '--- all tests mentioning admin actions or enrollment ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' \
  -e 'admin enroll' \
  -e 'enrollment' \
  -e 'requireAdmin' \
  -e 'auditLog' \
  -e 'revalidatePath' \
  tests src app lib

Repository: projectamazonph/amph-v2

Length of output: 17311


Add direct tests for manualEnrollAction src/lib/__tests__/enrollment.test.ts covers grantManualEnrollment, but src/app/actions/admin-enroll.ts still needs coverage for validation, requireAdmin, success, and error paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/actions/admin-enroll.ts` around lines 1 - 88, Add direct unit tests
for manualEnrollAction covering invalid input validation, requireAdmin
authorization, successful enrollment with claim URL and revalidation/audit
calls, and grantManualEnrollment failure handling. Mock requireAdmin,
grantManualEnrollment, auditLog, and revalidatePath, and assert the returned
ActionResult for each path without duplicating lower-level grantManualEnrollment
tests.

Source: Coding guidelines

Comment on lines +47 to +58
try {
const result = await grantManualEnrollment({
email: parsed.data.email,
name: parsed.data.name || null,
pricingTierId: parsed.data.pricingTierId,
});

await auditLog({
action: 'MANUAL_ENROLL',
entityType: 'User',
entityId: result.userId,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Audit log is not in the same transaction as the enrollment.

grantManualEnrollment commits its transaction, then auditLog is called separately. If auditLog throws, the enrollment is committed but not audited, violating the guideline that every admin mutation must create an AuditLog entry. The admin also sees an error despite the enrollment succeeding.

Consider including the audit log inside the grantManualEnrollment transaction, or at minimum wrapping auditLog in its own try/catch and logging the failure via the structured logger without failing the action.

Fix: wrap auditLog to avoid failing the action on audit failure
   try {
     const result = await grantManualEnrollment({
       email: parsed.data.email,
       name: parsed.data.name || null,
       pricingTierId: parsed.data.pricingTierId,
     });

+    try {
       await auditLog({
         action: 'MANUAL_ENROLL',
         entityType: 'User',
         entityId: result.userId,
       });
+    } catch (auditErr) {
+      logger.error(
+        { component: 'admin-enroll', userId: result.userId, err: auditErr },
+        'auditLog failed after successful enrollment',
+      );
+    }
     revalidatePath('/admin/users');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const result = await grantManualEnrollment({
email: parsed.data.email,
name: parsed.data.name || null,
pricingTierId: parsed.data.pricingTierId,
});
await auditLog({
action: 'MANUAL_ENROLL',
entityType: 'User',
entityId: result.userId,
});
try {
const result = await grantManualEnrollment({
email: parsed.data.email,
name: parsed.data.name || null,
pricingTierId: parsed.data.pricingTierId,
});
try {
await auditLog({
action: 'MANUAL_ENROLL',
entityType: 'User',
entityId: result.userId,
});
} catch (auditErr) {
logger.error(
{ component: 'admin-enroll', userId: result.userId, err: auditErr },
'auditLog failed after successful enrollment',
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/actions/admin-enroll.ts` around lines 47 - 58, Wrap the auditLog call
in the admin enrollment action with its own try/catch so an audit failure does
not fail the already-committed enrollment or surface an error to the admin. In
the catch block, report the failure through the existing structured logger,
while preserving the current grantManualEnrollment and successful response flow.

Source: Coding guidelines

Comment on lines +62 to +69
let claimUrl: string | undefined;
if (result.rawClaimToken) {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
const url = new URL('/auth/signup', appUrl);
url.searchParams.set('claim', result.rawClaimToken);
url.searchParams.set('email', parsed.data.email);
url.searchParams.set('next', '/dashboard');
claimUrl = url.toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

NEXT_PUBLIC_APP_URL fallback to localhost could silently produce broken claim URLs.

If NEXT_PUBLIC_APP_URL is unset in production, the claim URL will point to http://localhost:3000. The admin may not notice until the student reports the link is broken. Consider logging a warning when the fallback is used, or making the absence of this env var an explicit error in production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/actions/admin-enroll.ts` around lines 62 - 69, Update the claim URL
construction in the rawClaimToken branch of the admin enrollment action to
handle a missing NEXT_PUBLIC_APP_URL explicitly in production instead of
silently falling back to localhost; either fail with a clear error or use the
project’s established warning mechanism before retaining the development
fallback for non-production environments.

Comment on lines +64 to +157
return (
<div className={styles.formCard}>
<label className={styles.field}>
<span>Student email</span>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="student@email.com"
className={styles.input}
autoComplete="off"
/>
</label>

<label className={styles.field}>
<span>Name (optional, used for new accounts)</span>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Student's full name"
className={styles.input}
autoComplete="off"
/>
</label>

<label className={styles.field}>
<span>Pricing tier</span>
<select
value={pricingTierId}
onChange={(e) => setPricingTierId(e.target.value)}
className={styles.select}
>
{tiers.map((tier) => (
<option key={tier.id} value={tier.id}>
{tier.name} — {tier.priceLabel} ({tier.courseCount}{' '}
{tier.courseCount === 1 ? 'course' : 'courses'})
</option>
))}
</select>
</label>

<Button onClick={handleSubmit} loading={isPending} variant="primary">
Enroll student
</Button>

{error && <p className={styles.error}>{error}</p>}

{result && (
<div className={styles.result}>
<p className={styles.resultTitle}>
✓ Enrolled in {result.tierName}
{result.enrolledCount > 0
? ` — ${result.enrolledCount} ${result.enrolledCount === 1 ? 'course' : 'courses'} granted`
: ''}
{result.alreadyEnrolledCount > 0
? ` (${result.alreadyEnrolledCount} already active)`
: ''}
</p>

{result.claimUrl ? (
<div className={styles.claimBlock}>
<p className={styles.claimNote}>
New account created. Send this one-time link to the student —
they use it to set their password. It expires in 7 days and is
shown only once:
</p>
<div className={styles.claimRow}>
<input
readOnly
value={result.claimUrl}
className={styles.claimInput}
onFocus={(e) => e.currentTarget.select()}
/>
<button
type="button"
className={styles.copyBtn}
onClick={() => copyClaimUrl(result.claimUrl!)}
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
) : (
<p className={styles.claimNote}>
The student already has an account — they can sign in as usual
and will see the new courses on their dashboard.
</p>
)}
</div>
)}
</div>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing <form> element prevents keyboard submission.

The form uses a <div> container with a Button onClick instead of a <form onSubmit>. Keyboard users who fill in the email and press Enter cannot submit the form, which is an accessibility blocker. Wrap the fields in a <form> and move handleSubmit to onSubmit with e.preventDefault().

Fix: wrap fields in a form element
-    <div className={styles.formCard}>
+    <form className={styles.formCard} onSubmit={(e) => {
+      e.preventDefault();
+      handleSubmit();
+    }}>
       <label className={styles.field}>
         {/* ... existing fields ... */}
       </label>

-      <Button onClick={handleSubmit} loading={isPending} variant="primary">
+      <Button type="submit" loading={isPending} variant="primary">
         Enroll student
       </Button>

       {error && <p className={styles.error}>{error}</p>}
       {result && ( <div className={styles.result}> {/* ... */} </div> )}
-    </div>
+    </form>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<div className={styles.formCard}>
<label className={styles.field}>
<span>Student email</span>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="student@email.com"
className={styles.input}
autoComplete="off"
/>
</label>
<label className={styles.field}>
<span>Name (optional, used for new accounts)</span>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Student's full name"
className={styles.input}
autoComplete="off"
/>
</label>
<label className={styles.field}>
<span>Pricing tier</span>
<select
value={pricingTierId}
onChange={(e) => setPricingTierId(e.target.value)}
className={styles.select}
>
{tiers.map((tier) => (
<option key={tier.id} value={tier.id}>
{tier.name} {tier.priceLabel} ({tier.courseCount}{' '}
{tier.courseCount === 1 ? 'course' : 'courses'})
</option>
))}
</select>
</label>
<Button onClick={handleSubmit} loading={isPending} variant="primary">
Enroll student
</Button>
{error && <p className={styles.error}>{error}</p>}
{result && (
<div className={styles.result}>
<p className={styles.resultTitle}>
Enrolled in {result.tierName}
{result.enrolledCount > 0
? ` — ${result.enrolledCount} ${result.enrolledCount === 1 ? 'course' : 'courses'} granted`
: ''}
{result.alreadyEnrolledCount > 0
? ` (${result.alreadyEnrolledCount} already active)`
: ''}
</p>
{result.claimUrl ? (
<div className={styles.claimBlock}>
<p className={styles.claimNote}>
New account created. Send this one-time link to the student
they use it to set their password. It expires in 7 days and is
shown only once:
</p>
<div className={styles.claimRow}>
<input
readOnly
value={result.claimUrl}
className={styles.claimInput}
onFocus={(e) => e.currentTarget.select()}
/>
<button
type="button"
className={styles.copyBtn}
onClick={() => copyClaimUrl(result.claimUrl!)}
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
) : (
<p className={styles.claimNote}>
The student already has an account they can sign in as usual
and will see the new courses on their dashboard.
</p>
)}
</div>
)}
</div>
);
return (
<form
className={styles.formCard}
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<label className={styles.field}>
<span>Student email</span>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="student@email.com"
className={styles.input}
autoComplete="off"
/>
</label>
<label className={styles.field}>
<span>Name (optional, used for new accounts)</span>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Student's full name"
className={styles.input}
autoComplete="off"
/>
</label>
<label className={styles.field}>
<span>Pricing tier</span>
<select
value={pricingTierId}
onChange={(e) => setPricingTierId(e.target.value)}
className={styles.select}
>
{tiers.map((tier) => (
<option key={tier.id} value={tier.id}>
{tier.name} {tier.priceLabel} ({tier.courseCount}{' '}
{tier.courseCount === 1 ? 'course' : 'courses'})
</option>
))}
</select>
</label>
<Button type="submit" loading={isPending} variant="primary">
Enroll student
</Button>
{error && <p className={styles.error}>{error}</p>}
{result && (
<div className={styles.result}>
<p className={styles.resultTitle}>
Enrolled in {result.tierName}
{result.enrolledCount > 0
? ` — ${result.enrolledCount} ${result.enrolledCount === 1 ? 'course' : 'courses'} granted`
: ''}
{result.alreadyEnrolledCount > 0
? ` (${result.alreadyEnrolledCount} already active)`
: ''}
</p>
{result.claimUrl ? (
<div className={styles.claimBlock}>
<p className={styles.claimNote}>
New account created. Send this one-time link to the student
they use it to set their password. It expires in 7 days and is
shown only once:
</p>
<div className={styles.claimRow}>
<input
readOnly
value={result.claimUrl}
className={styles.claimInput}
onFocus={(e) => e.currentTarget.select()}
/>
<button
type="button"
className={styles.copyBtn}
onClick={() => copyClaimUrl(result.claimUrl!)}
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
) : (
<p className={styles.claimNote}>
The student already has an account they can sign in as usual
and will see the new courses on their dashboard.
</p>
)}
</div>
)}
</form>
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/admin/enroll/EnrollForm.tsx` around lines 64 - 157, Replace the outer
formCard div in EnrollForm with a form element that handles onSubmit, calling
preventDefault before handleSubmit. Move submission behavior from the Button’s
onClick to the form submission flow, while preserving the existing fields,
validation, loading state, and result rendering.

Comment thread src/app/admin/page.tsx
Comment on lines +29 to 37
db.enrollment.count({ where: { status: 'ACTIVE' } }),
db.enrollment.findMany({
include: {
user: { select: { id: true, name: true, email: true } },
pricingTier: { select: { name: true } },
},
orderBy: { paidAt: 'desc' },
orderBy: { enrolledAt: 'desc' },
take: 10,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Filter enrollment queries by deletedAt: null.

The findMany (lines 30-37) and count (line 29) don't exclude soft-deleted enrollments. The Prisma schema defines deletedAt with @@index([deletedAt]), indicating soft deletes are used. Soft-deleted records could appear in "Recent Enrollments" and inflate the "Active Enrollments" count (the count filters by status: 'ACTIVE' but a soft-deleted enrollment may still have ACTIVE status).

🛡️ Proposed fix
     db.enrollment.count({ where: { status: 'ACTIVE', deletedAt: null } }),
     db.enrollment.findMany({
+      where: { deletedAt: null },
       include: {
         user: { select: { id: true, name: true, email: true } },
         pricingTier: { select: { name: true } },
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
db.enrollment.count({ where: { status: 'ACTIVE' } }),
db.enrollment.findMany({
include: {
user: { select: { id: true, name: true, email: true } },
pricingTier: { select: { name: true } },
},
orderBy: { paidAt: 'desc' },
orderBy: { enrolledAt: 'desc' },
take: 10,
}),
db.enrollment.count({ where: { status: 'ACTIVE', deletedAt: null } }),
db.enrollment.findMany({
where: { deletedAt: null },
include: {
user: { select: { id: true, name: true, email: true } },
pricingTier: { select: { name: true } },
},
orderBy: { enrolledAt: 'desc' },
take: 10,
}),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/admin/page.tsx` around lines 29 - 37, Update both enrollment queries
in the admin page: add deletedAt: null to the where clause of the active
enrollment count and to the findMany query used for Recent Enrollments. Preserve
the existing status filter, relations, ordering, and limit.

Comment on lines +134 to +137
{' — '}
<Link href={`/admin/enroll?email=${encodeURIComponent(user.email)}`}>
enroll in a tier
</Link>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace em-dash with a period or comma per coding guidelines.

Line 134 uses an em-dash () in {' — '}. The coding guidelines for **/*.{ts,tsx} files state: "Do not use em-dashes. Use periods, commas, or parentheses instead."

✏️ Proposed fix
             Enrollments ({user.enrollments.length})
-            {' — '}
+            {' · '}
             <Link href={`/admin/enroll?email=${encodeURIComponent(user.email)}`}>
               enroll in a tier
             </Link>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/admin/users/`[id]/page.tsx around lines 134 - 137, Replace the
em-dash separator in the user enrollment link section with a period or comma,
preserving the surrounding spacing and existing Link behavior.

Source: Coding guidelines

Comment thread src/lib/enrollment.ts
Comment on lines +108 to +114
if (!tier || !tier.isActive || tier.deletedAt) {
throw new Error('Pricing tier not found or inactive.');
}

logger.info(
{ sourceId, paymentId: payment.id, status: payment.status },
'source.chargeable: payment created from source',
);

// Only fulfil when the payment already settled; otherwise the payment.paid
// webhook completes it.
if (payment.status === 'paid') {
// State 2 - durable local fulfillment (DB writes only). A failure here does
// NOT undo the PayMongo charge and must not re-run State 1; payment.paid
// reconciles.
let result: PostPurchaseResult | null = null;
try {
result = await db.$transaction((tx) =>
processPaymentPaidInTransaction(
tx,
payment,
checkout,
event.data.attributes.data.attributes.type,
),
);
} catch (err) {
logger.error(
{ err, sourceId, paymentId: payment.id },
'source.chargeable: local fulfillment failed; payment.paid webhook will reconcile',
);
}
if (result) {
// Best-effort emails, AFTER commit - never awaited inside the transaction.
sendPostPurchaseEmails(result).catch((err: Error) =>
logger.error({ err }, 'Failed to send post-purchase emails'),
);
}
if (tier.courses.length === 0) {
throw new Error('This tier has no courses attached — check the seed data.');
}

return payment;
}

/**
* Handle `payment.paid` webhook.
*
* Fires when a Payment (created from a Source or Payment Intent) reaches
* `paid` status. We reconcile with the CheckoutSession and create the
* local Payment + Enrollment.
*/
export async function handlePaymentPaid(
event: PaymentPaidEvent,
): Promise<{ enrollmentId: string; paymentId: string } | null> {
const eventId = event.data.id;
const paymentIdPm = event.data.attributes.data.attributes.id;
const amountCentavos = event.data.attributes.data.attributes.amount;
const currency = event.data.attributes.data.attributes.currency;
const sourceId = event.data.attributes.data.attributes.source?.id;
// Forward the real payment method so non-GCash payments aren't mis-recorded.
const sourceType = event.data.attributes.data.attributes.source?.type;

const checkoutSelect = {
id: true,
email: true,
finalAmountPhp: true,
pricingTierId: true,
discountCodeId: true,
pricingTier: { select: { name: true, tier: true, slug: true } },
discountCode: { select: { maxUses: true } },
} as const;
const courseIds = tier.courses.map((c: { id: string }) => c.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove em-dashes across src/lib/enrollment.ts and src/lib/email.ts. Shared root cause: em-dash characters violate the repo style rule. As per coding guidelines: "do not use em-dashes. Use periods, commas, or parentheses instead."

  • src/lib/enrollment.ts#L108-L114: rewrite the thrown message at line 112 without an em-dash (and fix the em-dashes in comments at ~L6, L9, L34, L49).
  • src/lib/email.ts#L2-L2: rewrite the header comment "Email — disabled ..." without an em-dash.
📍 Affects 2 files
  • src/lib/enrollment.ts#L108-L114 (this comment)
  • src/lib/email.ts#L2-L2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/enrollment.ts` around lines 108 - 114, Remove all em-dash characters
across src/lib/enrollment.ts and src/lib/email.ts, replacing them with periods,
commas, or parentheses. In src/lib/enrollment.ts, update the message in the tier
course validation near the tier lookup and the comments around the referenced
sections; in src/lib/email.ts, update the header comment near the file start.
Preserve the existing behavior and wording intent.

Source: Coding guidelines

@projectamazonph

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 22-38: Remove all merge-conflict markers and resolve both affected
sites: in .env.example lines 22-38, discard the stale Sentry-to-Slack
configuration and retain a single NEXT_PUBLIC_APP_URL entry; in
src/app/admin/page.tsx lines 177-197, remove the obsolete refund branch
referencing pendingRefunds while preserving the valid surrounding admin
dashboard logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 730a888f-9cf8-4b87-82cb-e5084f2d4edc

📥 Commits

Reviewing files that changed from the base of the PR and between dc72506 and e9f15cb.

📒 Files selected for processing (3)
  • .env.example
  • src/app/(public)/pricing/pricing.module.css
  • src/app/admin/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(public)/pricing/pricing.module.css

Comment thread .env.example Outdated
Comment on lines +22 to +38
@@ -39,5 +35,10 @@ CSP_ENFORCE="false"

# Optional
NEXT_PUBLIC_APP_URL="http://localhost:3000"
>>>>>>> origin/main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove all committed merge-conflict markers.

Unresolved conflicts leave both the environment template and admin dashboard unusable.

  • .env.example#L22-L38: remove the conflict markers, discard the stale Sentry-to-Slack block, and retain one NEXT_PUBLIC_APP_URL.
  • src/app/admin/page.tsx#L177-L197: remove the markers and obsolete refund branch, which references the removed pendingRefunds value.
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 22-22: [KeyWithoutValue] The <<<<<<< HEAD key should be with a value or have an equal sign

(KeyWithoutValue)


[warning] 22-22: [LeadingCharacter] Invalid leading character detected

(LeadingCharacter)


[warning] 23-23: [LeadingCharacter] Invalid leading character detected

(LeadingCharacter)


[warning] 23-23: [UnorderedKey] The key should go before the <<<<<<< HEAD key

(UnorderedKey)


[warning] 26-26: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 27-27: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 28-28: [UnorderedKey] The ALERT_THRESHOLD key should go before the SENTRY_API_TOKEN key

(UnorderedKey)


[warning] 28-28: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 34-34: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 37-37: [DuplicatedKey] The NEXT_PUBLIC_APP_URL key is duplicated

(DuplicatedKey)


[warning] 37-37: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 38-38: [IncorrectDelimiter] The >>>>>>> origin/main key has incorrect delimiter

(IncorrectDelimiter)


[warning] 38-38: [KeyWithoutValue] The >>>>>>> origin/main key should be with a value or have an equal sign

(KeyWithoutValue)


[warning] 38-38: [LeadingCharacter] Invalid leading character detected

(LeadingCharacter)


[warning] 38-38: [LowercaseKey] The >>>>>>> origin/main key should be in uppercase

(LowercaseKey)


[warning] 38-38: [UnorderedKey] The >>>>>>> origin/main key should go before the NEXT_PUBLIC_APP_URL key

(UnorderedKey)

📍 Affects 2 files
  • .env.example#L22-L38 (this comment)
  • src/app/admin/page.tsx#L177-L197
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 22 - 38, Remove all merge-conflict markers and
resolve both affected sites: in .env.example lines 22-38, discard the stale
Sentry-to-Slack configuration and retain a single NEXT_PUBLIC_APP_URL entry; in
src/app/admin/page.tsx lines 177-197, remove the obsolete refund branch
referencing pendingRefunds while preserving the valid surrounding admin
dashboard logic.

Source: Linters/SAST tools

projectamazonph and others added 2 commits July 20, 2026 13:00
# Conflicts:
#	.env.example
#	src/app/actions/refunds.ts
#	src/app/api/paymongo/webhook/route.ts
#	src/lib/__tests__/enrollment.test.ts
#	src/lib/email.tsx
#	src/lib/enrollment.ts
Found while resolving the merge, unrelated to the conflict resolution
itself:
- src/app/admin/page.tsx had literal, already-committed merge-conflict
  markers (<<<<<<< HEAD / ======= / >>>>>>> origin/main) from an earlier
  botched resolution on this branch — a build-breaking syntax error.
  Kept the Analytics quick-link (HEAD side); dropped the Review Refunds
  link, which referenced an undefined `pendingRefunds` variable and a
  /admin/refunds page this branch already removes.
- src/styles/globals.css had the alias-token custom-property block
  (--color-text, --color-surface, etc.) sitting after :root's closing
  brace instead of inside it — invalid top-level CSS declarations that
  broke the Turbopack production build (same defect independently fixed
  on the design-review PR, #62).
- src/app/actions/checkout.ts was dead code: the branch's own commits
  removed CheckoutButton.tsx (its only caller) and deleted
  src/lib/paymongo.ts (its dependency), but never removed this file
  itself, so it no longer typechecked. Removed it.

Verified after all fixes: pnpm typecheck / lint clean, pnpm test
212/212, pnpm build succeeds with the expected stripped route table
(no /payments, /checkout, /admin/refunds, /api/paymongo; /admin/enroll
present).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ck2wurvqhL2r3cCAzmuhU
@projectamazonph
projectamazonph merged commit dabee94 into main Jul 20, 2026
5 checks passed
@projectamazonph
projectamazonph deleted the claude/deployment-plan-stripped-phwr1r branch July 20, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants