Skip to content

eng-182#282

Merged
Connorbelez merged 9 commits intomainfrom
ENG-182
Mar 26, 2026
Merged

eng-182#282
Connorbelez merged 9 commits intomainfrom
ENG-182

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Mar 26, 2026

Lender Payout Scheduling & Frequency Configuration

This pull request implements automated lender payout scheduling with configurable frequency settings, enabling both scheduled batch payouts and admin-triggered immediate payouts.

Schema Changes

Added three new optional fields to the lenders table:

  • payoutFrequency: Configurable frequency (monthly, bi_weekly, weekly, on_demand)
  • lastPayoutDate: Tracks the last payout execution date (YYYY-MM-DD format)
  • minimumPayoutCents: Per-lender override for minimum payout threshold

Added payoutDate field to dispersalEntries table for audit traceability.

Core Features

Batch Payout Processing

  • Daily cron job at 08:00 UTC evaluates all active lenders for payout eligibility
  • Respects individual lender frequency settings (defaults to monthly)
  • Groups dispersal entries by mortgage and applies minimum threshold checks
  • Posts lender payouts via the existing cash ledger system with proper idempotency

Admin Immediate Payout

  • Admin action to trigger immediate payouts bypassing frequency schedules
  • Optional mortgage-specific scoping for targeted payouts
  • Still respects hold periods and minimum thresholds

Configuration & Logic

  • isPayoutDue() function determines payout eligibility based on frequency and last payout date
  • Monthly frequency uses 28-day intervals, weekly uses 7-day intervals
  • Default minimum payout threshold of 100 cents ($1.00) to prevent micro-payouts
  • on_demand frequency excludes lenders from automated batch processing

Data Flow

  1. Batch Processing: Cron queries eligible dispersal entries (past hold period, pending status)
  2. Grouping: Entries grouped by mortgage ID for separate payout transactions
  3. Threshold Check: Only processes groups meeting minimum payout amounts
  4. Payout Execution: Calls postLenderPayout with unique idempotency keys
  5. Status Updates: Marks dispersal entries as disbursed and updates lender payout dates

Concurrency Safety

Optimistic concurrency guard in markEntriesDisbursed prevents double-payout scenarios when admin actions run concurrently with batch processing. Each dispersal entry must have pending status before being marked disbursed.

Testing

Comprehensive test coverage includes:

  • Unit tests for frequency calculation logic
  • Integration tests for batch payout workflows
  • Admin payout functionality with mortgage scoping
  • Hold period enforcement and minimum threshold validation
  • Concurrency protection and idempotency verification

Summary by CodeRabbit

Release Notes

  • New Features
    • Automatic daily lender payouts at 08:00 UTC with configurable frequency options (monthly, bi-weekly, weekly, on-demand)
    • Admin capability to trigger immediate payouts for specific lenders or mortgages
    • Customizable minimum payout thresholds and hold period enforcement
    • Robust idempotent payout processing with automatic failure recovery

@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 17 minutes and 11 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: 96df8c5d-7114-401c-83a1-838eae561572

📥 Commits

Reviewing files that changed from the base of the PR and between 3e8adde and c105500.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (6)
  • convex/payments/payout/__tests__/adminPayout.test.ts
  • convex/payments/payout/__tests__/batchPayout.test.ts
  • convex/payments/payout/adminPayout.ts
  • convex/payments/payout/batchPayout.ts
  • convex/payments/payout/mutations.ts
  • convex/payments/payout/refs.ts
📝 Walkthrough

Walkthrough

Introduces a daily lender payout batch system and admin-triggered immediate payout functionality. Adds schema fields for payout configuration to lenders table and payout tracking to dispersalEntries. Implements Convex queries for eligible dispersal entries and active lenders, mutations for marking entries as disbursed and updating lender payout dates, an admin action for immediate payouts with optional mortgage filtering, and a daily batch processor wired into the cron scheduler at 08:00 UTC.

Changes

