Strip PayMongo + Resend for manual-enrollment launch build#61
Conversation
- 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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
💤 Files with no reviewable changes (2)
📝 WalkthroughWalkthroughThe 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. ChangesLaunch payment and email surface
Manual enrollment domain
Admin enrollment interface
Admin operations and deployment
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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+manualEnrollActionand 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' }, |
| <h2 className={styles.cardTitle}> | ||
| Enrollments ({user.enrollments.length}) | ||
| {' — '} | ||
| <Link href={`/admin/enroll?email=${encodeURIComponent(user.email)}`}> | ||
| enroll in a tier | ||
| </Link> |
| <option key={tier.id} value={tier.id}> | ||
| {tier.name} — {tier.priceLabel} ({tier.courseCount}{' '} | ||
| {tier.courseCount === 1 ? 'course' : 'courses'}) | ||
| </option> |
| <p className={styles.resultTitle}> | ||
| ✓ Enrolled in {result.tierName} | ||
| {result.enrolledCount > 0 | ||
| ? ` — ${result.enrolledCount} ${result.enrolledCount === 1 ? 'course' : 'courses'} granted` | ||
| : ''} |
| <p className={styles.fineprint}> | ||
| Pay via GCash or bank transfer — message us on Facebook or email | ||
| after signing up and we'll activate your access within 24 hours. | ||
| </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. | ||
| </p> |
| (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. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (48)
.env.exampledocs/LAUNCH-DEPLOY.mdpackage.jsonscripts/setup-db.shsrc/app/(dashboard)/layout.tsxsrc/app/(dashboard)/payments/[id]/request-refund/RequestRefundForm.tsxsrc/app/(dashboard)/payments/[id]/request-refund/page.tsxsrc/app/(dashboard)/payments/[id]/request-refund/request-refund.module.csssrc/app/(dashboard)/payments/page.tsxsrc/app/(dashboard)/payments/payments.module.csssrc/app/(public)/checkout/complete/complete.module.csssrc/app/(public)/checkout/complete/page.tsxsrc/app/(public)/pricing/CheckoutButton.tsxsrc/app/(public)/pricing/checkout-button.module.csssrc/app/(public)/pricing/page.tsxsrc/app/(public)/pricing/pricing.module.csssrc/app/actions/admin-enroll.tssrc/app/actions/checkout.tssrc/app/actions/live-classes.tssrc/app/actions/refunds.tssrc/app/admin/enroll/EnrollForm.tsxsrc/app/admin/enroll/enroll.module.csssrc/app/admin/enroll/page.tsxsrc/app/admin/page.tsxsrc/app/admin/refunds/[id]/RefundDecisionForm.tsxsrc/app/admin/refunds/[id]/detail.module.csssrc/app/admin/refunds/[id]/page.tsxsrc/app/admin/refunds/page.tsxsrc/app/admin/refunds/refunds.module.csssrc/app/admin/users/[id]/page.tsxsrc/app/api/invoices/[id]/pdf/route.tssrc/app/api/paymongo/webhook/route.tssrc/app/api/resend/webhook/route.tssrc/components/ui/BottomNav.tsxsrc/lib/__tests__/enrollment.test.tssrc/lib/__tests__/paymongo.test.tssrc/lib/__tests__/refunds.test.tssrc/lib/__tests__/webhook-signature.test.tssrc/lib/email.tssrc/lib/email.tsxsrc/lib/enrollment.tssrc/lib/paymongo.tssrc/lib/receipt-pdf.tsxsrc/lib/receipts.tsxsrc/lib/refunds.tssrc/lib/webhook-signature.tssrc/middleware.tssrc/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 |
There was a problem hiding this comment.
📐 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
| Pay via GCash or bank transfer — message us on Facebook or email | ||
| after signing up and we'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. |
There was a problem hiding this comment.
📐 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-L27src/app/admin/enroll/page.tsx#L35-L35src/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
| '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.', | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 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 libRepository: 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
| 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, | ||
| }); |
There was a problem hiding this comment.
🗄️ 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.
| 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
| 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(); |
There was a problem hiding this comment.
🩺 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.
| 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> | ||
| ); |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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, | ||
| }), |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| {' — '} | ||
| <Link href={`/admin/enroll?email=${encodeURIComponent(user.email)}`}> | ||
| enroll in a tier | ||
| </Link> |
There was a problem hiding this comment.
📐 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
| 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); |
There was a problem hiding this comment.
📐 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
|
@copilot resolve the merge conflicts in this pull request |
… .env.example and admin page
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
.env.examplesrc/app/(public)/pricing/pricing.module.csssrc/app/admin/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/(public)/pricing/pricing.module.css
| @@ -39,5 +35,10 @@ CSP_ENFORCE="false" | |||
|
|
|||
| # Optional | |||
| NEXT_PUBLIC_APP_URL="http://localhost:3000" | |||
| >>>>>>> origin/main | |||
There was a problem hiding this comment.
🎯 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 oneNEXT_PUBLIC_APP_URL.src/app/admin/page.tsx#L177-L197: remove the markers and obsolete refund branch, which references the removedpendingRefundsvalue.
🧰 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
# 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
/checkout/complete, webhook routes (/api/paymongo, /api/resend), refunds
(student flow + admin review), receipts/invoice PDFs, webhook signatures,
and the dashboard Payments page/nav.
resend dependency. Callers (live-class reminders) keep working.
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.
Enroll Student quick action); link enroll form from user detail page.
FAQ updated for the manual workflow.
empty) so the payment stack can be restored from history without migration
surgery.
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
Changes
Tests