Skip to content

fix(db): rename status value cancelled->canceled (expand-contract, US spelling) - #1277

Merged
cristim merged 2 commits into
mainfrom
fix/db-rename-cancelled-to-canceled
Jul 17, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/db-rename-cancelled-to-canceled

Conversation

@cristim

@cristim cristim commented Jun 21, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Migration 000078 (EXPAND): widens CHECK constraints on purchase_executions and ri_exchange_history to accept both 'cancelled' and 'canceled'; adds canceled_by alongside cancelled_by; backfills all existing rows from 'cancelled' to 'canceled'.
  • Code cutover: all Go production paths now write/read "canceled" and canceled_by; all //nolint:misspell directives on these identifiers removed.
  • Deferred CONTRACT step: dropping the old spelling from constraints and removing the cancelled_by column is tracked in the follow-up issue (to be filed).

Expand-contract deploy ordering

  1. Apply migration 000078 (this PR) while old code is still running -- both spellings accepted, new column present.
  2. Deploy this code (writes "canceled" only).
  3. After deploy is stable, apply the CONTRACT migration (separate PR) to drop 'cancelled' from constraints and drop cancelled_by.

Test plan

  • go build ./... passes (no compilation errors)
  • go test ./... passes (5476 tests, 0 failures)
  • No remaining //nolint:misspell directives for cancelled/cancelled_by
  • Migration 000078 up.sql is idempotent (uses IF EXISTS guards and ADD COLUMN IF NOT EXISTS)
  • Migration 000078 down.sql reverts correctly (restores old constraints, drops canceled_by, backfills "canceled" -> "cancelled")

Summary by CodeRabbit

  • New Features

    • Standardized cancellation status responses and messages to “canceled.”
    • Scheduled purchases now direct users to the appropriate revoke flow.
    • Cancellation history, filters, badges, and totals recognize both current and legacy status spellings.
    • Cancellation details now identify the responsible user or approval method.
  • Bug Fixes

    • Improved handling of repeated cancellations, legacy records, and backend errors.
    • Preserved cancellation attribution during database migration and rollback.
    • Updated API documentation to support both status spellings during migration.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1adc13e3-2f8a-4c86-9534-7f4b49af19bd

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0cafb and 2e684b8.

📒 Files selected for processing (12)
  • frontend/src/__tests__/history.test.ts
  • frontend/src/history.ts
  • frontend/src/riexchange.ts
  • internal/api/handler_history.go
  • internal/api/handler_history_test.go
  • internal/api/handler_purchases.go
  • internal/api/handler_purchases_test.go
  • internal/api/openapi.yaml
  • internal/config/types.go
  • internal/database/postgres/migrations/000089_rename_cancelled_to_canceled.down.sql
  • internal/database/postgres/migrations/000089_rename_cancelled_to_canceled.up.sql
  • internal/database/postgres/migrations/000089_rename_cancelled_to_canceled_test.go

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR introduces canonical canceled status handling while retaining legacy cancelled compatibility during migration. It updates database schemas, persistence, purchase cancellation flows, RI exchange handling, history reporting, frontend rendering, and regression tests.

Changes

Cancellation schema and migration

