Skip to content

feat(ladder): refuse unannounced, 4-eyes and non-convertible RI ladder purchases at execution - #1901

Merged
cristim merged 1 commit into
mainfrom
feat/ladder-recheck-notify-4eyes-class
Nov 9, 2026
Merged

cristim merged 1 commit into
mainfrom
feat/ladder-recheck-notify-4eyes-class

Conversation

@cristim

@cristim cristim commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Summary

  • The execute-time ladder recheck now also refuses, before any provider call, a ladder execution when notification_sent is not stamped, when RequireDifferentApprover is on (a ladder purchase has no second approver), and when its tranche is on the convertible RI layer while the global offering class is neither empty nor convertible (the Manager buys the global class).
  • GetLadderExecutionGate also reads the tranche layer_type. Each refusal is a fixed server-authored note on the stored-error allowlist and ends the row failed with no_submission, like the other ladder refusals. Non-ladder executions are unaffected.
  • Dormant safety: no production path creates ladder executions or stamps notification_sent yet, so ladder purchases stay refused until later slices wire them.

Verification

  • Unit tests for every branch and ordering (the existing gate refusals still win); mutating each new check or the class comparison fails a test. Integration tests with the real store assert the row ends failed, no_submission, with zero provider calls, and that the gate returns the layer.
  • Rebased over fix(ladder): refuse to execute an execution whose tranche is already settled #853's note alternative: both it and the three new notes are in the allowlist alternation, covered verbatim and with appended text. Unit, internal/mocks, the ladder integration tests and golangci-lint pass locally on macOS; CI covers Linux.

Part of #70

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Scheduled ladder purchases are now prevented from proceeding when the required purchase notice has not been sent or a second approver is required.
    • Convertible RI purchases are blocked when their offering class is not convertible; eligible purchases can proceed when the class requirements are met.
    • Refusals now display the corresponding explanatory messages, and blocked purchases are recorded as not submitted.

…r purchases at execution

The execute-time ladder recheck now also refuses, before any provider
call, a ladder execution when:
- notification_sent is not stamped (no production path stamps it yet,
  so ladder purchases stay refused until one does);
- RequireDifferentApprover is on, since a ladder purchase has no second
  approver;
- its tranche is on the convertible RI layer and the global offering
  class is neither empty nor convertible, because the Manager buys the
  global class.

GetLadderExecutionGate now also reads the tranche layer_type. Each
refusal is a fixed server-authored note, added to the stored-error
allowlist, and ends the row failed with no_submission like the other
ladder refusals. Non-ladder executions are unaffected.

pre-commit run with SKIP=cfn-lint: cfn-lint cannot install on Python
3.15 and no CloudFormation file is touched.

Part of #70

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added severity/medium Moderate harm triaged Item has been triaged priority/p1 Next up; this sprint urgency/this-quarter Within the quarter impact/many Affects most users effort/xl Multi-week / refactor type/feat New capability labels Nov 9, 2026
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The ladder execution gate now returns the tranche layer type. Execution rechecks add refusals for missing purchase notice, required second approval, and non-convertible offering classes on RI tranches.

Changes: Ladder execution rechecks

Layer / File(s) Summary
Expose tranche layer type
internal/config/store_postgres_ladder_reconcile.go, internal/config/store_postgres_ladder_gate_integration_test.go
LadderExecutionGate now includes the tranche layer type. GetLadderExecutionGate selects and scans it, and the integration test checks the returned value.
Apply approval and offering-class refusals
internal/purchase/ladder_execution_recheck.go, internal/purchase/errnotes.go, internal/purchase/errnotes_test.go, internal/purchase/ladder_execution_recheck_test.go, internal/purchase/ladder_execution_recheck_integration_test.go
Rechecks refuse execution when notice is missing, a second approver is required, or an RI tranche has a nonempty offering class other than convertible. Tests cover refused and allowed cases. The refusal notes are included in the stored-error allowlist.

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant fireOneDue
  participant ladderExecutionRefusal
  participant ladderFlagsRefusal
  participant GetLadderExecutionGate
  participant Provider
  fireOneDue->>ladderExecutionRefusal: Recheck scheduled execution
  ladderExecutionRefusal->>ladderFlagsRefusal: Load flags and global config
  ladderFlagsRefusal-->>ladderExecutionRefusal: Return config or refusal
  ladderExecutionRefusal->>GetLadderExecutionGate: Read gate and tranche layer
  GetLadderExecutionGate-->>ladderExecutionRefusal: Return gate data
  ladderExecutionRefusal-->>fireOneDue: Return refusal or approval
  fireOneDue->>Provider: Submit approved purchase
Loading

Possibly related PRs

Suggested labels: type/feat

Suggested reviewers: iamjsd

Merge Risk: Minimal · up to 33b80

The new ladder refusal checks match the inspected offering-class configuration contract. No merge-blocking issue was identified.

Pre-merge checks: Passed 5

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: execution-time refusals for unannounced, four-eyes, and non-convertible RI ladder purchases.
Docstring Coverage Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentati…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 6 functions across 7 files.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 081a8c8 and 33b8044.

Files selected for processing: 7

  • internal/config/store_postgres_ladder_gate_integration_test.go
  • internal/config/store_postgres_ladder_reconcile.go
  • internal/purchase/errnotes.go
  • internal/purchase/errnotes_test.go
  • internal/purchase/ladder_execution_recheck.go
  • internal/purchase/ladder_execution_recheck_integration_test.go
  • internal/purchase/ladder_execution_recheck_test.go

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

@cristim

cristim commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

APPROVE @ 33b8044 by claude-nibblets

Cold review of #1901 (Part of #70; +207/-10, money path, dormant until later slices wire ladder executions; the branch is one commit behind main): the execute-time ladder recheck now also refuses, before any provider call, a ladder execution whose scheduled-purchase notice was not sent, one under 4-eyes mode (RequireDifferentApprover, because a ladder purchase has no second approver), and one whose tranche is on the convertible RI layer while the global offering class is neither empty nor convertible (the Manager buys the global class).

Read: ladderExecutionRefusal (internal/purchase/ladder_execution_recheck.go) keeps the existing order, so the flag and gate refusals still win, then ladderApprovalRefusal runs the three new checks. GetLadderExecutionGate (internal/config/store_postgres_ladder_reconcile.go) now also reads t.layer_type (NOT NULL in the schema), scanned into the new LayerType field, so the class check uses the tranche's own layer. Each refusal is a fixed server-authored note on the stored-error allowlist (errnotes.go, appended to the existing ladder alternation) and the caller wraps it as a before-submission error, so the row ends failed with no_submission like the other ladder refusals; non-ladder executions return early and are untouched. The class comparison is exact-string (convertible); a differently cased stored class would be refused, which fails closed.

Scoped probes (go test -run Ladder|ErrNote|ServerNote|Sanitiz|PreExecution in internal/purchase, scratch worktree at this head, baseline ok), each killed by TestLadderRecheck_ApprovalAndClassGate: the notice check dropped; the 4-eyes check dropped; the class comparison inverted; the layer condition dropped (so every tranche is held to the class rule); approval refusals made to win over gate refusals; an empty offering class refused too.

Noted, as the PR says: no production path creates ladder executions or stamps notification_sent yet, so ladder purchases stay refused until later slices wire them; this change only adds refusals.

CI at this head: all 43 checks pass (Unit Tests, Lint Code, CI Success, Integration Tests for config and purchase); 1 skipping (CodeRabbit).

@cristim
cristim merged commit a4dd0f3 into main Nov 9, 2026
43 checks passed
@cristim
cristim deleted the feat/ladder-recheck-notify-4eyes-class branch November 9, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xl Multi-week / refactor impact/many Affects most users priority/p1 Next up; this sprint severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant