import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import { pool } from "../db/pool.ts"; import { createApp } from "../index.ts"; import { resetDb, seedMinimal } from "./helpers.ts"; import type { Server } from "node:http"; let ids: Awaited>; let server: Server; let base: string; before(async () => { await resetDb(); ids = await seedMinimal(); server = createApp().listen(0); base = `http://127.0.0.1:${(server.address() as { port: number }).port}`; }); after(async () => { server.close(); await pool.end(); }); function req(path: string, init: RequestInit = {}, userId = ids.employeeUserId) { return fetch(`${base}${path}`, { ...init, headers: { "content-type": "application/json", authorization: `Bearer ${userId}`, ...(init.headers || {}), }, }); } async function crearCita(start: string): Promise { const { rows } = await pool.query( `INSERT INTO appointments (business_id, client_id, employee_id, service_id, start_at, end_at, price) VALUES ($1,$2,$3,$4,$5::timestamptz,$5::timestamptz + interval '90 minutes',850) RETURNING id`, [ids.businessId, ids.clientId, ids.employeeId, ids.serviceId, start] ); return rows[0].id; } test("marcar Vino crea la visita y completa la cita", async () => { const id = await crearCita("2026-09-03T16:00:00Z"); const r = await req(`/api/appointments/${id}/attendance`, { method: "POST", body: JSON.stringify({ attended: true, total_charged: 900, payment_method: "cash" }), }); assert.equal(r.status, 200); const { appointment, visit } = await r.json(); assert.equal(appointment.status, "completed"); assert.equal(visit.total_charged, 900); assert.equal(visit.payment_method, "cash"); assert.equal(visit.recorded_by_user_id, ids.employeeUserId); }); test("marcar No vino no crea visita", async () => { const id = await crearCita("2026-09-03T18:00:00Z"); const r = await req(`/api/appointments/${id}/attendance`, { method: "POST", body: JSON.stringify({ attended: false }), }); assert.equal(r.status, 200); const { appointment, visit } = await r.json(); assert.equal(appointment.status, "no_show"); assert.equal(visit, null); const { rows } = await pool.query( `SELECT count(*)::int c FROM visits WHERE appointment_id = $1`, [id] ); assert.equal(rows[0].c, 0); }); test("marcar dos veces la misma cita devuelve 409", async () => { const id = await crearCita("2026-09-03T20:00:00Z"); await req(`/api/appointments/${id}/attendance`, { method: "POST", body: JSON.stringify({ attended: true }), }); const r = await req(`/api/appointments/${id}/attendance`, { method: "POST", body: JSON.stringify({ attended: false }), }); assert.equal(r.status, 409); const body = await r.json(); assert.match(body.error, /ya se resolvió/i); }); test("no se puede marcar asistencia en una cita cancelada", async () => { const id = await crearCita("2026-09-04T16:00:00Z"); await pool.query( `UPDATE appointments SET status='cancelled', cancelled_by='client' WHERE id=$1`, [id] ); const r = await req(`/api/appointments/${id}/attendance`, { method: "POST", body: JSON.stringify({ attended: true }), }); assert.equal(r.status, 409); }); test("una empleada no puede resolver la cita de otra", async () => { const otra = await pool.query( `INSERT INTO employees (business_id, name) VALUES ($1,'Otra especialista') RETURNING id`, [ids.businessId] ); const { rows } = await pool.query( `INSERT INTO appointments (business_id, client_id, employee_id, service_id, start_at, end_at, price) VALUES ($1,$2,$3,$4,'2026-09-05T16:00:00Z','2026-09-05T17:00:00Z',0) RETURNING id`, [ids.businessId, ids.clientId, otra.rows[0].id, ids.serviceId] ); const r = await req(`/api/appointments/${rows[0].id}/attendance`, { method: "POST", body: JSON.stringify({ attended: true }), }); assert.equal(r.status, 403); // La administradora sí puede. const rOwner = await req( `/api/appointments/${rows[0].id}/attendance`, { method: "POST", body: JSON.stringify({ attended: true }) }, ids.ownerUserId ); assert.equal(rOwner.status, 200); }); test("el toque deja evento y auditoría", async () => { const { rows } = await pool.query( `SELECT action FROM appointment_events WHERE action IN ('attended','no_show')` ); assert.ok(rows.some((r) => r.action === "attended")); assert.ok(rows.some((r) => r.action === "no_show")); const audit = await pool.query( `SELECT count(*)::int c FROM audit_log WHERE action = 'attendance'` ); assert.ok(audit.rows[0].c >= 2); });