Skip to content

fix(purchases): add 'paused' to valid_status CHECK constraint so Pause succeeds - #779

Merged
cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/772-pause-purchase-status
May 28, 2026
Merged

cristim merged 2 commits into
feat/multicloud-web-frontendfrom
fix/772-pause-purchase-status

Conversation

@cristim

@cristim cristim commented May 27, 2026 •

Copy link
Copy Markdown
Member

Summary

P1 fix for QA Planned 6.1: clicking "Pause" on a scheduled purchase surfaced the raw Postgres CHECK-constraint violation to the user:

Failed to pause purchase: execution <uuid> cannot be paused: ERROR: ... violates check constraint "valid_status" (SQLSTATE 23514)

Root cause

The valid_status CHECK constraint on purchase_executions was missing the 'paused' value. pausePlannedPurchase calls TransitionExecutionStatus, which issues UPDATE ... SET status = 'paused'. Postgres rejected it with SQLSTATE 23514.

Fix

New migration 000055_add_paused_status drops and re-adds the constraint with paused included (mirrors the pattern from 000013_add_running_status).

New valid_status set (post-migration):
'pending', 'running', 'notified', 'approved', 'cancelled', 'completed', 'failed', 'paused'

No handler logic changes were needed -- the handler already converts any TransitionExecutionStatus error into a 409 with a clear message. The constraint was the only gap.

Files changed

  • internal/database/postgres/migrations/000055_add_paused_status.up.sql
  • internal/database/postgres/migrations/000055_add_paused_status.down.sql
  • internal/api/handler_purchases_test.go (regression test for ineligible-source-status 409)

Test plan

  • TestHandler_pausePlannedPurchase_IneligibleStatus asserts 409 + "cannot be paused" message when source status isn't pause-eligible.
  • All Go tests pass.
  • Post-deploy: run golang-migrate up; click Pause on a scheduled purchase, confirm toast says paused and row's status flips.

Closes #772.

Summary by CodeRabbit

  • Tests

    • Added regression test coverage for pause functionality, validating error responses when attempting to pause planned purchases in ineligible states.
  • Chores

    • Extended database schema to support paused status for planned purchases with forward and backward migration support.

Review Change Stack

…to valid_status

The Pause button surfaced the raw Postgres CHECK constraint error to
the user. Added 'paused' to the valid_status CHECK constraint via
a new migration (000055), and added a regression test that verifies
ineligible-status transitions return a clean 409.

Closes #772.
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/now Drop other things impact/many Affects most users effort/s Hours type/bug Defect labels May 27, 2026
@coderabbitai

coderabbitai Bot commented May 27, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8a1792b9-16b1-4cd8-b7d5-ece8b066f3e0

📥 Commits

Reviewing files that changed from the base of the PR and between 8dd8359 and d9bae6d.

📒 Files selected for processing (1)
  • internal/api/handler_purchases_test.go
📝 Walkthrough

Walkthrough

This PR adds support for pausing scheduled purchase executions by introducing the paused status to the database schema and validating handler error responses when source statuses are ineligible for pause transitions.

Changes

Paused Status Support

Layer / File(s) Summary
Database constraint update for paused status
internal/database/postgres/migrations/000055_add_paused_status.up.sql, internal/database/postgres/migrations/000055_add_paused_status.down.sql
The valid_status CHECK constraint on purchase_executions is expanded to include 'paused' in the migration UP script, with a corresponding DOWN script that reverts the constraint to the original set of allowed statuses and documents manual handling of any paused rows.
Ineligible status transition error handling test
internal/api/handler_purchases_test.go
A regression test TestHandler_pausePlannedPurchase_IneligibleStatus verifies that when the handler detects an ineligible status transition, it returns a 409 client error with a "cannot be paused" message instead of exposing the underlying SQL CHECK constraint violation.

