Skip to content

active mortgage payment system re-alignemnt#387

Merged
Connorbelez merged 4 commits intomainfrom
04-03-active_mortgage_payment_system_re-alignemnt
Apr 10, 2026
Merged

active mortgage payment system re-alignemnt#387
Connorbelez merged 4 commits intomainfrom
04-03-active_mortgage_payment_system_re-alignemnt

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Apr 9, 2026

Summary by Sourcery

Align the Active Mortgage Payment System architecture and contracts with the unified transfer-domain design while introducing a canonical collection plan execution API.

Enhancements:

  • Make the admin detail sheet and sidebar provider more robust and type-safe by relaxing entity type requirements and simplifying React node typing.
  • Clarify and realign technical and architectural documentation around payment rails, cash ledger, and provider abstractions, emphasizing TransferProvider as the canonical inbound contract and PaymentMethod as legacy compatibility.
  • Introduce a canonical internal Collection Plan execution contract that creates governed collection attempts from plan entries, records execution metadata, and hands off to the transfer domain with explicit idempotency and audit logging.
  • Refine Convex schema for collection plan entries and attempts to support execution timestamps, idempotency keys, actor attribution, trigger sources, and linkage to transfer requests.
  • Add internal design, PRD, tasks, and gap-analysis specs documenting spec/contract cleanup and the new plan-entry execution API, plus an alignment note capturing the current state of the Active Mortgage Payment System.

Documentation:

  • Update and annotate payment-rails and cash-ledger technical docs to reflect the current three-layer architecture, transfer-domain boundary, and legacy vs canonical provider contracts.
  • Add new specs documenting the spec/contract cleanup workstream and the canonical plan-entry execution API, including PRDs, designs, task lists, and gap analyses.

Tests:

  • Add contract-focused backend tests for the new collection plan execution API to validate attempt creation, replay behavior, eligibility guards, and handling of transfer handoff failures.

Summary by CodeRabbit

  • New Features

    • Add canonical collection-plan-entry execution: replay-safe attempt creation, idempotency, staged transfer handoff, dry-run and detailed outcome codes.
  • Schema

    • Extend collection plan entries and collection attempts with optional execution/audit fields for traceability.
  • Tests

    • Add contract-focused tests covering success, idempotent replay, rejection, dry-run, and handoff failure cases.
  • Documentation

    • Clarify payment-provider architecture: TransferProvider as canonical boundary; PaymentMethod retained for legacy compatibility.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Adds a canonical internal execution API to convert eligible collection plan entries into collection attempts, with idempotency, eligibility guards, transfer-request handoff, schema audit fields, comprehensive backend tests, and documentation aligning TransferProvider as the canonical inbound provider while marking PaymentMethod as legacy compatibility.

Changes

Cohort / File(s) Summary
Collection Plan Execution Core
convex/payments/collectionPlan/execution.ts, convex/payments/collectionPlan/executionContract.ts, convex/payments/collectionPlan/executionGuards.ts
New execution entrypoint, contract validators, outcome union types, eligibility/loading guards, staging mutation, attempt creation, and handoff-recording mutations implementing replay-safe execution and transfer-request handoff orchestration.
Contract Tests
convex/payments/collectionPlan/__tests__/execution.test.ts
New Vitest contract suite covering success, idempotent replay, pre-existing attempts, dry-run, rejection/not-eligible cases, transfer-handoff failure preservation, and environment-stub cleanup.
Schema Additions
convex/schema.ts
Adds optional execution/audit columns to collectionPlanEntries (executedAt, executionIdempotencyKey, collectionAttemptId) and collectionAttempts (triggerSource, executionRequestedAt, executionIdempotencyKey, requestedByActorType/Id, executionReason, transferRequestId).
Transfer Handoff / TransferProvider Boundary
convex/payments/transfers/..., convex/payments/methods/...
Documentation-only updates clarifying TransferProvider as canonical inbound provider and PaymentMethod as legacy compatibility; adapter/manual provider docs updated (no runtime changes).
Docs: Architecture & Specs
docs/..., specs/01-spec-and-contract-cleanup/*, specs/02-define-canonical-plan-entry-execution-api/*, specs/active-mortgage-payment-system-alignment-2026-04-03.md
New and updated design/spec docs and task/gap-analysis artifacts describing the execution API, contract cleanup workstream, and repo alignment notes.
Test Harness Module Map
convex/test/registerAuditLogComponent.ts
Replaces dynamic filesystem discovery with a static module map for audit-log component registration (loader functions enumerated).
Minor Formatting/Refactors
convex/crm/viewState.ts, convex/crm/__tests__/viewEngine.test.ts
Small formatting/boolean-expression reformat and punctuation adjustment; no logic changes.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant ExecuteAPI as executePlanEntry (Action)
    participant StageMut as stagePlanEntryExecutionMutation
    participant GuardLoad as loadExecutionPlanEntry
    participant GuardClass as classifyExecutionEligibility
    participant AttemptDB as collectionAttempts
    participant PaymentRails as createTransferRequestInternal
    participant HandoffMut as recordTransferHandoffMutation

    Caller->>ExecuteAPI: executePlanEntry(args)
    ExecuteAPI->>StageMut: stagePlanEntryExecutionMutation(ctx, args)
    StageMut->>GuardLoad: loadExecutionPlanEntry(ctx, planEntryId)
    GuardLoad-->>StageMut: LoadedExecutionPlanEntry
    StageMut->>GuardClass: classifyExecutionEligibility(loaded, request)
    alt Not eligible / rejected
        GuardClass-->>StageMut: rejected / not_eligible result
        StageMut-->>ExecuteAPI: StagePlanEntryExecutionResult
        ExecuteAPI-->>Caller: ExecutePlanEntryResult (rejected/not_eligible)
    else Eligible
        GuardClass-->>StageMut: null (eligible)
        StageMut->>AttemptDB: Create collectionAttempts(initiated + metadata)
        StageMut->>AttemptDB: Patch planEntry -> executing + link attemptId
        StageMut-->>ExecuteAPI: StagePlanEntryExecutionResult (attempt_created + transferHandoffRequest)
        ExecuteAPI->>PaymentRails: createTransferRequestInternal(transferHandoffRequest)
        alt Handoff Success
            PaymentRails-->>ExecuteAPI: transferRequestId
            ExecuteAPI->>HandoffMut: recordTransferHandoffSuccessMutation(attemptId, transferRequestId)
            HandoffMut->>AttemptDB: Patch attempt (transfer_requested + transferRequestId)
            ExecuteAPI-->>Caller: ExecutePlanEntryResult (attempt_created, success)
        else Handoff Failure
            PaymentRails-->>ExecuteAPI: Error
            ExecuteAPI->>HandoffMut: recordTransferHandoffFailureMutation(attemptId, failureReason)
            HandoffMut->>AttemptDB: Patch attempt (transfer_handoff_failed + failureReason)
            ExecuteAPI-->>Caller: ExecutePlanEntryResult (attempt_created, transfer_handoff_failed)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 I staged the plan beneath the moonlit patch,

Idempotent hops keep duplicates from scratch.
One attempt is born, then rails carry the cheer,
If handoff stumbles, the attempt still stays near.
Hooray — neat traces, tidy hops, and no mismatch! 🥕✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title contains a typo ('re-alignemnt' instead of 're-alignment') and is somewhat vague; it refers to a real part of the change but not the main point, which is the introduction of the canonical collection plan execution API. Consider revising to either 'Add canonical collection plan execution API' or 'Realign AMPS architecture with transfer-domain contracts' to clarify the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 04-03-active_mortgage_payment_system_re-alignemnt

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

❤️ Share

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

Copy link
Copy Markdown
Owner Author

Connorbelez commented Apr 9, 2026

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 9, 2026

Reviewer's Guide

Introduces a canonical internal Collection Plan execution command that turns executable plan entries into governed collection attempts and hands off to Unified Payment Rails, enriches collection-related schemas with execution metadata, clarifies TransferProvider vs PaymentMethod roles in docs and interfaces, hardens the admin record-detail UI against missing route state, and adds local specs/PRDs plus contract-focused backend tests for the new execution API.

Sequence diagram for canonical collection plan entry execution command

sequenceDiagram
    actor Caller
    participant ExecutePlanEntryAction as executePlanEntry_action
    participant StageExecution as stagePlanEntryExecution_mutation
    participant ExecutionGuards as executionGuards
    participant PlanEntries as collectionPlanEntries_table
    participant Attempts as collectionAttempts_table
    participant Obligations as obligations_table
    participant Transfers as transfers_domain
    participant TransferRequests as transferRequests_table
    participant AuditLog as auditLog_service

    Caller->>ExecutePlanEntryAction: executePlanEntry(args)
    ExecutePlanEntryAction->>StageExecution: stagePlanEntryExecutionMutation(args)

    Note over StageExecution,PlanEntries: Validate idempotency key and load plan entry
    StageExecution->>StageExecution: normalizeExecutionIdempotencyKey
    alt invalid idempotency key
        StageExecution->>AuditLog: log rejected_result invalid_idempotency_key
        StageExecution-->>ExecutePlanEntryAction: StagePlanEntryExecutionResult(rejected)
        ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(rejected)
    else valid idempotency key
        StageExecution->>PlanEntries: get(planEntryId)
        alt plan entry not found
            StageExecution->>AuditLog: log rejected_result plan_entry_not_found
            StageExecution-->>ExecutePlanEntryAction: StagePlanEntryExecutionResult(rejected)
            ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(rejected)
        else plan entry loaded
            StageExecution->>Attempts: get(planEntry.collectionAttemptId?)
            StageExecution->>Attempts: query by_plan_entry(planEntryId) if needed
            StageExecution->>Obligations: load each obligationId
            StageExecution->>ExecutionGuards: classifyExecutionEligibility(loaded,args)
            alt not eligible
                ExecutionGuards-->>StageExecution: ExecutePlanEntryResult(not_eligible)
                StageExecution->>AuditLog: log not_eligible_result
                StageExecution-->>ExecutePlanEntryAction: StagePlanEntryExecutionResult(not_eligible)
                ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(not_eligible)
            else existing attempt found
                ExecutionGuards-->>StageExecution: null
                StageExecution->>PlanEntries: patch sync collectionAttemptId and status
                StageExecution->>AuditLog: log already_executed_result
                StageExecution-->>ExecutePlanEntryAction: StagePlanEntryExecutionResult(already_executed)
                ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(already_executed)
            else dry run requested
                StageExecution->>AuditLog: log noop_result dry_run_requested
                StageExecution-->>ExecutePlanEntryAction: StagePlanEntryExecutionResult(noop)
                ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(noop)
            else eligible for execution
                Note over StageExecution,Attempts: Create governed collection attempt and link plan entry
                StageExecution->>Attempts: insert collectionAttempt (initiated)
                StageExecution->>Attempts: patch machineContext with attemptId
                StageExecution->>PlanEntries: patch status=executing, executedAt, executionIdempotencyKey, collectionAttemptId
                StageExecution->>AuditLog: log attempt_created_result
                StageExecution-->>ExecutePlanEntryAction: StagePlanEntryExecutionResult(attempt_created + transferHandoffRequest)
            end
        end
    end

    alt outcome is attempt_created
        Note over ExecutePlanEntryAction,Transfers: Unified Payment Rails handoff
        ExecutePlanEntryAction->>Transfers: createTransferRequestInternal(transferHandoffRequest)
        alt transfer handoff succeeds
            Transfers->>TransferRequests: insert transferRequest
            Transfers-->>ExecutePlanEntryAction: transferRequestId
            ExecutePlanEntryAction->>StageExecution: recordTransferHandoffSuccessMutation(attemptId, transferRequestId)
            StageExecution->>Attempts: patch transferRequestId, providerStatus=transfer_requested
            StageExecution->>AuditLog: log transfer_handoff.succeeded
            ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(attempt_created + transferRequestId)
        else transfer handoff fails
            Transfers-->>ExecutePlanEntryAction: throw error
            ExecutePlanEntryAction->>StageExecution: recordTransferHandoffFailureMutation(attemptId, reasonDetail)
            StageExecution->>Attempts: patch failureReason, providerStatus=transfer_handoff_failed
            StageExecution->>AuditLog: log transfer_handoff.failed
            ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(attempt_created, reasonCode=transfer_handoff_failed)
        end
    else outcome is not attempt_created
        ExecutePlanEntryAction-->>Caller: ExecutePlanEntryResult(result)
    end
Loading

Entity relationship diagram for collection plan execution and transfer handoff

erDiagram
    collectionPlanEntries {
        string id
        string[] obligationIds
        string method
        int amount
        string status
        int scheduledDate
        int executedAt
        string executionIdempotencyKey
        string collectionAttemptId
    }

    collectionAttempts {
        string id
        string planEntryId
        string method
        int amount
        string status
        string triggerSource
        int executionRequestedAt
        string executionIdempotencyKey
        string requestedByActorType
        string requestedByActorId
        string executionReason
        string transferRequestId
        string providerStatus
        string failureReason
    }

    transferRequests {
        string id
        string direction
        string transferType
        int amount
        string counterpartyType
        string counterpartyId
        string mortgageId
        string obligationId
        string planEntryId
        string collectionAttemptId
        string borrowerId
        string providerCode
        string idempotencyKey
    }

    obligations {
        string id
        string borrowerId
        string mortgageId
        string status
        string type
    }

    borrowers {
        string id
    }

    mortgages {
        string id
    }

    collectionPlanEntries ||--o{ obligations : references
    collectionPlanEntries ||--o| collectionAttempts : has_optional
    collectionAttempts ||--o| transferRequests : has_optional

    obligations }o--|| borrowers : belongs_to
    obligations }o--|| mortgages : belongs_to

    transferRequests }o--|| borrowers : counterparty
    transferRequests }o--|| mortgages : related_mortgage
Loading

Class diagram for collection plan execution contract and guards

classDiagram
    class ExecutePlanEntryArgs {
        string idempotencyKey
        Id_collectionPlanEntries planEntryId
        string triggerSource
        int requestedAt
        string requestedByActorType
        string requestedByActorId
        string reason
        boolean dryRun
    }

    class ExecutionTriggerSource {
        <<enumeration>>
        system_scheduler
        admin_manual
        workflow_replay
        migration_backfill
    }

    class ExecutionRequestedByActorType {
        <<enumeration>>
        system
        admin
        workflow
    }

    class ExecutePlanEntryReasonCode {
        <<enumeration>>
        dry_run_requested
        invalid_idempotency_key
        missing_execution_metadata
        obligation_not_collectible
        obligation_not_found
        plan_entry_not_due
        plan_entry_not_executable_state
        plan_entry_not_found
        plan_entry_already_executed
        transfer_handoff_failed
    }

    class PlanEntryExecutionOutcome {
        <<enumeration>>
        attempt_created
        already_executed
        not_eligible
        rejected
        noop
    }

    class PlanEntryStatus {
        <<enumeration>>
        planned
        executing
        completed
        cancelled
        rescheduled
    }

    class ExecutePlanEntryResultBase {
        int executionRecordedAt
        string idempotencyKey
        PlanEntryExecutionOutcome outcome
        Id_collectionPlanEntries planEntryId
        PlanEntryStatus planEntryStatusAfter
        ExecutePlanEntryReasonCode reasonCode
        string reasonDetail
    }

    class ExecutePlanEntryAttemptResult {
        string attemptStatusAfter
        Id_collectionAttempts collectionAttemptId
        Id_transferRequests transferRequestId
    }

    class AttemptCreatedResult {
    }

    class AlreadyExecutedResult {
    }

    class NotEligibleResult {
    }

    class RejectedResult {
    }

    class NoopResult {
    }

    class ExecutePlanEntryResult {
        <<union_type>>
    }

    class TransferHandoffRequest {
        int amount
        Id_borrowers borrowerId
        Id_collectionAttempts collectionAttemptId
        string counterpartyId
        string idempotencyKey
        string method
        Id_mortgages mortgageId
        Id_obligations[] obligationIds
        Id_collectionPlanEntries planEntryId
        string primaryObligationType
        CommandSource source
    }

    class StagePlanEntryExecutionResult {
        ExecutePlanEntryResult result
        TransferHandoffRequest transferHandoffRequest
    }

    class LoadedExecutionPlanEntry {
        PlanEntryStatus currentPlanEntryStatus
        Doc_collectionAttempts existingAttempt
        Doc_obligations[] obligations
        Doc_collectionPlanEntries planEntry
    }

    class ExecutionContractFunctions {
        +CommandSource buildExecutionSource(ExecutePlanEntryArgs args)
        +string normalizeExecutionIdempotencyKey(string idempotencyKey)
        +string buildTransferHandoffIdempotencyKey(Id_collectionPlanEntries planEntryId)
        +any buildTransferHandoffMetadata(TransferHandoffRequest request, string firstObligationType)
        +AttemptCreatedResult buildAttemptCreatedResult(AttemptCreatedResult result)
        +AlreadyExecutedResult buildAlreadyExecutedResult(AlreadyExecutedResult result)
        +NotEligibleResult buildNotEligibleResult(NotEligibleResult result)
        +RejectedResult buildRejectedResult(RejectedResult result)
        +NoopResult buildNoopResult(NoopResult result)
    }

    class ExecutionGuardsFunctions {
        +Promise~LoadedExecutionPlanEntry~ loadExecutionPlanEntry(MutationCtx ctx, Id_collectionPlanEntries planEntryId)
        +ExecutePlanEntryResult classifyExecutionEligibility(int executionRecordedAt, string idempotencyKey, LoadedExecutionPlanEntry loaded, ExecutePlanEntryArgs request)
    }

    ExecutePlanEntryArgs --> ExecutionTriggerSource : uses
    ExecutePlanEntryArgs --> ExecutionRequestedByActorType : uses

    ExecutePlanEntryResultBase <|-- ExecutePlanEntryAttemptResult
    ExecutePlanEntryResultBase <|-- NotEligibleResult
    ExecutePlanEntryResultBase <|-- RejectedResult
    ExecutePlanEntryResultBase <|-- NoopResult

    ExecutePlanEntryAttemptResult <|-- AttemptCreatedResult
    ExecutePlanEntryAttemptResult <|-- AlreadyExecutedResult

    ExecutePlanEntryResult "1" o-- "1" AttemptCreatedResult
    ExecutePlanEntryResult "1" o-- "1" AlreadyExecutedResult
    ExecutePlanEntryResult "1" o-- "1" NotEligibleResult
    ExecutePlanEntryResult "1" o-- "1" RejectedResult
    ExecutePlanEntryResult "1" o-- "1" NoopResult

    StagePlanEntryExecutionResult "1" o-- "1" ExecutePlanEntryResult
    StagePlanEntryExecutionResult "0..1" o-- "1" TransferHandoffRequest

    LoadedExecutionPlanEntry --> ExecutePlanEntryArgs : evaluated_against
    ExecutionGuardsFunctions --> LoadedExecutionPlanEntry : returns
    ExecutionGuardsFunctions --> ExecutePlanEntryResult : returns

    ExecutionContractFunctions ..> ExecutePlanEntryArgs : helper_for
    ExecutionContractFunctions ..> ExecutePlanEntryResult : builds
    ExecutionContractFunctions ..> TransferHandoffRequest : metadata_for
Loading

File-Level Changes

Change Details Files
Add canonical internal Collection Plan execution command that creates collection attempts and hands off to Unified Payment Rails with replay-safe semantics and audit logging.
  • Implement execution contract types, validators, outcomes, and reason codes for plan-entry execution, including transfer handoff metadata and idempotency handling.
  • Add execution guards that load a plan entry with its obligations, classify eligibility, and handle already-executed, ineligible, and rejected cases without creating attempts.
  • Implement stage-and-execute mutations/actions that create a single collection attempt, update the plan entry status and linkage, call transfer-request creation in the transfer domain, and record success/failure via dedicated mutations and audit logs.
convex/payments/collectionPlan/executionContract.ts
convex/payments/collectionPlan/executionGuards.ts
convex/payments/collectionPlan/execution.ts
Extend collection plan entry and collection attempt schemas to persist execution context, idempotency, and transfer-linkage metadata.
  • Add executedAt, executionIdempotencyKey, and collectionAttemptId to collectionPlanEntries to track execution consumption and linkage.
  • Add triggerSource, executionRequestedAt, executionIdempotencyKey, requestedByActorType/Id, executionReason, and transferRequestId to collectionAttempts to capture execution context and downstream transfer linkage.
convex/schema.ts
Clarify provider abstraction boundaries so TransferProvider is canonical for new inbound work and PaymentMethod is legacy compatibility, updating interfaces, registries, and technical docs accordingly.
  • Reword PaymentMethod interface comments to mark it as legacy compatibility and describe how it relates to the transfer-domain boundary.
  • Mark TransferProvider as the canonical transfer-domain provider interface and point new inbound work to the transfer provider registry.
  • Update Unified Payment Rails and cash-ledger technical docs to treat TransferProvider as canonical, explain Collection Attempt as the business execution record, and describe the production flow from Collection Plan through transfer/provider settlement and ledger posting.
  • Adjust the payment method registry docs to emphasize its legacy role and reference the transfer-provider registry for new work.
convex/payments/methods/interface.ts
convex/payments/methods/registry.ts
convex/payments/transfers/interface.ts
docs/technical-design/unified-payment-rails.md
docs/architecture/unified-payment-rails-technical-design.md
docs/cash-ledger-developer-guide.md
Harden admin detail sheet UI and tidy admin shell components and route-state helpers for better robustness and typing.
  • Make AdminDetailSheet.entityType optional and guard record-link generation so links are only rendered when both recordId and entityType are available, falling back to a disabled hint otherwise.
  • Minorly reformat RecordSidebarProvider props typing and admin route-state helper formatting for consistency.
src/components/admin/shell/AdminDetailSheet.tsx
src/components/admin/shell/RecordSidebarProvider.tsx
src/lib/admin-detail-route-state.ts
src/routes/admin/route.tsx
Add local PRDs, design docs, tasks, and alignment/gap-analysis specs to capture the Active Mortgage Payment System state and the canonical plan-entry execution API workstream.
  • Document the overall Active Mortgage Payment System alignment pass, including a detailed coverage matrix, integration seam review, and prioritized backlog.
  • Add PRD, design, tasks, and gap-analysis docs for spec/contract cleanup and for the canonical plan-entry execution API, clarifying scope, requirements, and verification steps.
specs/active-mortgage-payment-system-alignment-2026-04-03.md
specs/01-spec-and-contract-cleanup/PRD.md
specs/01-spec-and-contract-cleanup/design.md
specs/01-spec-and-contract-cleanup/tasks.md
specs/01-spec-and-contract-cleanup/gap-analysis.md
specs/02-define-canonical-plan-entry-execution-api/PRD.md
specs/02-define-canonical-plan-entry-execution-api/design.md
specs/02-define-canonical-plan-entry-execution-api/tasks.md
specs/02-define-canonical-plan-entry-execution-api/gap-analysis.md
Add contract-focused backend tests covering the new plan-entry execution API, including replay behavior, ineligible entries, missing plan entries, and transfer-handoff failures.
  • Introduce test helpers to seed borrower, mortgage, obligation, and plan-entry fixtures suitable for execution tests.
  • Test that executing an eligible plan entry creates exactly one attempt and associated transferRequest and updates plan-entry/attempt metadata correctly.
  • Test that replaying the same execution returns already_executed without duplicating attempts or transferRequests.
  • Test rejected outcomes for missing plan entries and not_eligible outcomes for future-scheduled or non-collectible obligations.
  • Test that when Payment Rails handoff fails, the attempt remains created and linked, with appropriate failure metadata and no transferRequest.
convex/payments/collectionPlan/__tests__/execution.test.ts
src/test/convex/payments/helpers.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Connorbelez Connorbelez marked this pull request as ready for review April 9, 2026 18:03
Copilot AI review requested due to automatic review settings April 9, 2026 18:03
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In mapMethodToProviderCode, silently defaulting unknown methods to manual risks masking config/enum drift; consider either tightening the mapping (e.g. rejecting unknown methods) or at least logging/recording an explicit reason code when a fallback provider is used.
  • The execution result/reason-code taxonomy is spread across executionContract, executionGuards, and the transfer handoff catch block; you might centralize reason-code construction helpers or enums so that adding new outcomes or codes is less error-prone and easier to audit.
  • The dryRun handling currently returns a noop outcome without exercising downstream paths; if you expect to rely on this for operational tooling, consider extending it to run the full eligibility logic and return the would-be attempt/transfer metadata (without writes) so callers can inspect what would happen.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `mapMethodToProviderCode`, silently defaulting unknown methods to `manual` risks masking config/enum drift; consider either tightening the mapping (e.g. rejecting unknown methods) or at least logging/recording an explicit reason code when a fallback provider is used.
- The execution result/reason-code taxonomy is spread across `executionContract`, `executionGuards`, and the transfer handoff catch block; you might centralize reason-code construction helpers or enums so that adding new outcomes or codes is less error-prone and easier to audit.
- The `dryRun` handling currently returns a `noop` outcome without exercising downstream paths; if you expect to rely on this for operational tooling, consider extending it to run the full eligibility logic and return the would-be attempt/transfer metadata (without writes) so callers can inspect what would happen.

## Individual Comments

### Comment 1
<location path="convex/payments/collectionPlan/__tests__/execution.test.ts" line_range="138-51" />
<code_context>
+	it("returns already_executed on replay without creating a duplicate attempt", async () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a test for reconciling an existing attempt when the plan entry’s collectionAttemptId is unset or mismatched

The mutation also handles the case where an `existingAttempt` exists but the plan entry’s `collectionAttemptId` is missing or points to a different attempt. Your current test only covers the aligned case.

Please add a test that:
- Pre-creates a `collectionAttempt` for the plan entry,
- Leaves `planEntry.collectionAttemptId` unset or set to a different ID,
- Calls `executePlanEntry`, and
- Asserts the result is `already_executed` and that `collectionAttemptId`, `executedAt`, `executionIdempotencyKey`, and status on the plan entry are updated to match the existing attempt.

This will directly validate the reconciliation behavior.

Suggested implementation:

```typescript
		expect(transfer?.planEntryId).toBe(planEntryId);
	});

	it("reconciles existing attempt when plan entry collectionAttemptId is mismatched", async () => {
		const t = createGovernedTestConvex();
		const { planEntryId } = await seedExecutionFixture(t);

		const requestedAt = Date.now();

		// First execution creates the initial attempt
		const firstResult = await t.mutation(
			api.payments.collectionPlan.executePlanEntry,
			{
				planEntryId,
				triggerSource: "system_scheduler",
				requestedAt,
				idempotencyKey: "exec-plan-entry-reconcile-1",
				requestedByActorType: "system",
				requestedByActorId: "scheduler",
			}
		);

		if (firstResult.type !== "success") {
			throw new Error(
				`Expected first execution to succeed, got ${firstResult.type}`
			);
		}

		const existingAttemptId = firstResult.collectionAttemptId;

		// Sanity check: we have an existing attempt associated with this plan entry
		expect(existingAttemptId).toBeDefined();

		// Load the plan entry created by the fixture / first execution
		const originalPlanEntry = await getPlanEntry(t, planEntryId);
		expect(originalPlanEntry?.collectionAttemptId).toEqual(existingAttemptId);

		// Simulate a mismatched collectionAttemptId on the plan entry while the attempt still exists
		// (either unset or pointing at a different attempt).
		// Here we explicitly clear it to exercise the reconciliation path.
		await updatePlanEntryCollectionAttemptIdForTest(t, planEntryId, null);

		const mutatedPlanEntry = await getPlanEntry(t, planEntryId);
		expect(mutatedPlanEntry?.collectionAttemptId).toBeNull();

		// Re‑execute with a different idempotency key to hit the reconciliation logic
		const replayResult = await t.mutation(
			api.payments.collectionPlan.executePlanEntry,
			{
				planEntryId,
				triggerSource: "system_scheduler",
				requestedAt: requestedAt + 1000,
				idempotencyKey: "exec-plan-entry-reconcile-2",
				requestedByActorType: "system",
				requestedByActorId: "scheduler",
			}
		);

		// The mutation should recognize the existing attempt and treat this as already executed
		expect(replayResult.type).toBe("already_executed");
		expect(replayResult.collectionAttemptId).toEqual(existingAttemptId);

		// The plan entry should now be reconciled to match the existing attempt
		const reconciledPlanEntry = await getPlanEntry(t, planEntryId);
		expect(reconciledPlanEntry?.collectionAttemptId).toEqual(existingAttemptId);
		expect(reconciledPlanEntry?.executedAt).toEqual(
			originalPlanEntry?.executedAt
		);
		expect(reconciledPlanEntry?.executionIdempotencyKey).toEqual(
			originalPlanEntry?.executionIdempotencyKey
		);
		expect(reconciledPlanEntry?.status).toEqual(originalPlanEntry?.status);
	});

	it("returns already_executed on replay without creating a duplicate attempt", async () => {

```

To make this compile and align with the rest of your test utilities, you’ll need to:

1. Ensure `api` is imported at the top of this file (most Convex tests already import it via `import { api } from "convex/_generated/api";` or similar).
2. Implement or wire the following helper functions used in the new test (if they don’t already exist):
   - `getPlanEntry(t, planEntryId)` – should query the plan entry document (including `collectionAttemptId`, `executedAt`, `executionIdempotencyKey`, and `status`) by its ID.
   - `updatePlanEntryCollectionAttemptIdForTest(t, planEntryId, collectionAttemptId)` – should update just the `collectionAttemptId` field on the given plan entry (used here to clear it to `null`), ideally in a small internal/test‑only mutation.
3. If your plan entry model uses a different field name for `status`, `executedAt`, or `executionIdempotencyKey`, adjust the property names in the assertions accordingly.
4. If the existing tests use a different way to invoke the mutation (e.g. `t.runMutation` instead of `t.mutation`), update the two `t.mutation(api.payments.collectionPlan.executePlanEntry, ...)` calls to match the prevailing pattern in this file.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread convex/payments/collectionPlan/__tests__/execution.test.ts
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR realigns the Active Mortgage Payment System around the transfer-domain provider abstraction and introduces a canonical internal Collection Plan execution API that creates governed collectionAttempts and hands off to Unified Payment Rails via transferRequests, alongside a small admin UI robustness fix and extensive in-repo spec/docs alignment.

Changes:

  • Add canonical internal executePlanEntry contract (types/validators/guards + internal action/mutations) and schema fields for replay-safe execution + traceability.
  • Add backend contract tests for plan-entry execution outcomes and transfer-handoff failure preservation.
  • Update docs/spec artifacts to reflect the transfer-domain direction; harden admin record-link rendering when route state is incomplete.

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/routes/admin/route.tsx Import ordering cleanup for admin route states.
src/lib/admin-detail-route-state.ts Formatting-only signature change.
src/components/admin/shell/RecordSidebarProvider.tsx Formatting/type import ordering only.
src/components/admin/shell/AdminDetailSheet.tsx Makes entityType optional and guards record-link creation to prevent runtime/type errors.
convex/schema.ts Adds execution/linkage metadata fields to collectionPlanEntries and collectionAttempts.
convex/payments/collectionPlan/executionContract.ts Defines canonical execution input/result contract, validators, and metadata helpers.
convex/payments/collectionPlan/executionGuards.ts Adds plan-entry loading + eligibility/replay classification helpers.
convex/payments/collectionPlan/execution.ts Implements the canonical internal execution command + transfer-request handoff recording.
convex/payments/collectionPlan/tests/execution.test.ts Adds contract-focused tests for executePlanEntry.
convex/_generated/api.d.ts Updates generated API typings to include new collectionPlan execution modules.
convex/payments/transfers/interface.ts Clarifies TransferProvider as canonical for new inbound provider work (comments only).
convex/payments/transfers/providers/adapter.ts Updates adapter docs to emphasize temporary migration bridge.
convex/payments/transfers/providers/manual.ts Updates manual provider docs to emphasize canonical transfer-domain boundary.
convex/payments/methods/interface.ts Clarifies PaymentMethod as legacy compatibility (comments only).
convex/payments/methods/manual.ts Clarifies manual method as legacy compatibility (comments only).
convex/payments/methods/registry.ts Clarifies registry as legacy compatibility lookup (comments only).
docs/cash-ledger-developer-guide.md Adds contract alignment notes for execution boundary and provider abstraction.
docs/technical-design/unified-payment-rails.md Adds status note + updates wording to reflect transfer-domain direction.
docs/architecture/unified-payment-rails-technical-design.md Adds status note and clarifies legacy vs canonical abstractions.
specs/active-mortgage-payment-system-alignment-2026-04-03.md Adds comprehensive alignment/gap analysis doc.
specs/01-spec-and-contract-cleanup/* Adds local PRD/design/tasks/gap-analysis artifacts for doc/contract cleanup workstream.
specs/02-define-canonical-plan-entry-execution-api/* Adds local PRD/design/tasks/gap-analysis artifacts for execution API workstream.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread convex/payments/collectionPlan/execution.ts
Comment thread convex/payments/collectionPlan/executionGuards.ts
Comment thread convex/payments/collectionPlan/execution.ts
@Connorbelez Connorbelez force-pushed the 04-03-active_mortgage_payment_system_re-alignemnt branch from cc58a7c to 3f2a656 Compare April 9, 2026 18:48
Copy link
Copy Markdown
Owner Author

@coderabbitai please review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

@Connorbelez Sure! I'll review PR #387 now.


🧠 Learnings used
Learnt from: Connorbelez
Repo: Connorbelez/tanstackTemplate PR: 5
File: specs/governed-transitions/chunks/chunk-01-schema-machine/context.md:209-213
Timestamp: 2026-03-13T03:17:28.559Z
Learning: In the Connorbelez/tanstackTemplate repository, do not flag .md files for markdown formatting/linting issues (e.g., MD040 fenced code block language). Only reference .md files (specs, PRDs, design docs) when they reveal architectural drift or implementation inconsistencies against actual code.

Learnt from: Connorbelez
Repo: Connorbelez/tanstackTemplate PR: 241
File: convex/crons.ts:30-34
Timestamp: 2026-03-21T20:15:34.305Z
Learning: In Connorbelez/tanstackTemplate, the `convex-dev-crons` component (referenced in CLAUDE.md) is reserved exclusively for *dynamic*, runtime-registered cron jobs — e.g., per-tenant or per-entity schedules that are created/destroyed at runtime. Fixed-interval infrastructure crons (e.g., daily reconciliation, daily obligation transitions, dispersal self-healing) all use static `cronJobs()` registration in `convex/crons.ts` and are deployed with the code. Do NOT flag static `crons.interval(...)` or `crons.daily(...)` registrations for fixed system-level jobs as needing `convex-dev-crons`.

Learnt from: Connorbelez
Repo: Connorbelez/tanstackTemplate PR: 115
File: convex/engine/effects/dealClosingPayments.ts:37-47
Timestamp: 2026-03-18T03:33:54.456Z
Learning: In Connorbelez/tanstackTemplate, `internalAction` handlers in Convex cannot atomically combine `ctx.runQuery` + `ctx.runMutation` — they are always separate transactions. The read-then-write idempotency pattern in effect handlers (e.g., `updatePaymentSchedule` in `convex/engine/effects/dealClosingPayments.ts`, `prorateAccrualBetweenOwners` in `convex/engine/effects/dealClosingProrate.ts`) is intentionally acceptable because the Transition Engine's OCC serializes concurrent transitions for the same entity upstream, preventing duplicate effect scheduling. Do NOT flag the split read-then-write pattern in `internalAction` effect handlers as a race condition requiring atomic fix — the upstream OCC is the primary guard. A defense-in-depth improvement (atomic `createIfNotExists` mutation) is tracked as a follow-up but is not a blocking issue.

Learnt from: Connorbelez
Repo: Connorbelez/tanstackTemplate PR: 105
File: convex/ledger/__tests__/concurrency.test.ts:0-0
Timestamp: 2026-03-17T11:00:19.343Z
Learning: In Connorbelez/tanstackTemplate, convex-test (used in convex/ledger/__tests__/) serializes all mutations sequentially. True OCC contention (e.g., two concurrent mints racing on the same mortgage) cannot be exercised in this harness — only the steady-state guards (e.g., ALREADY_MINTED) can be validated under serialized execution. Do NOT flag sequential-before-concurrent patterns in these test files as failing to test OCC; the file header comments document this limitation explicitly. True OCC retry behavior is guaranteed by Convex's production runtime and is out of scope for unit tests.

Learnt from: CR
Repo: Connorbelez/tanstackTemplate PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-16T06:17:10.192Z
Learning: After completing a major unit of work, run `coderabbit review --plain` to get a code review summary and check for potential issues or improvements

Learnt from: CR
Repo: Connorbelez/tanstackTemplate PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-09T18:13:25.131Z
Learning: Run `coderabbit review --plain` after completing major units of work to get code review summary

Learnt from: CR
Repo: Connorbelez/tanstackTemplate PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-09T18:13:04.013Z
Learning: After completing a major unit of work like a full SPEC, run `coderabbit review --plain` to get a code review summary and check for potential issues or improvements
✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
docs/technical-design/unified-payment-rails.md (1)

178-191: ⚠️ Potential issue | 🟡 Minor

Align the concrete provider examples with the new canonical boundary.

These sections still describe new Rotessa/Stripe integrations as PaymentMethod implementations, which conflicts with the historical note at the top and with the new execution flow handing off through the transfer domain. Leaving these examples as-is makes the doc internally inconsistent and points future provider work at the legacy abstraction.

Also applies to: 233-265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/technical-design/unified-payment-rails.md` around lines 178 - 191, The
document's integration examples still model Rotessa/Stripe as direct
PaymentMethod implementations (e.g., PaymentMethod, RealPADMethod.initiate(),
Collection Plan entries using method: "rotessa_pad"), which conflicts with the
new canonical boundary that routes provider-specific work through the transfer
domain; update the examples and flow text to replace direct PaymentMethod
implementation language with the new transfer-domain handoff: show creating a
transfer instruction/transfer request in place of calling PaymentMethod
implementations, describe RealPADMethod.initiate() as producing a
transfer-domain payload handed to TransferService (or equivalent), and adjust
Collection Plan steps to record a transfer-type method and reference the
transfer processing and webhooks rather than legacy PaymentMethod APIs so all
provider examples align with the transfer-domain execution flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/payments/collectionPlan/execution.ts`:
- Around line 42-46: The current mapMethodToProviderCode silently maps
unknown/typoed method strings to "manual", which masks bad input; change
mapMethodToProviderCode to fail closed by validating that method exists in
PROVIDER_CODES (the PROVIDER_CODES array and ProviderCode type) and throwing a
descriptive Error (including the invalid method value) if it's not present so
callers cannot accidentally route to the manual provider; after this change
update any call sites that relied on the implicit fallback to handle or
propagate the thrown error appropriately.
- Around line 198-208: The code currently uses only firstObligation for
borrowerId/mortgageId/type and downstream handoff; change execution validation
to ensure all obligations in the obligations array share the same borrowerId,
mortgageId and type before proceeding (i.e., iterate obligations and compare
each obligation.borrowerId, obligation.mortgageId and obligation.type to
firstObligation's values), and if any mismatch is found return
buildRejectedResult (same shape as existing rejection using executionRecordedAt,
normalizedIdempotencyKey, planEntry._id, planEntry.status and reasonCode
"mixed_obligation_context" with a clear reasonDetail). Update any other branches
that read only firstObligation (referenced symbols: firstObligation,
obligations, buildRejectedResult, normalizedIdempotencyKey, planEntry) to rely
on this validation so mixed or malformed plan entries are rejected rather than
producing an incorrect downstream transfer.
- Around line 134-168: The existingAttempt branch currently returns early with
buildAlreadyExecutedResult and prevents reconciling a partially completed
handoff (createTransferRequestInternal succeeded but
recordTransferHandoffSuccessMutation never ran), so change the logic in the
existingAttempt handling inside execution.ts to detect a partial handoff
(existingAttempt exists AND existingAttempt.transferRequestId is null but the
idempotency matches normalizedIdempotencyKey /
planEntry.executionIdempotencyKey) and instead continue reconciliation: update
planEntry fields as before, then invoke the handoff reconciliation path (e.g.,
call or re-run recordTransferHandoffSuccessMutation or the same
success-recording routine that sets transferRequestId) and only return
buildAlreadyExecutedResult after transferRequestId has been reconciled/logged
via logPlanEntryExecutionAudit; reference symbols: existingAttempt, planEntry,
normalizedIdempotencyKey, transferRequestId, createTransferRequestInternal,
recordTransferHandoffSuccessMutation, buildAlreadyExecutedResult,
logPlanEntryExecutionAudit.

In `@convex/payments/collectionPlan/executionContract.ts`:
- Around line 148-171: The buildExecutionSource function collapses the
"workflow_replay" trigger into the default "scheduler" case; add an explicit
case for "workflow_replay" in the switch (alongside the existing "admin_manual"
and "migration_backfill" cases) that returns channel: "workflow_replay",
actorId: args.requestedByActorId, and actorType: "system" so workflow replay
executions preserve their distinct trigger source and actor attribution in
downstream records.

In `@specs/01-spec-and-contract-cleanup/gap-analysis.md`:
- Around line 28-30: The markdown table contains machine-local absolute file
paths; update those three links to repo-relative paths so they work for
everyone: replace "/Users/.../convex/payments/transfers/interface.ts",
"/Users/.../convex/payments/methods/interface.ts", and
"/Users/.../docs/cash-ledger-developer-guide.md" with relative links like
"convex/payments/transfers/interface.ts",
"convex/payments/methods/interface.ts", and
"docs/cash-ledger-developer-guide.md" (preserve any markdown link text and
ensure paths are correct relative to the repository root).

In `@specs/02-define-canonical-plan-entry-execution-api/design.md`:
- Around line 83-90: The doc incorrectly calls executePlanEntry an internal
mutation; update the design text to state that executePlanEntry is an internal
action and that the staging step is the internal mutation (referencing the
"staging mutation" that creates the Collection Attempt). Explicitly document
that replay/eligibility guards and the staging mutation occur in the transaction
boundary, while the transfer-request handoff (creation through the Unified
Payment Rails contract / transfer-request) occurs after the staging mutation as
a separate step and therefore outside the same transaction boundary.

In `@specs/active-mortgage-payment-system-alignment-2026-04-03.md`:
- Around line 43-50: Update the alignment document to remove or rephrase the
obsolete "canonical plan-entry → collection-attempt execution path is missing"
entries and mark that the execution spine added in this PR is implemented;
specifically edit the occurrences of that assessment in the bullet list ("What
is still missing") and the other referenced sections (around the blocks noted as
102-105, 232-243, 361-362) to reflect that the API and tests for the plan-entry
→ collection-attempt path now exist, and if appropriate add a short note linking
to the PR/tests or the canonical API name so readers know the implemented
coverage.

---

Outside diff comments:
In `@docs/technical-design/unified-payment-rails.md`:
- Around line 178-191: The document's integration examples still model
Rotessa/Stripe as direct PaymentMethod implementations (e.g., PaymentMethod,
RealPADMethod.initiate(), Collection Plan entries using method: "rotessa_pad"),
which conflicts with the new canonical boundary that routes provider-specific
work through the transfer domain; update the examples and flow text to replace
direct PaymentMethod implementation language with the new transfer-domain
handoff: show creating a transfer instruction/transfer request in place of
calling PaymentMethod implementations, describe RealPADMethod.initiate() as
producing a transfer-domain payload handed to TransferService (or equivalent),
and adjust Collection Plan steps to record a transfer-type method and reference
the transfer processing and webhooks rather than legacy PaymentMethod APIs so
all provider examples align with the transfer-domain execution flow.
🪄 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: 62595e4f-fe6c-4721-97fb-565fff4ed363

📥 Commits

Reviewing files that changed from the base of the PR and between 8a6c61e and 3f2a656.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (23)
  • convex/payments/collectionPlan/__tests__/execution.test.ts
  • convex/payments/collectionPlan/execution.ts
  • convex/payments/collectionPlan/executionContract.ts
  • convex/payments/collectionPlan/executionGuards.ts
  • convex/payments/methods/interface.ts
  • convex/payments/methods/manual.ts
  • convex/payments/methods/registry.ts
  • convex/payments/transfers/interface.ts
  • convex/payments/transfers/providers/adapter.ts
  • convex/payments/transfers/providers/manual.ts
  • convex/schema.ts
  • docs/architecture/unified-payment-rails-technical-design.md
  • docs/cash-ledger-developer-guide.md
  • docs/technical-design/unified-payment-rails.md
  • specs/01-spec-and-contract-cleanup/PRD.md
  • specs/01-spec-and-contract-cleanup/design.md
  • specs/01-spec-and-contract-cleanup/gap-analysis.md
  • specs/01-spec-and-contract-cleanup/tasks.md
  • specs/02-define-canonical-plan-entry-execution-api/PRD.md
  • specs/02-define-canonical-plan-entry-execution-api/design.md
  • specs/02-define-canonical-plan-entry-execution-api/gap-analysis.md
  • specs/02-define-canonical-plan-entry-execution-api/tasks.md
  • specs/active-mortgage-payment-system-alignment-2026-04-03.md

Comment thread convex/payments/collectionPlan/execution.ts
Comment thread convex/payments/collectionPlan/execution.ts
Comment thread convex/payments/collectionPlan/execution.ts Outdated
Comment thread convex/payments/collectionPlan/executionContract.ts
Comment thread specs/01-spec-and-contract-cleanup/gap-analysis.md Outdated
Comment thread specs/02-define-canonical-plan-entry-execution-api/design.md
Comment thread specs/active-mortgage-payment-system-alignment-2026-04-03.md
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
convex/payments/collectionPlan/__tests__/execution.test.ts (1)

39-63: Minor: seedMortgage isn't linked to the seeded borrower.

seedMortgage(t) uses HARDCODED_BORROWER_ID internally while seedObligation uses the freshly seeded borrowerId. The execution logic reads borrowerId from the obligation (not the mortgage), so tests pass, but the fixture has a subtle data inconsistency.

Consider passing the borrowerId to seedMortgage for cleaner test data:

 const borrowerId = await seedBorrowerProfile(t);
-const mortgageId = await seedMortgage(t);
+const mortgageId = await seedMortgage(t, { borrowerId });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/payments/collectionPlan/__tests__/execution.test.ts` around lines 39 -
63, The fixture seeds a mortgage with seedMortgage(t) using
HARDCODED_BORROWER_ID which mismatches the borrower created by
seedBorrowerProfile(t); update seedExecutionFixture to pass the freshly created
borrowerId into seedMortgage (i.e., call seedMortgage(t, borrowerId)) and adjust
seedMortgage's signature/implementation to accept and use an optional borrowerId
(falling back to HARDCODED_BORROWER_ID only if none provided), so seedMortgage,
seedObligation and the resulting planEntry all reference the same borrower.
convex/payments/collectionPlan/executionContract.ts (1)

182-187: Consider documenting the null-return behavior.

normalizeExecutionIdempotencyKey returns null for empty/whitespace-only keys. The caller (stagePlanEntryExecution) correctly handles this, but a brief JSDoc would clarify this is a validation helper, not just a normalizer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/payments/collectionPlan/executionContract.ts` around lines 182 - 187,
Add a short JSDoc to the normalizeExecutionIdempotencyKey function documenting
that it trims the input and returns null for empty or whitespace-only strings
(i.e., it's a validation helper, not just a normalizer), and mention that
callers such as stagePlanEntryExecution must handle the null case; keep the
comment directly above the normalizeExecutionIdempotencyKey function and be
concise about its contract and return type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@convex/payments/collectionPlan/__tests__/execution.test.ts`:
- Around line 39-63: The fixture seeds a mortgage with seedMortgage(t) using
HARDCODED_BORROWER_ID which mismatches the borrower created by
seedBorrowerProfile(t); update seedExecutionFixture to pass the freshly created
borrowerId into seedMortgage (i.e., call seedMortgage(t, borrowerId)) and adjust
seedMortgage's signature/implementation to accept and use an optional borrowerId
(falling back to HARDCODED_BORROWER_ID only if none provided), so seedMortgage,
seedObligation and the resulting planEntry all reference the same borrower.

In `@convex/payments/collectionPlan/executionContract.ts`:
- Around line 182-187: Add a short JSDoc to the normalizeExecutionIdempotencyKey
function documenting that it trims the input and returns null for empty or
whitespace-only strings (i.e., it's a validation helper, not just a normalizer),
and mention that callers such as stagePlanEntryExecution must handle the null
case; keep the comment directly above the normalizeExecutionIdempotencyKey
function and be concise about its contract and return type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a677066a-55bc-4683-8c32-0949ac86c128

📥 Commits

Reviewing files that changed from the base of the PR and between 3f2a656 and 5f84f29.

📒 Files selected for processing (10)
  • convex/crm/__tests__/viewEngine.test.ts
  • convex/crm/viewState.ts
  • convex/payments/collectionPlan/__tests__/execution.test.ts
  • convex/payments/collectionPlan/execution.ts
  • convex/payments/collectionPlan/executionContract.ts
  • convex/payments/collectionPlan/executionGuards.ts
  • convex/test/registerAuditLogComponent.ts
  • specs/01-spec-and-contract-cleanup/gap-analysis.md
  • specs/02-define-canonical-plan-entry-execution-api/design.md
  • specs/active-mortgage-payment-system-alignment-2026-04-03.md
✅ Files skipped from review due to trivial changes (4)
  • convex/crm/tests/viewEngine.test.ts
  • convex/crm/viewState.ts
  • specs/active-mortgage-payment-system-alignment-2026-04-03.md
  • specs/02-define-canonical-plan-entry-execution-api/design.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • convex/payments/collectionPlan/executionGuards.ts

Copy link
Copy Markdown
Owner Author

Connorbelez commented Apr 10, 2026

Merge activity

  • Apr 10, 2:31 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 10, 2:31 AM UTC: @Connorbelez merged this pull request with Graphite.

@Connorbelez Connorbelez merged commit 8dea0ef into main Apr 10, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants