# 14 · BACKEND — Sales API Reference

> **Authoritative for:** sales API contracts for invoices, slips, bundle sales, 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. Bundle Sales

Bundle sales are a separate sales document family for fixed bundle compositions.

### `GET /api/v1/bundle-sales`
Query:
- `page`
- `pageSize`
- `q`
- `customerId`
- `warehouseId`

Returns a paged list of `BundleSaleSummaryDto`.

### `GET /api/v1/bundle-sales/{bundleSaleId}`
Returns `BundleSaleDto`.

### `GET /api/v1/bundle-sales/{bundleSaleId}/posting-check`
Validates component stock before posting.

### `POST /api/v1/bundle-sales`
Creates a draft bundle sale from a fixed template.

Request:
```json
{
  "customerId": 2,
  "warehouseId": 1,
  "cashierUserId": 1,
  "bundleSaleTemplateId": 1,
  "bundleName": "Summer Promo Pack",
  "bundleCode": "BND-001",
  "bundlePrice": 2500,
  "allowPriceOverride": true
}
```

### `PUT /api/v1/bundle-sales/{bundleSaleId}`
Updates a draft bundle sale. Requires `If-Match`.

### `POST /api/v1/bundle-sales/{bundleSaleId}/post`
Posts the bundle and consumes stock from the included component lines.

### `POST /api/v1/bundle-sales/{bundleSaleId}/cancel`
Cancels a draft bundle sale.

Business rule:
- bundle composition is fixed by template lines
- posting consumes component stock, not a synthetic bundle stock item
- print pages should show both the bundle summary and the component breakdown

---

## 5. 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

---

## 6. 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

---

## 7. Notes

- The sales report service reads both invoices and slips where relevant.
- Free issue reporting is derived from the same sales document lines.
- Bundle sales are treated as a separate fixed-composition document family.
- There is no separate free-issue table in the current schema.
