fix(api): use admin app URL for teacher auto-enroll email links (#771)

* fix(api): use admin app URL for teacher auto-enroll links

Teacher payment-request emails now build course and grant-access URLs
with getAppBaseUrl() instead of getDashboardBaseUrl(org), so links
point at app.classroomio.com rather than a custom or tenant domain.

Document the teacher vs student email link rule in AGENTS.md.

Co-authored-by: Rotimi Best <[email protected]>

* docs: surface email link URL rules where emails are wired

Document getAppBaseUrl vs getDashboardBaseUrl in dashboard-url.ts,
packages/email/README.md, enqueueTransactionalEmail JSDoc, and AGENTS.md
so staff vs learner hosts are discoverable when building email links.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-19 13:50:12 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent e09b3caa8a
commit 0f23453f91
5 changed files with 67 additions and 12 deletions
@@ -5,7 +5,7 @@ import { getCourseWithOrgData } from '@cio/db/queries/course';
import { buildEmailFromName, buildEmailBranding } from '@cio/email';
import { enqueueTransactionalEmail } from '@api/services/jobs';
import { trackServerEvent, SERVER_EVENTS } from '@cio/analytics';
import { getDashboardBaseUrl } from '@cio/core/config/dashboard-url';
import { getAppBaseUrl } from '@cio/core/config/dashboard-url';
export interface PaymentRequestData {
courseId: string;
@@ -48,14 +48,9 @@ export async function createPaymentRequest(data: PaymentRequestData) {
const teacherEmail = teacherResult[0].email;
const dashboardOrg = {
siteName: course.orgSiteName,
customDomain: course.orgCustomDomain,
isCustomDomainVerified: course.orgIsCustomDomainVerified
};
const dashboardBaseUrl = getDashboardBaseUrl(dashboardOrg);
const courseUrl = `${dashboardBaseUrl}/courses/${data.courseId}`;
const autoEnrollUrl = `${dashboardBaseUrl}/courses/${data.courseId}/people?grantAccess=${encodeURIComponent(data.studentEmail)}`;
const appBaseUrl = getAppBaseUrl();
const courseUrl = `${appBaseUrl}/courses/${data.courseId}`;
const autoEnrollUrl = `${appBaseUrl}/courses/${data.courseId}/people?grantAccess=${encodeURIComponent(data.studentEmail)}`;
try {
await enqueueTransactionalEmail('teacherStudentBuyRequest', {
+5
View File
@@ -59,6 +59,11 @@ function recipientKey(base: string | undefined, recipient: string, total: number
* against the template's Zod schema up front so bad payloads fail in the
* domain handler instead of silently inside the worker.
*
* **Link fields** (`courseUrl`, `autoEnrollUrl`, `loginUrl`, `inviteUrl`, etc.)
* must be built by the caller before enqueueing. Use `getAppBaseUrl()` for
* teacher/tutor/admin dashboard links and `getDashboardBaseUrl(org)` for
* student/learner links — see `packages/email/README.md` § Email link URLs.
*
* BullMQ tracks send state, retries, and failure history — no DB ledger is
* written. Final failures are recorded in `dead_letter_job` by the worker
* for operator triage.