Cohort / File(s) Summary
Schema & Configuration
convex/schema.ts, convex/payments/payout/validators.ts, convex/payments/payout/config.ts
Added payout configuration fields to lenders table (payoutFrequency, lastPayoutDate, minimumPayoutCents) and payoutDate field to dispersalEntries. Introduced PayoutFrequency type union and validator, plus DEFAULT_PAYOUT_FREQUENCY, MINIMUM_PAYOUT_CENTS constant, and isPayoutDue() eligibility logic with 7/14/28-day thresholds for weekly/bi-weekly/monthly frequencies.
Payout Backend Operations
convex/payments/payout/queries.ts, convex/payments/payout/mutations.ts
Added internal queries getEligibleDispersalEntries, getActiveLenders, and getLenderById for fetching payout-ready data with hold-period filtering. Added internal mutations markEntriesDisbursed and updateLenderPayoutDate with optimistic concurrency checks (status validation) for safe state transitions.
Admin & Batch Payout Actions
convex/payments/payout/adminPayout.ts, convex/payments/payout/batchPayout.ts
Implemented triggerImmediatePayout admin action for on-demand payouts with optional mortgage scoping and minimum threshold enforcement. Implemented processPayoutBatch internal action for daily cron-triggered payouts with frequency gating, entry grouping by mortgage, cash-ledger posting via idempotency keys, and error recovery via revert mutations.
Cron Registration
convex/crons.ts
Registered new daily cron job "lender payout batch" at 08:00 UTC invoking processPayoutBatch.
Test Suites
convex/payments/payout/__tests__/config.test.ts, convex/payments/payout/__tests__/adminPayout.test.ts, convex/payments/payout/__tests__/batchPayout.test.ts
Added comprehensive Vitest suites validating payout configuration constants and frequency thresholds, admin payout mortgage filtering and minimum threshold logic, and batch payout end-to-end behavior including hold-period respect, concurrency guards, and idempotency.
Test Infrastructure
convex/payments/cashLedger/__tests__/e2eLifecycle.test.ts
Updated test helper import path from ./e2eHelpers to ./e2eHelpers.test-utils.
Specification Documentation
specs/ENG-182/*
Added comprehensive specification chunks documenting schema/config updates, backend queries/mutations, batch processing & cron design, test strategy, and master task manifest with inter-chunk dependencies.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin/Cron
    participant Query as Queries
    participant Mutation as Mutations
    participant CashLedger as Cash Ledger
    participant DB as Database
    
    Admin->>Query: getActiveLenders() or getLenderById()
    Query->>DB: Fetch lender(s) with payout config
    DB-->>Query: Lender document(s)
    Query-->>Admin: Active lender(s)
    
    Admin->>Query: getEligibleDispersalEntries(lenderId, today)
    Query->>DB: Query pending dispersalEntries by status
    DB-->>Query: Pending entries
    Query->>Query: Filter by payoutEligibleAfter ≤ today
    Query-->>Admin: Eligible entries
    
    Admin->>Admin: Group entries by mortgageId
    Admin->>Admin: Validate sum ≥ minimumPayoutCents
    
    Admin->>Mutation: markEntriesDisbursed(entryIds, payoutDate)
    Mutation->>DB: Verify status === "pending"
    Mutation->>DB: Patch status to "disbursed"
    DB-->>Mutation: Updated entries
    Mutation-->>Admin: Success
    
    Admin->>CashLedger: postLenderPayout(idempotencyKey, ...)
    CashLedger->>DB: Create ledger entry
    DB-->>CashLedger: Ledger created
    
    alt Ledger Post Fails
        Admin->>Mutation: revertClaimedEntries(entryIds)
        Mutation->>DB: Revert status to "pending"
        DB-->>Mutation: Reverted
    else Ledger Post Succeeds
        Admin->>Mutation: updateLenderPayoutDate(lenderId, today)
        Mutation->>DB: Update lastPayoutDate
        DB-->>Mutation: Updated
    end
    
    Admin-->>Admin: Return payout summary or error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A batch of hops through ledger rows,
Disbursement magic where the payout flows,
Hold periods respected, thresholds clear,
Lenders rejoice—their paydays are here!
From cron to queue, idempotent and bright,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'eng-182' is a vague ticket reference that does not convey the actual changes implemented; it lacks specificity about the feature (automated lender payout scheduling). Use a clear, descriptive title that summarizes the primary change, such as 'Add automated lender payout scheduling with configurable frequency and admin triggers' or 'Implement ENG-182: lender payout automation feature'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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-182

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 lender payout scheduling in the Convex backend by adding per-lender payout configuration, new payout processing actions (scheduled batch + admin-triggered), and supporting queries/mutations/tests, plus cron registration to run the batch daily.

Changes:

  • Extend lenders and dispersalEntries schema to support payout configuration + payout audit date.
  • Add payout module (config, validators, queries, mutations) plus batch and admin payout actions.
  • Register a daily cron at 08:00 UTC and add initial Vitest/convex-test coverage for payout helpers.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
specs/ENG-182/tasks.md High-level task checklist for ENG-182.
specs/ENG-182/chunks/manifest.md Chunk breakdown and dependencies for implementation plan.
specs/ENG-182/chunks/chunk-01-schema-config/tasks.md Tracks schema/config chunk tasks.
specs/ENG-182/chunks/chunk-01-schema-config/context.md Implementation notes for schema/config chunk.
specs/ENG-182/chunks/chunk-02-backend-core/tasks.md Tracks backend core chunk tasks.
specs/ENG-182/chunks/chunk-02-backend-core/context.md Implementation notes for backend core chunk.
specs/ENG-182/chunks/chunk-03-batch-cron/tasks.md Tracks batch/cron chunk tasks.
specs/ENG-182/chunks/chunk-03-batch-cron/context.md Implementation notes for batch + cron registration.
specs/ENG-182/chunks/chunk-04-tests/tasks.md Tracks tests chunk tasks.
specs/ENG-182/chunks/chunk-04-tests/context.md Testing guidance and intended integration test cases.
convex/schema.ts Adds lender payout config fields and dispersal entry payoutDate.
convex/payments/payout/validators.ts Introduces payout frequency validator + derived TS type.
convex/payments/payout/config.ts Defines frequency defaults/min threshold and isPayoutDue.
convex/payments/payout/queries.ts Adds internal queries for eligible dispersals and lender lookup(s).
convex/payments/payout/mutations.ts Adds internal mutations for marking entries disbursed and updating lender payout date.
convex/payments/payout/refs.ts Centralizes string-based function refs for payout module calls.
convex/payments/payout/batchPayout.ts Implements daily batch payout action logic.
convex/payments/payout/adminPayout.ts Implements admin-triggered immediate payout action.
convex/payments/payout/tests/config.test.ts Unit tests for payout config constants and isPayoutDue.
convex/payments/payout/tests/batchPayout.test.ts Component tests for payout query/mutation helpers (not full batch action).
convex/payments/payout/tests/adminPayout.test.ts Component tests for scoping/threshold logic (not the admin action).
convex/crons.ts Registers a daily 08:00 UTC payout cron (via function reference).

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

Comment thread convex/payments/payout/adminPayout.ts
Comment thread convex/payments/payout/adminPayout.ts Outdated
Comment thread convex/payments/payout/refs.ts Outdated
Comment thread convex/payments/payout/queries.ts
Comment thread convex/payments/payout/__tests__/adminPayout.test.ts
Comment thread convex/payments/payout/batchPayout.ts
Comment thread convex/crons.ts Outdated
Comment thread convex/payments/payout/queries.ts
Comment thread convex/payments/payout/__tests__/batchPayout.test.ts
Comment thread convex/payments/payout/batchPayout.ts Outdated
Connorbelez and others added 6 commits March 26, 2026 02:02
- Aggregate per-entry console.warn into a single warning per query call
  to prevent log spam during batch payout runs with many legacy entries
- Rename getLendersWithPayableBalance → getActiveLenders since the query
  returns all active lenders without filtering by payable balance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…API refs

Root cause: `convex codegen` failed because `e2eHelpers.ts` (a non-test
utility file) imported vitest's `expect` at the top level. Convex's bundler
skips files with multiple dots (*.test.ts) but analyzed e2eHelpers.ts as a
normal module, triggering vitest's "failed to access internal state" error.

Fix:
- Rename e2eHelpers.ts → e2eHelpers.test-utils.ts (second dot makes Convex
  skip it during bundling)
- Delete refs.ts — it used makeFunctionReference with string-based paths
  that bypassed API type safety
- Update adminPayout.ts and batchPayout.ts to use internal.payments.payout.*
  references directly from the generated API
- Update crons.ts to use internal.payments.payout.batchPayout.processPayoutBatch
  instead of makeFunctionReference

Addresses PR #282 review comments on threads 3 and 7.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Implement claim-before-post pattern: call claimEntriesForPayout (pending
  -> disbursed) BEFORE postLenderPayout. If ledger posting fails, revert
  claimed entries via revertClaimedEntries. This prevents double payouts
  when admin and batch cron run concurrently on the same entries.
- Replace ad-hoc idempotency keys (payout-batch:...) with
  buildIdempotencyKey("lender-payout-sent", "batch", ...) to use the
  standard cash-ledger: prefix and eliminate noisy log warnings.
- Extract processMortgageGroup helper to reduce handler complexity.
- Fix unused variable lint errors in test scaffolding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nal.*

The internal.payments.payout.* namespace does not exist in the generated
Convex API types, so direct references cause TypeScript errors. Restore
imports from ./refs.ts which uses makeFunctionReference to work around
the codegen limitation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses PR review comment requesting end-to-end coverage of the
batch payout action. Adds two integration tests that run the full
processPayoutBatch action via convex-test and assert:

1. LENDER_PAYOUT_SENT journal entry exists with correct amount and
   idempotency key format (cash-ledger: prefix)
2. Dispersal entries are marked disbursed with payoutDate set
3. Lender's lastPayoutDate is updated
4. Entries from different mortgages produce separate journal entries
   (grouping-by-mortgage validation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses PR review feedback requesting tests that invoke the action
end-to-end rather than simulating behavior in test code. The new
integration test suite covers:

- LENDER_PAYOUT_SENT journal entry creation with correct idempotency key
- Dispersal entries marked as disbursed with payoutDate
- Lender lastPayoutDate updated after successful payout
- mortgageId scoping (only targeted mortgage entries are paid out)
- Minimum threshold skipping (entries below MINIMUM_PAYOUT_CENTS)
- Zero-payout early return for lenders with no eligible entries
- Inactive lender rejection
- Partial failure reporting with ConvexError details
- Multi-entry summation per mortgage group

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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: 1

🧹 Nitpick comments (5)
specs/ENG-182/chunks/chunk-02-backend-core/tasks.md (1)

4-4: Minor documentation inconsistency: function was renamed.

The PR commits indicate getLendersWithPayableBalance was renamed to getActiveLenders to better reflect the actual query intent. Consider updating the task description to match the final implementation name.

📝 Suggested documentation update
-- [ ] T-005: Create `convex/payments/payout/queries.ts` — `getLendersWithPayableBalance` internal query
+- [ ] T-005: Create `convex/payments/payout/queries.ts` — `getActiveLenders` internal query
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@specs/ENG-182/chunks/chunk-02-backend-core/tasks.md` at line 4, Update the
task description for T-005 to use the final function name: replace references to
getLendersWithPayableBalance with getActiveLenders and adjust the brief phrasing
to say "Create convex/payments/payout/queries.ts — getActiveLenders internal
query" so the task matches the implemented function name; ensure any mentions in
the task list or surrounding text are also updated to avoid inconsistent naming.
convex/payments/payout/__tests__/config.test.ts (1)

64-72: Consider adding bi_weekly "never paid out" test for completeness.

The "no previous payout" section tests monthly and weekly but not bi_weekly. For full coverage consistency, consider adding:

it("never paid out (bi_weekly) — always due", () => {
  expect(isPayoutDue("bi_weekly", undefined, "2026-03-15")).toBe(true);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/payments/payout/__tests__/config.test.ts` around lines 64 - 72, The
tests for "never paid out" cover monthly and weekly but miss bi_weekly; add a
new test in convext/payments/payout/__tests__/config.test.ts alongside the
existing "never paid out" cases that calls isPayoutDue("bi_weekly", undefined,
"2026-03-15") and asserts toBe(true) so the bi_weekly schedule has the same
coverage as monthly/weekly.
convex/payments/payout/__tests__/adminPayout.test.ts (1)

15-23: Consider aligning handler return type with actual query return.

The GetEligibleHandler interface declares _id: string but the actual dispersal entry _id is Id<"dispersalEntries">. While the as unknown as cast makes this work, aligning the type would improve type safety:

interface GetEligibleHandler {
  _handler: (
    ctx: QueryCtx,
    args: { lenderId: Id<"lenders">; today: string }
  ) => Promise<Array<{ _id: Id<"dispersalEntries">; mortgageId: Id<"mortgages">; amount: number }>>;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/payments/payout/__tests__/adminPayout.test.ts` around lines 15 - 23,
The GetEligibleHandler return type is too loose: it declares _id as string and
mortgageId as string while the real query returns typed IDs; update the
interface used for getEligibleQuery (GetEligibleHandler) to return
Promise<Array<{ _id: Id<"dispersalEntries">; mortgageId: Id<"mortgages">;
amount: number }>> so it matches the actual getEligibleDispersalEntries output
and remove the need for the unsafe as unknown as cast when assigning
getEligibleQuery to getEligibleDispersalEntries.
convex/payments/payout/__tests__/batchPayout.test.ts (1)

55-486: Tests comprehensively cover existing mutations but miss coverage for claim/revert mutations.

The tests correctly validate:

  • getEligibleDispersalEntries: hold period filtering, legacy entries, status filtering
  • getActiveLenders: active-only filtering
  • markEntriesDisbursed: status update and concurrency guard

However, once claimEntriesForPayout and revertClaimedEntries are implemented, tests should be added to verify:

  1. Claim transitions entries from pending to disbursed
  2. Revert transitions entries back to pending
  3. The full claim→post→revert failure path

Would you like me to generate test cases for the claim/revert mutations once they're implemented?

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

In `@convex/payments/payout/__tests__/batchPayout.test.ts` around lines 55 - 486,
Tests are missing coverage for the new claim/revert flow: add tests exercising
claimEntriesForPayout and revertClaimedEntries that (1) assert
claimEntriesForPayout transitions entries from "pending" to the claimed state
used by your implementation (e.g., "claimed" or "disbursed" depending on how
claimEntriesForPayout marks them), (2) assert revertClaimedEntries transitions
those entries back to "pending", and (3) simulate the full claim→post→revert
failure path (claim, attempt post that fails, then call revertClaimedEntries)
verifying statuses and persisted fields at each step; reference the functions
claimEntriesForPayout and revertClaimedEntries and reuse the existing test
harness patterns (seedMinimalEntities, ctx.db.insert into "dispersalEntries",
and calling mutation handlers via claimEntriesForPayout._handler /
revertClaimedEntries._handler) so tests mirror markEntriesDisbursed/expect
patterns.
specs/ENG-182/chunks/chunk-02-backend-core/context.md (1)

53-72: Spec defines only markEntriesDisbursed but implementation expects additional mutations.

The spec documents two mutations:

  • markEntriesDisbursed: patches entries to disbursed status
  • updateLenderPayoutDate: patches lender's lastPayoutDate

However, both batchPayout.ts and adminPayout.ts expect:

  • claimEntriesForPayout: claim entries before posting (concurrency lock)
  • revertClaimedEntries: revert claims if posting fails

The spec should be updated to include these mutations, or the implementation should be revised to use markEntriesDisbursed for both claiming and reverting.

Would you like me to draft the additional mutation specifications for claimEntriesForPayout and revertClaimedEntries?

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

In `@specs/ENG-182/chunks/chunk-02-backend-core/context.md` around lines 53 - 72,
The spec currently documents markEntriesDisbursed and updateLenderPayoutDate but
the code expects two additional internal mutations used for concurrency control:
claimEntriesForPayout and revertClaimedEntries; add spec entries for both in
convex/payments/payout/mutations.ts: declare them as internalMutation with args
{ entryIds: v.array(v.id("dispersalEntries")), lockId: v.string() } (or similar
unique lock token) for claimEntriesForPayout and { entryIds:
v.array(v.id("dispersalEntries")) } for revertClaimedEntries, and describe
behavior: claimEntriesForPayout must atomically mark each dispersal entry as a
claimed state (e.g., set status to "claimed" and record lockId/claimedAt) to act
as a concurrency lock, while revertClaimedEntries must clear that claim (restore
status to "eligible" and remove lockId/claimedAt) so batchPayout.ts and
adminPayout.ts can safely claim and rollback entries; alternatively, if you
prefer changing code instead of spec, update batchPayout.ts/adminPayout.ts to
use markEntriesDisbursed and updateLenderPayoutDate only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@specs/ENG-182/tasks.md`:
- Line 12: Update the T-005 task to reference the actual exported function name
getActiveLenders instead of the outdated getLendersWithPayableBalance; locate
the task entry labeled "T-005" in the spec and replace the function name so the
spec matches the implementation (export getActiveLenders in
convex/payments/payout/queries.ts).

---

Nitpick comments:
In `@convex/payments/payout/__tests__/adminPayout.test.ts`:
- Around line 15-23: The GetEligibleHandler return type is too loose: it
declares _id as string and mortgageId as string while the real query returns
typed IDs; update the interface used for getEligibleQuery (GetEligibleHandler)
to return Promise<Array<{ _id: Id<"dispersalEntries">; mortgageId:
Id<"mortgages">; amount: number }>> so it matches the actual
getEligibleDispersalEntries output and remove the need for the unsafe as unknown
as cast when assigning getEligibleQuery to getEligibleDispersalEntries.

In `@convex/payments/payout/__tests__/batchPayout.test.ts`:
- Around line 55-486: Tests are missing coverage for the new claim/revert flow:
add tests exercising claimEntriesForPayout and revertClaimedEntries that (1)
assert claimEntriesForPayout transitions entries from "pending" to the claimed
state used by your implementation (e.g., "claimed" or "disbursed" depending on
how claimEntriesForPayout marks them), (2) assert revertClaimedEntries
transitions those entries back to "pending", and (3) simulate the full
claim→post→revert failure path (claim, attempt post that fails, then call
revertClaimedEntries) verifying statuses and persisted fields at each step;
reference the functions claimEntriesForPayout and revertClaimedEntries and reuse
the existing test harness patterns (seedMinimalEntities, ctx.db.insert into
"dispersalEntries", and calling mutation handlers via
claimEntriesForPayout._handler / revertClaimedEntries._handler) so tests mirror
markEntriesDisbursed/expect patterns.

In `@convex/payments/payout/__tests__/config.test.ts`:
- Around line 64-72: The tests for "never paid out" cover monthly and weekly but
miss bi_weekly; add a new test in
convext/payments/payout/__tests__/config.test.ts alongside the existing "never
paid out" cases that calls isPayoutDue("bi_weekly", undefined, "2026-03-15") and
asserts toBe(true) so the bi_weekly schedule has the same coverage as
monthly/weekly.

In `@specs/ENG-182/chunks/chunk-02-backend-core/context.md`:
- Around line 53-72: The spec currently documents markEntriesDisbursed and
updateLenderPayoutDate but the code expects two additional internal mutations
used for concurrency control: claimEntriesForPayout and revertClaimedEntries;
add spec entries for both in convex/payments/payout/mutations.ts: declare them
as internalMutation with args { entryIds: v.array(v.id("dispersalEntries")),
lockId: v.string() } (or similar unique lock token) for claimEntriesForPayout
and { entryIds: v.array(v.id("dispersalEntries")) } for revertClaimedEntries,
and describe behavior: claimEntriesForPayout must atomically mark each dispersal
entry as a claimed state (e.g., set status to "claimed" and record
lockId/claimedAt) to act as a concurrency lock, while revertClaimedEntries must
clear that claim (restore status to "eligible" and remove lockId/claimedAt) so
batchPayout.ts and adminPayout.ts can safely claim and rollback entries;
alternatively, if you prefer changing code instead of spec, update
batchPayout.ts/adminPayout.ts to use markEntriesDisbursed and
updateLenderPayoutDate only.

In `@specs/ENG-182/chunks/chunk-02-backend-core/tasks.md`:
- Line 4: Update the task description for T-005 to use the final function name:
replace references to getLendersWithPayableBalance with getActiveLenders and
adjust the brief phrasing to say "Create convex/payments/payout/queries.ts —
getActiveLenders internal query" so the task matches the implemented function
name; ensure any mentions in the task list or surrounding text are also updated
to avoid inconsistent naming.
🪄 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: 13735075-c873-4ace-bdd5-1cf855f9d0d1

📥 Commits

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

📒 Files selected for processing (23)
  • convex/crons.ts
  • convex/payments/cashLedger/__tests__/e2eHelpers.test-utils.ts
  • convex/payments/cashLedger/__tests__/e2eLifecycle.test.ts
  • convex/payments/payout/__tests__/adminPayout.test.ts
  • convex/payments/payout/__tests__/batchPayout.test.ts
  • convex/payments/payout/__tests__/config.test.ts
  • convex/payments/payout/adminPayout.ts
  • convex/payments/payout/batchPayout.ts
  • convex/payments/payout/config.ts
  • convex/payments/payout/mutations.ts
  • convex/payments/payout/queries.ts
  • convex/payments/payout/validators.ts
  • convex/schema.ts
  • specs/ENG-182/chunks/chunk-01-schema-config/context.md
  • specs/ENG-182/chunks/chunk-01-schema-config/tasks.md
  • specs/ENG-182/chunks/chunk-02-backend-core/context.md
  • specs/ENG-182/chunks/chunk-02-backend-core/tasks.md
  • specs/ENG-182/chunks/chunk-03-batch-cron/context.md
  • specs/ENG-182/chunks/chunk-03-batch-cron/tasks.md
  • specs/ENG-182/chunks/chunk-04-tests/context.md
  • specs/ENG-182/chunks/chunk-04-tests/tasks.md
  • specs/ENG-182/chunks/manifest.md
  • specs/ENG-182/tasks.md

Comment thread specs/ENG-182/tasks.md

### Chunk 2: Backend Queries & Mutations ✅
- [x] T-004: Create `convex/payments/payout/queries.ts` — `getEligibleDispersalEntries` (internal query: pending entries past hold period for a lender)
- [x] T-005: Create `convex/payments/payout/queries.ts` — `getLendersWithPayableBalance` (internal query: active lenders)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Task T-005 references outdated function name.

The task references getLendersWithPayableBalance, but the implementation in convex/payments/payout/queries.ts exports getActiveLenders (per commit history, this was renamed). Update the spec to reflect the actual function name for consistency.

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

In `@specs/ENG-182/tasks.md` at line 12, Update the T-005 task to reference the
actual exported function name getActiveLenders instead of the outdated
getLendersWithPayableBalance; locate the task entry labeled "T-005" in the spec
and replace the function name so the spec matches the implementation (export
getActiveLenders in convex/payments/payout/queries.ts).

- Replace makeFunctionReference with internal.* API refs in refs.ts
  now that codegen is fixed
- Fix ConvexError<unknown> → ConvexError<Record<string, string | number>>
  to satisfy Value type constraint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Connorbelez Connorbelez merged commit 3583708 into main Mar 26, 2026
1 of 3 checks passed
Connorbelez added a commit that referenced this pull request Apr 20, 2026
# Lender Payout Scheduling & Frequency Configuration

This pull request implements automated lender payout scheduling with configurable frequency settings, enabling both scheduled batch payouts and admin-triggered immediate payouts.

## Schema Changes

Added three new optional fields to the `lenders` table:
- `payoutFrequency`: Configurable frequency (`monthly`, `bi_weekly`, `weekly`, `on_demand`)
- `lastPayoutDate`: Tracks the last payout execution date (YYYY-MM-DD format)
- `minimumPayoutCents`: Per-lender override for minimum payout threshold

Added `payoutDate` field to `dispersalEntries` table for audit traceability.

## Core Features

### Batch Payout Processing
- Daily cron job at 08:00 UTC evaluates all active lenders for payout eligibility
- Respects individual lender frequency settings (defaults to monthly)
- Groups dispersal entries by mortgage and applies minimum threshold checks
- Posts lender payouts via the existing cash ledger system with proper idempotency

### Admin Immediate Payout
- Admin action to trigger immediate payouts bypassing frequency schedules
- Optional mortgage-specific scoping for targeted payouts
- Still respects hold periods and minimum thresholds

### Configuration & Logic
- `isPayoutDue()` function determines payout eligibility based on frequency and last payout date
- Monthly frequency uses 28-day intervals, weekly uses 7-day intervals
- Default minimum payout threshold of 100 cents ($1.00) to prevent micro-payouts
- `on_demand` frequency excludes lenders from automated batch processing

## Data Flow

1. **Batch Processing**: Cron queries eligible dispersal entries (past hold period, pending status)
2. **Grouping**: Entries grouped by mortgage ID for separate payout transactions  
3. **Threshold Check**: Only processes groups meeting minimum payout amounts
4. **Payout Execution**: Calls `postLenderPayout` with unique idempotency keys
5. **Status Updates**: Marks dispersal entries as `disbursed` and updates lender payout dates

## Concurrency Safety

Optimistic concurrency guard in `markEntriesDisbursed` prevents double-payout scenarios when admin actions run concurrently with batch processing. Each dispersal entry must have `pending` status before being marked `disbursed`.

## Testing

Comprehensive test coverage includes:
- Unit tests for frequency calculation logic
- Integration tests for batch payout workflows
- Admin payout functionality with mortgage scoping
- Hold period enforcement and minimum threshold validation
- Concurrency protection and idempotency verification

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

# Release Notes

* **New Features**
  * Automatic daily lender payouts at 08:00 UTC with configurable frequency options (monthly, bi-weekly, weekly, on-demand)
  * Admin capability to trigger immediate payouts for specific lenders or mortgages
  * Customizable minimum payout thresholds and hold period enforcement
  * Robust idempotent payout processing with automatic failure recovery

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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