Hi Karan,
Following up on GH issue #2297 with additional findings from our continued testing. The same root cause (`calculateOverdueState()` first-match behavior) affects more scenarios than we initially reported.
---
## 1. State-Skip Applies to ALL Condition Types (Additional Details for #2297)The state-skip issue is not limited to `timeSinceEarliestUnpaidInvoiceEqualsOrExceeds`. Since `calculateOverdueState()` always returns the first matching state in XML order regardless of the current account state, the same intermediate state-skip will occur with any condition type that can change suddenly:
### State skip with `totalUnpaidInvoiceBalanceEqualsOrExceeds`Example config:
- WARNING: balance >= $50
- BLOCKED: balance >= $200
- CANCELLATION: balance >= $500
**Scenario:** Account has $40 unpaid balance (CLEAR state). A large external charge of $600 is added. Balance jumps from $40 to $640.
**Expected:** CLEAR → WARNING ($640 >= $50) → then re-evaluate → BLOCKED → CANCELLATION
**Actual:** `calculateOverdueState()` checks CANCELLATION first: $640 >= $500? YES → returns CANCELLATION. Account goes CLEAR → CANCELLATION directly. WARNING and BLOCKED skipped.
### State skip with `numberOfUnpaidInvoicesEqualsOrExceeds`Example config:
- WARNING: unpaid invoices >= 2
- BLOCKED: unpaid invoices >= 4
- CANCELLATION: unpaid invoices >= 6
**Scenario:** Account has 1 unpaid invoice (CLEAR state). On the next billing date, multiple invoices are generated simultaneously (subscription + usage + add-on charges), pushing the count from 1 to 7 in one cycle.
**Expected:** CLEAR → WARNING (7 >= 2) → then re-evaluate → BLOCKED → CANCELLATION
**Actual:** `calculateOverdueState()` checks CANCELLATION first: 7 >= 6? YES → returns CANCELLATION directly. WARNING and BLOCKED skipped.
### State skip with combined conditions (AND logic)Example config:
- WARNING: balance >= $50 AND unpaid >= 5 days
- BLOCKED: balance >= $100 AND unpaid >= 7 days
**Scenario:** Account has $40 balance, unpaid for 8 days (CLEAR — balance below $50 threshold, so WARNING never matched). On Day 8, a $150 external charge is added. Balance jumps to $190.
**Expected:** CLEAR → WARNING ($190 >= $50 AND 8 >= 5) → then re-evaluate → BLOCKED
**Actual:** `calculateOverdueState()` checks BLOCKED first: $190 >= $100 AND 8 >= 7? YES → returns BLOCKED. WARNING skipped entirely.
### SummaryAll these scenarios share the same root cause: `calculateOverdueState()` does not enforce sequential transitions. The fix for #2297 (making state transitions sequential regardless of which conditions match) would address all of these cases together.
---
## 2. Additional Scenario: Overdue Config Uploaded AFTER Invoices Already Exceed All ThresholdsWe also discovered a separate but related scenario that we'd like clarity on.
### Steps to reproduce:1. **Aug 12**: Account created. Subscription invoice generated ($300 = $100 FIXED + $200 RECURRING). Payment fails. Invoice remains unpaid.
2. **No overdue config exists** at this point — no overdue scheduling happens.
3. **Sep 12** (30 days later): Overdue XML config uploaded to the tenant (WARNING=5d, BLOCKED=7d, CANCELLATION=11d).
4. **Sep 12**: The next billing cycle fires (monthly subscription renewal). This triggers invoice generation → overdue REFRESH is triggered for the account.
5. Overdue evaluates: earliest unpaid invoice = Aug 12. Today = Sep 12. Days unpaid = **31 days**.
6. `calculateOverdueState()` checks conditions:
- CANCELLATION: 31 >= 11? **YES → returns CANCELLATION immediately**
- BLOCKED and WARNING are never checked.
7. Subscription is **cancelled immediately** via `subscriptionCancellationPolicy=IMMEDIATE`.
### Result:- Account went **directly CLEAR → CANCELLATION** — skipping WARNING and BLOCKED entirely.
- No `initialReevaluationInterval` scheduling occurred — the cancellation happened during the overdue REFRESH itself (not via a scheduled `OverdueCheckNotif`).
- No WARNING email notification was sent.
- No grace period for the customer.
### DB Evidence:**blocking_states table:**```
| blockable_id | state | service | effective_date |
|--------------|--------------|-----------------|---------------------|
| 8e1d988a... | CANCELLATION | overdue-service | 2026-09-12 06:58:05 |
```
No WARNING or BLOCKED rows exist.
**notifications_history (overdue entries):**```
| # | class_name | queue_name | effective_date |
|---|-------------------------|------------------------------------------|---------------------|
| 2 | OverdueAsyncBusNotif | overdue:service:overdue-async-bus-queue | 2026-09-12 06:58:05 |
| 3 | OverdueAsyncBusNotif | overdue:service:overdue-async-bus-queue | 2026-09-12 06:58:05 |
| 4 | OverdueAsyncBusNotif | overdue:service:overdue-async-bus-queue | 2026-09-12 06:58:09 |
```
All are REFRESH actions triggered by billing events on Sep 12. No scheduled `OverdueCheckNotif` was ever created for this account.
### Our Question:**Is this expected behavior, or should it be handled differently?**
Specifically:1. When overdue config is uploaded to a tenant that already has accounts with unpaid invoices exceeding all state thresholds, is it expected that those accounts will be immediately cancelled (highest severity) on the next overdue evaluation?
2. Is there any documented recommendation for deploying overdue config to a tenant with existing accounts that already have old unpaid invoices? We could not find guidance on this in the overdue documentation.
3. Should the fix for #2297 (sequential state enforcement) also cover this case — meaning even if an invoice has been unpaid for 31 days, the system should still transition sequentially (CLEAR → WARNING → BLOCKED → CANCELLATION), giving each state a chance to fire its email notifications?
### Production Impact:If a company enables overdue on an existing tenant, ALL accounts with old unpaid invoices beyond the CANCELLATION threshold would be immediately cancelled on their next billing event — with no prior warning to customers.
### Our Workaround:Before uploading overdue config, we plan to:
1. Tag all accounts with old unpaid invoices using `OVERDUE_ENFORCEMENT_OFF`
2. Upload the overdue config
3. Gradually remove tags so each account starts fresh
Would appreciate knowing if this is the recommended approach or if this scenario should be handled gracefully by the overdue system itself.
---
Thank you for your continued support on this.
Best regards,
Tamil Thendral