Files
AgendaPro Dev d796538fd9 feat: rebrand AgendaPro -> AgendaMax + calendar fixes + current-week demo seed
- Rebrand all user-facing text to AgendaMax
- Fix sidebar scrolling: card container no longer overflows html (flex h-full min-h-0)
- Fix calendar toolbar hover: active buttons keep readable contrast
- Sticky calendar toolbar (month/week/day always visible on scroll)
- Seed guarantees 2-4 appointments per day for current week (Mon-Sat)
2026-07-27 10:11:16 -06:00

7.4 KiB

Task 5 Report — backend booking.ts POST /book con autoAssign + guard transaccional

Status

DONE — all verification green; no commit (per repo policy + dispatch constraint).

Files changed

  • server/routes/booking.ts — only the POST /:slug/book handler rewritten + 3 symbols added to the existing scheduling import. No other file touched by this task.

What changed

Import (line 3-7)

Added autoAssign, isAvailable, runInTransaction to the existing scheduling import list (Task 4 had already imported parseWorkingHours, getWorkingHoursForDate, getCandidates, getExistingBusy, pickBestSlotEmployee, WorkingHoursMap).

POST /:slug/book handler (lines 125-214)

Replaced the old handler (which did a naive SELECT … LIMIT 1 to pick any employee and had no conflict guard) with the transactional, auto-assigning version per the plan verbatim:

  1. Resolve specialist inside runInTransaction(db, …) (BEGIN IMMEDIATE … COMMIT/ROLLBACK):
    • autoAssignOn = !!biz.auto_assign_specialist
    • clientChose = !autoAssignOn && employee_id ? Number(employee_id) : null
    • If !clientChose → call autoAssign(db, ctx) with bizWh = parseWorkingHours(biz.working_hours). If it returns null (no candidate free) → throw { status: 409, error: "ESE_HORARIO_OCUPADO" }.
    • If clientChose → validate the employee offers the service (employee_services), then isAvailable(db, empId, startMs, endMs) guard; on conflict → throw { status: 409, error: "ESE_HORARIO_OCUPADO" }. reasons = ["Tu especialista elegido"].
  2. Client resolution preserved verbatim: lookup by phone (priority) else email, else INSERT … tags='Online' RETURNING id.
  3. INSERT appointments uses new Date(endMs/startMs).toISOString().replace(/\.\d{3}Z$/, "Z") for both ISOs (matches existing convention; confirmed in smoke response 2026-07-27T15:00:00Z).
  4. 201 response now includes appointment.employee_id (number) and appointment.reasons: string[] alongside the previous fields.
  5. catch block: if e && typeof e === "object" && e.statusres.status(e.status).json({ error: e.error === "ESE_HORARIO_OCUPADO" ? "Esa hora acaba de ocuparse, elige otra." : e.error }); otherwise throw e (rethrow genuine errors → handled by Express error middleware).

Verification

1. npm run typecheck0 errors

2. Integration smoke (temp .mjs, deleted after)

