# Flexport Billing Reconciliation — Pascucci USA Inc — Handoff V1

> **Audience**: anyone maintaining or extending the Flexport-billing reconciliation flow.
> **Last updated**: 2026-05-01 (session 2 — first 2 production exports landed)
> **Tenant scope**: Currently wired specifically for `Pascucci USA Inc`
> (`tenant.slug = pascucci`, `tenant.id = 837eebaa-1ae8-5b79-95c3-26649fb25c42`).
> **Status**: Live in production. 2 Flexport invoices posted (PI+PE pairs, both
> submitted, BTs Unreconciled per the post-submit lifecycle below). 2 of 4
> pending bank lines deliberately preserved for follow-up testing.
> **Sister doc**: `docs/features/shopify_payout_recon/SHOPIFY_PAYOUT_RECON_PASCUCCI_HANDOFF_V1.md`

---

## ⚠️ Critical lessons learned (read before any maintenance)

### A. The Bank Transaction lifecycle — `Unreconciled` is the right post-export status, NOT `Reconciled`

ERPNext's BT statuses encode a multi-stage workflow:
- `Pending` — imported, no action yet
- `Unreconciled` — payment doc exists and is linked to BT, awaiting accountant's bank-statement reconciliation
- `Reconciled` — accountant has matched against the actual bank statement (final state)
- `Settled`, `Cancelled` — other terminal states

