Booking POST is a plain INSERT — reschedule re-fires EA-<id> and 500s on PK conflict #1

Closed
opened 2026-07-16 10:37:36 +02:00 by mivanchenko · 2 comments
Owner

backoffice/app/app.py:205 (add_entity) does an unconditional INSERT INTO <entity> .... Easy!Appointments fires appointment_save again on reschedule with the same booking_id (EA-<id>), so the second POST hits a primary-key conflict and returns 500 instead of updating the row.

Fix: add ON CONFLICT (<pk>) DO UPDATE SET ... for bookings (mirror the pattern already in backoffice/app/import_from_sheets.py:35). Consider doing the same for all keyed entities so re-delivered webhooks are idempotent.

Acceptance: reschedule an existing appointment → the existing bookings row is updated (start/end/status), no 500, activity logged as update.

`backoffice/app/app.py:205` (`add_entity`) does an unconditional `INSERT INTO <entity> ...`. Easy!Appointments fires `appointment_save` again on **reschedule** with the same `booking_id` (`EA-<id>`), so the second POST hits a primary-key conflict and returns 500 instead of updating the row. **Fix:** add `ON CONFLICT (<pk>) DO UPDATE SET ...` for `bookings` (mirror the pattern already in `backoffice/app/import_from_sheets.py:35`). Consider doing the same for all keyed entities so re-delivered webhooks are idempotent. **Acceptance:** reschedule an existing appointment → the existing `bookings` row is updated (start/end/status), no 500, activity logged as update.
mivanchenko added the bugcrmbooking labels 2026-07-16 10:37:36 +02:00
mivanchenko added the needs-triage label 2026-07-23 10:31:48 +02:00
Author
Owner

Superseded by the booking-module replacement (#14): bookings will be written directly through the new schema/API (#15, #16) with a Postgres EXCLUDE constraint instead of synced in from Easy!Appointments, so this failure mode cannot occur once cutover lands. Left open until #25 (decommission EA) actually ships and verifies/closes this.

Superseded by the booking-module replacement (#14): bookings will be written directly through the new schema/API (#15, #16) with a Postgres EXCLUDE constraint instead of synced in from Easy!Appointments, so this failure mode cannot occur once cutover lands. Left open until #25 (decommission EA) actually ships and verifies/closes this.
Author
Owner

Fixed in 1b8f8c1.

add_entity (backoffice/app/app.py) now does INSERT ... ON CONFLICT (pk) DO UPDATE SET col = EXCLUDED.col ... for every entity except activity_log (no client-supplied pk there), mirroring the old import_from_sheets.py upsert pattern. Insert vs. update is detected via RETURNING (xmax = 0), logged to activity_log as add <entity>/update <entity> accordingly, and the response is {"added": pk}/201 or {"updated": pk}/200.

Extended to all keyed entities (bookings, invoices, leads, clients, credentials, projects) per "consider doing the same for all keyed entities" — with one fix along the way: a redelivered leads/clients/credentials POST that omits created_at/received_at no longer clobbers the original creation timestamp on update (excluded from the DO UPDATE SET via a new INSERT_ONLY_COLS map).

Tests added in backoffice/app/tests/test_app_api.py: reschedule-repost updates the existing booking row (start/end/status) with no 500 and an "update bookings" activity-log entry; cancel-repost updates status; invoice repost updates instead of erroring; activity_log stays plain-insert (no client pk); leads/clients repost preserves the original received_at/created_at. Full suite: 155 passed.

Acceptance criteria met: reschedule updates the existing row, no 500, activity logged as update.

Fixed in 1b8f8c1. `add_entity` (`backoffice/app/app.py`) now does `INSERT ... ON CONFLICT (pk) DO UPDATE SET col = EXCLUDED.col ...` for every entity except `activity_log` (no client-supplied pk there), mirroring the old `import_from_sheets.py` upsert pattern. Insert vs. update is detected via `RETURNING (xmax = 0)`, logged to `activity_log` as `add <entity>`/`update <entity>` accordingly, and the response is `{"added": pk}`/201 or `{"updated": pk}`/200. Extended to all keyed entities (bookings, invoices, leads, clients, credentials, projects) per "consider doing the same for all keyed entities" — with one fix along the way: a redelivered `leads`/`clients`/`credentials` POST that omits `created_at`/`received_at` no longer clobbers the original creation timestamp on update (excluded from the `DO UPDATE SET` via a new `INSERT_ONLY_COLS` map). Tests added in `backoffice/app/tests/test_app_api.py`: reschedule-repost updates the existing booking row (start/end/status) with no 500 and an "update bookings" activity-log entry; cancel-repost updates status; invoice repost updates instead of erroring; activity_log stays plain-insert (no client pk); leads/clients repost preserves the original `received_at`/`created_at`. Full suite: 155 passed. Acceptance criteria met: reschedule updates the existing row, no 500, activity logged as update.
Sign in to join this conversation.