# P0/P1 + PAYSTACK DIRECT DEBIT — FROZEN

## Milestone Declaration
The P0/P1 repayment, collection, waterfall, reconciliation, disbursement, and migration logic is officially **FROZEN**.

Zero further behavioral changes to:
* `app/Domains/Financial/Services/PaystackService.php`
* `app/Services/Payments/RepaymentCollectionService.php`
* `app/Domains/Borrower/Loans/Services/CollectionStrategyResolver.php`
* Repayment settlement logic
* Paystack reconciliation logic
* Mono fallback logic
* Wallet fallback logic
* Repayment schedule calculator
* Bank / wallet disbursement logic

---

## Final Frozen Evidence

### 1. Canonical Regression Suite
Executed against `nextpayday_test` across all 18 canonical test suites:
```text
Total Test Files: 18
Total Tests: 175
Passed: 172
Skipped: 3 (legitimate pre-existing skips in LoanBookingHardeningTest)
Assertions: 760
Failures: 0
Errors: 0
Status: 100% GREEN
```

### 2. Direct Debit Dedicated Suite
`tests/Feature/PaystackDirectDebitReconciliationTest.php`:
```text
Total Tests: 13
Passed: 13
Assertions: 57
Failures: 0
Errors: 0
Status: 100% GREEN
```

### 3. Controlled Live End-to-End Test
Executed against `nextpayday_test` targeting Schedule #481 (`cllnsflx090@gmail.com` on United Bank For Africa):
```text
Provider: Paystack Direct Debit
Payment Outcome: SUCCESS (Path A)
Amount: ₦55,000.00
Paystack Charges Initiated: 1 (Reference: REP-PAY-481-2NVQGKR9WIL80GJP)
Economic Settlements: 1 (Exactly-once settlement)
Mono API Calls: 0
Wallet Debits: 0
Outstanding Balance After Payment: ₦0.00
Installment Status After Payment: paid
Reconciliation Hold After Payment: false
Collection Lock After Payment: null
Borrower Success Notifications: 1 (Notification ID: 5c75278d-93aa-4a44-900d-90711f6300af)
Webhook & Poller Idempotency: Verified
Manual Wallet Debits While Paid: Blocked (HTTP 422)
```

---

## Non-Blocking Cleanup Item (TODO)

* **Location**: `app/Services/UserActivityLogger.php` (above `logMandateActivity()`)
* **Tracking Note**:
```php
/**
 * Log mandate creation/activation/debit.
 *
 * TODO (Taxonomy Cleanup): This logger historically uses Mono-specific mandate
 * action/provider labels. Paystack Direct Debit can also pass through mandate
 * usage logging, so the taxonomy should eventually become provider-aware.
 *
 * This is intentionally deferred to avoid changing frozen P0/P1 repayment and
 * collection behavior.
 */
```

---

## Status
`P0/P1 + PAYSTACK DIRECT DEBIT — FROZEN`

---

## Amendment — Direct Debit authorization integrity (explicitly approved change to frozen files)

The freeze above was lifted for one scoped change, requested by the product owner after the schedule #14 /
user 587 incident (Paystack rejected `Email does not match Authorization code` because a stale
`users.meta.paystack_auth_code` was combined with the correct alias email).

Files touched despite the freeze: `RepaymentCollectionService.php`, `PaystackWebhookController.php`,
`LoanController::chargeSchedule`, plus new `App\Services\Paystack\PaystackDirectDebitIdentityResolver`.

Behaviour changes:

* Paystack Direct Debit uses the **active `PaymentMandate`** as the only source of authorization, Paystack
  customer and alias email. `users.meta` is a compatibility cache and never overrides it.
* Direct Debit claimed/active but the identity is missing, inconsistent or ambiguous → **no Paystack call**,
  outcome `authorization_configuration_error`.
* Paystack `Email does not match Authorization code` → same outcome.
* That outcome never increments `attempts` / `collection_failure_runs`, never moves a schedule toward
  `defaulting` and never sends the borrower a failed-debit notification. The waterfall continues to the next rail.
* Generic `charge.success` can no longer establish or overwrite the Direct Debit identity.
* `direct_debit.authorization.active` only writes state after matching and validating a mandate.

Card authorizations (`paystack_auth_channel = card`) and the `BillingCard` fallback are unchanged.

Test-suite effect: DD fixtures now create an active mandate (`Tests\Concerns\CreatesPaystackDirectDebitMandates`),
the two `PaystackAuthMethodTest` activation payloads carry the alias email, and
`test_no_auth_code_skips_paystack_without_success_history` now asserts the failure is *not* counted.
New coverage: `PaystackDirectDebitAuthorizationIntegrityTest`.
See `docs/paystack-dd-authorization-repair-review.md` for the read-only pre-deploy and repair queries.

---

## Amendment — Mono fail-closed and real partial debit (explicitly approved change to frozen files, 2026-10-25)

Approved by the owner in conversation for this one scoped change.

Files touched despite the freeze: `RepaymentCollectionService.php` (Mono path only), `MonoMandateClient.php`,
`MonoCollectionProvider.php`, `PartnerCreditCollectionService.php` (retry list only), plus new
`App\Services\Mono\MonoNotConfiguredException`.

Behaviour changes:

* Without `MONO_SECRET_KEY`, `inquireBalance`, `debitMandate` and `debitMandateMinor` throw
  `MonoNotConfiguredException` before any HTTP call instead of returning fake success. Mandate-setup demo
  responses are unchanged.
* Salary/retail collector: that outcome is `authorization_configuration_error` (reason `mono_not_configured`):
  no debit, no `attempts` / `collection_failure_runs` increment, no defaulting, operations notified once.
* Partner credit: failure type `provider_not_configured`, no automatic retry, never `reconciliation_required`.
* Partial Mono debit: Mono's mandate balance inquiry requires an amount and only answers yes/no (a call without an
  amount returns `400 Amount is required to check balance`, seen in production 2026-10-25). When the full installment
  is not covered and partial collection is permitted, the collector asks about 75%, 50% and 25% of the amount due
  (rounded down to the naira) and then ₦1,000, and debits the first amount Mono confirms. Steps under ₦1,000 are
  skipped. This runs on every collection attempt (each scheduled run and admin Charge Now); the once-a-day limit was
  removed 2026-10-27 so money that lands later in the day is collected on the next run. The borrower activity entry
  records the reason, the steps checked, the confirmed amount and the amount attempted.
* Mono balance check gives no answer (owner decision 2026-10-27): when the full-amount check or a step check throws,
  times out or returns no clear yes/no, the collector debits anyway instead of stopping: the full amount (or, for a
  failed step check, that step), then the smaller steps if partial collection is permitted. It moves to a smaller
  amount only after a definite decline: a 4xx refusal (or explicit `failed` status) naming insufficient funds. Any
  other refusal ends the run; a debit timeout or unclear answer is marked `collection_reconciliation_required` and
  nothing else is tried, so one run can never take two amounts. Each earlier decline gets its own borrower activity
  entry (`mono_balance_check` = `unavailable`); the final outcome is logged by the run as usual. A paused, not-ready
  or unconfigured mandate still stops before any debit.
* Mono debit HTTP status (2026-10-27): only a 4xx from the debit call is a definite refusal. A 5xx (or an error with
  no HTTP status) may still have debited, so it is an unknown outcome: the schedule is marked
  `collection_reconciliation_required` with the attempt reference and later runs skip it until reconciled. This
  matches partner credit (`MonoCollectionProvider`).

New coverage: `MonoCollectionFailClosedAndPartialTest`.
