Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions mentorme_backend/PHASE2_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Phase 2 – Scheduling & Sessions (Prep Only)

This document outlines the intended design for scheduling/sessions before implementation. No code has been added yet.

## Data Model (planned)
- `TutorAvailability`: day_of_week, start_minute, end_minute, timezone, locationType.
- `TutorUnavailability` (new): date/time ranges to block off-time.
- `ClassSchedule`: belongs to Class; recurrence rule (weekly slots) or ad-hoc sessions list; timezone; total_sessions.
- `Session`: belongs to Class; scheduled_start/end (UTC), status (SCHEDULED/IN_PROGRESS/COMPLETED/MISSED/CANCELLED), flags for tutor/student start/complete confirmations, dispute_flagged_at.

## Proposed Endpoints (names only)
- Tutor:
- `GET /api/tutors/me/availability` / `PUT /api/tutors/me/availability`
- `GET /api/tutors/me/unavailability` / `PUT /api/tutors/me/unavailability`
- Class scheduling:
- `POST /api/classes/:id/schedule` (create recurring or explicit sessions)
- `GET /api/classes/:id/schedule` (view schedule + generated sessions)
- Session lifecycle:
- `PATCH /api/sessions/:id/start` (tutor/student confirm start)
- `PATCH /api/sessions/:id/complete` (tutor/student confirm completion)
- `PATCH /api/sessions/:id/cancel`
- Calendars:
- `GET /api/calendar/tutor` / `GET /api/calendar/student` (upcoming sessions merged)

## Conflict Rules (to enforce)
- Tutor cannot schedule/accept sessions overlapping:
- other ACTIVE/CONFIRMED sessions
- tutor unavailability blocks
- Student cannot schedule overlapping sessions across their ACTIVE classes.
- Timezone-aware comparison (store UTC, keep user timezone for display).

## Integration Points
- Booking → once accepted, require schedule creation before class becomes ACTIVE.
- Class status progression uses session completion counts.
- Notifications/reminders to be wired via existing or new job runner (outside Phase 1.5 scope).

## Open Questions
- Whether verified tutors can edit availability while PENDING suspension.
- Payment/escrow hooks on session confirmation (if needed later).
28 changes: 28 additions & 0 deletions mentorme_backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ The API listens on `http://localhost:4000` by default. A basic health probe is a

Use any REST client (Insomnia, Postman, VS Code `rest` files) to call the endpoints. Enable an `Authorization: Bearer <token>` header when exercising protected routes such as students/tutors CRUD.

## Verification API (Phase 1)

- Tutor:
- `GET /api/tutors/me/verification` – view status + masked CCCD and submitted documents.
- `PUT /api/tutors/me/verification` – submit/resubmit; sets status to `PENDING`. Duplicate CCCD returns `409 Conflict`.
- Admin (role `ADMIN` only):
- `GET /api/admin/tutor-verifications?status=PENDING|VERIFIED|REJECTED` – list requests (CCCD masked).
- `PATCH /api/admin/tutor-verifications/:id/approve` – approve (status → VERIFIED).
- `PATCH /api/admin/tutor-verifications/:id/reject` – reject with optional note.
- Public surfaces (search/matching/class detail) only include `VERIFIED` tutors and never expose CCCD or document URLs.

## Scheduling API (Phase 2)

- Tutor availability: `GET/PUT /api/tutors/me/availability`, `GET/PUT /api/tutors/me/unavailability`
- Class schedule: `POST /api/classes/:id/schedule` (recurrence or explicit sessions, conflict-checked), `GET /api/classes/:id/schedule`
- Calendars: `GET /api/calendar/tutor`, `GET /api/calendar/student` (upcoming sessions)
- Session statuses currently: `SCHEDULED`, `CANCELLED`, `MISSED` (start/complete flow deferred to Phase 3).

## Sessions & Payments (Phase 3 & 4A/4B)

- Session lifecycle: `PATCH /api/sessions/:id/start`, `PATCH /api/sessions/:id/complete` (both tutor & student must confirm). Disputed sessions are flagged after long one-sided confirmations.
- Notifications: `GET /api/notifications/me`, `PATCH /api/notifications/:id/read` plus automated reminders at T-24h/T-1h (see `npm run reminders:run`).
- Payments (mock escrow):
- Create intent: `POST /api/classes/:id/payments/intents` (student/admin; requires schedule & booking)
- Confirm mock payment: `POST /api/payments/:intentId/confirm`
- Summary & ledger: `GET /api/classes/:id/payments/summary`
- Escrow auto-releases on session completion (90% tutor / 10% platform); insufficient escrow notifies payer. Cancelling a class (`PATCH /api/classes/:id/cancel`) refunds remaining balance.

## Deploying to AWS (ECS + RDS)

This backend is ready to run as a container on ECS Fargate behind an Application Load Balancer (ALB), with PostgreSQL hosted on RDS and secrets in SSM/Secrets Manager.
Expand Down
15 changes: 9 additions & 6 deletions mentorme_backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "tsc --noEmit",
"test": "jest --runInBand",
"embeddings:backfill": "ts-node-dev --transpile-only src/scripts/backfillTutorEmbeddings.ts"
},
"build": "tsc",
"start": "node dist/index.js",
"lint": "tsc --noEmit",
"test": "jest --runInBand",
"embeddings:backfill": "ts-node-dev --transpile-only src/scripts/backfillTutorEmbeddings.ts",
"reminders:run": "ts-node-dev --transpile-only src/jobs/sessionReminders.ts",
"seed": "ts-node-dev --transpile-only src/scripts/seedDemo.ts",
"demo:reset": "npx prisma migrate reset --force --skip-generate --skip-seed && npm run seed"
},
"keywords": [],
"author": "",
"license": "ISC",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- Add verification status enum
CREATE TYPE "VerificationStatus" AS ENUM ('UNVERIFIED', 'PENDING', 'VERIFIED', 'REJECTED');

-- Extend TutorProfile for verification data
ALTER TABLE "TutorProfile"
ADD COLUMN "verificationStatus" "VerificationStatus" NOT NULL DEFAULT 'UNVERIFIED',
ADD COLUMN "nationalIdNumber" TEXT,
ADD COLUMN "nationalIdFrontImageUrl" TEXT,
ADD COLUMN "nationalIdBackImageUrl" TEXT,
ADD COLUMN "proofDocuments" JSONB,
ADD COLUMN "certificatesDetail" JSONB,
ADD COLUMN "verificationSubmittedAt" TIMESTAMP(3),
ADD COLUMN "verificationReviewedAt" TIMESTAMP(3),
ADD COLUMN "verificationNotes" TEXT;

-- Keep backward compatibility with existing verified flag
UPDATE "TutorProfile"
SET "verificationStatus" =
CASE
WHEN "verified" = true THEN 'VERIFIED'::"VerificationStatus"
ELSE 'UNVERIFIED'::"VerificationStatus"
END;

-- Unique constraint for national ID (allows NULLs)
CREATE UNIQUE INDEX "TutorProfile_nationalIdNumber_key" ON "TutorProfile"("nationalIdNumber");

-- Helpful index for moderation/status filtering
CREATE INDEX "TutorProfile_verificationStatus_idx" ON "TutorProfile"("verificationStatus");
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
-- Phase 2 scheduling additions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Add timezone to tutor availability
ALTER TABLE "TutorAvailability" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC';

-- Tutor unavailability blocks
CREATE TABLE "TutorUnavailability" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"tutorId" TEXT NOT NULL,
"startAt" TIMESTAMPTZ NOT NULL,
"endAt" TIMESTAMPTZ NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX "TutorUnavailability_tutorId_startAt_endAt_idx" ON "TutorUnavailability" ("tutorId", "startAt", "endAt");