Estimated Code Review Effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A pause button springs to life so fine,
With paused status in the database line,
The constraint now knows this state is real,
And errors flow back with honest appeal—
No SQL noise, just "cannot pause" clear! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the primary fix: adding 'paused' to the valid_status CHECK constraint to enable the Pause operation.
Linked Issues check ✅ Passed The PR fully addresses all coding objectives from issue #772: adds 'paused' to the CHECK constraint via migration [#772], includes regression test for ineligible status 409 response [#772], and implements the handler pre-validation pattern [#772].
Out of Scope Changes check ✅ Passed All changes are in-scope: two migration SQL files adding 'paused' status and one regression test for pause transitions; no unrelated refactoring or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/772-pause-purchase-status

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

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/api/handler_purchases_test.go (1)

988-989: ⚡ Quick win

Assert mockAuth expectations to avoid silent test false-positives.

ValidateSession is expected but never asserted. If auth is bypassed in a regression, this test can still pass.

Proposed patch
-	t.Cleanup(func() { mockStore.AssertExpectations(t) })
+	t.Cleanup(func() {
+		mockStore.AssertExpectations(t)
+		mockAuth.AssertExpectations(t)
+	})

Also applies to: 996-997

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_purchases_test.go` around lines 988 - 989, Test is
missing assertions for mockAuth leading to silent false-positives; add
expectations assertion for mockAuth after test execution. Specifically, after
the existing t.Cleanup call that asserts mockStore (and at the other location
around lines 996-997), call mockAuth.AssertExpectations(t) (or the equivalent
mockAuth.AssertExpectations in those tests) to ensure the expected
ValidateSession call was actually invoked; ensure you place the assertion in
each test that sets up mockAuth so ValidateSession failures are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/api/handler_purchases_test.go`:
- Around line 988-989: Test is missing assertions for mockAuth leading to silent
false-positives; add expectations assertion for mockAuth after test execution.
Specifically, after the existing t.Cleanup call that asserts mockStore (and at
the other location around lines 996-997), call mockAuth.AssertExpectations(t)
(or the equivalent mockAuth.AssertExpectations in those tests) to ensure the
expected ValidateSession call was actually invoked; ensure you place the
assertion in each test that sets up mockAuth so ValidateSession failures are
caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22a6af2b-3407-4138-9972-ebd0850a89a4

📥 Commits

Reviewing files that changed from the base of the PR and between d986b4d and 8dd8359.

📒 Files selected for processing (3)
  • internal/api/handler_purchases_test.go
  • internal/database/postgres/migrations/000055_add_paused_status.down.sql
  • internal/database/postgres/migrations/000055_add_paused_status.up.sql

…pause regression

CR nitpick: `ValidateSession` was expected on `mockAuth` but never asserted in
the t.Cleanup block, so a regression that bypasses auth could leave this test
falsely green. Add `mockAuth.AssertExpectations(t)` alongside the existing
`mockStore.AssertExpectations(t)` so both surfaces are verified.

Matches the project-wide "always assert mock expectations" rule.
@cristim

cristim commented May 28, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit dca027c into feat/multicloud-web-frontend May 28, 2026
4 checks passed
cristim added a commit that referenced this pull request Jun 1, 2026
…Resume)

After #779/#772 added the paused DB status, clicking Pause made the scheduled
purchase silently disappear from the list with no feedback. The
planned-purchases list read GetPendingExecutions, whose status set
(pending, notified) the scheduler relies on to decide what to FIRE and which
excludes paused, so paused rows dropped out.

Switch the list handler to GetExecutionsByStatuses with
[pending, notified, paused] so paused executions stay listed (with the
existing Paused badge and Resume button) while the scheduler keeps using the
narrower GetPendingExecutions and never fires paused rows. Restore the
soonest-first ordering. Add success toasts on Pause and Resume.

