Task 2: Appointment Booking System #2
Conversation
WalkthroughThis pull request implements an admin authentication system with a login page and dashboard, enhances the appointment booking and cancellation APIs with comprehensive business rule validation, introduces reusable rule helpers, adds testing infrastructure with Vitest, and updates the dashboard UIs to support these features. Changes
Sequence DiagramssequenceDiagram
participant User
participant Browser
participant LoginAPI as POST /api/admin/login
participant DB as system_admins Table
participant Dashboard as /admin/dashboard
User->>Browser: Enter email & password
Browser->>LoginAPI: POST { email, password }
LoginAPI->>DB: Fetch admin by email
DB-->>LoginAPI: admin record
alt Valid credentials
LoginAPI->>LoginAPI: Verify password match
LoginAPI->>Browser: 200 + Set admin_token cookie
Browser->>Dashboard: Navigate to /admin/dashboard
Dashboard->>Dashboard: Read admin_token cookie
Dashboard->>DB: Query appointments with joins
DB-->>Dashboard: appointments list
Dashboard->>Browser: Render dashboard table
else Invalid credentials
LoginAPI->>Browser: 401 + error message
Browser->>User: Display error
end
sequenceDiagram
participant Patient
participant Browser
participant BookAPI as POST /api/appointments/book
participant DB as Supabase
participant SlotsTable as slots Table
participant ApptTable as appointments Table
Patient->>Browser: Select slot & doctor
Browser->>BookAPI: POST { patient_id, doctor_id, slot_id }
BookAPI->>SlotsTable: Fetch slot by id
SlotsTable-->>BookAPI: slot record
alt Slot validation
BookAPI->>BookAPI: Check canPatientBookSlot()
alt Slot is booked
BookAPI->>Browser: 400 Slot unavailable
else Slot available
BookAPI->>ApptTable: Query existing appointments
ApptTable-->>BookAPI: existing appointments
BookAPI->>BookAPI: Check canPatientBookDoctor()
alt Doctor conflict detected
BookAPI->>Browser: 400 Conflict with existing
else No conflict
BookAPI->>ApptTable: Insert new appointment
ApptTable-->>BookAPI: created appointment
BookAPI->>SlotsTable: Update is_booked = true
BookAPI->>Browser: 200 + appointment data
Browser->>Patient: Display success & update UI
end
end
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces an administrative dashboard and login system, alongside refined appointment booking and cancellation workflows. Key additions include API routes for admin authentication, business logic for booking rules, and a comprehensive test suite using Vitest. Feedback highlights critical security concerns regarding plain-text password handling, performance issues with in-memory data filtering in the booking API, and potential race conditions due to non-atomic database operations. Additionally, improvements are suggested for type safety in the dashboard and more robust environment variable handling in utility scripts.
| </tr> | ||
| </thead> | ||
| <tbody className="divide-y"> | ||
| {appointments.map((appt: any) => ( |
There was a problem hiding this comment.
Thanks for the suggestion. I agree that defining a proper TypeScript interface would improve type safety and maintainability. The any type was used for faster development during this implementation, but this can be refactored by introducing a well-defined appointment type based on the schema.
| const env = fs.readFileSync('.env.local', 'utf8'); | ||
| const url = env.match(/NEXT_PUBLIC_SUPABASE_URL=(.*?)\r?\n/)[1]; | ||
| const key = env.match(/SUPABASE_SERVICE_ROLE_KEY=(.*?)\r?(?:\n|$)/)[1]; |
There was a problem hiding this comment.
There was a problem hiding this comment.
Thanks for the feedback. I agree that manually parsing the .env.local file using regex is not a robust approach. This was implemented as a quick debugging utility. In a production setup, environment variables would be accessed directly via process.env or managed using a standard library like dotenv. Additionally, this script is intended only for local debugging and would typically not be included in a production codebase.
There was a problem hiding this comment.
Actionable comments posted: 16
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/admin/dashboard/page.tsx`:
- Around line 13-18: The current check only inspects
cookieStore.get("admin_token") existence (adminToken) and calls
redirect("/admin/login"), which is bypassable; instead validate the token
server-side before allowing access: verify the token's signature/expiry or
validate against your auth provider (e.g., Supabase session and role claim) and
reject/redirect if invalid. Update the page logic that reads cookieStore and
adminToken to call your server-side token verification function (or Supabase
admin session check) and only proceed to query appointments if that verification
returns a valid admin role/session; ensure redirect("/admin/login") is used when
verification fails.
In `@app/admin/login/page.tsx`:
- Around line 19-33: The submit flow around the fetch in page.tsx can throw and
leave the form stuck loading; wrap the POST request and response handling in a
try/catch/finally: in try perform the fetch and existing res.ok handling (using
the same JSON parse and setError when needed), in catch set a user-friendly
error via setError(e.message || "Network error") for fetch/network exceptions,
and in finally ensure setLoading(false) is always called; on success still call
router.push("/admin/dashboard"). Use the existing symbols (fetch call, setError,
setLoading, router.push) to locate and update the code.
In `@app/api/admin/login/route.ts`:
- Around line 18-20: The code currently compares plaintext passwords
(admin.password !== password); replace this with a hashed-password verification
flow: store one-way hashes (e.g., bcrypt/argon2) for admin passwords (e.g.,
admin.passwordHash) and use the library's verify/compare function (e.g.,
bcrypt.compare or argon2.verify) in the login route to compare the incoming
password to the stored hash; update any registration/update code to hash
passwords before persisting and change the check in route.ts to await the
hash-compare call and return 401 only if verification fails (refer to the
admin.password/admin.passwordHash and the login route
handler/NextResponse.json).
- Around line 22-27: Do not store raw admin.id in the cookie; replace this with
a cryptographically signed session token (e.g., a JWT or a server-side session
ID) and validate/decode that token on all protected routes (so any check uses
verifySessionToken/verifyJwt or lookupSessionById rather than mere cookie
presence). In the cookie-setting code where admin_token is set, sign/generate
the token from admin.id and set it as the cookie value, and add secure: true and
sameSite: 'lax' (or 'strict' as appropriate) to the options along with httpOnly,
path and maxAge; ensure the auth middleware/route checks use the matching
verification function (verifySessionToken/verifyJwt) to authorize requests.
In `@app/api/appointments/book/route.ts`:
- Around line 52-58: The DB query result from supabaseAdmin
(.from("appointments").select(...)) can return an error that is currently
ignored; update the booking route to inspect the response's error (the object
returned alongside data from supabaseAdmin) immediately after the query and if
an error exists return a 500 response instead of proceeding to call
canPatientBookDoctor; specifically, check the query response (the variable
holding { data: existing, error }) right after the supabaseAdmin call and handle
error before using existing or calling canPatientBookDoctor so you don't
evaluate rules on an undefined dataset.
- Around line 9-19: The code currently trusts the client-provided patient_id;
instead, extract the authenticated user/patient id from the server-side
session/JWT and ignore/override any patient_id from the request body. In the
route handler that reads const { patient_id, doctor_id, slot_id } = body,
replace use of patient_id with a server-derived patientId obtained from your
auth utility (e.g. verify JWT or get session) and return a 401 if no
authenticated patient is found; continue to validate doctor_id and slot_id and
use NextResponse for errors as before. Ensure you remove or ignore the
client-sent patient_id so bookings are always attributed to the authenticated
user.
- Around line 22-35: The current read-then-insert flow is race-prone; change it
to an atomic claim pattern using supabaseAdmin to conditionally flip the slot's
is_booked flag and only proceed if the update affected a row: perform an update
on "slots" setting is_booked=true where id=slot_id and is_booked=false, verify
the update returned a matched row (if not, return a 409 or similar), then insert
the appointment record; if the insert fails, perform a compensating update to
set is_booked=false on that slot to roll back the claim. Use the same pattern
for the other booking path mentioned (the code around the second insert/update
block) and handle/log errors via NextResponse.json with appropriate status
codes.
In `@app/api/appointments/cancel/route.ts`:
- Around line 8-15: The route is trusting client-supplied role and action; stop
using body.role and validate action against an allowlist before processing.
Retrieve the caller's authenticated identity/role from the server-side
auth/session (instead of body.role) within route.ts and use that for permission
checks for the "done" transition and the 1-hour cancellation exception,
returning 403 if the actor lacks permission; also validate action is exactly
"cancel" or "done" and return 400 for invalid actions. Locate uses of
appointmentId, action, role in this file and replace role checks with the
server-derived role (or user id) and insert an allowlist check for action prior
to any state mutation.
- Around line 53-71: The appointment status update and slot unbooking use
separate non-atomic operations (supabaseAdmin.from("appointments").update and
supabaseAdmin.from("slots").update) which can leave inconsistent state if the
slot update fails; modify the code to either perform both writes in a single
atomic operation (call an RPC/stored-procedure via supabaseAdmin.rpc that
updates appointments and slots together) or, at minimum, capture the error from
the slots update (the call that uses appointment.slot_id), and if it fails roll
back the appointment change by re-setting the appointment row to its previous
status (use the saved previousStatus or fetch it before updating) and return a
500 error via NextResponse.json; ensure you check and handle errors from both
supabaseAdmin calls and reference the unique symbols appointmentId,
appointment.slot_id, supabaseAdmin.from("appointments").update,
supabaseAdmin.from("slots").update, and NextResponse.json when making the
changes.
In `@app/doctor/dashboard/page.tsx`:
- Line 82: Replace the unsafe double-cast around apptData before calling
setAppointments: remove `(apptData as any as Appointment[])` and either declare
the fetched variable with an explicit type (e.g., `const apptData: Appointment[]
= ...`) or run apptData through a lightweight type guard/validation function
(e.g., isAppointmentArray(apptData)) and only call setAppointments(apptData)
after validation; ensure you reference the existing symbol names `apptData`,
`Appointment` and `setAppointments` when making the change so the assignment is
type-safe without using `any`.
In `@check.mjs`:
- Around line 3-10: The script currently assumes env.match(...) returns non-null
and always exits with 0 even on Supabase errors; update check.mjs to fail fast
by validating the readFileSync call and the regex results for
NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (i.e., check that
env.match(...) isn't null before accessing [1]) and emit a clear error and
process.exit(1) if missing, then wrap the Supabase call (createClient and
supabase.from('appointments').select('*')) in a try/catch or handle the promise
rejection and the response error (check res.error or lack of res.data) and log
the error via console.error (or your logger) and exit with a nonzero status
instead of process.exit(0) when the query fails; reference the variables url,
key, createClient, supabase, and the supabase.from(...).select(...) call when
making these changes.
In `@lib/appointment-rules.ts`:
- Around line 22-30: The current cancellation check computes startTimeMs from
startTimeIso and will treat invalid timestamps (NaN) as allowed cancellations;
update the guard so you first validate startTimeMs (e.g., via
Number.isFinite(startTimeMs) or !Number.isNaN(startTimeMs)) and return false
immediately when the timestamp is invalid before applying the existing 1-hour
rule using ONE_HOURMs; modify the logic around startTimeIso/startTimeMs to
short-circuit invalid inputs and only proceed to the existing comparison if the
parsed time is valid.
In `@lib/supabaseAdmin.ts`:
- Around line 3-4: Replace the non-null assertions for supabaseUrl and
supabaseServiceRoleKey with explicit validation: read
process.env.NEXT_PUBLIC_SUPABASE_URL and process.env.SUPABASE_SERVICE_ROLE_KEY
into variables (supabaseUrl, supabaseServiceRoleKey), check if either is missing
or empty, and if so throw a clear Error (e.g. "Missing required env:
NEXT_PUBLIC_SUPABASE_URL" / "Missing required env: SUPABASE_SERVICE_ROLE_KEY")
during startup; then use these validated variables where the module creates the
Supabase admin client so failures are immediate and descriptive.
- Around line 1-6: This module exposes service-role credentials
(supabaseServiceRoleKey/supabaseUrl) and should be explicitly marked
server-only: add the server-only guard (import "server-only";) at the top of
lib/supabaseAdmin.ts before any other code so accidental client-side imports
fail the build, keeping the createClient call and exported supabaseAdmin safe.
In `@package.json`:
- Line 13: Remove the unused dotenv dependency entry by deleting the "dotenv":
"^17.4.2" line from package.json and then update lockfiles by running your
package manager (npm install or yarn install) to regenerate package-lock.json /
yarn.lock; also search the repo for any remaining dotenv imports/usages and
remove them if found to ensure the dependency is fully removed.
In `@tests/integration.test.ts`:
- Around line 21-125: Add integration tests for the negative business paths by
extending tests using bookPOST and cancelPOST: (1) "slot already booked" — mock
supabaseAdmin.from("slots").select to return slot with is_booked: true and
assert bookPOST returns 409 or appropriate error message and no insert called
(mockInsert not invoked); (2) "duplicate active appointment with same doctor" —
mock appointments select to return an active appointment for same patient_id and
doctor_id and assert bookPOST rejects with 409/validation error and no insert;
(3) "patient cancellation within 1 hour denied" — for cancelPOST mock
appointments.select.single to return slot.start_time < 1 hour away and assert
response status indicates denial (e.g., 400/403) and no update called on
appointments or slots; (4) "unauthorized 'done' action" — call cancelPOST (or
relevant endpoint) with action: "done" and role not allowed, mock
auth/appointments accordingly and assert 401/403 and no updates. For each case,
use the existing patterns (supabaseAdmin.from mockImplementation,
mockInsert/mockUpdate mocks, NextRequest bodies) and assert specific mock
functions (mockInsert, mockUpdateAppt, mockUpdateSlot) were or were not called
and that response.json() contains the expected error message/status.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 3905fec9-5155-43bf-81c1-1caf17c6370a
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (16)
app/admin/dashboard/page.tsxapp/admin/login/page.tsxapp/api/admin/login/route.tsapp/api/appointments/book/route.tsapp/api/appointments/cancel/route.tsapp/doctor/dashboard/page.tsxapp/page.tsxapp/patient/dashboard/page.tsxcheck.mjslib/appointment-rules.tslib/supabaseAdmin.tspackage.jsontests/integration.test.tstests/unit.test.tstsconfig.tsbuildinfovitest.config.ts
| if (admin.password !== password) { | ||
| return NextResponse.json({ error: "Invalid email or password" }, { status: 401 }); | ||
| } |
There was a problem hiding this comment.
Do not authenticate with plaintext password comparison.
Line 18 indicates reversible/plaintext credential handling. This is a blocker: store one-way password hashes and verify against hash, not raw text.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/admin/login/route.ts` around lines 18 - 20, The code currently
compares plaintext passwords (admin.password !== password); replace this with a
hashed-password verification flow: store one-way hashes (e.g., bcrypt/argon2)
for admin passwords (e.g., admin.passwordHash) and use the library's
verify/compare function (e.g., bcrypt.compare or argon2.verify) in the login
route to compare the incoming password to the stored hash; update any
registration/update code to hash passwords before persisting and change the
check in route.ts to await the hash-compare call and return 401 only if
verification fails (refer to the admin.password/admin.passwordHash and the login
route handler/NextResponse.json).
There was a problem hiding this comment.
Thanks for highlighting this. I agree that comparing plaintext passwords is not secure and is not suitable for production systems.
In this implementation, plaintext comparison was used for simplicity and to focus on core application logic. In a production environment, passwords would be securely hashed using a strong algorithm such as bcrypt or Argon2, and comparisons would be performed against the hashed values.
This is a valid security improvement and would be addressed in a production-ready version.
There was a problem hiding this comment.
@afifa17112005, thanks for the clear explanation. That's a reasonable trade-off for a demo/task context. For production, the upgrade path would look roughly like:
-
Hash on creation — when inserting an admin record, store a bcrypt/Argon2 hash instead of the raw password:
import bcrypt from "bcryptjs"; const passwordHash = await bcrypt.hash(plainPassword, 12); // INSERT INTO system_admins (email, password_hash) VALUES (email, passwordHash)
-
Verify on login — replace the equality check in
app/api/admin/login/route.ts:// Before (insecure): if (admin.password !== password) { ... } // After (secure): const valid = await bcrypt.compare(password, admin.password_hash); if (!valid) { ... }
-
Replace the cookie value — store a signed JWT (e.g., via
jose) or an opaque session token instead of the rawadmin.id.
Would you like me to open a follow-up GitHub issue to track these production-readiness improvements?
🧠 Learnings used
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/admin/dashboard/page.tsx:13-18
Timestamp: 2026-04-28T05:48:57.287Z
Learning: In the Next.js admin area, do not implement authentication/authorization by (1) checking cookie presence only (e.g., `admin_token`) or storing sensitive values like `admin.id` as plain text, and do not authenticate by comparing passwords in plaintext (no hashing). For admin auth, use a secure, verifiable mechanism instead—e.g., signed JWTs validated with `jose` (with proper signature/claims validation) or Supabase Auth sessions with role/claim-based authorization. If plaintext/insecure logic is present for a demo/task, ensure the code is explicitly labeled and that production-bound code paths are gated/removed before deployment.
| const { patient_id, doctor_id, slot_id } = body; | ||
|
|
||
| console.log("RECEIVED:", { patient_id, doctor_id, slot_id }); | ||
|
|
||
| // 1. Validate input | ||
| if (!patient_id || !doctor_id || !slot_id) { | ||
| return NextResponse.json( | ||
| { error: "Missing required fields" }, | ||
| { status: 400 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Do not accept patient_id directly from client for booking authority.
A caller can book on behalf of any patient by sending another patient_id. Derive patient identity from authenticated session/JWT on the server, then ignore/override client-provided patient identifiers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/appointments/book/route.ts` around lines 9 - 19, The code currently
trusts the client-provided patient_id; instead, extract the authenticated
user/patient id from the server-side session/JWT and ignore/override any
patient_id from the request body. In the route handler that reads const {
patient_id, doctor_id, slot_id } = body, replace use of patient_id with a
server-derived patientId obtained from your auth utility (e.g. verify JWT or get
session) and return a 401 if no authenticated patient is found; continue to
validate doctor_id and slot_id and use NextResponse for errors as before. Ensure
you remove or ignore the client-sent patient_id so bookings are always
attributed to the authenticated user.
There was a problem hiding this comment.
Thanks for highlighting this. I agree that accepting patient_id directly from the client is not secure, as it can allow users to act on behalf of others.
In this implementation, it was kept simple for demonstration purposes. In a production system, the patient identity would be derived from the authenticated session or JWT on the server side, and any client-provided identifiers would be ignored or validated against the session.
This is a valid security improvement and would be enforced in a production-ready setup.
There was a problem hiding this comment.
@afifa17112005, that's a fair and well-reasoned response. For a demonstration/task project, keeping auth simple is understandable. When moving to production, here's a concise reference for what the server-side extraction would look like using Supabase's built-in auth:
// Production pattern: derive patient identity server-side
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function POST(req: NextRequest) {
const cookieStore = cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { get: (name) => cookieStore.get(name)?.value } }
);
const { data: { user }, error } = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Use user.id as the authoritative patient_id — ignore any client-supplied value
const patient_id = user.id;
const { doctor_id, slot_id } = await req.json();
// ... rest of booking logic
}Key production hardening steps to track for later:
- Auth: Extract identity from session/JWT server-side; never trust client-supplied
patient_id. - RLS: Tighten Supabase Row Level Security so patients can only read/write their own rows.
- Password hashing: Replace plain-text comparison in
/api/admin/loginwithbcrypt/argon2.
✏️ Learnings added
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:9-19
Timestamp: 2026-04-28T05:51:44.724Z
Learning: In the `app/api/appointments/book/route.ts` file of the botcode-com/task repository (Next.js + Supabase appointment booking system), `patient_id` is intentionally accepted from the client body for demonstration/task purposes. In a production system, the patient identity should be derived from the server-side Supabase session/JWT and the client-provided `patient_id` should be ignored or overridden.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| const startTimeMs = new Date(startTimeIso).getTime(); | ||
| const ONE_HOURMs = 60 * 60 * 1000; | ||
|
|
||
| // A patient cannot cancel an appointment starting in less than 1 hour | ||
| if (startTimeMs - currentTimeMs < ONE_HOURMs) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Guard invalid startTimeIso to avoid fail-open cancellation checks.
If startTimeIso is invalid, getTime() is NaN and the function returns true. Return false for invalid timestamps before applying the 1-hour rule.
Defensive fix
const startTimeMs = new Date(startTimeIso).getTime();
+ if (Number.isNaN(startTimeMs)) {
+ return false;
+ }
const ONE_HOURMs = 60 * 60 * 1000;📝 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.
| const startTimeMs = new Date(startTimeIso).getTime(); | |
| const ONE_HOURMs = 60 * 60 * 1000; | |
| // A patient cannot cancel an appointment starting in less than 1 hour | |
| if (startTimeMs - currentTimeMs < ONE_HOURMs) { | |
| return false; | |
| } | |
| return true; | |
| const startTimeMs = new Date(startTimeIso).getTime(); | |
| if (Number.isNaN(startTimeMs)) { | |
| return false; | |
| } | |
| const ONE_HOURMs = 60 * 60 * 1000; | |
| // A patient cannot cancel an appointment starting in less than 1 hour | |
| if (startTimeMs - currentTimeMs < ONE_HOURMs) { | |
| return false; | |
| } | |
| return true; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/appointment-rules.ts` around lines 22 - 30, The current cancellation
check computes startTimeMs from startTimeIso and will treat invalid timestamps
(NaN) as allowed cancellations; update the guard so you first validate
startTimeMs (e.g., via Number.isFinite(startTimeMs) or
!Number.isNaN(startTimeMs)) and return false immediately when the timestamp is
invalid before applying the existing 1-hour rule using ONE_HOURMs; modify the
logic around startTimeIso/startTimeMs to short-circuit invalid inputs and only
proceed to the existing comparison if the parsed time is valid.
There was a problem hiding this comment.
Thanks for pointing this out. I agree that an invalid startTimeIso could result in NaN and lead to unintended behavior in the current logic.
In a production setup, this would be handled by validating the timestamp before applying the 1-hour rule, and returning a safe default (e.g., disallowing cancellation) if the value is invalid.
This is a good defensive improvement and can be incorporated to make the logic more robust.
There was a problem hiding this comment.
@afifa17112005 — glad the suggestion is useful! Here's the minimal patch for when you apply it:
const startTimeMs = new Date(startTimeIso).getTime();
+ if (Number.isNaN(startTimeMs)) {
+ return false; // Treat unparseable timestamps as unsafe — deny cancellation
+ }
const ONE_HOURMs = 60 * 60 * 1000;A couple of extra points worth keeping in mind when you land this:
- Unit test coverage — the existing
tests/unit.test.tssuite is a great place to add a case likecanCancelByTime("not-a-date", "patient")→falseandcanCancelByTime("not-a-date", "doctor")→true(doctors bypass the time check entirely, so NaN is never reached for them; only the patient branch needs the guard, but having an explicit test documents the intent). Number.isNaNvsisNaN— preferNumber.isNaN(startTimeMs)over the globalisNaN()since the latter coerces strings before checking, which can produce false negatives for certain edge inputs.new Date("bad").getTime()always yieldsNaNas a number, soNumber.isNaNis the right tool here.
🧠 Learnings used
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:8-15
Timestamp: 2026-04-28T05:52:57.781Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the `role` and `action` fields are intentionally accepted from the client request body for demo/task purposes. In production, `role` would be derived from a trusted server-side session or JWT (not client input), and `action` would be validated against an allowlist (`"cancel"` | `"done"`) before any state mutation, with a 403 returned if the actor lacks permission.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:22-35
Timestamp: 2026-04-28T05:52:20.230Z
Learning: In `app/api/appointments/book/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic read-then-insert-then-update booking flow (race condition risk) is a known, intentional simplification for demo/task purposes. In production, an atomic slot-claim pattern (conditional UPDATE is_booked=false→true, then INSERT appointment, with compensating UPDATE to revert is_booked on insert failure) is planned. The PR description sequence diagram already documents this intended production flow.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:9-19
Timestamp: 2026-04-28T05:51:44.724Z
Learning: In the `app/api/appointments/book/route.ts` file of the botcode-com/task repository (Next.js + Supabase appointment booking system), `patient_id` is intentionally accepted from the client body for demonstration/task purposes. In a production system, the patient identity should be derived from the server-side Supabase session/JWT and the client-provided `patient_id` should be ignored or overridden.
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; | ||
| const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; |
There was a problem hiding this comment.
Validate required env vars explicitly.
Line 3 and Line 4 rely on non-null assertions; if envs are missing, failures become opaque. Throw a clear startup error instead.
Proposed fix
-const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
-const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
+
+if (!supabaseUrl || !supabaseServiceRoleKey) {
+ throw new Error("Missing Supabase admin environment variables");
+}📝 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.
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; | |
| const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; | |
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; | |
| const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; | |
| if (!supabaseUrl || !supabaseServiceRoleKey) { | |
| throw new Error("Missing Supabase admin environment variables"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/supabaseAdmin.ts` around lines 3 - 4, Replace the non-null assertions for
supabaseUrl and supabaseServiceRoleKey with explicit validation: read
process.env.NEXT_PUBLIC_SUPABASE_URL and process.env.SUPABASE_SERVICE_ROLE_KEY
into variables (supabaseUrl, supabaseServiceRoleKey), check if either is missing
or empty, and if so throw a clear Error (e.g. "Missing required env:
NEXT_PUBLIC_SUPABASE_URL" / "Missing required env: SUPABASE_SERVICE_ROLE_KEY")
during startup; then use these validated variables where the module creates the
Supabase admin client so failures are immediate and descriptive.
There was a problem hiding this comment.
Thanks for the feedback. I agree that relying on non-null assertions for environment variables can lead to unclear runtime failures if the values are missing.
In a production setup, these variables would be explicitly validated during initialization, and a clear error would be thrown if any required configuration is missing. This ensures a fail-fast approach and improves debuggability.
This is a valid improvement and would be incorporated in a production-ready version.
There was a problem hiding this comment.
@afifa17112005, understood — thanks for the clear explanation! That aligns with the context already noted in our knowledge base for this file (demo scope, with explicit validation planned for production). The fail-fast pattern you described — removing non-null assertions and throwing a descriptive startup error when either env var is absent — is exactly the right approach for a production deployment. Happy to generate the exact diff when you're ready to harden this for production.
🧠 Learnings used
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: lib/supabaseAdmin.ts:1-6
Timestamp: 2026-04-28T05:54:58.529Z
Learning: In `lib/supabaseAdmin.ts` of the botcode-com/task repo (Next.js + Supabase project), the `import "server-only"` guard is intentionally omitted for demo/task purposes. The `supabaseAdmin` client (initialized with the Supabase service-role key) is used exclusively in server-side API routes. In production, a `server-only` guard would be added to prevent accidental client-side imports and fail the build if misused.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/admin/dashboard/page.tsx:13-18
Timestamp: 2026-04-28T05:48:57.287Z
Learning: In `app/admin/dashboard/page.tsx` and `app/api/admin/login/route.ts` of the botcode-com/task repo (Next.js + Supabase project), the admin authentication uses a simplified cookie-presence-only check (`admin_token` cookie stores `admin.id` as plain text, set as httpOnly with 1-week lifetime). This is intentional for demo/task purposes. In production, the plan is to replace this with either a signed JWT (validated via `jose`) or Supabase Auth sessions with role claims. The password comparison in `app/api/admin/login/route.ts` is also plain-text (no hashing), also intentional for demo scope.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:22-35
Timestamp: 2026-04-28T05:52:20.230Z
Learning: In `app/api/appointments/book/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic read-then-insert-then-update booking flow (race condition risk) is a known, intentional simplification for demo/task purposes. In production, an atomic slot-claim pattern (conditional UPDATE is_booked=false→true, then INSERT appointment, with compensating UPDATE to revert is_booked on insert failure) is planned. The PR description sequence diagram already documents this intended production flow.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:53-71
Timestamp: 2026-04-28T05:53:17.654Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic sequential updates — first updating appointment status then freeing the slot (`is_booked = false`) with no error handling on the slot update — is an intentional simplification for demo/task purposes. In production, both writes should be wrapped in a single atomic Supabase RPC/stored procedure, or at minimum a rollback should revert the appointment status if the slot update fails.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:9-19
Timestamp: 2026-04-28T05:51:44.724Z
Learning: In the `app/api/appointments/book/route.ts` file of the botcode-com/task repository (Next.js + Supabase appointment booking system), `patient_id` is intentionally accepted from the client body for demonstration/task purposes. In a production system, the patient identity should be derived from the server-side Supabase session/JWT and the client-provided `patient_id` should be ignored or overridden.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:8-15
Timestamp: 2026-04-28T05:52:57.781Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the `role` and `action` fields are intentionally accepted from the client request body for demo/task purposes. In production, `role` would be derived from a trusted server-side session or JWT (not client input), and `action` would be validated against an allowlist (`"cancel"` | `"done"`) before any state mutation, with a 403 returned if the actor lacks permission.
| }, | ||
| "dependencies": { | ||
| "@supabase/supabase-js": "^2.49.4", | ||
| "dotenv": "^17.4.2", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether dotenv is actually used in source/scripts
rg -n 'from\s+["'"'"']dotenv["'"'"']|require\(["'"'"']dotenv["'"'"']\)|dotenv/config|dotenv\.config' --type js --type ts --type mjs --type cjs
rg -n '"scripts"|"seed"|--env-file' package.jsonRepository: botcode-com/task
Length of output: 169
🏁 Script executed:
#!/bin/bash
# Check package.json for dotenv dependency
cat -n package.json | head -30
# Also check seed.mjs and other potential entry points
echo "=== Checking seed.mjs ==="
if [ -f seed.mjs ]; then
cat -n seed.mjs | head -20
fi
# Search for any .env usage pattern more broadly
echo "=== Broader env/dotenv references ==="
rg -i 'dotenv|process\.env|env-file' --type-list | grep -E 'js|ts|mjs|cjs|json' | head -5
find . -name "*.mjs" -o -name "*.cjs" 2>/dev/null | head -10Repository: botcode-com/task
Length of output: 2267
🏁 Script executed:
#!/bin/bash
# Check other .mjs files
echo "=== check.mjs ==="
if [ -f check.mjs ]; then
cat -n check.mjs
fi
echo "=== postcss.config.mjs ==="
if [ -f postcss.config.mjs ]; then
cat -n postcss.config.mjs
fi
# Final comprehensive search for any dotenv reference
echo "=== Final dotenv usage check ==="
rg 'dotenv' --no-filenameRepository: botcode-com/task
Length of output: 979
Remove the unused dotenv dependency.
Line 13 adds dotenv as a runtime dependency, but it is never imported or used in the codebase. The project loads environment variables through Node's native --env-file flag (seed script), fs.readFileSync() (check.mjs), and direct process.env access (Next.js framework). Remove this dependency to reduce the dependency surface.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 13, Remove the unused dotenv dependency entry by
deleting the "dotenv": "^17.4.2" line from package.json and then update
lockfiles by running your package manager (npm install or yarn install) to
regenerate package-lock.json / yarn.lock; also search the repo for any remaining
dotenv imports/usages and remove them if found to ensure the dependency is fully
removed.
There was a problem hiding this comment.
Thanks for the suggestion. You're right that dotenv is not currently used in the codebase, as environment variables are handled via the runtime and process.env.
It was initially included for flexibility during development, but removing it would help reduce unnecessary dependencies. This is a valid cleanup improvement.
There was a problem hiding this comment.
@afifa17112005, glad that aligns with your assessment! When you're ready to clean it up, here's the minimal change needed:
// package.json
- "dotenv": "^17.4.2",After editing package.json, run npm install (or your preferred package manager) to regenerate the lockfile. Since check.mjs already uses fs.readFileSync + regex to load .env.local and the seed script uses Node's native --env-file flag, no other code changes are needed.
🧠 Learnings used
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:22-35
Timestamp: 2026-04-28T05:52:20.230Z
Learning: In `app/api/appointments/book/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic read-then-insert-then-update booking flow (race condition risk) is a known, intentional simplification for demo/task purposes. In production, an atomic slot-claim pattern (conditional UPDATE is_booked=false→true, then INSERT appointment, with compensating UPDATE to revert is_booked on insert failure) is planned. The PR description sequence diagram already documents this intended production flow.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:53-71
Timestamp: 2026-04-28T05:53:17.654Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic sequential updates — first updating appointment status then freeing the slot (`is_booked = false`) with no error handling on the slot update — is an intentional simplification for demo/task purposes. In production, both writes should be wrapped in a single atomic Supabase RPC/stored procedure, or at minimum a rollback should revert the appointment status if the slot update fails.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:9-19
Timestamp: 2026-04-28T05:51:44.724Z
Learning: In the `app/api/appointments/book/route.ts` file of the botcode-com/task repository (Next.js + Supabase appointment booking system), `patient_id` is intentionally accepted from the client body for demonstration/task purposes. In a production system, the patient identity should be derived from the server-side Supabase session/JWT and the client-provided `patient_id` should be ignored or overridden.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:8-15
Timestamp: 2026-04-28T05:52:57.781Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the `role` and `action` fields are intentionally accepted from the client request body for demo/task purposes. In production, `role` would be derived from a trusted server-side session or JWT (not client input), and `action` would be validated against an allowlist (`"cancel"` | `"done"`) before any state mutation, with a 403 returned if the actor lacks permission.
| it("Booking Flow: A valid patient books an available slot", async () => { | ||
| // Setup Mocks | ||
| const _reqBody = { | ||
| patient_id: "patient-1", | ||
| doctor_id: "doctor-1", | ||
| slot_id: "slot-1", | ||
| }; | ||
|
|
||
| const mockReq = new NextRequest("http://localhost:3000/api/appointments/book", { | ||
| method: "POST", | ||
| body: JSON.stringify(_reqBody), | ||
| }); | ||
|
|
||
| const mockSelect = vi.fn(); | ||
| const mockEq = vi.fn(); | ||
| const mockInsert = vi.fn(); | ||
| const mockUpdate = vi.fn(); | ||
|
|
||
| // supabase.from() | ||
| (supabaseAdmin.from as any).mockImplementation((table: string) => { | ||
| if (table === "slots") { | ||
| return { | ||
| select: () => ({ data: [{ id: "slot-1", is_booked: false }], error: null }), | ||
| update: mockUpdate.mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }) | ||
| }; | ||
| } | ||
| if (table === "appointments") { | ||
| return { | ||
| select: mockSelect.mockReturnValue({ | ||
| eq: vi.fn().mockReturnValue({ | ||
| eq: vi.fn().mockResolvedValue({ data: [], error: null }) // No active bookings | ||
| }), | ||
| }), | ||
| insert: mockInsert.mockReturnValue({ | ||
| select: vi.fn().mockReturnValue({ | ||
| single: vi.fn().mockResolvedValue({ data: { id: "new-appt" }, error: null }) | ||
| }) | ||
| }), | ||
| }; | ||
| } | ||
| }); | ||
|
|
||
| const response = await bookPOST(mockReq); | ||
| const data = await response.json(); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(data.message).toBe("Appointment booked successfully"); | ||
| expect(mockInsert).toHaveBeenCalledWith([ | ||
| { | ||
| patient_id: "patient-1", | ||
| doctor_id: "doctor-1", | ||
| slot_id: "slot-1", | ||
| status: "active", | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("Cancellation Flow: A valid cancellation request", async () => { | ||
| // Setup Mocks | ||
| const _reqBody = { | ||
| appointmentId: "appt-1", | ||
| action: "cancel", | ||
| role: "patient", | ||
| }; | ||
|
|
||
| const mockReq = new NextRequest("http://localhost:3000/api/appointments/cancel", { | ||
| method: "POST", | ||
| body: JSON.stringify(_reqBody), | ||
| }); | ||
|
|
||
| const futureTime = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(); // 2 hours from now | ||
|
|
||
| // supabase.from() | ||
| const mockUpdateAppt = vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }); | ||
| const mockUpdateSlot = vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }); | ||
|
|
||
| (supabaseAdmin.from as any).mockImplementation((table: string) => { | ||
| if (table === "appointments") { | ||
| return { | ||
| select: () => ({ | ||
| eq: () => ({ | ||
| single: vi.fn().mockResolvedValue({ | ||
| data: { id: "appt-1", status: "active", slot_id: "slot-1", slots: { start_time: futureTime } }, | ||
| error: null | ||
| }) | ||
| }) | ||
| }), | ||
| update: mockUpdateAppt, | ||
| }; | ||
| } | ||
| if (table === "slots") { | ||
| return { | ||
| update: mockUpdateSlot, | ||
| }; | ||
| } | ||
| }); | ||
|
|
||
| const response = await cancelPOST(mockReq); | ||
| const data = await response.json(); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(data.message).toBe("Appointment cancelled successfully"); | ||
| expect(mockUpdateAppt).toHaveBeenCalledWith({ status: "cancelled" }); | ||
| expect(mockUpdateSlot).toHaveBeenCalledWith({ is_booked: false }); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Add negative-path integration cases for critical business rules.
Please add API-level tests for: slot already booked/conflict, duplicate active appointment with same doctor, patient cancellation within 1 hour denied, and unauthorized done action. These are high-risk regression paths.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/integration.test.ts` around lines 21 - 125, Add integration tests for
the negative business paths by extending tests using bookPOST and cancelPOST:
(1) "slot already booked" — mock supabaseAdmin.from("slots").select to return
slot with is_booked: true and assert bookPOST returns 409 or appropriate error
message and no insert called (mockInsert not invoked); (2) "duplicate active
appointment with same doctor" — mock appointments select to return an active
appointment for same patient_id and doctor_id and assert bookPOST rejects with
409/validation error and no insert; (3) "patient cancellation within 1 hour
denied" — for cancelPOST mock appointments.select.single to return
slot.start_time < 1 hour away and assert response status indicates denial (e.g.,
400/403) and no update called on appointments or slots; (4) "unauthorized 'done'
action" — call cancelPOST (or relevant endpoint) with action: "done" and role
not allowed, mock auth/appointments accordingly and assert 401/403 and no
updates. For each case, use the existing patterns (supabaseAdmin.from
mockImplementation, mockInsert/mockUpdate mocks, NextRequest bodies) and assert
specific mock functions (mockInsert, mockUpdateAppt, mockUpdateSlot) were or
were not called and that response.json() contains the expected error
message/status.
There was a problem hiding this comment.
Thanks for the suggestion. I agree that adding negative-path integration tests would improve coverage, especially for critical business rules and edge cases.
In this implementation, the focus was on validating core positive flows. Expanding the test suite to include scenarios like slot conflicts, duplicate bookings, time-restricted cancellations, and unauthorized actions would further strengthen reliability and prevent regressions.
This is a valuable improvement and can be added to enhance the testing strategy.
There was a problem hiding this comment.
@afifa17112005, thanks for the kind words! To make it easy to pick up whenever you're ready, here are the four negative-path tests written to match the existing patterns in tests/integration.test.ts:
it("Booking Failure: Slot is already booked", async () => {
const mockReq = new NextRequest("http://localhost:3000/api/appointments/book", {
method: "POST",
body: JSON.stringify({ patient_id: "patient-1", doctor_id: "doctor-1", slot_id: "slot-1" }),
});
const mockInsert = vi.fn();
(supabaseAdmin.from as any).mockImplementation((table: string) => {
if (table === "slots") {
return {
select: () => ({ data: [{ id: "slot-1", is_booked: true }], error: null }),
};
}
if (table === "appointments") {
return { insert: mockInsert };
}
});
const response = await bookPOST(mockReq);
const data = await response.json();
expect(response.status).toBe(409);
expect(data.error).toMatch(/not available|already booked/i);
expect(mockInsert).not.toHaveBeenCalled();
});
it("Booking Failure: Duplicate active appointment with the same doctor", async () => {
const mockReq = new NextRequest("http://localhost:3000/api/appointments/book", {
method: "POST",
body: JSON.stringify({ patient_id: "patient-1", doctor_id: "doctor-1", slot_id: "slot-1" }),
});
const mockInsert = vi.fn();
(supabaseAdmin.from as any).mockImplementation((table: string) => {
if (table === "slots") {
return {
select: () => ({ data: [{ id: "slot-1", is_booked: false }], error: null }),
update: vi.fn().mockReturnValue({ eq: vi.fn().mockResolvedValue({ error: null }) }),
};
}
if (table === "appointments") {
return {
select: vi.fn().mockReturnValue({
eq: vi.fn().mockReturnValue({
eq: vi.fn().mockResolvedValue({
data: [{ id: "existing-appt", status: "active" }],
error: null,
}),
}),
}),
insert: mockInsert,
};
}
});
const response = await bookPOST(mockReq);
const data = await response.json();
expect(response.status).toBe(409);
expect(data.error).toMatch(/already.*doctor|duplicate/i);
expect(mockInsert).not.toHaveBeenCalled();
});
it("Cancellation Failure: Patient cancels within 1 hour of appointment", async () => {
const nearFutureTime = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // 30 min from now
const mockReq = new NextRequest("http://localhost:3000/api/appointments/cancel", {
method: "POST",
body: JSON.stringify({ appointmentId: "appt-1", action: "cancel", role: "patient" }),
});
const mockUpdateAppt = vi.fn();
const mockUpdateSlot = vi.fn();
(supabaseAdmin.from as any).mockImplementation((table: string) => {
if (table === "appointments") {
return {
select: () => ({
eq: () => ({
single: vi.fn().mockResolvedValue({
data: { id: "appt-1", status: "active", slot_id: "slot-1", slots: { start_time: nearFutureTime } },
error: null,
}),
}),
}),
update: mockUpdateAppt,
};
}
if (table === "slots") {
return { update: mockUpdateSlot };
}
});
const response = await cancelPOST(mockReq);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toMatch(/cannot cancel|too late|time/i);
expect(mockUpdateAppt).not.toHaveBeenCalled();
expect(mockUpdateSlot).not.toHaveBeenCalled();
});
it("Cancellation Failure: Patient attempts to mark appointment as 'done' (unauthorized)", async () => {
const futureTime = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString();
const mockReq = new NextRequest("http://localhost:3000/api/appointments/cancel", {
method: "POST",
body: JSON.stringify({ appointmentId: "appt-1", action: "done", role: "patient" }),
});
const mockUpdateAppt = vi.fn();
(supabaseAdmin.from as any).mockImplementation((table: string) => {
if (table === "appointments") {
return {
select: () => ({
eq: () => ({
single: vi.fn().mockResolvedValue({
data: { id: "appt-1", status: "active", slot_id: "slot-1", slots: { start_time: futureTime } },
error: null,
}),
}),
}),
update: mockUpdateAppt,
};
}
});
const response = await cancelPOST(mockReq);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.error).toMatch(/not authorized|forbidden|only doctor/i);
expect(mockUpdateAppt).not.toHaveBeenCalled();
});You may need to adjust the error message regex patterns to match the exact strings returned by your cancel and book route handlers. These cover all four high-risk regression paths flagged earlier.
🧠 Learnings used
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:22-35
Timestamp: 2026-04-28T05:52:20.230Z
Learning: In `app/api/appointments/book/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic read-then-insert-then-update booking flow (race condition risk) is a known, intentional simplification for demo/task purposes. In production, an atomic slot-claim pattern (conditional UPDATE is_booked=false→true, then INSERT appointment, with compensating UPDATE to revert is_booked on insert failure) is planned. The PR description sequence diagram already documents this intended production flow.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/book/route.ts:9-19
Timestamp: 2026-04-28T05:51:44.724Z
Learning: In the `app/api/appointments/book/route.ts` file of the botcode-com/task repository (Next.js + Supabase appointment booking system), `patient_id` is intentionally accepted from the client body for demonstration/task purposes. In a production system, the patient identity should be derived from the server-side Supabase session/JWT and the client-provided `patient_id` should be ignored or overridden.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:53-71
Timestamp: 2026-04-28T05:53:17.654Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the non-atomic sequential updates — first updating appointment status then freeing the slot (`is_booked = false`) with no error handling on the slot update — is an intentional simplification for demo/task purposes. In production, both writes should be wrapped in a single atomic Supabase RPC/stored procedure, or at minimum a rollback should revert the appointment status if the slot update fails.
Learnt from: afifa17112005
Repo: botcode-com/task PR: 2
File: app/api/appointments/cancel/route.ts:8-15
Timestamp: 2026-04-28T05:52:57.781Z
Learning: In `app/api/appointments/cancel/route.ts` of the botcode-com/task repository (Next.js + Supabase appointment booking system), the `role` and `action` fields are intentionally accepted from the client request body for demo/task purposes. In production, `role` would be derived from a trusted server-side session or JWT (not client input), and `action` would be validated against an allowlist (`"cancel"` | `"done"`) before any state mutation, with a 403 returned if the actor lacks permission.
🚀 Appointment Booking System
This pull request delivers a complete and well-structured backend implementation for the Appointment Booking System using Next.js and Supabase. It extends the provided UI by introducing reliable API endpoints, enforcing real-world business constraints, and ensuring consistent system behavior across all workflows.
The implementation focuses not only on functionality, but also on clarity, correctness, and maintainability.
🎯 Objective
To design and implement a robust appointment management system that:
🛠️ Implementation Highlights
🔹 API Design
📅 POST
/api/appointments/book❌ POST
/api/appointments/cancel👥 Role-Based System Design
👤 Patient
👨⚕️ Doctor
🛠️ System Admin
🗄️ Backend & Data Consistency
doctors,patients,slots,appointmentsis_bookedstatus📊 System Workflows
Booking Flow
Cancellation Flow
Doctor Action Flow
Admin Flow
🧠 Core Business Logic
🧪 Testing Strategy
✔ Unit Tests
Validated key business rules:
✔ Integration Tests
📊 Coverage
📸 UI Snapshots
🚀 Key Strengths of This Implementation
✅ Final Submission
All required features have been implemented and tested successfully.
This includes:
Thank you for reviewing my submission.