ALTER TABLE "TutorUnavailability"
ADD CONSTRAINT "TutorUnavailability_tutorId_fkey"
FOREIGN KEY ("tutorId") REFERENCES "TutorProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Session status enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'SessionStatus') THEN
CREATE TYPE "SessionStatus" AS ENUM ('SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'MISSED');
ELSE
-- add missing values if enum already exists
IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'IN_PROGRESS' AND enumtypid = 'SessionStatus'::regtype) THEN
ALTER TYPE "SessionStatus" ADD VALUE 'IN_PROGRESS';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'COMPLETED' AND enumtypid = 'SessionStatus'::regtype) THEN
ALTER TYPE "SessionStatus" ADD VALUE 'COMPLETED';
END IF;
END IF;
END$$;

-- Class schedule per class
CREATE TABLE "ClassSchedule" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"classId" TEXT NOT NULL UNIQUE,
"timezone" TEXT NOT NULL DEFAULT 'UTC',
"recurrenceRule" JSONB,
"explicitSessions" JSONB,
"totalSessions" INTEGER NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE "ClassSchedule"
ADD CONSTRAINT "ClassSchedule_classId_fkey"
FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Sessions generated from schedules
CREATE TABLE "Session" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"classId" TEXT NOT NULL,
"scheduledStartAt" TIMESTAMPTZ NOT NULL,
"scheduledEndAt" TIMESTAMPTZ NOT NULL,
"status" "SessionStatus" NOT NULL DEFAULT 'SCHEDULED',
"disputeFlaggedAt" TIMESTAMPTZ,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX "Session_classId_scheduledStartAt_idx" ON "Session" ("classId", "scheduledStartAt");
CREATE INDEX "Session_scheduledStartAt_scheduledEndAt_idx" ON "Session" ("scheduledStartAt", "scheduledEndAt");

ALTER TABLE "Session"
ADD CONSTRAINT "Session_classId_fkey"
FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- Phase 3: session lifecycle and class progress
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Extend SessionStatus enum if needed
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_enum e
JOIN pg_type t ON t.oid = e.enumtypid
WHERE t.typname = 'SessionStatus'
AND e.enumlabel = 'IN_PROGRESS'
) THEN
ALTER TYPE "SessionStatus" ADD VALUE 'IN_PROGRESS';
END IF;

IF NOT EXISTS (
SELECT 1
FROM pg_enum e
JOIN pg_type t ON t.oid = e.enumtypid
WHERE t.typname = 'SessionStatus'
AND e.enumlabel = 'COMPLETED'
) THEN
ALTER TYPE "SessionStatus" ADD VALUE 'COMPLETED';
END IF;
END $$;


-- Class lifecycle status enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'ClassLifecycleStatus') THEN
CREATE TYPE "ClassLifecycleStatus" AS ENUM ('PENDING', 'ACTIVE', 'COMPLETED', 'CANCELLED');
END IF;
END$$;

ALTER TABLE "Class"
ADD COLUMN IF NOT EXISTS "lifecycleStatus" "ClassLifecycleStatus" NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS "totalSessions" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS "sessionsCompleted" INTEGER NOT NULL DEFAULT 0;

ALTER TABLE "Session"
ADD COLUMN IF NOT EXISTS "tutorStartConfirmedAt" TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS "studentStartConfirmedAt" TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS "tutorCompleteConfirmedAt" TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS "studentCompleteConfirmedAt" TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS "startedAt" TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS "completedAt" TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS "disputeFlaggedAt" TIMESTAMPTZ;
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- NotificationChannel enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'NotificationChannel') THEN
CREATE TYPE "NotificationChannel" AS ENUM ('IN_APP', 'EMAIL');
END IF;
END$$;

-- ReminderType enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'ReminderType') THEN
CREATE TYPE "ReminderType" AS ENUM ('REMINDER_24H', 'REMINDER_1H');
END IF;
END$$;

CREATE TABLE "Notification" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"metadata" JSONB,
"channel" "NotificationChannel" NOT NULL DEFAULT 'IN_APP',
"dedupKey" TEXT NOT NULL,
"readAt" TIMESTAMPTZ,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX "Notification_userId_dedupKey_key" ON "Notification"("userId", "dedupKey");
CREATE INDEX "Notification_userId_createdAt_idx" ON "Notification"("userId", "createdAt" DESC);

ALTER TABLE "Notification"
ADD CONSTRAINT "Notification_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

CREATE TABLE "ReminderLog" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"userId" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"reminderType" "ReminderType" NOT NULL,
"sentAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE "ReminderLog"
ADD CONSTRAINT "ReminderLog_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "ReminderLog"
ADD CONSTRAINT "ReminderLog_sessionId_fkey"
FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE;

CREATE UNIQUE INDEX "ReminderLog_userId_sessionId_reminderType_key" ON "ReminderLog"("userId", "sessionId", "reminderType");
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- PaymentStatus enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'PaymentStatus') THEN
CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'PAID', 'FAILED', 'REFUNDED');
END IF;
END$$;

-- LedgerType enum
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'LedgerType') THEN
CREATE TYPE "LedgerType" AS ENUM ('DEPOSIT', 'RELEASE_TO_TUTOR', 'PLATFORM_FEE', 'REFUND');
END IF;
END$$;

CREATE TABLE IF NOT EXISTS "PaymentIntent" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"classId" TEXT NOT NULL,
"payerId" TEXT NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'VND',
"status" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
"provider" TEXT NOT NULL DEFAULT 'MOCK',
"providerRef" TEXT,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS "PaymentIntent_classId_idx" ON "PaymentIntent"("classId");
CREATE INDEX IF NOT EXISTS "PaymentIntent_payerId_idx" ON "PaymentIntent"("payerId");

ALTER TABLE "PaymentIntent"
ADD CONSTRAINT "PaymentIntent_classId_fkey"
FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "PaymentIntent"
ADD CONSTRAINT "PaymentIntent_payerId_fkey"
FOREIGN KEY ("payerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

CREATE TABLE IF NOT EXISTS "EscrowAccount" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"classId" TEXT UNIQUE NOT NULL,
"totalDeposited" DOUBLE PRECISION NOT NULL DEFAULT 0,
"availableBalance" DOUBLE PRECISION NOT NULL DEFAULT 0,
"releasedAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
"refundedAmount" DOUBLE PRECISION NOT NULL DEFAULT 0,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE "EscrowAccount"
ADD CONSTRAINT "EscrowAccount_classId_fkey"
FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;

CREATE TABLE IF NOT EXISTS "LedgerEntry" (
"id" TEXT PRIMARY KEY DEFAULT uuid_generate_v4(),
"classId" TEXT NOT NULL,
"sessionId" TEXT,
"paymentIntentId" TEXT,
"type" "LedgerType" NOT NULL,
"amount" DOUBLE PRECISION NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

ALTER TABLE "LedgerEntry"
ADD CONSTRAINT "LedgerEntry_classId_fkey"
FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "LedgerEntry"
ADD CONSTRAINT "LedgerEntry_sessionId_fkey"
FOREIGN KEY ("sessionId") REFERENCES "Session"("id") ON DELETE CASCADE ON UPDATE CASCADE;

ALTER TABLE "LedgerEntry"
ADD CONSTRAINT "LedgerEntry_paymentIntentId_fkey"
FOREIGN KEY ("paymentIntentId") REFERENCES "PaymentIntent"("id") ON DELETE CASCADE ON UPDATE CASCADE;

CREATE INDEX IF NOT EXISTS "LedgerEntry_classId_createdAt_idx" ON "LedgerEntry"("classId", "createdAt" DESC);
CREATE UNIQUE INDEX IF NOT EXISTS "ledger_session_unique" ON "LedgerEntry"("type", "sessionId");
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE UNIQUE INDEX "Subject_name_key" ON "Subject"("name");
Loading