Pause stays distinct from Disable plan (#774, whole plan) and Cancel
(terminal): it is reversible and scoped to a single execution; the plan stays
enabled. Resume (paused -> pending) and the 409 on ineligible transitions
already existed from #772.

Tests: backend asserts the paused status set is requested, paused rows stay
returned, and ordering is ascending; frontend asserts the paused row renders
visibly with a badge and that Pause/Resume fire success toasts.
cristim added a commit that referenced this pull request Jun 1, 2026
…Resume) (#904)

* fix(planned-purchases): Pause is visible + reversible (badge, toast, Resume)

After #779/#772 added the paused DB status, clicking Pause made the scheduled
purchase silently disappear from the list with no feedback. The
planned-purchases list read GetPendingExecutions, whose status set
(pending, notified) the scheduler relies on to decide what to FIRE and which
excludes paused, so paused rows dropped out.

Switch the list handler to GetExecutionsByStatuses with
[pending, notified, paused] so paused executions stay listed (with the
existing Paused badge and Resume button) while the scheduler keeps using the
narrower GetPendingExecutions and never fires paused rows. Restore the
soonest-first ordering. Add success toasts on Pause and Resume.

Pause stays distinct from Disable plan (#774, whole plan) and Cancel
(terminal): it is reversible and scoped to a single execution; the plan stays
enabled. Resume (paused -> pending) and the 409 on ineligible transitions
already existed from #772.

Tests: backend asserts the paused status set is requested, paused rows stay
returned, and ordering is ascending; frontend asserts the paused row renders
visibly with a badge and that Pause/Resume fire success toasts.

* feat(config/store): add GetPlannedExecutions with ASC ordering

GetExecutionsByStatuses uses ORDER BY scheduled_date DESC + LIMIT, which is
correct for History (newest-first) but truncates the soonest rows when used
for the Planned Purchases list and total pending/notified/paused rows exceed
MaxListLimit. An in-memory ASC re-sort of the already-truncated subset cannot
recover what LIMIT dropped at the DB.

Add a dedicated GetPlannedExecutions method that mirrors the
GetExecutionsByStatuses shape (same status filter, same limit clamping,
same scan logic via queryExecutions) but flips the ORDER BY to
scheduled_date ASC NULLS LAST, id ASC. The secondary id ASC sort keeps the
ordering stable when multiple rows share a scheduled_date; NULLS LAST is
defensive against a future schema relaxation (today scheduled_date is
NOT NULL).

GetExecutionsByStatuses is left untouched so its other callers (History
queries that want newest-first) keep their semantics.

pgxmock regression coverage anchors the SQL contract:
- TestPGXMock_GetPlannedExecutions_UsesASCOrdering asserts the query has
  ASC + NULLS LAST + id ASC + LIMIT $2 via a strict regex matcher, so a
  regression to DESC or a dropped secondary sort fails the test.
- TestPGXMock_GetPlannedExecutions_EmptyStatuses guards the short-circuit.
- TestPGXMock_GetPlannedExecutions_LimitClamping covers the negative ->
  DefaultListLimit and over-MaxListLimit -> MaxListLimit clamps.

All store mocks (api, purchase, scheduler, analytics, server health) gain
a GetPlannedExecutions method so the StoreInterface contract is satisfied
across the codebase.

Refs CR on #904.

* fix(api/purchases): use GetPlannedExecutions to avoid truncating soonest rows

Handler.getPlannedPurchases was calling GetExecutionsByStatuses (ORDER BY
scheduled_date DESC + LIMIT $2) and then re-sorting ASC in-memory. When the
planned set (pending + notified + paused) exceeds MaxListLimit, the DB
returns only the LATEST rows; the in-memory sort just re-orders that
already-truncated subset. The soonest rows, exactly the ones the user has
to act on, can be omitted from the response entirely.

Switch the handler to the new GetPlannedExecutions store method (ASC at the
SQL level so LIMIT keeps the soonest rows), and drop the post-fetch
sort.SliceStable since the DB now returns rows in the correct order. The
"sort" import stays (still used for tuple normalisation elsewhere in the
file).

End-to-end regression coverage:
- TestHandler_getPlannedPurchases_SoonestRowsNotTruncated seeds 5 rows in
  ASC order, asserts all 5 reach the response in ASC order, and uses
  AssertNotCalled to guard against any future refactor re-introducing a
  parallel GetExecutionsByStatuses call on this code path.
- TestHandler_getPlannedPurchases_PausedStaysVisible updated to reflect
  the new ASC-from-store contract (was returning DESC then re-sorting).

Closes CR finding on #904.
@cristim
cristim deleted the fix/772-pause-purchase-status branch June 3, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant