Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/controllers/parking/bookings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions backend/repositories/parking/bookings.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
53 changes: 53 additions & 0 deletions backend/tests/parking/checkout-dates.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
4 changes: 0 additions & 4 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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í._
Expand Down
Loading