Skip to content

ENG-180#281

Merged
Connorbelez merged 4 commits intomainfrom
ENG-180
Mar 26, 2026
Merged

ENG-180#281
Connorbelez merged 4 commits intomainfrom
ENG-180

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Mar 26, 2026

TL;DR

Implemented corrective obligation creation after payment reversals to re-establish receivables in the domain layer when settled obligations are reversed.

What changed?

Added a complete corrective obligation system with three main components:

Core mutation (createCorrectiveObligation.ts):

  • Creates new obligations linked to reversed settled obligations via sourceObligationId
  • Validates original obligation is settled and reversal amount is positive
  • Includes idempotency checks and cash ledger integration
  • Posts OBLIGATION_ACCRUED entries and audit journal entries

Integration wiring:

  • Modified emitPaymentReversed to schedule corrective obligation creation after reversal cascade
  • Only creates correctives for settled obligations using ctx.scheduler.runAfter
  • Added query functions getCorrectiveObligations and getObligationWithCorrectives

Schema and indexing:

  • Added by_source_obligation index to enable querying corrective obligations
  • Comprehensive test suite covering happy path, validation, idempotency, and cash ledger integration

How to test?

Run the test suite in convex/payments/obligations/__tests__/correctiveObligation.test.ts which covers:

  • Creating corrective obligations from settled originals
  • Idempotency (duplicate calls return existing)
  • Validation (non-settled status, invalid amounts)
  • Cash ledger integration (OBLIGATION_ACCRUED entries)
  • Query functionality (getCorrectiveObligations, getObligationWithCorrectives)
  • Original obligation preservation

Why make this change?

When payments are reversed, the original obligation remains in settled status (XState final state cannot be reopened). Without corrective obligations, the borrower appears fully paid in all domain systems despite the cash ledger showing the reversed receivable balance. This creates a disconnect between the cash layer and domain layer, breaking collection workflows and admin views. Corrective obligations restore this link by creating new receivables that enter the normal obligation lifecycle.

Summary by CodeRabbit

Release Notes

  • New Features

    • Payment reversals on settled obligations now automatically create corrective obligations.
    • Added ability to query and retrieve corrective obligations alongside original obligations.
  • Tests

    • Comprehensive test suite covering corrective obligation creation, idempotency, validation, and financial ledger integration.

@linear
Copy link
Copy Markdown

linear bot commented Mar 26, 2026

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.

Sorry @Connorbelez, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Warning

Rate limit exceeded

@Connorbelez has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c9ae66f-d0bf-45b1-b96c-cd1b5cf3206d

📥 Commits

Reviewing files that changed from the base of the PR and between f0411ea and acf5a72.

📒 Files selected for processing (7)
  • convex/engine/effects/collectionAttempt.ts
  • convex/payments/cashLedger/__tests__/paymentReversalIntegration.test.ts
  • convex/payments/obligations/__tests__/correctiveObligation.test.ts
  • convex/payments/obligations/createCorrectiveObligation.ts
  • convex/payments/obligations/queries.ts
  • convex/schema.ts
  • specs/ENG-180/chunks/chunk-02-wiring-and-query/context.md
📝 Walkthrough

Walkthrough

This PR implements a corrective obligation feature triggered by payment reversals. It adds a mutation to create corrective obligations with validation and idempotency checks, wires the creation into the reversal cascade flow via scheduler, extends the schema with source-obligation indexing, and provides comprehensive test coverage for the new functionality.

Changes