Layer / File(s) Summary
Status contract and migration compatibility
internal/config/types.go, internal/config/interfaces.go, internal/api/openapi.yaml, internal/database/postgres/migrations/*, internal/config/types_test.go
Defines canonical and legacy status constants, renames cancellation attribution fields, adds immediate-cancel eligibility, expands database constraints, and tests upgrade and rollback behavior.

Persistence and cancellation flows

Layer / File(s) Summary
Store writes and compatibility reads
internal/config/store_postgres.go, internal/mocks/stores.go, internal/server/test_helpers_test.go, internal/config/store_postgres_pgxmock_test.go
Writes canceled and canceled_by, reads attribution through a legacy-compatible projection, updates cleanup and exchange cancellation, and aligns mocks.
Purchase cancellation and scheduled revocation
internal/api/handler_purchases.go, internal/api/handler_purchases_revoke.go, internal/purchase/approvals.go, internal/api/handler_purchases_test.go, internal/purchase/*test.go
Separates immediate cancellation from scheduled revocation, normalizes legacy canceled responses, classifies backend errors as server errors, and returns canonical status payloads.

History and RI exchange handling

Layer / File(s) Summary
History and frontend status compatibility
internal/api/handler_history.go, internal/api/handler_history_test.go, frontend/src/history.ts, frontend/src/__tests__/history.test.ts
Treats both spellings as canceled for annotations, KPI exclusion, badges, counts, and filtering.
RI exchange cancellation
internal/api/handler_ri_exchange.go, internal/api/*ri_exchange*test.go, frontend/src/riexchange.ts
Uses canceled for rejection transitions and responses while rendering both spellings with disabled styling.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PurchaseHandler
  participant PurchaseExecutionStore
  participant HistoryUI
  Client->>PurchaseHandler: request cancellation
  PurchaseHandler->>PurchaseExecutionStore: conditional status transition
  PurchaseExecutionStore-->>PurchaseHandler: canceled status or conflict
  PurchaseHandler-->>Client: canceled response
  Client->>HistoryUI: load purchase history
  HistoryUI->>HistoryUI: group canceled and cancelled statuses
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.00% 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 clearly summarizes the main change: renaming the status value from cancelled to canceled during an expand-contract migration.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/db-rename-cancelled-to-canceled

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

@cristim

cristim commented Jun 21, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/config/store_postgres.go (1)

1117-1125: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Project scheduled_execution_at before scanning planned executions.

Line 1125 stops the GetPlannedExecutions SELECT at idempotency_key, but Line 1438 scans scheduled_execution_at as the final destination. Real DB rows for this query will fail with a scan column-count mismatch.

Proposed projection fix
-		       executed_by_user_id, executed_at, pre_approval_skip_reason,
-		       idempotency_key
+		       executed_by_user_id, executed_at, pre_approval_skip_reason,
+		       idempotency_key, scheduled_execution_at

Also applies to: 1411-1438

🤖 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/config/store_postgres.go` around lines 1117 - 1125, The SELECT
statement in the GetPlannedExecutions query is missing the
scheduled_execution_at column. The query ends at idempotency_key on line 1125,
but the rows.Scan() call at line 1438 attempts to scan scheduled_execution_at as
the final field, causing a column-count mismatch. Add scheduled_execution_at to
the SELECT clause in the GetPlannedExecutions query before the closing backtick
to match the number of fields being scanned.
🧹 Nitpick comments (1)
internal/config/store_postgres_pgxmock_test.go (1)

546-575: ⚡ Quick win

Assert scheduled_execution_at in the projection guard.

This mock row includes scheduled_execution_at, but the expectation only matches idempotency_key, so the production query can omit the final scan column while this test still passes. Make the matcher enforce both columns.

Proposed matcher tightening
-	mock.ExpectQuery("idempotency_key").
+	mock.ExpectQuery(`idempotency_key,\s*scheduled_execution_at`).
🤖 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/config/store_postgres_pgxmock_test.go` around lines 546 - 575, The
mock expectation in TestPGXMock_GetPlannedExecutions_ProjectsAllScanColumns uses
a simple regex matcher that only validates idempotency_key is projected, but the
test row includes scheduled_execution_at as the final column. Strengthen the
ExpectQuery regex pattern to enforce that both idempotency_key and
scheduled_execution_at columns are included in the SELECT projection, preventing
the production query from omitting the final scan column while the test
incorrectly passes.
🤖 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.

Inline comments:
In `@internal/api/handler_history.go`:
- Line 117: The historyExecutionStatuses list on line 117 needs to include both
"cancelled" and "canceled" spellings to maintain backward compatibility during
the migration period, as legacy instances may still write the old spelling to
the database. Similarly, the status comparison logic at lines 348-349 and
1025-1029 currently only branch on "canceled" and must be updated to also check
for "cancelled". Ensure the read paths accept both spellings while normalizing
all output to consistently use "canceled" in responses.

In `@internal/config/interfaces.go`:
- Around line 85-92: The comment documentation for the CancelExecutionAtomic
method incorrectly claims it handles the 'scheduled' status, but the actual
implementation only supports canceling executions in pending or notified states.
Remove the reference to 'scheduled' status from the CancelExecutionAtomic
comment and clarify that it only handles pending and notified statuses, since
scheduled execution cancellation is handled by the separate
CancelScheduledExecutionAtomic method. This will prevent callers from being
misled about which method to use for each status type.

In `@internal/config/store_postgres.go`:
- Line 1080: The store_postgres.go file has multiple SELECT queries that project
only the new column spelling canceled_by (at line 1080 and also at lines 1428
and 1524) but during the EXPAND phase, legacy code can still write to the old
column spelling cancelled_by. To maintain compatibility with late legacy writes,
modify the column projections to dual-read both the old cancelled_by and new
canceled_by columns using COALESCE to prefer the new spelling but fall back to
the legacy one. Additionally, ensure any cleanup or filtering predicates that
currently match only "canceled" are updated to also match "cancelled" to prevent
late legacy cancellations from being skipped.

In
`@internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql`:
- Around line 22-27: The rollback migration adds CHECK constraints that restrict
status to 'cancelled' (lines 22-27 and 42-43) before converting existing
'canceled' rows back to 'cancelled' (lines 55-61). This causes the constraint to
fail if any 'canceled' rows exist. Reorder the migration by moving the UPDATE
statements that convert 'canceled' to 'cancelled' to execute before the ALTER
TABLE ADD CONSTRAINT statements for both the purchase_executions and
refund_executions tables.
- Around line 47-50: The down migration in
000078_rename_cancelled_to_canceled.down.sql is dropping the canceled_by column
without first restoring its values back to the cancelled_by column, causing
actor attribution data to be lost during rollback. Before the ALTER TABLE
purchase_executions DROP COLUMN IF EXISTS canceled_by statement, add an UPDATE
statement to backfill the cancelled_by column with values from canceled_by where
canceled_by is not null, ensuring data preservation during migration reversal.

In `@internal/purchase/approvals.go`:
- Around line 264-275: The IsCancelable() predicate currently allows scheduled
status to pass through, but the CancelExecutionAtomic function only accepts
pending and notified statuses in its WHERE clause, causing scheduled executions
to fail with a misleading 409 error. Align the two checks by either updating the
IsCancelable() predicate to explicitly reject scheduled status (to match the
documented pending/notified-only policy), or adding scheduled to the allowed
statuses in the CancelExecutionAtomic WHERE clause if that is the intended
product behavior. Ensure both the session cancel path and email-link cancel path
use the same consistent predicate.

---

Outside diff comments:
In `@internal/config/store_postgres.go`:
- Around line 1117-1125: The SELECT statement in the GetPlannedExecutions query
is missing the scheduled_execution_at column. The query ends at idempotency_key
on line 1125, but the rows.Scan() call at line 1438 attempts to scan
scheduled_execution_at as the final field, causing a column-count mismatch. Add
scheduled_execution_at to the SELECT clause in the GetPlannedExecutions query
before the closing backtick to match the number of fields being scanned.

---

Nitpick comments:
In `@internal/config/store_postgres_pgxmock_test.go`:
- Around line 546-575: The mock expectation in
TestPGXMock_GetPlannedExecutions_ProjectsAllScanColumns uses a simple regex
matcher that only validates idempotency_key is projected, but the test row
includes scheduled_execution_at as the final column. Strengthen the ExpectQuery
regex pattern to enforce that both idempotency_key and scheduled_execution_at
columns are included in the SELECT projection, preventing the production query
from omitting the final scan column while the test incorrectly passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c99dae24-1276-4570-a642-e3fc55590f93

📥 Commits

Reviewing files that changed from the base of the PR and between 451a70f and bd64087.

📒 Files selected for processing (30)
  • internal/api/coverage_extras_test.go
  • internal/api/handler_dashboard.go
  • internal/api/handler_history.go
  • internal/api/handler_history_test.go
  • internal/api/handler_purchases.go
  • internal/api/handler_purchases_revoke.go
  • internal/api/handler_purchases_revoke_test.go
  • internal/api/handler_purchases_test.go
  • internal/api/handler_ri_exchange.go
  • internal/api/handler_ri_exchange_test.go
  • internal/api/handler_test.go
  • internal/api/router_660_permission_flips_test.go
  • internal/api/router_handlers_test.go
  • internal/config/interfaces.go
  • internal/config/store_postgres.go
  • internal/config/store_postgres_pgxmock_test.go
  • internal/config/types.go
  • internal/config/types_test.go
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql
  • internal/mocks/stores.go
  • internal/purchase/approvals.go
  • internal/purchase/approvals_test.go
  • internal/purchase/coverage_extra_test.go
  • internal/purchase/manager_test.go
  • internal/purchase/messages_test.go
  • internal/purchase/reaper_test.go
  • internal/purchase/scheduled_fire.go
  • internal/purchase/scheduled_fire_test.go
  • internal/server/test_helpers_test.go

Comment thread internal/api/handler_history.go Outdated
Comment thread internal/config/interfaces.go Outdated
Comment thread internal/config/store_postgres.go Outdated
Comment thread internal/purchase/approvals.go Outdated
@cristim cristim added severity/medium Moderate harm urgency/this-quarter Within the quarter impact/internal Team-internal only effort/m Days labels Jun 24, 2026
cristim added a commit that referenced this pull request Jun 24, 2026
…touched files

Make every .go file in this PR's diff fully golangci-lint v2.10.1 clean
(all linters), so the change introduces zero new lint findings. No
.golangci.yml changes; handled in code, with justified inline nolint only
for genuinely-unfixable cases.

Code fixes (no nolint):
- godot: added terminating periods to flagged comments (auto-fixed).
- misspell: British -> US spelling in prose comments and user-facing
  messages (behaviour->behavior, defence->defense, cancelled->canceled in
  ctx/error-message prose, etc.).
- gocritic: equalFold (strings.ToLower ==  -> strings.EqualFold),
  builtinShadow (max -> maxDepth), sloppyReassign, exitAfterDefer
  (rekey main runs cancel before log.Fatalf), importShadow (renamed local
  accounts/mock/provider vars), paramTypeCombine (merged same-type params),
  unnamedResult (named multi-returns).
- govet: shadow (renamed inner err in if-init statements),
  fieldalignment (reordered struct fields via the fieldalignment fixer).
- gosec G115: added explicit [0, MaxInt32] bounds guards before the int32
  / int narrowing conversions in connection.go and migrate.go.
- revive: reordered gcpTokenExchangeAttempt results so error is last.
- staticcheck: removed a dead freshness read (SA4006); switched the GCP
  loader off the deprecated CredentialsFromJSON.
- unused: removed dead planIntersectsAllowed and triggerColdStartCollect.
- unconvert: dropped a redundant int() on syscall.Stdin.
- noctx: exec.Command -> exec.CommandContext in the configure-* CLI flows.

Justified nolint (genuinely unfixable / cross-cutting):
- misspell: DB status value 'cancelled' (status CHECK constraint) and
  column cancelled_by (migration 000035) inside SQL strings / status
  literals; documented and tracked for rename in PR #1277.
- gosec: G505 HMAC-SHA1 is the RFC 6238 TOTP default; G117 fields must
  carry credential secrets (not logged); G204/G702/G703 are local CLI
  subprocess/file reads with hardcoded or validated arguments; G705 the
  Lambda->HTTP adapter relays an already-escaped handler response.
- gocritic: hugeParam and tooManyResults on interface-bound / range-fed /
  constructor signatures where a pointer/struct rewrite is broad
  aliasing-prone churn for a marginal copy saving; rangeValCopy on
  read-only loops over large elements.
- revive: established exported names (SchedulerConfig, MockSESClient,
  MockSNSClient) and the package name "api".
- unparam: router-handler contract error returns, the buildMigrateDSN
  sslmode override seam, and executePurchase's documented wasMultiAccount
  signal.

Verified with golangci-lint v2.10.1: 0 issues across all touched files,
--new-from-rev=origin/main reports 0 new issues, .golangci.yml is identical
to origin/main, no errcheck suppression. go build ./... passes;
go test ./... passes (the one pre-existing Azure scheduler mock failure is
unrelated).
cristim added a commit that referenced this pull request Jun 24, 2026
Eliminate the gocritic/unparam/revive/staticcheck //nolint suppressions
that were genuinely fixable, leaving only the gosec security exceptions,
the DB-schema 'cancelled' misspell exceptions, and a small set of
interface/SDK-contract judgment cases.

Code-fixed (nolint removed):
- gocritic rangeValCopy (15): converted every read-only range-over-large-
  element loop to an index loop (for i := range xs { x := &xs[i] / xs[i] }),
  preserving value-copy semantics where the body mutates or appends.
- gocritic hugeParam (28): pointer-ized the value params and threaded the
  change through all callers and tests. The account/rec fan-out paths in
  purchase/execution.go and scheduler.go (incl. fanOutPerAccount's callback
  type) are race-clean (verified with go test -race). NewHandler and
  NewScheduler now take *Config; ListStoredRecommendations' standalone
  helper buildRecommendationFilter takes *RecommendationFilter.
- gocritic tooManyResults (1): collectAllProviders now returns a single
  collectAllResult struct instead of six values.
- unparam (5): removed the always-"gcloud" program param from the GCP CLI
  helpers; removed the always-"" sslModeOverride param from buildMigrateDSN;
  dropped executePurchase's never-used wasMultiAccount result; dropped the
  always-nil error returns from getPublicInfo/getDeploymentInfo (router
  wrappers + tests updated).

Kept as documented exceptions:
- 18 gosec (each with a specific security justification).
- 16 misspell on the DB status value 'cancelled' / column cancelled_by
  (status CHECK + migration 000035; rename tracked in #1277).

Verified with golangci-lint v2.10.1: 0 issues on the 33 PR-touched files,
--new-from-rev=origin/main reports 0 new issues, .golangci.yml is identical
to origin/main, no nolint:errcheck. go build ./... and go test ./... pass
(the one pre-existing Azure scheduler mock failure is unrelated).
@cristim

cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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: 4

♻️ Duplicate comments (5)
internal/api/handler_history.go (1)

117-117: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep dual-read status compatibility during EXPAND.

History filtering/annotation/summary now accept only "canceled". In EXPAND rollout, old instances can still write "cancelled", so these rows can be skipped or misbucketed until CONTRACT. Accept both spellings on read paths, while continuing to normalize outward text to "canceled".

Suggested patch
-var historyExecutionStatuses = []string{"pending", "notified", "scheduled", "approved", "running", "paused", "completed", "partially_completed", "failed", "expired", "canceled"}
+var historyExecutionStatuses = []string{"pending", "notified", "scheduled", "approved", "running", "paused", "completed", "partially_completed", "failed", "expired", "cancelled", "canceled"}
@@
-	case "canceled":
+	case "cancelled", "canceled":
 		annotateCancelled(row, exec, approver)
@@
-		case "canceled":
+		case "cancelled", "canceled":
 			// A canceled purchase represents zero committed spend and zero

Also applies to: 348-348, 1025-1028

🤖 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_history.go` at line 117, The history status read path
currently only recognizes "canceled", which breaks EXPAND compatibility with
older rows still written as "cancelled". Update the status handling in the
history-related code paths (including historyExecutionStatuses and the
filtering/annotation/summary logic around the referenced symbols) to accept both
spellings on read, while keeping outward normalization and emitted text as
"canceled". Ensure any matching, bucketing, or comparisons treat "cancelled" and
"canceled" as equivalent until CONTRACT.
internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql (2)

47-50: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Restore cancelled_by before dropping canceled_by.

Any cancellations written by the new code keep attribution only in canceled_by. Dropping the column here loses that data on rollback.

Suggested fix
--- 3. Remove canceled_by column added by the up migration.
+-- 3. Backfill canceled_by -> cancelled_by before dropping canceled_by.
+UPDATE purchase_executions
+SET cancelled_by = canceled_by
+WHERE cancelled_by IS NULL
+  AND canceled_by IS NOT NULL;
+
+-- 4. Remove canceled_by column added by the up migration.
 ALTER TABLE purchase_executions
     DROP COLUMN IF EXISTS canceled_by;
🤖 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/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql`
around lines 47 - 50, The down migration currently drops canceled_by without
restoring the older cancelled_by field, which would lose rollback attribution
data. Update the migration around the purchase_executions ALTER TABLE step to
add or restore cancelled_by from the existing canceled_by values before removing
canceled_by, so the rollback preserves cancellation ownership. Use the
migration’s purchase_executions block and the canceled_by/cancelled_by column
names to locate the change.

22-27: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Backfill canceled rows before restoring cancelled-only constraints.

If any canceled rows exist—which is the normal post-up state—the ADD CONSTRAINT statements run before Lines 55-61 and the rollback aborts.

Suggested fix
--- 1. Restore purchase_executions status CHECK to 'cancelled' only.
+-- 1. Backfill rows first: 'canceled' -> 'cancelled'.
+UPDATE purchase_executions
+SET status = 'cancelled'
+WHERE status = 'canceled';
+
+UPDATE ri_exchange_history
+SET status = 'cancelled'
+WHERE status = 'canceled';
+
+-- 2. Restore purchase_executions status CHECK to 'cancelled' only.
@@
--- 2. Restore ri_exchange_history status CHECK to 'cancelled' only.
+-- 3. Restore ri_exchange_history status CHECK to 'cancelled' only.
@@
--- 4. Backfill rows back: 'canceled' -> 'cancelled'.
-UPDATE purchase_executions
-SET status = 'cancelled'
-WHERE status = 'canceled';
-
-UPDATE ri_exchange_history
-SET status = 'cancelled'
-WHERE status = 'canceled';
+-- already done above

Also applies to: 42-43, 55-61

🤖 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/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql`
around lines 22 - 27, The down migration restores a `cancelled`-only check
constraint before handling existing `canceled` data, so rollback fails when
post-up rows are present. Update the `purchase_executions` rollback path to
backfill any `status = 'canceled'` rows to `cancelled` before re-adding
`purchase_executions_status_check`, and ensure the same ordering is applied for
the other affected constraint blocks in this migration so the constraint changes
succeed cleanly.
internal/config/interfaces.go (1)

85-92: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

CancelExecutionAtomic still documents the wrong status set.

The caller path and implementation only CAS pending/notified; scheduled cancels go through CancelScheduledExecutionAtomic below. Leaving scheduled here advertises the wrong contract.

Suggested fix
-	// CancelExecutionAtomic atomically flips status from pending / notified /
-	// scheduled to canceled, setting canceled_by. The 'scheduled' status
-	// supports the Gmail-style pre-fire delay revoke path (issue `#290`).
+	// CancelExecutionAtomic atomically flips status from pending / notified
+	// to canceled, setting canceled_by.
🤖 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/config/interfaces.go` around lines 85 - 92, `CancelExecutionAtomic`
is documenting the wrong status transition set. Update the comment on
`CancelExecutionAtomic` in `interfaces.go` so it only describes the actual CAS
behavior for `pending` and `notified`, and remove the `scheduled`/Gmail-style
revoke wording since that path belongs to `CancelScheduledExecutionAtomic`; keep
the return contract and `WithTx` requirement aligned with the real
implementation.
internal/config/store_postgres.go (1)

1076-1089: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep execution reads and canceled-only cleanup compatible with late legacy writes.

Migration 000078 backfills once, but the rollout still leaves old writers alive for a window. Projecting only canceled_by and matching only status = 'canceled' means late cancelled/cancelled_by writes can lose actor attribution or miss canceled-only cleanup until the CONTRACT step. The same issue applies to the other execution SELECTs updated in this block.

Example compatibility shape
-		       cloud_account_id, source, approved_by, canceled_by, capacity_percent,
+		       cloud_account_id, source, approved_by,
+		       COALESCE(canceled_by, cancelled_by) AS canceled_by, capacity_percent,
-		        status IN ('completed', 'canceled')
+		        status IN ('completed', 'canceled', 'cancelled')

Also applies to: 1470-1485, 1520-1525

🤖 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/config/store_postgres.go` around lines 1076 - 1089, The execution
read queries and canceled-only cleanup are still too strict for the migration
rollout window. Update the purchase_executions SELECTs and the canceled cleanup
path to remain compatible with late legacy writes by accepting both
canceled/cancelled spellings and both canceled_by/cancelled_by attribution
fields, instead of relying only on a single status/value or column name. Apply
the same compatibility handling in the affected execution query helpers in this
block so older writers do not lose actor data or get skipped before the CONTRACT
step.
🧹 Nitpick comments (1)
internal/config/store_postgres_pgxmock_test.go (1)

440-440: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the SQL projection, not just the mock column names.

Changing these pgxmock column labels to canceled_by is good, but the current ExpectQuery(...) matchers still don't require the production SQL to project canceled_by. That means a query regression can stay green even though these fixtures were renamed. Please make at least one matcher per query contract assert the approved_by, canceled_by portion explicitly.

Example tightening
- mock.ExpectQuery("SELECT").WithArgs(pgxmock.AnyArg()).WillReturnRows(rows)
+ mock.ExpectQuery(`(?s)SELECT.*approved_by,\s*canceled_by,\s*capacity_percent.*FROM purchase_executions`).
+   WithArgs(pgxmock.AnyArg()).
+   WillReturnRows(rows)

Also applies to: 490-490, 557-557, 1896-1896

🤖 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/config/store_postgres_pgxmock_test.go` at line 440, The pgxmock
expectations for the store queries only rename fixture columns and still don’t
verify the actual SQL projection, so tighten the relevant ExpectQuery matchers
to explicitly require the approved_by, canceled_by portion in the projected
SELECT. Update the query contracts in store_postgres_pgxmock_test.go for the
affected cases so at least one matcher per query asserts those columns in the
SQL text, rather than relying only on the mock column labels.
🤖 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.

Inline comments:
In `@internal/api/handler_history_test.go`:
- Around line 1711-1714: The KPI regression fixtures in handler_history_test
were fully switched to the new spelling, which removes coverage for mixed-fleet
rollout behavior. In the regression table used for dollar KPI and TotalCompleted
checks, keep one legacy-spelled status value ("cancelled") alongside the new
"canceled" rows so the test still exercises both spellings. Update the affected
fixture entries in the KPI regression block and the corresponding later block to
preserve that legacy case.

In `@internal/api/handler_purchases.go`:
- Around line 390-393: The idempotent cancel path in the purchase handler is too
strict during the migration window: in the logic around the existing execution
status check, treat both "canceled" and legacy "cancelled" as already-canceled
so retries remain idempotent. Update the status guard in the handler that uses
GetExecutionByID and the cancellation flow to dual-read both spellings until the
CONTRACT migration is complete, while still preserving the plan-disable side
effect for already-canceled executions.
- Around line 380-381: The cancel path in TransitionExecutionStatus is
incorrectly converting real backend failures into a 409 conflict. Update the
error handling around errors.Is(err, config.ErrExecutionNotInExpectedStatus) in
the execution cancel flow so only ErrExecutionNotInExpectedStatus maps to
NewClientError(409), and all other errors are returned as server-side failures
instead of client conflicts. Use the existing executionID and
TransitionExecutionStatus call site to keep the behavior limited to the intended
status-conflict case.

In
`@internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql`:
- Around line 18-29: The EXPAND step in the migration is not backward-compatible
with the stated rollout order because it rewrites live rows to canceled and only
copies cancelled_by once, while pre-rename code may still read/write cancelled
and cancelled_by. Update the migration logic around the affected
rename/normalize blocks to keep old and new spellings synchronized during the
overlap window, or defer the row rewrite until after the code rollout; use the
existing DO blocks and status/column migration section to ensure both binaries
remain functional until the follow-up CONTRACT step.

---

Duplicate comments:
In `@internal/api/handler_history.go`:
- Line 117: The history status read path currently only recognizes "canceled",
which breaks EXPAND compatibility with older rows still written as "cancelled".
Update the status handling in the history-related code paths (including
historyExecutionStatuses and the filtering/annotation/summary logic around the
referenced symbols) to accept both spellings on read, while keeping outward
normalization and emitted text as "canceled". Ensure any matching, bucketing, or
comparisons treat "cancelled" and "canceled" as equivalent until CONTRACT.

In `@internal/config/interfaces.go`:
- Around line 85-92: `CancelExecutionAtomic` is documenting the wrong status
transition set. Update the comment on `CancelExecutionAtomic` in `interfaces.go`
so it only describes the actual CAS behavior for `pending` and `notified`, and
remove the `scheduled`/Gmail-style revoke wording since that path belongs to
`CancelScheduledExecutionAtomic`; keep the return contract and `WithTx`
requirement aligned with the real implementation.

In `@internal/config/store_postgres.go`:
- Around line 1076-1089: The execution read queries and canceled-only cleanup
are still too strict for the migration rollout window. Update the
purchase_executions SELECTs and the canceled cleanup path to remain compatible
with late legacy writes by accepting both canceled/cancelled spellings and both
canceled_by/cancelled_by attribution fields, instead of relying only on a single
status/value or column name. Apply the same compatibility handling in the
affected execution query helpers in this block so older writers do not lose
actor data or get skipped before the CONTRACT step.

In
`@internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql`:
- Around line 47-50: The down migration currently drops canceled_by without
restoring the older cancelled_by field, which would lose rollback attribution
data. Update the migration around the purchase_executions ALTER TABLE step to
add or restore cancelled_by from the existing canceled_by values before removing
canceled_by, so the rollback preserves cancellation ownership. Use the
migration’s purchase_executions block and the canceled_by/cancelled_by column
names to locate the change.
- Around line 22-27: The down migration restores a `cancelled`-only check
constraint before handling existing `canceled` data, so rollback fails when
post-up rows are present. Update the `purchase_executions` rollback path to
backfill any `status = 'canceled'` rows to `cancelled` before re-adding
`purchase_executions_status_check`, and ensure the same ordering is applied for
the other affected constraint blocks in this migration so the constraint changes
succeed cleanly.

---

Nitpick comments:
In `@internal/config/store_postgres_pgxmock_test.go`:
- Line 440: The pgxmock expectations for the store queries only rename fixture
columns and still don’t verify the actual SQL projection, so tighten the
relevant ExpectQuery matchers to explicitly require the approved_by, canceled_by
portion in the projected SELECT. Update the query contracts in
store_postgres_pgxmock_test.go for the affected cases so at least one matcher
per query asserts those columns in the SQL text, rather than relying only on the
mock column labels.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4df8fe4c-5f9b-4206-ab5b-26773daae389

📥 Commits

Reviewing files that changed from the base of the PR and between 451a70f and bd64087.

📒 Files selected for processing (30)
  • internal/api/coverage_extras_test.go
  • internal/api/handler_dashboard.go
  • internal/api/handler_history.go
  • internal/api/handler_history_test.go
  • internal/api/handler_purchases.go
  • internal/api/handler_purchases_revoke.go
  • internal/api/handler_purchases_revoke_test.go
  • internal/api/handler_purchases_test.go
  • internal/api/handler_ri_exchange.go
  • internal/api/handler_ri_exchange_test.go
  • internal/api/handler_test.go
  • internal/api/router_660_permission_flips_test.go
  • internal/api/router_handlers_test.go
  • internal/config/interfaces.go
  • internal/config/store_postgres.go
  • internal/config/store_postgres_pgxmock_test.go
  • internal/config/types.go
  • internal/config/types_test.go
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql
  • internal/mocks/stores.go
  • internal/purchase/approvals.go
  • internal/purchase/approvals_test.go
  • internal/purchase/coverage_extra_test.go
  • internal/purchase/manager_test.go
  • internal/purchase/messages_test.go
  • internal/purchase/reaper_test.go
  • internal/purchase/scheduled_fire.go
  • internal/purchase/scheduled_fire_test.go
  • internal/server/test_helpers_test.go

Comment thread internal/api/handler_history_test.go Outdated
Comment thread internal/api/handler_purchases.go Outdated
Comment thread internal/api/handler_purchases.go Outdated
Comment thread internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql Outdated
cristim added a commit that referenced this pull request Jun 24, 2026
… EXPAND

Address CodeRabbit review on PR #1277. The expand-contract rename must accept
both spellings/columns throughout the EXPAND phase; only the deferred contract
migration (#1278) removes the legacy spelling.

Down migration (CRITICAL rollback bug + actor loss):
- Reorder the DOWN path so data is converted 'canceled'->'cancelled' BEFORE the
  'cancelled'-only CHECK constraints are re-added. The old order re-added the
  narrowed constraint first, so any row still in the new state violated it and
  the whole rollback failed.
- Backfill canceled_by into cancelled_by BEFORE dropping canceled_by so actor
  attribution recorded by new code during the deploy window survives.

Reads stay dual-compatible:
- handler_history.go: accept both 'canceled' and 'cancelled' in the history
  fetch filter and in the KPI-summary / annotation switches so legacy rows
  written mid-deploy are neither dropped nor allowed to inflate KPIs.
- store_postgres.go: project COALESCE(canceled_by, cancelled_by) on every read
  so a late legacy write landing in cancelled_by is still surfaced.
- handler_purchases.go: the cancel-recovery idempotency check accepts both
  spellings.

Scheduled-cancel routing (doc + predicate reconciliation):
- interfaces.go: align the CancelExecutionAtomic doc to the implementation
  (pending/notified only; scheduled is handled by CancelScheduledExecutionAtomic).
- types.go: add IsImmediatelyCancelable (pending/notified) distinct from the
  broader IsCancelable (adds scheduled). The /cancel paths
  (cancelPurchaseViaSession, loadCancelableExecution) now gate on the narrow
  predicate so a scheduled row is routed to a clear "use the revoke endpoint"
  409 instead of misrouting into the pending/notified-only CAS and surfacing a
  misleading "concurrent operation" error.

Tests:
- New integration test 000078_..._test.go exercises up (both spellings accepted,
  legacy backfilled) and down (rollback succeeds with 'canceled' rows present,
  data converted back, actor attribution preserved, narrowed constraint restored).
- Unit tests for the cancelable predicates and the scheduled-routing behaviour
  on both cancel paths.
@cristim

cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

cristim added a commit that referenced this pull request Jun 25, 2026
…l errors

Address CodeRabbit round-2 review on PR #1277.

1. cancelOrRecoverExecution: classify errors correctly. A
   TransitionExecutionStatus failure that is NOT ErrExecutionNotInExpectedStatus
   is a real server-side fault, not a CAS conflict. Return a plain wrapped
   error (router -> generic 500, raw detail logged) instead of a 409 that
   misclassified a retriable backend failure as the caller's fault and leaked
   backend text. The genuine status-mismatch 409 path is unchanged.

2. Keep a legacy-spelling fixture in the KPI regression test so the
   dual-spelling read path stays covered during EXPAND.

3+4. Make the EXPAND migration genuinely deploy-order-safe with live old code.
   The UP migration is now purely ADDITIVE: it widens both CHECK constraints to
   accept both spellings and copies existing cancelled_by into canceled_by, but
   it NO LONGER normalizes status values. A one-time UP normalization was a
   false guarantee -- old code still running writes fresh 'cancelled' rows the
   instant after it runs. Instead the schema accepts both spellings and the
   application reads both at all times (status filter + KPI/cancel switches +
   COALESCE(canceled_by, cancelled_by) on every one of the 10 read projections),
   so any row written by old OR new code at any point reads correctly. The
   authoritative, complete normalization is deferred to the CONTRACT migration
   (#1278), which runs after all old code is gone. The down.sql still converts
   new-code 'canceled' rows back and drains canceled_by before narrowing the
   constraint (the round-1 critical-ordering fix is preserved). Migration
   header/down comments and issue #1278 updated to match exactly what the SQL
   does.

Misspell-clean without nolint in production code: add config.StatusCanceled and
config.LegacyStatusCanceled (the latter built by concatenation so the US-locale
misspell linter does not flag the legacy value) and reference them from the
dual-spelling code paths. The 000078 integration test carries one file-level
//nolint:misspell because it must reference the real legacy column name
cancelled_by and assert literal legacy values until #1278 renames them.

Tests: go build + go test ./... pass (5490). New unit test asserts the
backend-error path returns a non-ClientError (generic 5xx). Migration up/down
test updated for the additive design (status preserved, dual-read via COALESCE);
runs in CI (local Docker unavailable).
@cristim

cristim commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/api/handler_purchases.go (1)

399-404: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Normalize the legacy status before returning the idempotent result.

This dual-read path accepts LegacyStatusCanceled, but then returns existing unchanged. During EXPAND, a retry against a legacy-canceled row can still surface "cancelled" in the handler result even though the API contract is moving to "canceled".

Proposed fix
 	if existing.Status != config.StatusCanceled && existing.Status != config.LegacyStatusCanceled {
 		return nil, NewClientError(409, fmt.Sprintf(
 			"execution %s cannot be canceled (status=%s)",
 			executionID, existing.Status))
 	}
+	if existing.Status == config.LegacyStatusCanceled {
+		normalized := *existing
+		normalized.Status = config.StatusCanceled
+		existing = &normalized
+	}
 	return existing, nil
🤖 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.go` around lines 399 - 404, The idempotent
cancel path in the purchase handler currently accepts both current and legacy
canceled statuses but returns the legacy value unchanged. Update the return path
in the purchase cancellation logic so that when the status is
LegacyStatusCanceled it is normalized to StatusCanceled before returning the
existing record, keeping the handler response consistent with the API contract.
🤖 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.

Inline comments:
In `@internal/api/handler_purchases.go`:
- Around line 954-958: The cancel flow in guardImmediatelyCancelable and
authorizeSessionCancel is leaking execution state because the status-specific
409 check runs before authorization. Move the authorization check earlier in the
cancel path so an unauthorized caller cannot infer whether an execution exists
or is in a cancelable state, while still preserving the revoke guidance for
callers who are authorized for that execution.

In
`@internal/database/postgres/migrations/000078_rename_cancelled_to_canceled_test.go`:
- Around line 4-9: Remove the file-level misspell suppression from the migration
test and adjust the test/lint strategy so the legacy identifiers in
000078_rename_cancelled_to_canceled_test are still covered without adding a new
`//nolint:misspell`; update the relevant test setup around the migration
regression case so the file stays compliant with the PR’s no-suppression
requirement.
- Around line 60-62: The rollback test currently uses RunMigrations, which
advances to head and can include future migrations, so it is no longer pinned to
the 000078 behavior. Update the test in the 000078 migration rollback case to
migrate only up to renameCanceledVersion before rolling back to 77, using the
migration helper in that test instead of RunMigrations so the regression guard
stays fixed to migration 000078.

In
`@internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql`:
- Around line 42-46: The deployment-order comment in the 000078 migration is too
permissive and should be tightened to match the actual rollout sequence. Update
the note in the migration header to state that 000078 must run first and the
code deploy should follow, since the schema only accepts canceled after the
widened CHECKs are in place; reference the migration comment near the DEPLOY
ORDER note in 000078_rename_cancelled_to_canceled.up.sql.

---

Outside diff comments:
In `@internal/api/handler_purchases.go`:
- Around line 399-404: The idempotent cancel path in the purchase handler
currently accepts both current and legacy canceled statuses but returns the
legacy value unchanged. Update the return path in the purchase cancellation
logic so that when the status is LegacyStatusCanceled it is normalized to
StatusCanceled before returning the existing record, keeping the handler
response consistent with the API contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1f28d9d-e0f0-4566-befb-fbde533a47d8

📥 Commits

Reviewing files that changed from the base of the PR and between bd64087 and 0e28ba4.

📒 Files selected for processing (13)
  • internal/api/handler_history.go
  • internal/api/handler_history_test.go
  • internal/api/handler_purchases.go
  • internal/api/handler_purchases_test.go
  • internal/config/interfaces.go
  • internal/config/store_postgres.go
  • internal/config/types.go
  • internal/config/types_test.go
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.down.sql
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql
  • internal/database/postgres/migrations/000078_rename_cancelled_to_canceled_test.go
  • internal/purchase/approvals.go
  • internal/purchase/approvals_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/purchase/approvals_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/api/handler_history_test.go
  • internal/config/interfaces.go
  • internal/api/handler_history.go
  • internal/config/store_postgres.go

Comment thread internal/api/handler_purchases.go Outdated
Comment thread internal/database/postgres/migrations/000078_rename_cancelled_to_canceled_test.go Outdated
Comment thread internal/database/postgres/migrations/000078_rename_cancelled_to_canceled_test.go Outdated
Comment thread internal/database/postgres/migrations/000078_rename_cancelled_to_canceled.up.sql Outdated
cristim added a commit that referenced this pull request Jun 26, 2026
Security: move authorizeSessionCancel before guardImmediatelyCancelable in
cancelPurchaseViaSession so unauthorized callers cannot infer execution state
from 409 status-specific errors. Update the two status-guard tests to grant
cancel-own so auth passes before the guard fires (and the guard still rejects).

Migration test: pin the rollback test to MigrateToVersion(renameCanceledVersion)
instead of RunMigrations (head), so future migrations can't corrupt the
000078-specific regression guard. Remove the file-level //nolint:misspell
directive (non-functional with a space; contradicts PR's no-suppression goal)
and add a .golangci.yml exclusion for the migration test file instead, which is
the deliberate strategy for a file that must reference legacy DB spellings.

Migration comment: tighten the DEPLOY ORDER note in up.sql to state that
000078 must complete before new code writes 'canceled'; remove the misleading
"races ahead" phrasing that implied out-of-order deploys are safe.
@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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 added a commit that referenced this pull request Jun 26, 2026
Adversarial review of #1277 (cancelled -> canceled expand-contract) found
the rename was incomplete on the read side:

- frontend/src/history.ts: statusBadgeHTML, the chip-row counts, and the
  active-filter predicates only matched the British 'cancelled'. After
  the backend cut over to writing 'canceled', a row written by the new
  code fell through to the green Completed badge, was counted in the
  Completed total, and was invisible to the Cancelled chip filter -- the
  exact regression the rename is supposed to prevent. Match both
  spellings everywhere a status enum is compared.
- frontend/src/riexchange.ts: getStatusBadgeClass had the same
  single-spelling bug for ri_exchange_history rows.
- internal/api/openapi.yaml: PurchaseExecution.status enum still
  advertised only 'cancelled', so strict OpenAPI clients would treat the
  new 'canceled' value as an enum violation. Add both during the deploy
  window; the contract migration (#1278) drops 'cancelled'.

Also addresses the two outside-diff CodeRabbit comments on this PR:

- internal/api/handler_purchases.go: when cancelOrRecoverExecution
  recovers an idempotent cancel and finds the row in the legacy British
  spelling, normalize the in-memory copy to StatusCanceled before
  returning. Stored row is untouched -- the contract migration's
  authoritative backfill still observes every legacy row.
- internal/database/postgres/migrations/000078_*.up.sql: tighten the
  DEPLOY ORDER comment to the strict 3-step sequence (apply 000078
  against the OLD code; only then release the new code; then 1278) so a
  future operator can't infer a more permissive ordering and crash the
  cancel paths against the original 'cancelled'-only CHECK.

Tests:
- frontend/src/__tests__/history.test.ts: new test seeds rows in both
  spellings and a real completed row, asserts the badge renders
  Cancelled (not Completed) for both, the chip counts 2 (not 1), and
  clicking the Cancelled chip surfaces both rows. Fails on the pre-fix
  FE -- the canonical regression-replicates-the-real-scenario guard.
- internal/api/handler_purchases_test.go: new test seeds the recovery
  branch with status=LegacyStatusCanceled and asserts the response
  Status is normalized to StatusCanceled.

CI: Lint Code and Security Scanning are pre-existing main breakages
(2707 repo-wide lint issues; pgx GO-2026-5004 advisory) unrelated to
this PR's diff -- verified against the most recent main run.
@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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 added a commit that referenced this pull request Jul 16, 2026
Adversarial review of #1277 (cancelled -> canceled expand-contract) found
the rename was incomplete on the read side:

- frontend/src/history.ts: statusBadgeHTML, the chip-row counts, and the
  active-filter predicates only matched the British 'cancelled'. After
  the backend cut over to writing 'canceled', a row written by the new
  code fell through to the green Completed badge, was counted in the
  Completed total, and was invisible to the Cancelled chip filter -- the
  exact regression the rename is supposed to prevent. Match both
  spellings everywhere a status enum is compared.
- frontend/src/riexchange.ts: getStatusBadgeClass had the same
  single-spelling bug for ri_exchange_history rows.
- internal/api/openapi.yaml: PurchaseExecution.status enum still
  advertised only 'cancelled', so strict OpenAPI clients would treat the
  new 'canceled' value as an enum violation. Add both during the deploy
  window; the contract migration (#1278) drops 'cancelled'.

Also addresses the two outside-diff CodeRabbit comments on this PR:

- internal/api/handler_purchases.go: when cancelOrRecoverExecution
  recovers an idempotent cancel and finds the row in the legacy British
  spelling, normalize the in-memory copy to StatusCanceled before
  returning. Stored row is untouched -- the contract migration's
  authoritative backfill still observes every legacy row.
- internal/database/postgres/migrations/000078_*.up.sql: tighten the
  DEPLOY ORDER comment to the strict 3-step sequence (apply 000078
  against the OLD code; only then release the new code; then 1278) so a
  future operator can't infer a more permissive ordering and crash the
  cancel paths against the original 'cancelled'-only CHECK.

Tests:
- frontend/src/__tests__/history.test.ts: new test seeds rows in both
  spellings and a real completed row, asserts the badge renders
  Cancelled (not Completed) for both, the chip counts 2 (not 1), and
  clicking the Cancelled chip surfaces both rows. Fails on the pre-fix
  FE -- the canonical regression-replicates-the-real-scenario guard.
- internal/api/handler_purchases_test.go: new test seeds the recovery
  branch with status=LegacyStatusCanceled and asserts the response
  Status is normalized to StatusCanceled.

CI: Lint Code and Security Scanning are pre-existing main breakages
(2707 repo-wide lint issues; pgx GO-2026-5004 advisory) unrelated to
this PR's diff -- verified against the most recent main run.
@cristim
cristim force-pushed the fix/db-rename-cancelled-to-canceled branch from 1b16ab9 to 6e6a992 Compare July 16, 2026 20:34
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Renumbered this PR's migration 000082 -> 000084 (also re-rebased onto current origin/main).

Why: main now tops at migration 000083 (ladder_execution_enabled, merged via #1396). golang-migrate applies only versions strictly greater than the current schema version, so a 000082 migration would be silently skipped on any DB already advanced to 000083 (prod/staging) and the cancelled->canceled rename would never run. 000084 is the next free slot above main's max (#808 is taking 000085, so 000084 exactly).

Changes: renamed up/down SQL + integration test file; bumped pinned version constants (renameCanceledVersion 82->84, renameCanceledPrevVersion 81->83, the version now directly beneath); updated every 000082 reference in comments, OpenAPI, the frontend history/riexchange notes, and the .golangci.yml misspell exemption path.

Verified:

  • Collision check: git ls-tree origin/main .../migrations/ | grep 000084 is EMPTY (main max = 000083).
  • go build ./... = 0, go vet ./... = 0, golangci-lint --new-from-rev=origin/main = 0 issues.
  • Fresh testcontainer PG migrates cleanly THROUGH 000084 (Database migrations completed successfully (version: 84)); the 000084 expand/rollback integration tests and the full internal/config integration suite pass (serial exit 0).

@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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 added a commit that referenced this pull request Jul 16, 2026
Defect 1 -- migration number collision:
Renumber marketplace migration from 000068 (already deployed, skipped by
golang-migrate on prod) to 000084 (next free after main's 000083).
Verified: main tops at 000083; #1277 uses 000082; no open PR holds 000084.
All "000068" references in SQL comments and types.go updated to 000084.

Defect 2 -- offering_class never written:
CUDly's EC2 client hardwires OfferingClassTypeConvertible; savePurchaseHistory
never stamped the field, so every row landed with offering_class=NULL and the
Sell button never rendered.
Fix: (a) stamp offering_class='convertible' in savePurchaseHistory for all
AWS EC2 purchases so future CUDly-bought rows are correctly classified at write
time; (b) add FetchOfferingClass to the marketplaceEC2Client interface and
implement it via DescribeReservedInstances so the marketplace-list handler can
lazily populate offering_class for pre-migration rows and for
externally-created Standard RIs (purchased before CUDly existed); (c) add
StampOfferingClass to ConfigStore + PostgresStore + mock to persist the
fetched class so subsequent requests skip the extra AWS call; (d) a new
populateOfferingClass helper in the handler ties it together -- it runs when
offering_class is empty after validateMarketplaceListRequest and before the
definitive "standard only" gate.

How a 'standard' row comes to exist: an externally-created Standard RI will
get its offering_class populated from AWS DescribeReservedInstances on the
first POST .../marketplace-list call and the value persisted for future calls.
Rows purchased by CUDly are stamped 'convertible' at write time (CUDly only
ever buys Convertible EC2 RIs).

Defect 3 -- $0 price guardrail gap:
resolveMarketplacePriceSchedule accepted Price >= 0, allowing zero-dollar
listings. Fix: reject Price <= 0 with a clear error; add a named constant
awsMarketplaceMinPriceFloorFraction (5%) and a total-schedule floor check so
a schedule that sums to less than 5% of the prorated residual value is also
rejected with an explicit message. Extract floor check into
checkSuppliedScheduleFloor and the class-check predicate into
isKnownNonStandardOfferingClass to keep all touched functions below gocyclo 10.

Regression tests:
- TestMigration084_MarketplaceColumns: integration test proves the three new
  columns exist and round-trip after migrating a fresh DB through 000084.
- TestMarketplaceList_EmptyOfferingClassFetchedStandard: proves an
  externally-created Standard RI (offering_class="") is listable after the
  lazy-populate path fetches and stamps 'standard' from AWS.
- TestMarketplaceList_EmptyOfferingClassFetchedConvertible: proves the gate
  still rejects when AWS reports 'convertible' even if DB had no class.
- TestResolveMarketplacePriceSchedule_ZeroPriceRejected: Price=0 rejected.
- TestResolveMarketplacePriceSchedule_BelowFloorRejected: sub-floor rejected.
cristim added a commit that referenced this pull request Jul 16, 2026
…collision

PR #1277 (cancelled->canceled rename) was concurrently renumbered to 000084
and merges before #808. Once it lands, main will hold 000084, so #808's
000084 would collide on the merge ref (pre-commit --all-files migration
check) and give golang-migrate two version-84 migrations. Since #1277 takes
the lower number and merges first, #808 moves to 000085.

- git mv the up/down/test migration files 000084_* -> 000085_*
- update the "-- Migration" header in the up.sql and "-- Revert migration"
  in the down.sql to 000085
- update every 000084 reference in comments: config/types.go (x3),
  config/interfaces.go, config/store_postgres.go, api/handler_marketplace.go
  (x2), providers/aws/services/ec2/client.go
- rename TestMigration084_MarketplaceColumns -> TestMigration085_* and its
  fixture identifiers (ri-085-test, ril-085-test)

Also fix the integration-test fixture surfaced by running it against a real
testcontainer PG: plan_id is a UUID FK, so the literal 'plan-085' failed with
SQLSTATE 22P02. The INSERT now lists only NOT-NULL-without-default columns
plus the three new marketplace columns and omits plan_id/plan_name/ramp_step/
source (nullable or defaulted), so the test needs no purchase_plans fixture.
Verified: migrations apply cleanly through version 85 and the three columns
round-trip.
cristim added a commit that referenced this pull request Jul 16, 2026
)

The Approve button was visible for a user's own queued purchase even when
their role lacked approve-any or approve-own permission. Ownership alone
was incorrectly treated as sufficient authorization.

Root causes fixed:
- canApprovePendingRow() in history.ts now requires an explicit
  approve-own permission before consulting ownership; approve-any
  continues to bypass the ownership check as before.
- DefaultUserPermissions() no longer includes approve-own:purchases.
  The Standard Users group must not self-approve by default; an
  explicit custom group grant is required (four-eyes principle).
- Migration 000086 removes approve-own from the seeded Standard Users
  Postgres group for existing deployments. Numbered 000086 to sit after
  the in-flight 000084 (#1277) and 000085 (#808), which merge first.
- permissions.generated.ts regenerated to match the updated Go default.

Regression tests added (fail-before / pass-after):
- Frontend: "user without any approve permission sees NO Approve button
  even on their own rows (issue #1407 four-eyes)"
  in src/__tests__/history-approve-button.test.ts
- Backend: TestHandler_approvePurchase_RejectsCreatorWithoutApprovePermission
  in internal/api/handler_purchases_test.go -- asserts 403 when the
  creator calls approve without holding approve-any or approve-own.
- Migration: TestMigration_RemoveApproveOwnFromStandardUsers (integration)
  confirms 000086 removes approve-own while retaining cancel-own/retry-own,
  and that its down migration restores approve-own.
- Unit: DefaultUserPermissions four-eyes guard in types_test.go.
cristim added a commit that referenced this pull request Jul 16, 2026
… EXPAND

Address CodeRabbit review on PR #1277. The expand-contract rename must accept
both spellings/columns throughout the EXPAND phase; only the deferred contract
migration (#1278) removes the legacy spelling.

Down migration (CRITICAL rollback bug + actor loss):
- Reorder the DOWN path so data is converted 'canceled'->'cancelled' BEFORE the
  'cancelled'-only CHECK constraints are re-added. The old order re-added the
  narrowed constraint first, so any row still in the new state violated it and
  the whole rollback failed.
- Backfill canceled_by into cancelled_by BEFORE dropping canceled_by so actor
  attribution recorded by new code during the deploy window survives.

Reads stay dual-compatible:
- handler_history.go: accept both 'canceled' and 'cancelled' in the history
  fetch filter and in the KPI-summary / annotation switches so legacy rows
  written mid-deploy are neither dropped nor allowed to inflate KPIs.
- store_postgres.go: project COALESCE(canceled_by, cancelled_by) on every read
  so a late legacy write landing in cancelled_by is still surfaced.
- handler_purchases.go: the cancel-recovery idempotency check accepts both
  spellings.

Scheduled-cancel routing (doc + predicate reconciliation):
- interfaces.go: align the CancelExecutionAtomic doc to the implementation
  (pending/notified only; scheduled is handled by CancelScheduledExecutionAtomic).
- types.go: add IsImmediatelyCancelable (pending/notified) distinct from the
  broader IsCancelable (adds scheduled). The /cancel paths
  (cancelPurchaseViaSession, loadCancelableExecution) now gate on the narrow
  predicate so a scheduled row is routed to a clear "use the revoke endpoint"
  409 instead of misrouting into the pending/notified-only CAS and surfacing a
  misleading "concurrent operation" error.

Tests:
- New integration test 000078_..._test.go exercises up (both spellings accepted,
  legacy backfilled) and down (rollback succeeds with 'canceled' rows present,
  data converted back, actor attribution preserved, narrowed constraint restored).
- Unit tests for the cancelable predicates and the scheduled-routing behaviour
  on both cancel paths.
cristim added a commit that referenced this pull request Jul 16, 2026
…l errors

Address CodeRabbit round-2 review on PR #1277.

1. cancelOrRecoverExecution: classify errors correctly. A
   TransitionExecutionStatus failure that is NOT ErrExecutionNotInExpectedStatus
   is a real server-side fault, not a CAS conflict. Return a plain wrapped
   error (router -> generic 500, raw detail logged) instead of a 409 that
   misclassified a retriable backend failure as the caller's fault and leaked
   backend text. The genuine status-mismatch 409 path is unchanged.

2. Keep a legacy-spelling fixture in the KPI regression test so the
   dual-spelling read path stays covered during EXPAND.

3+4. Make the EXPAND migration genuinely deploy-order-safe with live old code.
   The UP migration is now purely ADDITIVE: it widens both CHECK constraints to
   accept both spellings and copies existing cancelled_by into canceled_by, but
   it NO LONGER normalizes status values. A one-time UP normalization was a
   false guarantee -- old code still running writes fresh 'cancelled' rows the
   instant after it runs. Instead the schema accepts both spellings and the
   application reads both at all times (status filter + KPI/cancel switches +
   COALESCE(canceled_by, cancelled_by) on every one of the 10 read projections),
   so any row written by old OR new code at any point reads correctly. The
   authoritative, complete normalization is deferred to the CONTRACT migration
   (#1278), which runs after all old code is gone. The down.sql still converts
   new-code 'canceled' rows back and drains canceled_by before narrowing the
   constraint (the round-1 critical-ordering fix is preserved). Migration
   header/down comments and issue #1278 updated to match exactly what the SQL
   does.

Misspell-clean without nolint in production code: add config.StatusCanceled and
config.LegacyStatusCanceled (the latter built by concatenation so the US-locale
misspell linter does not flag the legacy value) and reference them from the
dual-spelling code paths. The 000078 integration test carries one file-level
//nolint:misspell because it must reference the real legacy column name
cancelled_by and assert literal legacy values until #1278 renames them.

Tests: go build + go test ./... pass (5490). New unit test asserts the
backend-error path returns a non-ClientError (generic 5xx). Migration up/down
test updated for the additive design (status preserved, dual-read via COALESCE);
runs in CI (local Docker unavailable).
cristim added a commit that referenced this pull request Jul 16, 2026
Security: move authorizeSessionCancel before guardImmediatelyCancelable in
cancelPurchaseViaSession so unauthorized callers cannot infer execution state
from 409 status-specific errors. Update the two status-guard tests to grant
cancel-own so auth passes before the guard fires (and the guard still rejects).

Migration test: pin the rollback test to MigrateToVersion(renameCanceledVersion)
instead of RunMigrations (head), so future migrations can't corrupt the
000078-specific regression guard. Remove the file-level //nolint:misspell
directive (non-functional with a space; contradicts PR's no-suppression goal)
and add a .golangci.yml exclusion for the migration test file instead, which is
the deliberate strategy for a file that must reference legacy DB spellings.

Migration comment: tighten the DEPLOY ORDER note in up.sql to state that
000078 must complete before new code writes 'canceled'; remove the misleading
"races ahead" phrasing that implied out-of-order deploys are safe.
cristim added a commit that referenced this pull request Jul 16, 2026
Adversarial review of #1277 (cancelled -> canceled expand-contract) found
the rename was incomplete on the read side:

- frontend/src/history.ts: statusBadgeHTML, the chip-row counts, and the
  active-filter predicates only matched the British 'cancelled'. After
  the backend cut over to writing 'canceled', a row written by the new
  code fell through to the green Completed badge, was counted in the
  Completed total, and was invisible to the Cancelled chip filter -- the
  exact regression the rename is supposed to prevent. Match both
  spellings everywhere a status enum is compared.
- frontend/src/riexchange.ts: getStatusBadgeClass had the same
  single-spelling bug for ri_exchange_history rows.
- internal/api/openapi.yaml: PurchaseExecution.status enum still
  advertised only 'cancelled', so strict OpenAPI clients would treat the
  new 'canceled' value as an enum violation. Add both during the deploy
  window; the contract migration (#1278) drops 'cancelled'.

Also addresses the two outside-diff CodeRabbit comments on this PR:

- internal/api/handler_purchases.go: when cancelOrRecoverExecution
  recovers an idempotent cancel and finds the row in the legacy British
  spelling, normalize the in-memory copy to StatusCanceled before
  returning. Stored row is untouched -- the contract migration's
  authoritative backfill still observes every legacy row.
- internal/database/postgres/migrations/000078_*.up.sql: tighten the
  DEPLOY ORDER comment to the strict 3-step sequence (apply 000078
  against the OLD code; only then release the new code; then 1278) so a
  future operator can't infer a more permissive ordering and crash the
  cancel paths against the original 'cancelled'-only CHECK.

Tests:
- frontend/src/__tests__/history.test.ts: new test seeds rows in both
  spellings and a real completed row, asserts the badge renders
  Cancelled (not Completed) for both, the chip counts 2 (not 1), and
  clicking the Cancelled chip surfaces both rows. Fails on the pre-fix
  FE -- the canonical regression-replicates-the-real-scenario guard.
- internal/api/handler_purchases_test.go: new test seeds the recovery
  branch with status=LegacyStatusCanceled and asserts the response
  Status is normalized to StatusCanceled.

CI: Lint Code and Security Scanning are pre-existing main breakages
(2707 repo-wide lint issues; pgx GO-2026-5004 advisory) unrelated to
this PR's diff -- verified against the most recent main run.
@cristim
cristim force-pushed the fix/db-rename-cancelled-to-canceled branch from 6e6a992 to 25454aa Compare July 16, 2026 21:08
@cristim

cristim commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

Rebased onto current origin/main (81cd1f4) to pick up the flaky-test fix from #1399 (TestVerifyRetry_RebuildRateLimited is now deterministic). All 7 PR-owned commits carried over cleanly; validator_test.go now matches main's version. Migration 000084 remains valid (main stops at 000083 - no collision). All gates passed: go build, go vet, scheduledauth -count=5 (exit 0), affected packages (internal/config, internal/api, internal/purchase, internal/server), gocyclo -over 10 on PR-owned non-test files (clean), golangci-lint full run (0 issues).

cristim added a commit that referenced this pull request Jul 16, 2026
Defect 1 -- migration number collision:
Renumber marketplace migration from 000068 (already deployed, skipped by
golang-migrate on prod) to 000084 (next free after main's 000083).
Verified: main tops at 000083; #1277 uses 000082; no open PR holds 000084.
All "000068" references in SQL comments and types.go updated to 000084.

Defect 2 -- offering_class never written:
CUDly's EC2 client hardwires OfferingClassTypeConvertible; savePurchaseHistory
never stamped the field, so every row landed with offering_class=NULL and the
Sell button never rendered.
Fix: (a) stamp offering_class='convertible' in savePurchaseHistory for all
AWS EC2 purchases so future CUDly-bought rows are correctly classified at write
time; (b) add FetchOfferingClass to the marketplaceEC2Client interface and
implement it via DescribeReservedInstances so the marketplace-list handler can
lazily populate offering_class for pre-migration rows and for
externally-created Standard RIs (purchased before CUDly existed); (c) add
StampOfferingClass to ConfigStore + PostgresStore + mock to persist the
fetched class so subsequent requests skip the extra AWS call; (d) a new
populateOfferingClass helper in the handler ties it together -- it runs when
offering_class is empty after validateMarketplaceListRequest and before the
definitive "standard only" gate.

How a 'standard' row comes to exist: an externally-created Standard RI will
get its offering_class populated from AWS DescribeReservedInstances on the
first POST .../marketplace-list call and the value persisted for future calls.
Rows purchased by CUDly are stamped 'convertible' at write time (CUDly only
ever buys Convertible EC2 RIs).

Defect 3 -- $0 price guardrail gap:
resolveMarketplacePriceSchedule accepted Price >= 0, allowing zero-dollar
listings. Fix: reject Price <= 0 with a clear error; add a named constant
awsMarketplaceMinPriceFloorFraction (5%) and a total-schedule floor check so
a schedule that sums to less than 5% of the prorated residual value is also
rejected with an explicit message. Extract floor check into
checkSuppliedScheduleFloor and the class-check predicate into
isKnownNonStandardOfferingClass to keep all touched functions below gocyclo 10.

Regression tests:
- TestMigration084_MarketplaceColumns: integration test proves the three new
  columns exist and round-trip after migrating a fresh DB through 000084.
- TestMarketplaceList_EmptyOfferingClassFetchedStandard: proves an
  externally-created Standard RI (offering_class="") is listable after the
  lazy-populate path fetches and stamps 'standard' from AWS.
- TestMarketplaceList_EmptyOfferingClassFetchedConvertible: proves the gate
  still rejects when AWS reports 'convertible' even if DB had no class.
- TestResolveMarketplacePriceSchedule_ZeroPriceRejected: Price=0 rejected.
- TestResolveMarketplacePriceSchedule_BelowFloorRejected: sub-floor rejected.
@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Rebased onto main (473f69b) and force-pushed. Summary of what changed:

Rebase: Resolved conflicts with merged PR #889 (email revoke / SetCanceledBy). Both branches touched overlapping code; conflicts were resolved by keeping #889's new functionality (SetCanceledBy, revoke block in handler_purchases.go) and applying #1277's US-spelling rename throughout. router_handlers_test.go and handler_ri_exchange_test.go took main's "canceled" status strings.

Migration renumbered: 000082 -> 000089. Previous migration on main is 000086; in-flight #808=000087 and #1428=000088 merge before this one, so 000089 is the correct slot. Updated all code/comment/test/OpenAPI/frontend references from 000082 to 000089. renameCanceledPrevVersion in the integration test set to 86 (highest existing file) because 000087 and 000088 SQL files are not present yet -- note to update to 88 after both in-flight PRs merge.

Bug fix found during rebase: The conflict resolution had incorrectly changed loadCancelableExecution in approvals.go to use IsImmediatelyCancelable() (pending/notified only) instead of IsCancelable() (pending/notified/scheduled). This broke TestManager_CancelExecution_AllowsCancelableStatus/scheduled. Fixed by restoring IsCancelable() on the token path (matching main's behavior) and correcting the contradictory rejected list in TestManager_CancelExecution_RejectsNonCancelableStatus (removing "scheduled" from it). IsImmediatelyCancelable() stays in types.go and is correctly used only by guardImmediatelyCancelable() in handler_purchases.go.

Two lint fixes: status="cancelled" comment in handler_purchases.go -> "canceled"; authorise -> authorize in handler_purchases_test.go.

Gate results (commit 421dbc5):

  • go build ./... exit:0
  • go vet ./... exit:0
  • go test ./internal/server/scheduledauth/ -count=5 exit:0 (275 passed)
  • go test ./internal/execution/ -run TestFanOut -count=20 exit:0 (120 passed)
  • go test ./internal/api/... ./internal/config/... ./internal/purchase/... exit:0 (2595 passed)
  • migration integration test TestMigration_000089_{RollbackWithCanceledRows,UpIsAdditiveAndDualCompatible} exit:0 (2 passed)
  • golangci-lint run ./... exit:0 (no issues)

This PR should merge AFTER #808 (000087) and #1428 (000088).

@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Rebase onto current main (7f0573d) complete + fix commit applied

Branch fix/db-rename-cancelled-to-canceled rebased onto origin/main (old base was 9a8a9c7). Two commits now above main:

  1. fix(db): rename cancelled->canceled (expand-contract, migration 000089) -- the original PR content rebased, with renameCanceledPrevVersion updated to 88 (was 86) and migration renumbered 000082->000089
  2. fix(db): restore correct cancel error paths lost in rebase conflict -- restores cancelOrRecoverExecution 409->fmt.Errorf (HTTP status), adds guardCancelableViaSession for the session cancel path (routes 'scheduled' to revoke endpoint), fixes residual cancelledBy->canceledBy rename missed in rebase

Force pushed: 421dbc5 -> a11f036

Migration 000089 confirmed present. renameCanceledPrevVersion=88 confirmed. Local gate results:

@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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 added 2 commits July 17, 2026 17:03
Expand-contract rename of all British-spelled 'cancelled'/'cancellable'
variants to US-spelled 'canceled'/'cancelable' across the codebase.

- Add migration 000089: adds canceled_by column alongside cancelled_by,
  widens CHECK constraints to accept both spellings, COALESCE reads both
  during the expand window (prev version 088)
- Rename field CancelledBy->CanceledBy in PurchaseExecution, update
  json tag to canceled_by; add IsImmediatelyCancelable() predicate
- Rename SetCancelledBy->SetCanceledBy in StoreInterface + all
  implementations and mocks
- Update handler_purchases, handler_purchases_revoke and all tests to
  use US spellings; replace em-dashes with double hyphens in comments
- Patch frontend history.ts, riexchange.ts and OpenAPI spec to use
  'canceled' status string

Rebased onto main (473f69b); migration renumbered from 000082 to
000089 to land after in-flight #808 (000087) and #1428 (000088).
The rebase of 000089 onto current main incorrectly resolved two conflicts
in handler_purchases.go:

1. cancelOrRecoverExecution: reverted fmt.Errorf (router->500) back to
   NewClientError(409,...), misclassifying retriable backend faults as
   caller faults (feedback_http_status_classification).

2. cancelPurchaseViaSession: took main's broad !IsCancelable() guard
   instead of the PR's narrower guardImmediatelyCancelable, allowing
   "scheduled" executions through the pending/notified-only CAS path
   and producing a misleading "concurrent operation" 409 instead of the
   clear "use the revoke endpoint" message.

Fix: restore fmt.Errorf for the backend-failure branch; add
guardCancelableViaSession that explicitly routes "scheduled" to the
revoke endpoint before falling through to IsCancelable; rename residual
cancelledBy -> canceledBy; remove extra blank line in types.go.

Regression tests: TestHandler_deletePlannedPurchase_BackendErrorReturns5xx
and TestHandler_cancelPurchase_Session_ScheduledRoutedToRevoke now pass.
@cristim

cristim commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

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

Labels

effort/m Days impact/internal Team-internal only priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant