Sales Module
Overview
The Sales module manages the full lifecycle of retail sale documents. It handles creation, retrieval, update, deletion, PDF/print generation, public link sharing, line-level and cart-level discounts, bundle operations, and electronic tax document (FEL) certification.
Architecture
The module follows Hexagonal Architecture with four layers:
sales/
├── domain/
│ ├── sales-repository.domain.ts # Repository port (interface + injection token)
│ └── sale-item.types.ts # SaleDetailItem, SaleDetailPayload types
├── application/
│ ├── sales.service.ts # Core orchestrator (CRUD, events)
│ ├── sale-discount.service.ts # Line & cart discount operations
│ ├── sale-bundle.service.ts # Bundle evaluate/apply/remove/combo
│ ├── sale-document.service.ts # PDF, preview, public link, template data
│ ├── sale-pricing.service.ts # Price list resolution (feature-flagged)
│ └── events/
│ ├── on-create-sale.event.ts
│ └── on-update-sale.event.ts
├── infrastructure/
│ └── sales.repository.ts # Kysely DB adapter (implements ISalesRepository)
└── interfaces/
├── sales.controller.ts # REST controller
├── dtos/
│ ├── create-sale.dto.ts
│ ├── update-sale.dto.ts
│ ├── apply-sale-discount.dto.ts
│ ├── apply-sale-bundle.dto.ts
│ └── generate-sale-link.dto.ts
└── query/
└── paginate-sales.query.ts
Dependency Flow
Controller → SalesService → SaleDiscountService
→ SaleBundleService
→ SaleDocumentService
→ ISalesRepository (port)
↓
SalesRepository (adapter)
Domain Concepts
| Concept | Description |
|---|---|
| Sale | A retail transaction document with items, payments, customer info, and tax data |
| SaleDetail | JSON field containing the items array (line items with product, quantity, price, tax) |
| PaymentDetail | JSON field containing payment method entries |
| CartDiscountDetail | JSON field for cart-level discount snapshots |
| CostDetail | JSON snapshot of inventory costs at time of sale creation |
| Session linking | Sales are automatically linked to the cashier's open cash register session |
Sale Status Flow
draft → submitted → reviewed → completed
→ cancelled
→ voided
Sale Types
sale— Standard salereturn— Return/credit noteexchange— Exchange transaction
API Endpoints
CRUD
| Method | Path | Description |
|---|---|---|
| POST | /sales | Create a sale |
| GET | /sales | List sales (paginated, filtered) |
| GET | /sales/:id | Get sale by ID |
| PATCH | /sales/:id | Update a sale |
| DELETE | /sales/:id | Delete a sale |
Document Generation
| Method | Path | Description |
|---|---|---|
| GET | /sales/:id/pdf | Download PDF (binary) |
| GET | /sales/:id/pdf/content | Get PDF as base64 |
| GET | /sales/:id/print | HTML print preview |
| POST | /sales/:id/public-link | Create time-limited signed URL |
| GET | /sales/:id/template-data | Get template rendering data |
Discounts
| Method | Path | Description |
|---|---|---|
| POST | /sales/:id/items/discount | Apply line-level discount |
| DELETE | /sales/:id/items/discount | Remove line-level discount |
| POST | /sales/:id/discount | Apply cart-level discount |
| DELETE | /sales/:id/discount | Remove cart-level discount |
Bundles
| Method | Path | Description |
|---|---|---|
| GET | /sales/:id/bundles/evaluate | Evaluate eligible bundles |
| POST | /sales/:id/bundles/apply | Apply bundle to existing items |
| DELETE | /sales/:id/bundles/remove | Remove bundle application |
| POST | /sales/:id/bundles/add-combo | Add combo bundle (atomic) |
Key Behaviors
Sale Creation
- Resolves default currency from business if not provided
- Generates a document number (sequence)
- Validates product variant references
- Validates header total matches line item totals
- Groups items by location/product and fetches inventory costs
- Creates cost detail snapshot
- Auto-links to open cash register session (or validates explicit sessionId)
- Emits
sale.createevent
Discount Application
- Line-level: Applied to a specific item by index. Recomputes item amount, taxes, and document totals.
- Cart-level: Applied to the whole sale. Stored in
cartDiscountDetailcolumn. - Stacking order: price_list → price_rule → discount_rule → manual → override
- Both operations create audit records via
DiscountsService.
Bundle Operations
- Evaluate: Pre-checks eligible bundles for untagged sale items
- Apply: Tags items with
bundleApplicationId, applies savings to first item, creates audit record - Remove: Restores original prices from DB, clears bundle fields, deletes audit record
- Combo builder: Atomically adds new items from component selections and applies the bundle
- Cascade cleanup: When a sale update removes items belonging to a bundle, the bundle is automatically removed and prices restored
FEL Integration
The service listens for OnDocumentCertifiedEvent and updates the sale record with electronic tax document data (authorization number, serial, dates).
Price Lists (Feature-Flagged)
Set ENABLE_PRICE_LISTS=true to enable price list resolution during sale item pricing. When disabled, base product/service prices are used directly.
Query Parameters (GET /sales)
| Parameter | Type | Description |
|---|---|---|
businessId | UUID | Filter by business |
status | string | Filter by sale status |
documentNumber | string | Filter by document number |
serviceBookingId | UUID | Filter by linked service booking |
createdAtFrom / createdAtTo | ISO date | Filter by creation date range |
saleDateFrom / saleDateTo | ISO date | Filter by sale date range |
search | string | Search by taxId, taxName, taxAddress, locationName |
size / page | number | Pagination (default: 10 per page) |
orderBy / order | string | Sort by taxId, taxName, taxAddress, or locationName |
Bruno API Collection
All endpoints are available as Bruno requests in:
api-client/flowpos/collections/sales/
Design Decisions
-
Service decomposition: The main
SalesServicedelegates toSaleDiscountService,SaleBundleService, andSaleDocumentServiceto maintain SRP while keeping a single entry point for the controller. -
Repository injection token: Uses
SALES_REPOSITORYSymbol token for proper port/adapter pattern, allowing the infrastructure implementation to be swapped. -
Snapshot-based pricing: Discounts and bundles store immutable snapshots (
discountDetail,bundleDetail) so the sale record is self-contained and auditable without needing to look up rules at read time. -
Cascade bundle cleanup: When items are removed from a sale that belong to a bundle, the bundle is automatically invalidated and prices restored. This prevents orphaned bundle references.