Booted a fresh one-shot tsx server/index.ts on PORT=3007 HOST=127.0.0.1 (did NOT touch the user's existing dev server on port 3000, which is a non-watch tsx server/index.ts running stale code). Login [email protected]/demo1234 → slug from /api/settings → first service from /api/public/<slug>.

  • (a) auto-assign (no employee_id): POST /api/public/<slug>/book { service_id, start_at:<iso>, client:{name:"Auto Test", phone:"+525500009991"} }201, body:
    {"appointment":{"id":442,"start_at":"2026-07-27T15:00:00Z","end_at":"2026-07-27T15:45:00Z","price":280,"service_name":"Corte + arreglo de barba","employee_id":2,"employee_name":"Mateo Herrera","reasons":["Buena disponibilidad"]},"business":{"name":"Lumière Estética & Spa","currency_symbol":"$"},"client_created":true}
    
    employee_id is a real number (2). ✓ reasons is a non-empty array (["Buena disponibilidad"]).
  • (b) double-booking guard (same slot + same employee_id=2): POST /api/public/<slug>/book { service_id, employee_id:2, start_at:<same iso>, client:{name:"Auto Test 2", phone:"+525500009992"} }409, body: {"error":"Esa hora acaba de ocuparse, elige otra."} ✓ friendly message present.

The slots pre-fetch used GET /api/public/<slug>/slots?service_id=X&date=2026-07-27 → returned a ranked slot (iso 2026-07-27T15:00:00.000Z, pre-assigned emp=2), confirming the slots endpoint from Task 4 still works.

3. npm run test:unit15/15 PASS ✓ (no regression)

Self-review

  • Is the whole booking + guard inside the transaction? Yes. runInTransaction(db, () => { … }) wraps: specialist resolution (autoAssign OR employee-services check OR isAvailable guard), client resolution, and the INSERT INTO appointments. Any thrown {status,error} triggers ROLLBACK before the catch translates it to HTTP.
  • Does autoAssign run when employee_id is omitted? Yes. clientChose is null when employee_id is absent (or when auto_assign_specialist is on, regardless of employee_id), and the if (!empId) branch calls autoAssign(db, ctx). Verified by smoke (a): no employee_id sent → got back employee_id=2 picked by the algorithm.
  • Does the 409 path work? Yes — verified two ways: (i) autoAssign returning null yields 409 (the "no specialist available" branch), and (ii) the isAvailable guard yields 409 on a concrete double-book (smoke b returned exactly 409 with the friendly message, and the ROLLBACK prevented the second INSERT).
  • Does a client-chosen employee still get isAvailable-guarded? Yes. When auto_assign_specialist is off and employee_id is provided, the else branch runs: employee_services validation, then if (!isAvailable(db, empId, startMs, endMs)) throw {status:409,…}. Smoke (b) exercised exactly this path (client chose emp=2, slot was already taken by smoke a → 409).
  • Constraint: auto_assign_specialist=1 overrides client choice. Yes — clientChose = !autoAssignOn && employee_id ? … : null is null when autoAssignOn, so even an explicit employee_id is ignored and autoAssign runs. (Not exercised live in the smoke because the seed has auto_assign_specialist=0; the logic is straight from the plan.)
  • Preserved client-resolution logic? Yes — identical phone-then-email lookup with tags='Online' on create.
  • ISO format? Yes — .replace(/\.\d{3}Z$/, "Z") applied to both startIso and endIso; confirmed in the 201 response (...T15:00:00Z, no millis).
  • Scope discipline? Yes — only POST /:slug/book and the import list touched. Slots endpoint, publicBusiness, publicBusiness-exposing GET /:slug, and overlapsRange helper are untouched.
  • No commit made? Confirmed — git log shows latest commit is still e8d2435 (v1.0 base); all work is in the working tree.

Deviations

None. The handler is transcribed verbatim from the plan (lines 776-866 of the plan). The import addition merges into the existing multi-line import rather than adding a second import { autoAssign, isAvailable, runInTransaction } … line (the plan's Step 1 phrasing is "Añadir a la lista de imports de Task 4" — i.e. extend the existing list — which is what I did; a duplicate import statement would have been a TypeScript error).

Concerns

  • Dev-server caveat (informational, not a defect): the user's currently running dev server on port 3000 (PID 15992) is tsx server/index.ts (NOT tsx watch), so it has NOT picked up my booking.ts change. It will need a restart to serve the new code. I ran my smoke against a separate one-shot instance on port 3007 (now killed) to verify without disturbing the user's process. All my temp processes and log files have been cleaned up; port 3000 still belongs to the user's original process.
  • DB side-effect: the smoke test inserted 2 appointments (ids ~442) and 2 clients into the dev SQLite DB. This is expected per the plan's verification steps (the plan's own e2e script and seed note acknowledge this). A RESET_DB=1 npm run seed will reset if a clean DB is desired.