diff --git a/backend/controllers/parking/bookings.controller.ts b/backend/controllers/parking/bookings.controller.ts index 57bbd82e..9cb52ec5 100644 --- a/backend/controllers/parking/bookings.controller.ts +++ b/backend/controllers/parking/bookings.controller.ts @@ -440,6 +440,15 @@ class ParkingBookingsController { return } + if (errorMessage === ERROR_CODES.PARKING_INVALID_DATES) { + res.status(400).json({ + success: false, + error: ERROR_CODES.PARKING_INVALID_DATES, + code: ERROR_CODES.PARKING_INVALID_DATES, + }) + return + } + if (errorMessage.includes('No se puede hacer check-out')) { res.status(400).json({ success: false, diff --git a/backend/repositories/parking/bookings.repository.ts b/backend/repositories/parking/bookings.repository.ts index b7b134e7..d2d56325 100644 --- a/backend/repositories/parking/bookings.repository.ts +++ b/backend/repositories/parking/bookings.repository.ts @@ -3,6 +3,7 @@ // Versión profesional con booking_code // ============================================ import pool from '../../config/db.js' +import { ERROR_CODES } from '../../config/error-codes.js' import { targetSpot, type SpotRef } from '../../services/parking/target-spot.js' import { ResultSetHeader, RowDataPacket, type PoolConnection } from 'mysql2/promise' import type { @@ -467,6 +468,12 @@ class ParkingBookingsRepository { throw new Error(`No se puede hacer check-out: estado actual es '${booking[0].status}'`) } + // The check-out cannot be earlier than the check-in it closes + const checkin = booking[0].actual_checkin + if (checkin && actual_checkout.getTime() < new Date(checkin).getTime()) { + throw new Error(ERROR_CODES.PARKING_INVALID_DATES) + } + const payment_date = payment_amount ? new Date() : null await connection.query( diff --git a/backend/tests/parking/checkout-dates.test.ts b/backend/tests/parking/checkout-dates.test.ts new file mode 100644 index 00000000..70257a2e --- /dev/null +++ b/backend/tests/parking/checkout-dates.test.ts @@ -0,0 +1,53 @@ +// tests/parking/checkout-dates.test.ts +// Regression tests for PUT /api/parking/bookings/:code/checkout: the real +// check-out date was only checked for being readable, so a check-out earlier +// than its check-in was stored. It is now refused with PARKING_INVALID_DATES. + +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const connection = { + beginTransaction: vi.fn(), + commit: vi.fn(), + rollback: vi.fn(), + release: vi.fn(), + query: vi.fn(), +} + +vi.mock('../../config/db.js', () => ({ + default: { getConnection: vi.fn(async () => connection), query: vi.fn() }, +})) + +const { default: repository } = await import('../../repositories/parking/bookings.repository.js') +const { ERROR_CODES } = await import('../../config/error-codes.js') + +const checkin = new Date('2026-10-10T12:00:00Z') +const booked = [[{ id: 7, status: 'checked_in', actual_checkin: checkin }]] + +describe('checkOut dates', () => { + beforeEach(() => { + vi.clearAllMocks() + connection.query.mockReset() + vi.restoreAllMocks() + }) + + it('refuses a check-out earlier than the check-in and writes nothing', async () => { + connection.query.mockResolvedValueOnce(booked) + + await expect( + repository.checkOut(7, { actual_checkout: new Date('2026-10-10T11:59:00Z') }) + ).rejects.toThrow(ERROR_CODES.PARKING_INVALID_DATES) + expect(connection.query).toHaveBeenCalledOnce() // only the SELECT + expect(connection.rollback).toHaveBeenCalledOnce() + expect(connection.commit).not.toHaveBeenCalled() + }) + + it('accepts a check-out equal to or later than the check-in', async () => { + vi.spyOn(repository, 'findById').mockResolvedValue(null) + for (const at of ['2026-10-10T12:00:00Z', '2026-10-12T09:00:00Z']) { + connection.query.mockResolvedValueOnce(booked).mockResolvedValueOnce([{ affectedRows: 1 }]) + await repository.checkOut(7, { actual_checkout: new Date(at) }) + } + expect(connection.commit).toHaveBeenCalledTimes(2) + expect(connection.rollback).not.toHaveBeenCalled() + }) +}) diff --git a/docs/TODO.md b/docs/TODO.md index 473ba85c..a7da927e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -145,10 +145,6 @@ preview. - [ ] **Logbook: la fecha de una entrada admite cualquier día** — pasado o futuro, sin límite. Decidir qué rango tiene sentido. _Comprobado por mí el 2026-09-28._ -- [ ] **Parking: entrada y salida aceptan fechas reales arbitrarias** — el - cuerpo del `PUT` ya se valida con Zod (2026-10-02), pero las fechas de - entrada y salida reales no. _Según la revisión `security` L3 del - 2026-09-28 (fichero y línea en el informe); no repasado por mí._ - [ ] **Parking: `GET /vehicles` vuelca todos los vehículos** (matrícula y titular) sin paginar ni filtrar. _Según la revisión `security` L3 del 2026-09-28 (fichero y línea en el informe); no repasado por mí._