Collections System
Overview
The collections system handles automated payment processing for installment loans. It consists of scheduled jobs that run daily to process payments, send notifications, and handle retries.
Architecture
CloudWatch Events (Daily Schedule)
│
│ 5:00 AM CST ──► retry
│ 7:00 AM CST ──► due
│ 8:00 AM CST ──► upcoming (look_ahead_days: 3)
│ 9:00 AM CST ──► overdue
│
▼
Job.Run()
│
▼
HandleJob()
│
├──► RetryHandler() [5:00 AM] ──► enqueuePayment()
│
├──► DueHandler() [7:00 AM] ──┬──► enqueuePayment() (autopay)
│ └──► notifyUsersForPayments() (non-autopay)
│
├──► UpcomingHandler() [8:00 AM] ──► notifyUsersForPayments()
│
└──► OverdueHandler() [9:00 AM] ──► notifyUsersForPayments()
│
▼
reportToSlack()
Collection Processes
The system supports four main collection processes:
| Process | Description | Creates Payments | Sends Notifications |
|---|---|---|---|
|
Notifies users of payments coming due in the future |
No |
Yes |
|
Processes installments due today - autopay or notify |
Yes (autopay) |
Yes (non-autopay) |
|
Notifies users of past-due installments on a schedule |
No |
Yes (on schedule) |
|
Retries failed payments on a schedule |
Yes |
No |
Upcoming Handler
Purpose: Notify users about upcoming installment payments before they are due.
Trigger: Scheduled job with configurable look_ahead_days parameter (default: 3 days).
Behavior:
-
Queries all installments scheduled for
today + look_ahead_days -
Sends
loc_loan_payment_upcomingnotification via Iterable to all users -
Does not create any payments
Example: If look_ahead_days = 3 and today is January 1st, it will notify users with installments due on January 4th.
Due Handler
Purpose: Process installments that are due today.
Trigger: Scheduled job that runs daily at 7:00 AM CST.
Behavior:
-
Queries all installments with a due date of today in
SCHEDULEDstatus -
For each installment:
-
If autopay is enabled:
-
Checks if a payment already exists for today (prevents duplicates)
-
Creates a new payment record with
AUTOprocess andPINLESSmethod -
Enqueues the payment to the worker queue for processing
-
-
If autopay is disabled:
-
Sends
loc_loan_payment_duenotification via Iterable
-
-
Duplicate Prevention: The handler checks for existing payments created on the same day before creating new autopay payments.
Overdue Handler
Purpose: Notify users of past-due installments on a controlled schedule to avoid notification fatigue.
Trigger: Scheduled job that runs daily at 9:00 AM CST.
Behavior:
-
Queries all installments with a due date before today that are still in
SCHEDULEDstatus -
Filters installments based on notification schedule (see table below)
-
Sends
loc_loan_payment_overduenotification via Iterable only for eligible installments
Overdue Notification Schedule
Notifications are sent only on even-numbered days since the due date (where day 1 is the day after the due date):
| Days Overdue | Day Number (days since due + 1) | Even/Odd | Notification Sent |
|---|---|---|---|
1 day |
2 |
Even |
✅ Yes |
2 days |
3 |
Odd |
❌ No |
3 days |
4 |
Even |
✅ Yes |
4 days |
5 |
Odd |
❌ No |
5 days |
6 |
Even |
✅ Yes |
6 days |
7 |
Odd |
❌ No |
7 days |
8 |
Even |
✅ Yes |
Rationale: This schedule ensures users receive reminders every other day, reducing notification fatigue while maintaining regular communication about overdue payments.
Calculation:
daysSinceDue := int(queryDay.Sub(dueDay).Hours() / 24) + 1
if daysSinceDue > 0 && daysSinceDue%2 == 0 {
// Send notification
}
Retry Handler
Purpose: Retry failed payments on a controlled schedule.
Trigger: Scheduled job that runs daily at 5:00 AM CST.
Behavior:
-
Queries all payments in
RETRYstatus -
Filters payments based on retry schedule (even days since creation)
-
Enqueues eligible payments to the worker queue for reprocessing
Retry Schedule
Retries occur only on even-numbered days since the payment was created:
| Days Since Creation | Even/Odd | Retry Attempted |
|---|---|---|
0 (same day) |
- |
❌ No |
1 day |
Odd |
❌ No |
2 days |
Even |
✅ Yes |
3 days |
Odd |
❌ No |
4 days |
Even |
✅ Yes |
5 days |
Odd |
❌ No |
Rationale: This schedule spaces out retry attempts to give time for underlying issues (insufficient funds, temporary card issues) to resolve.
Collections Worker
The worker Lambda processes payment events from the SQS queue:
SQS Queue
│
▼
Worker.Handle()
│
▼
Engine.Payback()
│
├──► Acquire distributed lock
│
├──► Get loan, installments, user
│
├──► Validate payment
│
├──► Submit to Payments Service
│
├──► Apply to installments
│
├──► Save all changes (transactional)
│
└──► Send receipt notification
Payment Engine
The Engine struct in pkg/collections/engine.go handles the core payment logic:
Payback Flow
-
Lock Acquisition - Acquire distributed lock on the loan to prevent concurrent modifications
-
Validation - Verify loan is open, has balance, payment amount is valid
-
Payment Submission - Submit payment to payments service (ACH or Pinless)
-
Application - Apply successful payment to installments in due date order
-
Persistence - Save all changes in a transactional write
-
Notification - Send receipt to user
ACH Webhook Handler
The ach-handler Lambda processes payment webhooks from the payments service via Kinesis:
Job Schedule
All jobs run daily and are enabled only in production:
| Process | Schedule (UTC) | Schedule (CST) | Cron Expression |
|---|---|---|---|
|
11:00 UTC |
5:00 AM CST |
|
|
12:00 UTC |
7:00 AM CST |
|
|
14:00 UTC |
8:00 AM CST |
|
|
15:00 UTC |
9:00 AM CST |
|
Pagination
All handlers support pagination through DynamoDB’s LastKey mechanism. The job loop continues processing pages until no LastKey is returned.
Notifications
Notifications are sent via Iterable with the following event types:
| Event | Description |
|---|---|
|
Upcoming payment reminder |
|
Payment due today |
|
Payment is past due |
Metrics
Each handler emits DataDog metrics:
-
collections.upcoming- Count of upcoming installments -
collections.due- Count of due installments -
collections.overdue- Count of overdue installments -
collections.retry- Count of payments in retry status
Slack Reporting
After each job completes, a summary is posted to Slack with:
-
Number of payments created
-
Number of notifications sent
-
Any errors encountered
Code Location
| Component | Path |
|---|---|
Job Lambda |
|
Worker Lambda |
|
Job Logic |
|
Worker Logic |
|
Payment Engine |
|
Webhook Handler |
|