diff --git a/docs/api/backend-data.md b/docs/api/backend-data.md index 2849766..badf282 100644 --- a/docs/api/backend-data.md +++ b/docs/api/backend-data.md @@ -54,6 +54,9 @@ Booking safety - не пересекается ли новая бронь с активной бронью; - можно ли создать запись в транзакции. +Обновление брони должно идти через optimistic concurrency contract: +`docs/api/booking-concurrency.md`. + Команды npm run prisma:generate — сгенерировать Prisma Client. diff --git a/docs/api/booking-concurrency.md b/docs/api/booking-concurrency.md new file mode 100644 index 0000000..4adc4ea --- /dev/null +++ b/docs/api/booking-concurrency.md @@ -0,0 +1,39 @@ +# Booking Concurrency + +Этот документ фиксирует целевой HTTP contract для optimistic concurrency control. + +## Проблема + +Два клиента могут одновременно обновить одну бронь. Например, оба отправляют +`PATCH /bookings/{bookingId}` со статусом `cancelled`. Без версии записи второй +запрос может выглядеть успешным, хотя он работал со stale состоянием. + +## Целевой contract + +Booking responses должны возвращать `ETag`, построенный из версии записи. +Изменяющие запросы должны передавать текущий token в `If-Match`. + +Пример: + +```http +PATCH /bookings/booking-1 +If-Match: "booking-1:7" +Content-Type: application/json + +{ "status": "cancelled" } +``` + +Если версия совпала, сервис применяет обновление и возвращает новый `ETag`. +Если запись уже изменилась, сервис возвращает: + +```http +HTTP/1.1 412 Precondition Failed +``` + +## Следующие изменения в коде + +- добавить поле `version` в `booking.Booking`; +- инкрементировать `version` при каждом статусном изменении; +- вернуть `ETag` в `GET /bookings` и `PATCH /bookings/{bookingId}`; +- проверять `If-Match` перед отменой или переносом брони; +- покрыть тестом конфликт двух параллельных отмен одной брони. diff --git a/docs/openapi/metrix-bot-api.yaml b/docs/openapi/metrix-bot-api.yaml index 999dcbf..fa5d7b0 100644 --- a/docs/openapi/metrix-bot-api.yaml +++ b/docs/openapi/metrix-bot-api.yaml @@ -263,6 +263,7 @@ paths: summary: Update booking status. parameters: - $ref: '#/components/parameters/BookingId' + - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: @@ -282,6 +283,8 @@ paths: $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' + '412': + $ref: '#/components/responses/PreconditionFailed' /invoices: post: tags: @@ -720,6 +723,13 @@ components: required: true schema: type: string + IfMatch: + name: If-Match + in: header + required: false + description: Optimistic concurrency token from the current booking ETag. + schema: + type: string LocationId: name: locationId in: path @@ -775,6 +785,12 @@ components: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + PreconditionFailed: + description: If-Match does not match the current booking version. + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' schemas: HealthResponse: type: object