**Finanly auto-creates+submits the PI/PE/JE; that gets the BT to `Unreconciled`.** The
accountant later runs ERPNext's Bank Reconciliation Tool against an actual bank
statement to flip to `Reconciled`. The connector code MUST set status to `Unreconciled`
after creating the linkage — never `Reconciled` (we'd be lying about the rec state and
locking out the accountant's tool).

This was a real bug fixed in this session. All 3 BT-status writes in `connectors/erpnext/.../app.py` were changed `"Reconciled"` → `"Unreconciled"`.

### B. The export `submit` default is `True` — clicking Export = fully posted in ERPNext

`_ExportRequest.submit` defaults to `True` in `routers/recon.py`. Clicking Export auto-submits
the PI and PE (Flexport) or JE (Shopify). The legacy ERPNext bank-transaction-ui worked the
same way — operators expect "click = done". If anyone changes this default to `False`,
half-state bugs reappear (BT shows reconciled but GL is empty until someone manually
submits the draft).

### C. `set_posting_time: 1` is required on EVERY doc that should land in a past period

Without `set_posting_time: 1`, ERPNext **silently overrides** `posting_date` to "today"
during submit. This wrecks P&L periods (expense in May for a March bill), AP aging,
month-end close, everything.

All 4 doc creations in `connectors/erpnext/.../app.py` now include `"set_posting_time": 1`:
- Shopify JE (`_post_shopify_payout_journal_entry`)
- Flexport JE fallback (`_post_flexport_billing_journal_entry_fallback`)
- Flexport PI (`_post_flexport_billing_purchase_invoice`)
- Flexport PE (same function)

### D. PE's `paid_to` MUST match the PI's `credit_to`

ERPNext's PI auto-populates `credit_to` from `tabCompany.default_payable_account`
(varies per tenant). The PE's `paid_to` must match, or ERPNext rejects the PE with
"Purchase Invoice X is associated with Y, but Party Account is Z".

The connector now reads the freshly-created PI's `credit_to` and uses that as the PE's
`paid_to`. Don't hardcode `Creditors - {abbr}` — it'll break for tenants whose company
default is `Accounts Payable (A/P) - PUI` (Pascucci) vs `Creditors - PUI`.

### E. ERPNext Company doc requires several account defaults — set them all per tenant

Pascucci required ALL of these to be set on `tabCompany` before PI/PE flow worked:
- `default_payable_account` (e.g. `Accounts Payable (A/P) - PUI`)
- `stock_received_but_not_billed` (e.g. `Stock Received But Not Billed - PUI`) ← required even for non-stock PIs
- `round_off_account` (e.g. `Round Off - PUI`) ← required on PI submit
- `round_off_cost_center` (e.g. `Admin and Overheads - PUI`)
- `write_off_account` (e.g. `Write Off - PUI`)

Mirror Ad Astrum's working setup when onboarding a new tenant. New tenants will hit
"Please set default X in Company" errors during the first export until configured.

### F. NO silent fallbacks — every ERPNext error must surface

The connector previously had silent JE-fallback paths in `_post_flexport_billing_purchase_invoice`
that hid PI/PE failures and posted the books a different way without telling the operator.
Removed in this session. The connector now raises HTTP 502 with the full ERPNext error
body so the operator sees the root cause and fixes it.

---

## 1. Why this feature exists

Pascucci uses Flexport (Deliverr/Ship) for DTC fulfillment. Flexport bills Pascucci
periodically as a single bank withdrawal that aggregates many underlying charges:
storage, fulfillment, inbound handling, returns, and assorted operational fees.

This feature decomposes each Flexport invoice into one ERPNext-ready Purchase
Invoice (or Journal Entry fallback) with **one GL line per (Charge Category × product family)**
that reconciles to the cent against the bank withdrawal. Pascucci's books then show
where every dollar of Flexport's invoice landed: Coffee storage vs Machines storage,
Coffee fulfillment vs Machines fulfillment, and so on.

---

## 2. End-to-end flow

```
Mercury bank deposit
        │ shows up in canonical_transactions (unreviewed)
        ▼
User opens row → clicks Flexport radio → /v1/recon/flexport/billing-rows lists
candidate Flexport invoices (one recon_item per Invoice ID, post-2026-05-01).
        │
        ▼
User picks invoice → /v1/recon/flexport/billing-rows/{id}/preview
        │
        ▼
core-api `_build_flexport_preview_payload`:
  • walks payload.rows (verbatim 5-column CSV rows)
  • per row: `pascucci_flexport_routing(charge_category, family)`
  • per family: `_attribute_flexport_row_shares(...)` for per-row split:
      - Inbound charges → 2-signal classifier (plan_name + fc_location)
      - Returns        → match by (date, amount), pro-rate by SKU mix
      - Fulfillment    → pro-rate by VOLUME of items shipped that day
      - Storage        → pro-rate by VOLUME of inventory composition
      - Penalties      → flat single-account
  • cent-precise distribution: `_split_amount_cent_precise()`
  • aggregates by (account, cost_center, category)
  • verifies: round(sum(gl_entries)) == round(sum(rows.total_charge))
  • refuses if any unmapped category (HTTP 412)
  • refuses if drift > $0.005 (HTTP 502)
        │
        ▼
User reviews preview → clicks Export → /v1/recon/flexport/billing-rows/{id}/export
        │
        ▼
Approval + ExportBatch + ExportItem (still NO ledger write yet)
        │
        ▼
Celery worker picks up export → connector-erpnext
`_post_flexport_billing_purchase_invoice`:
  • creates Supplier `Flexport - PUI` (idempotent)
  • creates Purchase Invoice (bill_no = Invoice ID), one line per gl_entry
  • creates Payment Entry against the PI for the bank amount
  • reconciles ERPNext Bank Transaction to the PE
```

---

## 3. Critical files

### Connector (`finanly/services/connectors/flexport/.../app.py`)
| Function | Purpose |
|---|---|
| `_normalize_billing_row` | Parses real 5-column CSV (`Charge Category`, `Charged Date`, `Billed Date`, `Invoice ID`, `Total Charge`) |
| `stable_flexport_invoice_id(invoice_id)` | `flexport:invoice:<sha256>` — recon_item primary id |
| `stable_flexport_billing_row_id(row, ordinal)` | per-row id used inside payload |
| `_safe_pull_report_csv(...)` | best-effort wrapper (returns [] on failure) |
| `_build_inbound_attribution(...)` | `Inbounds-Shipping_Plan_Reconciliation_V2` filtered to ACTIVE COMPLETED |
| `_build_returns_attribution(...)` | Joins `Returns-All_Returns` + `Returns-All_SKUs` by RMA |
| `_build_orders_attribution(...)` | `Orders-All_Orders` shipped-date + items[] |
| `_build_inventory_attribution(...)` | `Inventory-Units_In_Long_Term_Storage` per-SKU snapshots |
| `_build_products_catalog(...)` | `/products` paginated → `dsku → {name, volume_in3, weight_lb}` |
| `sync()` | Groups CSV rows by `Invoice ID`, embeds attribution data into each invoice's `payload.attribution` |

### Core‑API (`finanly/services/core-api/src/finanly_core_api/`)
| Path | Purpose |
|---|---|
| `services/product_classifier.py` | `PASCUCCI_FLEXPORT_CATEGORY_MAP` (per-family), `PASCUCCI_FLEXPORT_FLAT_FEES` (flat), `pascucci_flexport_routing(category, family)`, `pascucci_flexport_inbound_classifier(plan_name, fc_location, sku_names, min_signals=2)`, `classify_dsku_via_product_name(name)` |
| `routers/recon.py:_build_flexport_preview_payload` | Per-invoice preview builder; refuses with HTTP 412 on unmapped, 502 on drift |
| `routers/recon.py:_attribute_flexport_row_shares` | The cross-reference resolver (inbound, returns, storage volume, fulfillment volume) |
| `routers/recon.py:_split_amount_cent_precise` | Distributes a Decimal across families with rounding drift absorbed by the last family |
| `routers/recon.py:list_unmapped_categories` (`GET /v1/recon/unmapped-categories`) | Aggregates unmapped categories across Flexport + Shopify for the active tenant |
| `routers/internal_recon_digest.py` | Internal endpoint `POST /internal/v1/recon-digest/run-daily` — iterates all active tenants, sends 1 email per tenant via `email_smtp.send_email_with_attachments` |

### Connector‑ERPNext (`finanly/services/connectors/erpnext/.../app.py`)
| Function | Purpose |
|---|---|
| `_post_flexport_billing_purchase_invoice` (line ~2286) | Creates Supplier + PI + PE + reconciles BT. Resume-safe via prechecks (PI by bill_no, PE by reference_no, BT linkage). |
| `_post_flexport_billing_journal_entry_fallback` (line ~2154) | Used when supplier creation or PI creation fails |
| Dispatch at line ~9060 | `posting_kind == "flexport_billing_purchase_invoice"` |

### Jobs (`finanly/services/jobs/src/finanly_jobs/`)
| Path | Purpose |
|---|---|
| `tasks.py:unmapped_categories_digest_email_all_tenants` | Daily Celery beat task (3:30 AM); calls the internal digest endpoint |
| `celery_app.py:beat_schedule['unmapped-categories-digest-daily-330am']` | Cron entry |
| `tasks.py:_default_sync_resources_for("flexport")` | Returns `["billing_rows"]` for Flexport nightly sync |
| `tasks.py:_connector_timeout_s_for_sync("flexport")` | 900s (covers 4 reports + product catalog) |

### UI (`ui/finanly-ui/app/business/transactions/page.tsx`)
| State / Function | Purpose |
|---|---|
| `FlexportInvoiceCandidate` type | List item shape (incl. `invoice_id`, `row_count`, `categories_seen`) |
| `loadFlexportCandidates(txnId)` | GET `/v1/recon/flexport/billing-rows`, populates state |
| `doFlexportPreview(txnId, invoiceSourceId)` | POST `.../preview`, stores preview + recon_preview_id |
| `doFlexportExport(txnId, invoiceSourceId)` | POST `.../export`, then `waitForExportBatchCompletion`, then `refreshAfterMutation(txnId, "reconciled", ...)` |
| Inline accordion preview (line ~3870+) | Mirrors Shopify panel; adds red `unmapped_categories` banner + yellow `classification_failures` banner |

---

## 4. Pascucci routing map — verified live in ERPNext (`erpnext_recovered_20250814`)

```
05 - Expenses - PUI
├── Shipping & Fulfillment - PUI
│   ├── Fulfillment Coffee - PUI
│   ├── Fulfillment Machines - PUI
│   ├── Fulfillment Others - PUI
│   └── Other Logistics Expense - PUI    ← all penalty / non-compliance flat fees
├── Storage - PUI (group)
│   ├── Storage Coffee - PUI
│   ├── Storage Machines - PUI
│   └── Storage Others - PUI
└── Return handling & Labels - PUI (group)
    ├── Return handling & Labels - Coffee - PUI
    ├── Return handling & Labels - Machines - PUI
    └── Return handling & Labels - Others - PUI

10 - Landed Cost - PUI
└── Inbound Freight & Handling - PUI (group)
    ├── Inbound Freight & Handling - Coffee - PUI
    ├── Inbound Freight & Handling - Machines - PUI
    └── Inbound Freight & Handling - Others - PUI

09 - Current Liabilities - PUI
└── Creditors - PUI (group)
    └── Flexport - PUI    ← per-supplier payable, auto-created by ERPNext
```

Cost centers reused (no new ones): `eCommerce - Coffee/Machines/Other - PUI`.

Supplier doctype `Flexport - PUI` is created on demand by the connector at first post.

---

## 5. The 11 Charge Categories — locked routing

### Per-family categories (volume-weighted attribution)
| Charge Category | GL Account family | Cost center family | Attribution method |
|---|---|---|---|
| `Fulfillment Fees` | `Fulfillment {Coffee/Machines/Others} - PUI` | `eCommerce - {Coffee/Machines/Other} - PUI` | Pro-rate by `unit_volume × qty` of items shipped on `Charged Date` (from `Orders-All_Orders` + product dimensions) |
| `Storage Fees - DTC` | `Storage {Coffee/Machines/Others} - PUI` | same | Pro-rate by `unit_volume × units_stored` from inventory snapshot ≤ Charged Date |
| `Reserve Storage - Inbounding Handling (Palletized)` | `Inbound Freight & Handling - {Coffee/Machines/Others} - PUI` | same | 2-signal inbound classifier (plan_name + fc_location); requires both signals to agree |
| `Reserve Storage - Receipt Storage` | same | same | same as above |
| `Prep - BCL Labels` | same | same | same as above |
| `Returns - Shipping` | `Return handling & Labels - {Coffee/Machines/Others} - PUI` | same | Match return by (`Shipped At`, `Shipping Cost`); pro-rate by SKU count per family from returned items |

### Flat fees (single account, no product split)
| Charge Category | GL Account | Cost center |
|---|---|---|
| `DTC Non-Compliance Fees` | `Other Logistics Expense - PUI` | `eCommerce - Other - PUI` |
| `B2B Non-Compliance Fees` | same | same |
| `Missing Container Label` | same | same |
| `Over Receive` | same | same |
| `Unexpected Sku` | same | same |

---

## 6. The 2-signal inbound classifier

For each inbound-related row, the classifier looks at:
1. **Origin keyword** in `Shipping Plan Name` (matches `italy/italia/china/ningbo/cn/it`)
2. **FC coast** from `Fulfillment Center Location` (Phillipsburg/NY/NJ → east → coffee; San Bernardino/LA/CA → west → machines)
3. (Optional) Most-frequent product family among receiver SKU names (only used when `min_signals=3`)

Default `min_signals=2` because Flexport's `Inbounds-Shipping_Plan_Reconciliation_V2`
report only carries MSKU strings (no product names). For Pascucci's data, signals 1
and 2 always agree:
- "from Italy" + Phillipsburg, NJ → COFFEE ✓
- "from China" + San Bernardino, CA → MACHINES ✓

If signals disagree OR fewer than `min_signals` are present, the classifier returns
`family=None` → preview falls back to OTHER family AND emits a `classification_failures`
entry that the UI shows as a yellow banner.

**Filter**: only inbounds with `IS_PLAN_ARCHIVED = NO` AND `SHIPMENT_STATUS = COMPLETED`
are considered. Historical/archived plans are ignored.

---

## 7. Volume-based pro-rate (Storage + Fulfillment)

Flexport bills storage and fulfillment by physical space, not unit count.

```
unit_volume_in3 = H × L × W                    (from /products dimensions)
family_volume[fam] += unit_volume_in3 × units_stored   (or × qty_shipped)
share[fam]          = family_volume[fam] / total
```

**Verified for Pascucci's 2026-04-29 inventory snapshot:**
- 1 machine SKU × 1,440 units × 1,281.3 in³ = 1,845,072 in³ (57%)
- 9 coffee SKUs × ~1,200 units avg × ~120 in³ avg = 1,390,131 in³ (43%)

Storage fee splits: 57% → Storage Machines, 43% → Storage Coffee. Per Pascucci's actual
warehouse mix.

If a SKU lacks dimensions in the catalog, the resolver falls back to **unit count**
for that SKU (preserving Phase 4b.1 behavior); operator sees the row in the
`classification_failures` banner.

---

## 8. Reconciliation invariant

Every preview is verified before persisting:

```python
round(sum(gl_entries.amount), 2) == round(sum(rows.total_charge), 2)
abs(diff) <= Decimal("0.005")
unmapped_categories == []
```

If any check fails, the preview HTTP-errors with diagnostic data. **Misbalanced
previews never persist.**

---

## 9. Anti-silent-failure: unmapped-category mechanism

Three surfaces detect and surface new Charge Categories Flexport (or Shopify) might
invent in the future:

1. **HTTP 412 on Preview**: if a preview encounters an unmapped category, it returns
   `412 Precondition Failed` with `{code: "flexport_unmapped_charge_categories",
   unmapped_categories: [...], operator_action_required: true}`. UI shows red banner;
   Export button is disabled.
2. **`GET /v1/recon/unmapped-categories`**: aggregates per (source_system, category)
   from `recon_items.payload.categories_seen` (Flexport) and
   `recon_items.payload.types_seen` (Shopify). Returns count + total + sample IDs.
3. **Daily email cron**: `unmapped-categories-digest-daily-330am` (Celery beat) calls
   the internal `POST /internal/v1/recon-digest/run-daily`, which iterates all active
   tenants and sends one email per tenant with unresolved unmapped categories.
   Recipient: `FINANLY_OPS_NOTIFY_EMAIL` env (fallback: tenant owner from memberships).

Resolution: operator adds the missing category to `PASCUCCI_FLEXPORT_CATEGORY_MAP`
or `PASCUCCI_FLEXPORT_FLAT_FEES` (or `PASCUCCI_SHOPIFY_TXN_TYPE_MAP` for Shopify),
redeploys core-api. Next sync re-evaluates.

---

## 10. Operational state at handoff

### Tenant: Pascucci USA Inc
- Flexport integration instance: `f79aaf08-e186-50b8-8efe-25eeef422307` (status: active)
- Active shipping plans (verified 2026-05-01):
  - `from Italy` (plan_id 1018194, FC Phillipsburg, NJ) — 9 SKUs received → COFFEE
  - `from China` (plan_id 1024667, FC San Bernardino, CA) — 1 SKU `DSBJNSFJ6KG` → MACHINES

### Production postings live in ERPNext (2026-05-01)

| Bank Tx | Date | $ | Flexport Invoice | PI | PE | BT status |
|---|---|---|---|---|---|---|
| ACC-BTN-2026-00219 | 2026-03-16 | $4.00 | `639186` | `ACC-PINV-2026-00002` ✓ Submitted | `ACC-PAY-2026-00010` ✓ Submitted | Unreconciled |
| ACC-BTN-2026-00290 | 2026-04-06 | $2,141.70 | `642158` | `ACC-PINV-2026-00003` ✓ Submitted | `ACC-PAY-2026-00011` ✓ Submitted | Unreconciled |

Both PIs reconcile to the cent (sum of GL DRs = grand_total = bank withdrawal). Posting
dates correctly aligned with bill_date / bank-transaction date (after fixing
`set_posting_time: 1` in the connector).

### 2 bank lines deliberately preserved for follow-up testing

| Bank Tx | Date | $ | Flexport Invoice | Status |
|---|---|---|---|---|
| ACC-BTN-2026-00319 | 2026-04-10 | $1,179.25 | `645137` | Pending — kept for testing |
| ACC-BTN-2026-00413 | 2026-04-25 | $982.99 | `648112` | Pending — kept for testing |

These were intentionally left unreconciled. They serve as live regression-test inputs
for any future maintenance work that touches the recon pipeline. Do NOT auto-reconcile
them without coordinating with the user — they're our standing reproduction set.

### 11 Charge Categories observed (last 120 days)
All 11 are routed correctly. No unmapped categories at handoff time.

### Yesterday's Shopify JEs — also fixed in this session
- `ACC-JV-2026-00367` ($115.49 Shopify payout 4/23) — was Draft, now Submitted (date 2026-04-23)
- `ACC-JV-2026-00368` ($95.05 Shopify payout 4/27) — was Draft, now Submitted (date 2026-04-27)
- Both linked to their BTs, BT status `Unreconciled`.

---

## 11. How to test (without writing to ERPNext)

```bash
# Inside core-api container — pure preview, no DB writes, no ERPNext writes:
docker exec finanly-core-api-1 python3 -c "
from sqlalchemy import select
from finanly_core_api.db.session import SessionLocal
from finanly_core_api.db.models import ReconItem
from finanly_core_api.routers.recon import _build_flexport_preview_payload
import uuid
tid = uuid.UUID('837eebaa-1ae8-5b79-95c3-26649fb25c42')
with SessionLocal() as s:
    s.connection().execute(__import__('sqlalchemy').text(f\"SET app.tenant_id = '{tid}'\"))
    rows = s.execute(select(ReconItem).where(
        ReconItem.tenant_id == tid,
        ReconItem.source_system == 'flexport',
        ReconItem.object_type == 'flexport_invoice',
    )).scalars().all()
    for r in rows:
        out = _build_flexport_preview_payload(company='Pascucci USA Inc',
                                               recon_item_payload=r.raw_payload or {})
        print(out['summary'])
        for e in out['gl_entries']:
            print(' ', e)
"
```

---

## 12. Quick troubleshooting

### Preview returns HTTP 412 `flexport_unmapped_charge_categories`
A new Charge Category appeared. Add it to `PASCUCCI_FLEXPORT_CATEGORY_MAP` or
`PASCUCCI_FLEXPORT_FLAT_FEES` in `product_classifier.py`. Restart core-api.

### Preview returns HTTP 502 `flexport_billing_reconciliation_drift`
Sum of `gl_entries.amount` doesn't match invoice total. Inspect
`preview.summary.drift_amount` and the rows. Likely a routing-map bug or a row
with a malformed Total Charge field.

### Inbound charge defaults to OTHER family
Either no active inbound covers `charged_date`, or 2-signal classifier disagrees.
Check the `classification_failures` list in the preview response. Add new
plan-name keywords or FC-location keywords in `product_classifier.py` if needed.

### Storage/Fulfillment falls back to unit count
SKU lacks dimensions in `/products` catalog. Update the SKU in Flexport's seller
portal (Products → set H/L/W/weight) and resync.

### Connector sync timeout
Flexport's Reports API can be slow. The connector now has 900s budget per sync
(`_connector_timeout_s_for_sync`) and resumes the primary Billing-Summary report
via `cursor.flexport_report_ref` across attempts. Attribution reports are
best-effort; if they timeout the preview falls back to OTHER family.

### Email digest didn't send
Check `docker logs finanly-jobs-1` for `unmapped_digest_done` event around 3:30
AM. If `failures` list is non-empty, inspect each per-tenant entry. SMTP config
lives in core-api env (`FINANLY_SMTP_*`).

---

## Session 2026-05-04 — bug fixes + behavior clarifications

### Storage attribution endpoint
- Storage Fees - DTC volume-weighting now reads `GET /products/inventory/all` (current per-DSKU `onHand`), NOT the legacy `Inventory-Units_In_Long_Term_Storage` report.
- The LTS report is age-bucketed and represents only stored-long-term inventory (>290 days), giving wrong family composition. The active inventory endpoint is the correct source.
- Connector code: `services/connectors/flexport/.../app.py:_build_inventory_attribution`.
- `GetAllInventory` is current-state only — no `as_of` date param. For historical invoices, the composition reflects "now" not the billing date. Empirically this drift is sub-percent for Pascucci's 10-active-DSKU catalog; cent-level totals match Billing-Summary.

### Fulfillment Fees — per-order priority routing (not volume-weighted)
- New rule per Pascucci tenant: each Flexport order's full `Deliverr Order Cost` routes atomically based on the order's content:
  - Any MACHINE/Starter Kit DSKU in the order → entire cost → `Fulfillment Machines - PUI`
  - All-COFFEE order → entire cost → `Fulfillment Coffee - PUI`
  - All-OTHER or Coffee+Other mixed → entire cost → `Fulfillment Other - PUI`
- Replaces earlier within-order volume-weighted approximation.
- Code: `services/core-api/.../routers/recon.py:2610-2658` Phase 4c.

### Disposal resolver hardening
- Lookback window: **365 days** (was 60). Older orders' refunds now resolve via Flexport instead of falling back to Shopify-status-only.
- Force-refresh on cache miss within TTL: if a recent fulfillment isn't in the cached order index, the resolver retries one fresh pull before returning empty.
- Constants: `_FLEXPORT_ORDER_INDEX_LOOKBACK_DAYS=365`, `_FLEXPORT_ORDER_INDEX_TTL_S=1800` in `services/connectors/flexport/.../app.py`.

### PI line items vs reclass JEs
- A Purchase Invoice's family split (Storage Coffee/Machines, Fulfillment Coffee/Machines) is FROZEN at submission time and reflects the connector's split at the moment the PI was created.
- If system-design split has since changed (e.g., new volume-weighted endpoint, new per-order priority rule), reclass JEs are needed to align the per-account balance to current truth without amending the PI doc.
- Inbound Freight & Handling is NOT family-rule-affected; PI line items for Inbound stay as posted.

### Pascucci specifics observed
- 10 active DSKUs in inventory: 8 Coffee (capsules + sticks + ground) + 2 Machine (variety pack DNHKE7VNRG7 + machine DSBJNSFJ6KG)
- DSKU overrides force `DNHKE7VNRG7`+`DSBJNSFJ6KG` to MACHINE family (Starter Kit components)
- Volume share at audit: machines ~77.0%, coffee ~23.0% (drives Storage Fees split)

### Known fragility (accept-and-monitor)

- **Existing PI line items rely on chain reclass JEs.** A Purchase Invoice's family split is FROZEN at submission time. When system-design changes (e.g., new volume-weighted endpoint, per-order priority rule), the connector posts reclass JEs alongside the PI rather than amending it. The aggregate per-account balance matches system-correct truth; the per-PI-line detail does not. Per-PI-line perfection requires cancel + re-post (touches supplier/AP balances). Trade-off accepted.
- **DB connection pool tuning.** Heavy multi-call audit/probe sessions can exhaust core-api's SQLAlchemy pool. Symptom: `remaining connection slots are reserved for roles with the SUPERUSER attribute`. Fix: restart `finanly-core-api-1`. Pool size in `services/core-api/.../db/session.py` could be hardened if this becomes recurring.
