Skip to content

fix(registration): delete rejected reservation orders on saga rollback - #621

Merged
smarcet merged 1 commit into
mainfrom
fix/reserve-saga-undo-detached-order
Oct 7, 2026
Merged

smarcet merged 1 commit into
mainfrom
fix/reserve-saga-undo-detached-order

Conversation

@smarcet

@smarcet smarcet commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

ref: https://app.clickup.com/t/86bcd8mh1

Problem

When a reservation is rejected after its order was created (for example ApplyPromoCodeTask rejecting a speakers promo code that is not valid for the buyer), the saga rollback fails and the order is left behind:

  • ReserveOrderTask::undo() throws Detached entity SummitAttendeeBadge@N cannot be removed.
  • The order and its tickets stay Reserved until the revocation job cancels them (~20 minutes).
  • The caller receives the Doctrine exception instead of the original ValidationException.
  • ReserveTicketsTask::undo() (ticket stock) and PreProcessReservationTask::undo() (member quota) never run, because the exception aborts the compensation loop.

Seen on OCP Global (2026-10-05): a speaker retried a SPEAKERS_DISCOUNT_CODE reservation three times in 21 seconds. The one-use rule worked and rejected attempts 2 and 3, but the failed rollback left their orders Reserved, which looked like three redemptions.

Root cause

DoctrineTransactionService::runRootTransaction clears the EntityManager when a root transaction fails (#533). The order and summit cached in the saga state were loaded by an earlier, already committed transaction, so they are detached when ReserveOrderTask::undo() runs.

Fix

  • ReserveOrderTask::undo() reloads the order (getByIdExclusiveLock) and the summit from their repositories inside its transaction, the same way ApplyPromoCodeTask and processOrder2Revoke do. Only the order id is read from the saga state.
  • Saga::abort() catches and logs a failing undo() and continues with the remaining ones, so one broken compensation neither masks the original exception nor skips the stock and quota release.

Tests

  • SummitOrderServiceTest::testReserveRejectedByPromoCodeLeavesNoOrderAndRestoresStock: reserving with a speakers discount code not assigned to the buyer throws the original ValidationException, leaves no order and restores quantity_sold. Failed before the fix with the production error, passes now.
  • SagaCompensationTest::testSagaAbortKeepsUndoingAndRethrowsOriginalWhenAnUndoFails: failed before the fix, passes now.
  • SagaCompensationTest::testUndoDeletesOrderAndDetachesTicketsFromAttendees updated: it pinned the old behaviour (same instances passed in by mock). It now models the reload through the repositories.

Run inside the summit-api container:

vendor/bin/phpunit tests/SummitOrderServiceTest.php
vendor/bin/phpunit tests/Unit/Services/SagaCompensationTest.php

tests/SummitOrderServiceTest.php: 42 tests, 0 failures, 3 skipped (existing markTestSkipped). tests/Unit/Services/ has 2 failures in SponsorUserPermissionTrackingTest that fail identically without this change.

Not covered

The cron revocation still clears a speaker's redeemed flag by email when it cancels any order carrying the code (SpeakersPromoCodeTrait::removeUsage). Orders orphaned by this bug were one source of that; other abandoned orders remain legitimate. Not changed here.

Summary by CodeRabbit

  • Bug Fixes
    • Order cleanup now continues if an individual rollback step fails, while still reporting the error.
    • Failed reservations caused by an invalid promo code now remove the reserved order and restore the ticket quantity.
    • Order cleanup checks that the order still exists before proceeding, helping prevent errors when it has already been removed.

When a reservation is rejected after the order was created (e.g. a speakers
promo code that is not valid for the buyer, raised by ApplyPromoCodeTask),
ReserveOrderTask::undo() failed with "Detached entity SummitAttendeeBadge
cannot be removed". The order and its tickets stayed Reserved until the
revocation job cancelled them, and the caller received the Doctrine error
instead of the ValidationException.

Since the root transaction rollback clears the EntityManager (#533), the order
and summit cached in the task state are detached by the time undo() runs.
undo() now reloads both from their repositories, as ApplyPromoCodeTask and
processOrder2Revoke already do.

Saga::abort() also keeps running the remaining undo() calls when one fails and
no longer replaces the original exception, so ticket stock and member quota
are still released.
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 072b8fc0-dcf7-41b9-a8c3-b15f26eb29b1
📥 Commits

Reviewing files that changed from the base of the PR and between 98b17cb and 406faa5.

📒 Files selected for processing (3)
  • app/Services/Model/Imp/SummitOrderService.php
  • tests/SummitOrderServiceTest.php
  • tests/Unit/Services/SagaCompensationTest.php

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Saga compensation now continues after an undo operation throws. Order cleanup reloads the order and summit before removal. Promo-code quota checking no longer runs after order creation; quota enforcement occurs during reservation preprocessing.

Changes

Reservation compensation

Layer / File(s) Summary
Continue compensation after undo failures
app/Services/Model/Imp/SummitOrderService.php, tests/Unit/Services/SagaCompensationTest.php
Saga abort catches and logs exceptions from undo() and continues through earlier tasks. The test checks that the original downstream exception is rethrown.
Reload entities before order cleanup
app/Services/Model/Imp/SummitOrderService.php, tests/Unit/Services/SagaCompensationTest.php, tests/SummitOrderServiceTest.php
Order compensation reloads the order under an exclusive lock and reloads the summit before removal. Tests check entity reloads and verify that an invalid guest promo code leaves no order and does not change sold quantity.

Priority: ⚪ Not assessed

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: romanetar

Merge Risk: ⚪ Minimal · up to 406fa

No merge-blocking issue was identified in the changed reservation rollback behavior. Normal checks can proceed.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: deleting rejected reservation orders during saga rollback.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-621/

This page is automatically updated on each push to this PR.

@smarcet smarcet self-assigned this Oct 6, 2026
@smarcet
smarcet requested review from romanetar and a balanced review from Copilot October 6, 2026 21:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The rollback behavior is correctly implemented and covered by focused unit and integration regression tests.

Review effort: Balanced
Findings: None

What changed in this PR

Fixes reservation saga rollback so rejected orders are removed while all compensations continue.

Changes:

  • Reloads detached orders and summits before deletion.
  • Continues rollback after individual compensation failures.
  • Adds regression coverage for cleanup, stock restoration, and exception propagation.
File Description
app/​Services/​Model/​Imp/​SummitOrderService.php Makes saga compensation resilient and reloads entities before order removal.
tests/​SummitOrderServiceTest.php Tests rejected reservation cleanup and stock restoration.
tests/​Unit/​Services/​SagaCompensationTest.php Tests entity reload and continued compensation after failure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@romanetar romanetar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@smarcet
smarcet merged commit 63e301b into main Oct 7, 2026
71 of 72 checks passed
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