# Employer configuration and virtual accounts

A local round-trip regression using the production form values reproduced the save failure:
a disabled loan fee sends a blank amount, Laravel converts it to null, and the non-nullable
`loan_fee_amount` column rejects the update with a 500 error. The API now uses the saved
amount (or zero for a new employer) for a blank optional amount; the form sends zero for a
blank disabled fee. Enabled fees still require a positive amount. Previously these errors
were hidden behind the configuration modal.

The employer configuration form exposes `virtual_account_required`, enabled by default.
The effective requirement is the global financial setting AND the saved employer flag.
Without a saved employer, or if its flag is absent, the global requirement applies.
A request-supplied employer ID at stage two cannot grant an exemption.
New applicants reach identity before employment; the employer exception only applies after
employment is saved (including admin-assigned employment). Existing virtual accounts stay intact.
Per-employer exemptions do not write synthetic account-created proof, so re-enabling the flag
requires a virtual account unless an account was already verified. BVN stays required.
The existing global-off behavior remains compatible with its existing tests.

## Callers and scope

- Admin employer create/update: validate and persist the boolean; omission preserves it;
  changed values produce admin audit records and Activity records for linked borrowers.
- Employer model: fillable, boolean cast and strict default; migration defaults to enabled.
- Stage-two verification action: skips creation for a saved exempt employer; account-link
  authorization is independent and retains its existing rules.
- UserVerificationService `getSteps`, `stageBreakdown`, `getApplicationProgressBatch`:
  apply the same exemption to identity and VA completion. Batch uses loaded employers.
- Identity progress action and CustomerJourneyService: use the borrower's saved employer.
- User `fullyVerified` SQL scope: applies the same employer exception, retaining BVN and
  all existing non-VA predicates. Downstream onboarding/booking consumers inherit these results.
- Existing proof writers (`markStep`, Paystack identification webhook, admin manual identity
  override), reconciliation, raw verification model and diagnostics remain unchanged.
- Admin form: display API validation errors above the modal, show saving status and prevent
  repeat submissions; normalize legacy comma-separated states and preserve zero rates/limits.

Salary, retail and local-government employers use the flag. Borrowers and admin/affiliate
progress screens consume the shared identity results; the scheduler and Pay Later-specific
mandate services retain their rules. Paystack dedicated account creation is affected.
Paystack authorization/transfers, Mono, Monnify, manual payouts, wallets, repayment schedules
and existing balances are unchanged. Legacy borrowers default to the strict requirement.

## Validation and release

Feature coverage includes employer defaults, create/update, invalid/null flags, omitted-field
preservation, admin and borrower audit records, a salary form round trip, exemption and
re-enabling, individual/batch/SQL identity consistency, and rejection of unsaved employer
input as an exemption. Provider interactions use the existing fake Paystack gateway.

Production employer list was inspected read-only. Kiloowat has the values shown in the
support screenshot. No production save or payment-provider request was made.
Live provider behavior and mobile rendering were not tested.

Deploy the employer column migration before releasing the API that reads it; then release
the admin. All employers start enabled; choose exceptions explicitly. This agent ran the
migration only against `nextpayday_test`. The existing API main-push deployment workflow runs `php artisan migrate --force` during
maintenance. No separate production migration command was run by the agent.

Final validation on 21 October 2026:

```text
OK, but some tests were skipped!
Tests: 1186, Assertions: 6590, Skipped: 3.
```

Admin: `npx tsc --noEmit -p .` passed. Both repositories passed `git diff --check`.
