# Loans CSV Import

`POST https://polarisapp.doctor-peso.co/api/incoming-updates/loans-csv`

Updates existing loans in `9_cobranzas` from a CRM collector report export.
Rows are matched by `obligacion` = the `ORDEN` column. No new loans are created.

## Request

```bash
curl -i -X POST https://polarisapp.doctor-peso.co/api/incoming-updates/loans-csv \
  -H "access-token: NfzjRyHJUeoUvEJSSigTewrjhdKdeePbsazf8Fjo5e715327" \
  -F "file=@collector_report.csv"
```

- `multipart/form-data`, field `file`, 50 MB max.
- Send the header as `access-token` with a hyphen — headers containing underscores are dropped.
- The import runs synchronously; the response is returned once the whole file is processed.

## File format

Comma-separated, first row is the header. Case does not matter; surrounding spaces, quotes and a
BOM are stripped. All 8 columns are required, order is arbitrary, extra columns are ignored.

```
ORDEN,CEDULA,DPD,CAPITAL VIGENTE,AMOUNT TO REPAY,AMOUNT FOR PROLONGATION,PROLONGATION 10 DAYS,ESTADO
58022,LOOC811025HDFPRR02,15,1000.50,1200,300,150,sold
58026,SACE700311HCLLSL06,20,,2000,,,overdue
```

- `ORDEN` — must not be empty.
- `DPD` — integer.
- The four amount columns — numeric or empty.
- `ESTADO` — one of: `active`, `outloanterm`, `grace`, `repaid`, `sold`, `canceled`, `overdue`,
  `closed`. An unknown value rejects the whole file with `422` and nothing is written.

Rows that fail these rules are skipped and counted in `invalid_rows`. If the same `ORDEN` appears
twice in the file, the first occurrence wins and the rest are counted in `duplicate_rows`.

## What gets updated

One table: `9_cobranzas`.

| CSV column | Database field |
|---|---|
| `ORDEN` | `obligacion` — lookup key, not written |
| `CEDULA` | compared against `documento`, **not written** |
| `DPD` | `dpd`, `dias_actual` |
| `CAPITAL VIGENTE` | `credito` |
| `AMOUNT TO REPAY` | `amount_to_repay` |
| `AMOUNT FOR PROLONGATION` | `amount_for_loan_prologation` |
| `PROLONGATION 10 DAYS` | `prolongation_10_days` |
| `ESTADO` | `estado`, `status_id` |

`updated_at` is set to the time of the import.

## Response

```json
{
  "status": "success",
  "path": "public/incomingUpdates/2026-08-26/1787757726_report.csv",
  "report": {
    "data_rows": 1200,
    "invalid_rows": 3,
    "duplicate_rows": 1,
    "matched_rows": 1150,
    "updated_rows": 1150,
    "dry_run": false,
    "resolved_statuses": ["active", "overdue"],
    "not_found": { "count": 46, "sample": ["1001", "1002"] },
    "document_mismatches": { "count": 2, "sample": [
      { "obligacion": "1003", "csv": "123", "db": "124" }
    ] }
  }
}
```

- `matched_rows` — rows found in `9_cobranzas`; `updated_rows` — rows actually written.
- `not_found` — `ORDEN` values with no matching loan.
- `document_mismatches` — `CEDULA` in the file differs from `documento` in the database.
- `sample` lists are capped at 20 entries; `count` is the full number.

## Status codes

| Code | Meaning |
|---|---|
| `200` | Import finished, report in the body |
| `401` | Missing or invalid `access-token` |
| `422` | No file, file over 50 MB, missing required columns, or unknown `ESTADO` |
| `429` | Another import is already running |

A `422` caused by the file itself returns the reason and what was wrong:

```json
{
  "status": "failed",
  "reason": "Missing required columns: dpd, estado.",
  "details": { "missing": ["dpd", "estado"], "found": ["orden", "cedula"] },
  "path": "public/incomingUpdates/2026-08-26/1787757734_report.csv"
}
```
