- 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)
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 thePOST /:slug/bookhandler 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:
- Resolve specialist inside
runInTransaction(db, …)(BEGIN IMMEDIATE … COMMIT/ROLLBACK):autoAssignOn = !!biz.auto_assign_specialistclientChose = !autoAssignOn && employee_id ? Number(employee_id) : null- If
!clientChose→ callautoAssign(db, ctx)withbizWh = 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), thenisAvailable(db, empId, startMs, endMs)guard; on conflict →throw { status: 409, error: "ESE_HORARIO_OCUPADO" }.reasons = ["Tu especialista elegido"].
- Client resolution preserved verbatim: lookup by
phone(priority) elseemail, elseINSERT … tags='Online' RETURNING id. - INSERT appointments uses
new Date(endMs/startMs).toISOString().replace(/\.\d{3}Z$/, "Z")for both ISOs (matches existing convention; confirmed in smoke response2026-07-27T15:00:00Z). - 201 response now includes
appointment.employee_id(number) andappointment.reasons: string[]alongside the previous fields. - catch block: if
e && typeof e === "object" && e.status→res.status(e.status).json({ error: e.error === "ESE_HORARIO_OCUPADO" ? "Esa hora acaba de ocuparse, elige otra." : e.error }); otherwisethrow e(rethrow genuine errors → handled by Express error middleware).
Verification
1. npm run typecheck → 0 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_idis a real number (2). ✓reasonsis 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:unit → 15/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 theINSERT INTO appointments. Any thrown{status,error}triggers ROLLBACK before the catch translates it to HTTP. - Does autoAssign run when
employee_idis omitted? Yes.clientChoseis null whenemployee_idis absent (or whenauto_assign_specialistis on, regardless ofemployee_id), and theif (!empId)branch callsautoAssign(db, ctx). Verified by smoke (a): noemployee_idsent → got backemployee_id=2picked by the algorithm. - Does the 409 path work? Yes — verified two ways: (i)
autoAssignreturning null yields 409 (the "no specialist available" branch), and (ii) theisAvailableguard 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_specialistis off andemployee_idis provided, theelsebranch runs: employee_services validation, thenif (!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=1overrides client choice. Yes —clientChose = !autoAssignOn && employee_id ? … : nullis null whenautoAssignOn, so even an explicitemployee_idis ignored and autoAssign runs. (Not exercised live in the smoke because the seed hasauto_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 bothstartIsoandendIso; confirmed in the 201 response (...T15:00:00Z, no millis). - Scope discipline? Yes — only
POST /:slug/bookand the import list touched. Slots endpoint,publicBusiness,publicBusiness-exposingGET /:slug, andoverlapsRangehelper are untouched. - No commit made? Confirmed —
git logshows latest commit is stille8d2435(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(NOTtsx 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 seedwill reset if a clean DB is desired.