Cohort / File(s) Summary
Corrective Obligation Mutation
convex/payments/obligations/createCorrectiveObligation.ts
New internal mutation that validates the original obligation is settled, checks reversedAmount is a positive safe integer, enforces idempotency via by_type_and_source index, inserts a new obligation with status upcoming, and posts accrual ledger entries.
Query Helpers
convex/payments/obligations/queries.ts
Adds getCorrectiveObligations and getObligationWithCorrectives internal queries to retrieve correctives by source obligation ID, excluding late_fee types.
Reversal Effect Wiring
convex/engine/effects/collectionAttempt.ts
Modifies emitPaymentReversed to compute shouldCreateCorrective per obligation based on settled status, capture postPaymentReversalCascade result, and schedule corrective creation via ctx.scheduler.runAfter(0, ...) for settled obligations only.
Schema Indexing
convex/schema.ts
Adds by_source_obligation index on the obligations table to support querying corrective obligations by source obligation ID.
Test Suite
convex/payments/obligations/__tests__/correctiveObligation.test.ts
Comprehensive test file (559 lines) covering happy-path creation, idempotency, validation failures, cash ledger integration, query behavior, partial reversals, and original obligation immutability.
Feature Specification
specs/ENG-180/tasks.md, specs/ENG-180/chunks/manifest.md, specs/ENG-180/chunks/chunk-01-corrective-mutation/*, specs/ENG-180/chunks/chunk-02-wiring-and-query/*, specs/ENG-180/chunks/chunk-03-tests/*
Documentation and task breakdown defining the corrective obligation feature requirements, implementation steps, test plan, and tracking across three chunks (mutation, wiring, and tests).

Sequence Diagram(s)

sequenceDiagram
    participant PM as Payment Manager
    participant EA as emitPaymentReversed
    participant PRC as postPaymentReversalCascade
    participant Scheduler as ctx.scheduler
    participant COM as createCorrectiveObligation
    participant ODB as Obligation DB
    participant CL as Cash Ledger

    PM->>EA: Trigger payment reversal
    EA->>PRC: Call cascade for each obligation
    PRC-->>EA: Return cascadeResult with reversedAmount
    
    alt Obligation Status = "settled"
        EA->>Scheduler: Schedule corrective creation (runAfter 0)
        Scheduler->>COM: Invoke after 0ms
        COM->>ODB: Load & validate original obligation
        alt Original valid and settled
            COM->>ODB: Check idempotency (by_type_and_source)
            alt No existing corrective
                COM->>ODB: Insert new obligation (status: upcoming)
                ODB-->>COM: Return new obligationId
                COM->>CL: Post OBLIGATION_ACCRUED entry
                CL-->>COM: Ledger written
                COM-->>COM: Return {obligationId, created: true}
            else Existing corrective found
                COM-->>COM: Return {obligationId, created: false}
            end
        else Validation failed
            COM-->>COM: Throw validation error
        end
    else Obligation not settled
        EA->>EA: Skip corrective scheduling
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related issues

  • Issue #193: Directly addresses the corrective obligation feature implementation with the same requirement to create correctives after reversals and query them by source obligation ID.

Possibly related PRs

  • PR #276: Modifies emitPaymentReversed to capture postPaymentReversalCascade results and compute reversedAmount/postingGroupId for corrective creation.
  • PR #279: Updates emitPaymentReversed to schedule corrective obligation creation for settled obligations using the same integration pattern.

Poem

🐰 Hops of joy through reversals grand,
Corrective bonds now hand in hand,
When settlements unwind and turn,
Fresh obligations help us learn!
With sourceObligationId bright,
All accounts are put aright!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'ENG-180' is extremely vague and provides no meaningful information about the changeset beyond a ticket number, making it impossible to understand the primary change from history. Replace with a descriptive title summarizing the main change, e.g., 'Implement corrective obligations for reversed settled payment obligations' or 'Add corrective obligation creation and querying for payment reversals'.
✅ 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ENG-180

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

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Connorbelez Connorbelez marked this pull request as ready for review March 26, 2026 05:36
Copilot AI review requested due to automatic review settings March 26, 2026 05:36
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.

Connorbelez has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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

Implements “corrective obligations” to restore domain-layer receivables after cash-ledger payment reversals, keeping the original (final-state) obligation settled while creating a new upcoming obligation linked via sourceObligationId.

Changes:

  • Added createCorrectiveObligation internal mutation that creates and accrues a new obligation linked to a settled original, plus audit journaling/logging.
  • Wired emitPaymentReversed to schedule corrective obligation creation after postPaymentReversalCascade.
  • Added by_source_obligation index and new internal queries to fetch correctives for an obligation, plus a dedicated test suite.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
specs/ENG-180/tasks.md ENG-180 execution plan/task checklist.
specs/ENG-180/chunks/manifest.md Chunk manifest for implementation progress tracking.
specs/ENG-180/chunks/chunk-01-corrective-mutation/tasks.md Tasks for the corrective obligation mutation + schema/index work.
specs/ENG-180/chunks/chunk-01-corrective-mutation/context.md Implementation context/plan excerpt for the mutation.
specs/ENG-180/chunks/chunk-02-wiring-and-query/tasks.md Tasks for wiring corrective creation into reversal flow + queries.
specs/ENG-180/chunks/chunk-02-wiring-and-query/context.md Context/plan excerpt for wiring + queries.
specs/ENG-180/chunks/chunk-03-tests/tasks.md Tasks for ENG-180 test coverage.
specs/ENG-180/chunks/chunk-03-tests/context.md Testing plan/context and references.
convex/schema.ts Adds obligations.by_source_obligation index.
convex/payments/obligations/queries.ts Adds getCorrectiveObligations and getObligationWithCorrectives internal queries.
convex/payments/obligations/createCorrectiveObligation.ts New internal mutation to create + accrue corrective obligations and emit audit entries.
convex/payments/obligations/tests/correctiveObligation.test.ts New Vitest/convex-test suite covering creation, validation, idempotency, and queries.
convex/engine/effects/collectionAttempt.ts Schedules corrective obligation creation after reversal cascade in emitPaymentReversed.
convex/_generated/api.d.ts Codegen update for the new Convex module.

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

Comment thread convex/engine/effects/collectionAttempt.ts
Comment thread convex/payments/obligations/createCorrectiveObligation.ts Outdated
Comment thread convex/payments/obligations/createCorrectiveObligation.ts
Comment thread convex/payments/obligations/queries.ts Outdated
Comment thread convex/engine/effects/collectionAttempt.ts
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: 3

🤖 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/obligations/createCorrectiveObligation.ts`:
- Around line 56-79: The current idempotency check using existingCorrectives =
ctx.db.query("obligations").withIndex("by_type_and_source", ...) and then
finding existingCorrective treats any corrective for the same
type+sourceObligationId as a duplicate; change this to key idempotency to the
reversal identity instead: include the reversal identity fields (e.g.
args.postingGroupId or args.reversalId and args.reversedAmount) in the
query/index or, if you must keep the same index, filter existingCorrectives to
match postingGroupId/reversedAmount before returning created: false;
alternatively, if you intend to accumulate, update the found corrective
(existingCorrective) by adding the new reversedAmount/postingGroupId instead of
short-circuiting — adjust the logic around withIndex("by_type_and_source"),
existingCorrectives, existingCorrective, args.originalObligationId,
args.postingGroupId and args.reversedAmount accordingly.

In `@convex/payments/obligations/queries.ts`:
- Around line 112-119: The current queries filter out obligations by checking
o.type !== "late_fee", which hides reversed late_fee correctives because
createCorrectiveObligation copies original.type; update the logic so late_fee
originals aren't lost: either (A) change createCorrectiveObligation to
reject/throw when original.type === "late_fee" so you never create a corrective
with type "late_fee", or (B) add an explicit corrective marker (e.g.,
corrective: true or correctiveOfId: string) on corrective documents when
creating them in createCorrectiveObligation and update the query helpers that
currently use the type check to instead filter by that marker (preserving the
original.type metadata) so correctives are discovered reliably.

In `@specs/ENG-180/chunks/chunk-02-wiring-and-query/context.md`:
- Around line 116-117: The spec currently instructs scheduling
createCorrectiveObligation with reversedAmount = obligation.amount which will
over-create receivables for partial reversals; instead, after
postPaymentReversalCascade returns (it yields { reversalEntries, postingGroupId,
clawbackRequired }), compute reversedAmount from the actual reversal entry
amounts (e.g., sum the reversed cash amounts in reversalEntries or use the
specific reversed amount field provided by the cascade) and pass that as
reversedAmount to ctx.scheduler.runAfter(0, () =>
createCorrectiveObligation(...)), and still only schedule when obligation.status
=== "settled"; also update the emitPaymentReversed snippet to use the same
reversalEntries-derived reversedAmount rather than obligation.amount.
🪄 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: 57f2a15d-9a9c-4b2d-82ba-494c892eb24a

📥 Commits

Reviewing files that changed from the base of the PR and between bd71c41 and f0411ea.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (13)
  • convex/engine/effects/collectionAttempt.ts
  • convex/payments/obligations/__tests__/correctiveObligation.test.ts
  • convex/payments/obligations/createCorrectiveObligation.ts
  • convex/payments/obligations/queries.ts
  • convex/schema.ts
  • specs/ENG-180/chunks/chunk-01-corrective-mutation/context.md
  • specs/ENG-180/chunks/chunk-01-corrective-mutation/tasks.md
  • specs/ENG-180/chunks/chunk-02-wiring-and-query/context.md
  • specs/ENG-180/chunks/chunk-02-wiring-and-query/tasks.md
  • specs/ENG-180/chunks/chunk-03-tests/context.md
  • specs/ENG-180/chunks/chunk-03-tests/tasks.md
  • specs/ENG-180/chunks/manifest.md
  • specs/ENG-180/tasks.md

Comment thread convex/payments/obligations/createCorrectiveObligation.ts Outdated
Comment thread convex/payments/obligations/queries.ts Outdated
Comment thread specs/ENG-180/chunks/chunk-02-wiring-and-query/context.md Outdated
Connorbelez and others added 3 commits March 26, 2026 02:00
…d of obligation.amount

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uard against late_fee sources

- Idempotency now keyed to (sourceObligationId, postingGroupId) instead of
  (type, sourceObligationId), allowing multiple independent reversals of the
  same obligation to each create their own corrective while still deduplicating
  retries of the same reversal event.
- postingGroupId is now persisted on the corrective obligation record.
- Added UNSUPPORTED_SOURCE_TYPE guard rejecting late_fee source obligations in
  the mutation, plus a defensive check at the scheduler call site.
- Updated tests: idempotency test uses same postingGroupId, added test for
  distinct reversals creating separate correctives, added late_fee guard test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exercises the full reversal flow end-to-end:
- Partial payment: corrective obligation uses CASH_RECEIVED reversal amount (60k), not obligation.amount (100k)
- Full payment: corrective obligation matches the full settled amount
- Non-settled obligations: no corrective obligation is created
- Ledger assertions: reversal entries share correct postingGroupId

Addresses PR #281 review thread 2 (missing integration test).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Connorbelez Connorbelez merged commit 0169fe1 into main Mar 26, 2026
1 of 3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 26, 2026
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