# 14 · BACKEND — Sales API Reference > **Authoritative for:** sales API contracts for invoices, slips, free issues, and sales reports. > **Navigation:** start from `00-CORE.md`. Sales business rules live in `docs/SALES_MODULE_PLAN.md` and the sales-related backend progress is tracked in `Backend/PROGRESS.md`. > **Scope:** this document covers the sales endpoints currently implemented in ERPCore. Free issue is modeled as a sales-slip alias, not a separate table. --- ## 1. Conventions - Base URL: `https://{host}/api/v1` - Authentication: same as the rest of the v1 API - Concurrency: `ETag` / `If-Match` on mutable resources - Validation: `ProblemDetails` / `ValidationProblemDetails` --- ## 2. Sales Invoices ### `GET /api/v1/sales-invoices` Query: - `page` - `pageSize` - `q` - `status` - `customerId` - `warehouseId` Returns a paged list of `SalesInvoiceSummaryDto`. The current frontend register uses this endpoint directly for the invoice hub view. ### `GET /api/v1/sales-invoices/{salesInvoiceId}` Returns `SalesInvoiceDto`. The detail page shows the document template and only exposes edit actions when the invoice is `Draft`. ### `POST /api/v1/sales-invoices` Creates a draft sales invoice. Request: ```json { "customerId": 2, "warehouseId": 1, "invoiceType": "B2C", "lines": [ { "itemId": 1, "uomId": 1, "warehouseId": 1, "qty": 1, "freeQty": 0, "unitPrice": 100, "allowManualPriceOverride": false, "discountMode": "Percentage", "discountPct": 0, "discountAmount": 0, "discountValue": 0, "taxPct": 0, "isFreeIssue": false, "parentLineId": null } ] } ``` ### `PUT /api/v1/sales-invoices/{salesInvoiceId}` Updates a draft invoice. Requires `If-Match`. ### `POST /api/v1/sales-invoices/{salesInvoiceId}/post` Posts stock out and marks the invoice as `Posted`. Before posting, the UI calls `GET /api/v1/sales-invoices/{salesInvoiceId}/posting-check` to show shortages. ### `POST /api/v1/sales-invoices/{salesInvoiceId}/cancel` Cancels a draft invoice. --- ## 3. Sales Slips ### `GET /api/v1/sales-slips` Query: - `page` - `pageSize` - `q` - `status` - `customerId` - `warehouseId` Returns a paged list of `SalesSlipSummaryDto`. ### `GET /api/v1/sales-slips/{salesSlipId}` Returns `SalesSlipDto`. The detail page follows the same document-style layout and draft-only edit behavior as invoices. ### `POST /api/v1/sales-slips` Creates a draft sales slip. Request: ```json { "customerId": 2, "warehouseId": 1, "cashierUserId": 1, "lines": [ { "itemId": 1, "uomId": 1, "warehouseId": 1, "qty": 1, "freeQty": 0, "unitPrice": 50, "allowManualPriceOverride": false, "discountMode": "Percentage", "discountPct": 0, "discountAmount": 0, "discountValue": 0, "taxPct": 0, "isFreeIssue": false, "parentLineId": null } ] } ``` ### `PUT /api/v1/sales-slips/{salesSlipId}` Updates a draft slip. Requires `If-Match`. ### `POST /api/v1/sales-slips/{salesSlipId}/post` Posts stock out and marks the slip as `Posted`. Before posting, the UI calls `GET /api/v1/sales-slips/{salesSlipId}/posting-check`. ### `POST /api/v1/sales-slips/{salesSlipId}/cancel` Cancels a draft slip. --- ## 4. Free Issues Free issue is a business alias over sales slips. There is no separate free-issue table in the current schema. ### `GET /api/v1/free-issues` Same contract as `GET /api/v1/sales-slips`. ### `GET /api/v1/free-issues/{freeIssueId}` Same contract as `GET /api/v1/sales-slips/{salesSlipId}`. ### `POST /api/v1/free-issues` Same contract as `POST /api/v1/sales-slips`. ### `PUT /api/v1/free-issues/{freeIssueId}` Same contract as `PUT /api/v1/sales-slips/{salesSlipId}`. ### `POST /api/v1/free-issues/{freeIssueId}/post` Same contract as `POST /api/v1/sales-slips/{salesSlipId}/post`. ### `POST /api/v1/free-issues/{freeIssueId}/cancel` Same contract as `POST /api/v1/sales-slips/{salesSlipId}/cancel`. Business rule: - free issue is represented by `IsFreeIssue=true` and/or `FreeQty>0` - it still posts stock out through the normal slip posting flow - the frontend free-issue screen is the main working page for create/list/edit/cancel - cancelled free-issue records are hidden from the working list view --- ## 5. Sales Reports ### `GET /api/v1/reports/sales` Returns the report catalog. Response: ```json [ { "id": "daily-summary", "name": "Daily Summary", "description": "Aggregated sales by day across posted invoices and slips.", "supportedFilters": ["from", "to"] } ] ``` ### `GET /api/v1/reports/sales/{reportId}` Returns a single report definition by id. ### `POST /api/v1/reports/sales/query` Returns the actual report data. Request: ```json { "reportType": "item-summary", "from": "2026-07-01", "to": "2026-07-28", "itemId": 1, "customerId": null, "warehouseId": 1 } ``` Supported report types: - `daily-summary` - `item-summary` - `customer-summary` - `warehouse-summary` - `discount-summary` - `free-issue-summary` Validation rule: - invalid filters for a given report type are rejected - unsupported report types are rejected - the frontend report page uses the same endpoint for all report types - `daily-summary` auto-loads with a default date range in the UI - the report results table is rendered from the backend response rows, not hardcoded data --- ## 6. Notes - The sales report service reads both invoices and slips where relevant. - Free issue reporting is derived from the same sales document lines. - There is no separate free-issue table in the current schema.