Apply Extraction (Create Bills)
POST/api/landed-cost-invoices/ocr/:landed_cost_extraction_id/apply
Accept the reviewed/edited extraction and create one or more Bill records — one per target entity (inbound shipment, purchase order, or warehouse transfer) — each with its own apportionment of the cost lines. Marks the extraction as confirmed and persists Bill rows linked via the polymorphic bills.link_type / link_id columns. Bridges supplier → vendor via VendorRepository::firstOrCreateByName($supplier->name) since Bills require vendor_id.
The extraction must be in pending_review or duplicate_detected status (a duplicate-detected extraction is allowed because the user may confirm it is not actually a duplicate and proceed). Returns 422 otherwise.
When multiple targets are supplied, the invoice number is suffixed per target ({invoice_number}-{type3}-{target_id}) to avoid bills.invoice_number unique constraint violations.
Request body fields:
| Field | Type | Required | Notes |
|---|---|---|---|
| supplier_id | integer | yes | Must exist in suppliers. |
| invoice_number | string | yes | Max 255 chars. |
| invoice_date | date | yes | |
| currency_code | string | yes | ISO 4217 (size:3). |
| currency_rate | numeric | yes | Min 0. Conversion rate to base currency. |
| total_amount | numeric | yes | Total invoice amount. |
| cost_lines | array | yes | At least 1 line. |
| cost_lines[].description | string | yes | Max 500 chars. |
| cost_lines[].quantity | numeric | yes | |
| cost_lines[].unit_price | numeric | yes | |
| cost_lines[].line_total | numeric | yes | |
| cost_lines[].cost_category_id | integer | no (nullable) | Must exist in cost_categories. Null = unclassified line. |
| cost_lines[].ignored | boolean | no | Skip persisting this line (audit-only). |
| targets | array | yes | At least 1 target. Allocation shares MUST sum to 1.0 ±0.001. |
| targets[].target_type | string | yes | One of: inbound_shipment, purchase_order, warehouse_transfer. |
| targets[].target_id | integer | yes | ID of target entity. |
| targets[].proration_strategy | string | yes | FinancialLineProrationStrategyEnum: revenue_based, cost_based, weight_based, volume_based, quantity_based, specific_line, manual. |
| targets[].allocation_share | numeric | yes | 0–1. Share of the total this target receives. |
| remember_mappings | boolean | no | When true, upserts (supplier_id, normalized_description) → cost_category_id mappings into supplier_landed_cost_mappings so future OCR imports auto-tag matching descriptions as cost lines (Tier 0 match). |
Response 201:
{
"data": {
"id": 18,
"status": "confirmed",
"extraction_bills": [
{"id": 33, "bill_id": 491, "target_type": "inbound_shipment", "target_id": 142, "allocation_share": 0.6},
{"id": 34, "bill_id": 492, "target_type": "purchase_order", "target_id": 88, "allocation_share": 0.4}
],
"confirmed_at": "2026-05-01T08:30:00.000000Z",
"confirmed_by_user_id": 7
},
"message": "Landed cost invoice applied."
}
Response 422 — wrong status:
{ "message": "Only extractions pending review can be applied." }
Response 422 — allocation shares do not sum to 1.0:
{ "message": "Target allocation shares must sum to 1.0." }
Request
Responses
- 200
Successful response