Skip to content

fix(self-scheduling): stop reporting a committed booking as a failure - #1059

Merged
rpgmem merged 1 commit into
developfrom
claude/altcha-captcha-integration-4vvuer
Sep 5, 2026
Merged

fix(self-scheduling): stop reporting a committed booking as a failure#1059
rpgmem merged 1 commit into
developfrom
claude/altcha-captcha-integration-4vvuer

Conversation

@rpgmem

@rpgmem rpgmem commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Fecha #1058. Encontrados no smoke manual do ambiente de testes; ambos pré-existentes e sem relação com o épico do captcha (#1053).

Bug 1 — a reserva era feita e o visitante ouvia que falhou

PHP Fatal error: Uncaught TypeError:
AppointmentReceiptHandler::get_receipt_url():
Argument #1 ($appointment_id) must be of type int, string given

$appointment['id'] chega do $wpdb como string — o driver MySQL devolve toda coluna como string — e o método declara int sob strict_types. Dispara dentro do e-mail de confirmação, que roda depois do commit da transação.

Virou HTTP 500 em vez de erro tratado porque o handler capturava \Exception, e TypeError é um \Error: \Throwable, mas não \Exception. Escapava como fatal não capturado — sem corpo JSON, deixando ao cliente apenas sua mensagem genérica, para um agendamento que já estava no banco. E a reação natural do usuário, reservar de novo, duplicava ou colidia com o guard de duplicidade.

Por que a suíte não pegou: makeAppointment() construía id como int. O teste era verde porque exercitava um mundo que não existe. A fixture agora devolve string, como o banco.

Bug 2 — todo activity log de agendamento criado era perdido

Cannot add or update a child row: a foreign key constraint fails (fk_ffc_activity_log_user)
INSERT INTO wp_ffc_activity_log (...) VALUES ('appointment_created', 'info', '{"appointment_id":7,…}', 7, …)

on_appointment_created() passa $appointment_id no 4º argumento de ActivityLog::log(), que é $user_id. Repare no payload: o contexto diz user_id: 1, a coluna recebeu 7 — o id do agendamento. O irmão on_appointment_cancelled() passa $cancelled_by ali, o que confirma a intenção do parâmetro.

Com a FK presente, o insert é rejeitado e a linha se perde. Sem a FK, é gravada atribuída ao usuário errado.

O que mudou além dos dois casts

A região pós-commit ficou estruturalmente segura: cada etapa — hook de after-create, e-mails, link de recibo — isolada com catch próprio e log que nomeia a etapa; o handler AJAX captura \Throwable.

E o principal: uma reserva commitada nunca mais é reportada como falha. A resposta vira sucesso com degraded: true e email_sent: false, em vez de afirmar uma confirmação que não saiu. Responder falha ali é pior do que responder uma verdade degradada, porque manda o visitante reservar de novo.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would alter existing behavior)
  • Refactor / chore (no functional change)
  • Documentation only

Test plan

  • composer test7510 testes, 21651 asserções, verde
  • vendor/bin/phpcs --standard=phpcs.xml.dist includes/ — limpo
  • vendor/bin/phpcs --standard=phpcs-tests.xml.dist tests/ — limpo
  • composer lint (PHPStan) — não executado (pacote dist-only, proxy deste ambiente dá 403 na API do GitHub). Fica para o CI.
  • Nenhuma mudança de JS/CSS
  • Smoke de confirmação no ambiente de testes — pendente, e é o teste que importa aqui: refazer o agendamento e verificar que a confirmação aparece em vez do erro

Os dois testes de regressão foram verificados falhando contra o código antigo:

  • test_booking_confirmation_survives_a_string_id_from_the_database reproduz o TypeError de produção literalmente
  • ActivityLogAppointmentAttributionTest (2 casos) falha com 777 is not identical to 42 e 777 is not identical to 0777 sendo o id do agendamento vazando para o campo de usuário

Checklist

  • CHANGELOG.md atualizado sob [Unreleased] / Fixed
  • Nenhuma entrada nova no baseline do PHPStan
  • Sem segredos, tokens ou PII no diff

Nota sobre o alias mock

ActivityLogAppointmentAttributionTest fica em classe própria porque alias-mockar ActivityLog é global ao processo, e as outras asserções do ActivityLogSubscriberTest dependem do buffering real. Ele também precisa de setConstantsMap — um alias mock não carrega constantes de classe, e o subscriber lê LEVEL_INFO.

O que este PR não faz

Não audita os demais parâmetros tipados que recebem colunas do $wpdb. O Bug 1 é de uma classe — string do banco em parâmetro int estrito — e nada garante que seja a única ocorrência. Uma varredura é trabalho próprio; se quiser, abro issue.

Closes #1058

🤖 Generated with Claude Code

https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU


Generated by Claude Code

Found by the manual smoke on the testes site. Two defects, both pre-existing
and unrelated to the captcha epic — the smoke only exposed them.

The visitor saw an error and the appointment was created anyway. The cause was
a TypeError: `$appointment['id']` arrives from `$wpdb` as a string — the MySQL
driver returns every column as one — and `AppointmentReceiptHandler::
get_receipt_url()` declares `int` under strict_types. It fires inside the
booking-confirmation email, which runs *after* the transaction commits.

It surfaced as HTTP 500 rather than a handled error because the handler caught
`\Exception`, and a TypeError is an `\Error`: `\Throwable`, but not
`\Exception`. So it escaped as an uncaught fatal — no JSON body, leaving the
client with nothing to render but its generic "an error occurred", for an
appointment that was already in the database. The natural response, booking
again, either duplicated it or hit the duplicate guard.

The suite never caught it because `makeAppointment()` built `id` as an int. The
test was green because it exercised a world that does not exist; the fixture now
returns a string, like the database does.

Beyond the cast, the post-commit region is now structurally safe. Each step —
the after-create hook, the notification emails, the receipt link — is isolated
with its own catch and a log naming the stage, and the AJAX handler catches
`\Throwable`. Once the row is committed the response is success, carrying
`degraded: true` and `email_sent: false` rather than claiming a confirmation
that never went out. Answering failure there is worse than answering a degraded
truth, because it sends the visitor to book a second time.

The error log also revealed a second, independent bug. `on_appointment_created()`
passes `$appointment_id` as the fourth argument of `ActivityLog::log()`, which is
`$user_id` — the sibling `on_appointment_cancelled()` passes `$cancelled_by`
there, confirming the intent. With `fk_ffc_activity_log_user` present MySQL
rejects the insert, so every appointment_created row is lost; without the FK the
row is written against whichever user holds that id.

Both regression tests were verified failing against the previous code: the email
one reproduces the production TypeError verbatim, and the attribution ones fail
with `777 is not identical to 42`.

Closes #1058

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XWx9qJdjZdAq8crxM9GCU
@rpgmem
rpgmem marked this pull request as ready for review September 5, 2026 05:03
@rpgmem
rpgmem enabled auto-merge (squash) September 5, 2026 05:04
@rpgmem
rpgmem merged commit 1b53da2 into develop Sep 5, 2026
19 checks passed
@rpgmem
rpgmem deleted the claude/altcha-captcha-integration-4vvuer branch September 5, 2026 05:12
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 33946135004

Warning

No base build found for commit 9f62702 on develop.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 89.932%

Details

  • Patch coverage: 18 uncovered changes across 1 file (44 of 62 lines covered, 70.97%).

Uncovered Changes

File Changed Covered %
includes/self-scheduling/class-ffc-self-scheduling-appointment-handler.php 34 16 47.06%
Total (4 files) 62 44 70.97%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 53507
Covered Lines: 48120
Line Coverage: 89.93%
Coverage Strength: 4.83 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants