Automating Provisional-Credit Decisions Under Reg E

Provisional credit is where a Regulation E program leaks money in two directions at once. Miss a mandatory credit and you have a compliance violation an examiner will find; issue one twice, forget to reverse it after an unfavorable finding, or grant one on a commercial account that was never covered, and you have booked a direct, unrecoverable loss. This reference gives you one annotated function that makes the decision deterministically: is credit required, how much, and by when must it reverse. It sits under Regulation E error resolution for ACH disputes, which specifies the timeline, inside the broader Reg E, NACHA and audit controls practice — and the guarded state machine that actually applies and reverses the credit is developed in encoding provisional-credit state transitions.

The rule at the center of §1005.11(c)(2) is narrow: provisional credit becomes mandatory only when the institution cannot complete its investigation within the base window (ten business days, or twenty for new-account, point-of-sale, and foreign-initiated disputes) and elects to use the extended window. Before that boundary it is optional; after it, refusing to credit forfeits the extension and puts you out of compliance. The decision is therefore a function of exactly three inputs — coverage, elapsed time against the investigation deadline, and resolution status — and the amount is a function of the disputed principal plus any related fees and interest, all held in integer cents so no float ever touches money.

The Full Decision Function

The function returns a typed decision object: whether credit is required, the amount in cents, the reversal deadline should the finding go against the consumer, and a reason string for the audit trail. It refuses to run on commercial accounts, refuses to double-credit, and computes the credit as principal plus related fees — never as a float.

python
from __future__ import annotations

from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from enum import Enum
from typing import Callable, Optional


class AccountType(Enum):
    CONSUMER = "consumer"
    COMMERCIAL = "commercial"      # UCC 4A, not Reg E — never provisionally credit here


class Decision(Enum):
    REQUIRED = "required"
    NOT_YET = "not_yet"            # inside the investigation window
    ALREADY_ISSUED = "already"     # idempotency guard against double credit
    NOT_APPLICABLE = "n/a"         # resolved, or not a consumer EFT


@dataclass(frozen=True, slots=True)
class ProvisionalCreditDecision:
    decision: Decision
    amount_cents: int              # 0 unless decision is REQUIRED
    reversal_deadline: Optional[date]
    reason: str


@dataclass(frozen=True, slots=True)
class DisputeState:
    account_type: AccountType
    disputed_cents: int            # principal amount in dispute
    related_fee_cents: int         # NSF/overdraft fees to restore, if any
    accrued_interest_cents: int    # interest on the disputed amount, if any
    investigation_due: date        # from the Reg E deadline calculator
    extended_resolution_due: date  # 45 or 90 calendar-day cap
    resolved: bool
    provisional_credit_issued: bool


def decide_provisional_credit(
    state: DisputeState,
    today: date,
    add_business_days: Callable[[date, int], date],
) -> ProvisionalCreditDecision:
    """Decide whether Reg E provisional credit is mandatory today.

    Provisional credit is required (12 CFR 1005.11(c)(2)) once the base
    investigation window has lapsed on an unresolved *consumer* dispute and no
    credit has yet been issued. The credit restores the disputed principal plus
    any related fees and accrued interest. If a later finding is unfavorable,
    the credit is reversed after a results notice with a 5-business-day honor
    period, so the reversal deadline anchors to the extended resolution date.
    """
    # 1. Coverage guard: Reg E is consumer-only. Never credit a commercial account.
    if state.account_type is not AccountType.CONSUMER:
        return ProvisionalCreditDecision(
            Decision.NOT_APPLICABLE, 0, None, "commercial_account_ucc_4a")

    # 2. Nothing to do once the dispute is resolved.
    if state.resolved:
        return ProvisionalCreditDecision(
            Decision.NOT_APPLICABLE, 0, None, "already_resolved")

    # 3. Idempotency: a credit already on the books must not be issued twice.
    if state.provisional_credit_issued:
        return ProvisionalCreditDecision(
            Decision.ALREADY_ISSUED, 0, None, "credit_already_on_ledger")

    # 4. Inside the investigation window credit is optional, not mandatory.
    if today <= state.investigation_due:
        return ProvisionalCreditDecision(
            Decision.NOT_YET, 0, state.investigation_due,
            "within_investigation_window")

    # 5. Window lapsed unresolved -> credit is mandatory.
    amount_cents = (
        state.disputed_cents
        + state.related_fee_cents
        + state.accrued_interest_cents
    )
    # Reversal, if the finding is unfavorable, follows the results notice with a
    # 5-business-day honor period after the extended resolution date.
    reversal_deadline = add_business_days(state.extended_resolution_due, 5)
    return ProvisionalCreditDecision(
        Decision.REQUIRED, amount_cents, reversal_deadline,
        f"mandatory_after_{state.investigation_due.isoformat()}")

Calibration: Account Type and Error Class

The decision surface is small, but two inputs change the outcome entirely and must be set from data:

  • Consumer vs commercial account. Only consumer EFTs are covered. The function fails closed on COMMERCIAL, returning NOT_APPLICABLE with a UCC 4A reason. Route those disputes to the commercial path; never let a commercial dispute reach a credit issuance step.
  • Error type and related amounts. The credit is not always just the disputed principal. Under §1005.11(c)(2)(i) it must restore the amount in question and any related fees and interest the error caused — an unauthorized debit that also triggered a $35 overdraft fee means related_fee_cents = 3500. Populate related_fee_cents and accrued_interest_cents from the account's own ledger, in cents, so the restored amount is exact.
  • Base window length. The investigation_due you pass already encodes ten vs twenty business days from the transaction class; this function does not re-derive it, so feed it the deadline from the banking-day calculator.

Before and After: A Worked Case

Dispute D-4471: a consumer reports an unauthorized ACH debit of $240.00 that also caused a $35.00 overdraft fee, with no accrued interest. Notice received 2026-02-13; the standard investigation deadline computes to 2026-03-02, and the 45-day extension cap to 2026-03-30.

Evaluation date resolved credit_issued Decision Amount Reversal deadline
2026-02-20 False False NOT_YET $0.00 2026-03-02
2026-03-03 False False REQUIRED $275.00 (24000 + 3500 cents) 2026-04-06
2026-03-03 (re-run) False True ALREADY_ISSUED $0.00
2026-03-15 True True NOT_APPLICABLE $0.00

Before this gate, an operator eyeballing a queue on 2026-03-03 might credit $240.00 and miss the $35.00 fee restoration, or re-credit the same dispute on a second pass. The function restores the exact $275.00 in cents, sets a reversal deadline of 2026-04-06 (five business days after the extension cap) in case the investigation later finds no error, and — because step 3 checks provisional_credit_issued — returns ALREADY_ISSUED on any re-run so the credit can never post twice.

Failure Modes & Guardrails

  1. Double credit. The single most expensive bug: two evaluation passes both decide REQUIRED and two credits post. The idempotency guard (step 3) makes the on-ledger flag authoritative, but the flag must be set transactionally with the credit posting — set it in the same database transaction that books the credit, or a crash between the two re-opens the double-credit window.
  2. Missing reversal after an unfavorable finding. A provisional credit that is never reversed when the investigation finds no error is a pure loss. Persist reversal_deadline on the dispute when credit issues, and drive a timer off it so an unfavorable resolution enqueues the reversal (after the results notice and the five-business-day honor period) rather than relying on anyone to remember.
  3. Float money. Computing the credit as 240.0 + 35.0 invites the same binary-rounding drift that corrupts tolerance matching. Every amount here is integer cents; the disputed principal, the fee, and the interest are summed as int, and if your ledger hands you Decimal, quantize to cents and convert before it reaches this function — never pass a float.
  4. Commercial account misclassified as consumer. If account_type is wrong, the function will happily compute a mandatory credit the statute never required, moving real money on a UCC 4A account. Classify account type from the system of record at intake and treat it as immutable for the life of the dispute; the coverage guard is only as good as that input.

Frequently Asked Questions

Is provisional credit ever optional?

Yes — up until the base investigation window lapses. Inside the ten (or twenty) business days the institution may resolve the dispute and never issue credit; the function returns NOT_YET there. Credit becomes mandatory only when you pass that deadline unresolved and want to use the extended 45- or 90-day window. Declining to credit at that point simply forfeits the extension and puts you out of compliance.

Does the credit include fees and interest, or just the disputed amount?

It includes related fees and interest. Section 1005.11(c)(2)(i) requires restoring the amount in question along with any fees and interest the alleged error caused — so an unauthorized debit that also triggered an overdraft fee must be credited for both. The function sums disputed_cents, related_fee_cents, and accrued_interest_cents; populate the latter two from the account ledger.

When exactly must a provisional credit be reversed?

Only if the completed investigation finds no error (or a smaller error). The institution first sends the results notice, then may reverse, and must honor items that would have paid against the credited funds for five business days after the notice. The function anchors reversal_deadline to five business days past the extended resolution date; the actual reversal is a guarded transition, not an automatic debit.

Why hold the amount in cents instead of Decimal dollars?

Integer cents are exact and cheap to compare and sum, which is what a credit decision needs. Decimal is equally exact and fine if your ledger speaks it, but you must quantize to two places before arithmetic. The rule that never bends is: no float. A float principal plus a float fee can land a fraction of a cent off, and at volume that discrepancy becomes a reconciliation break of its own.