Skip to main content

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

ConceptDescription
SaleA retail transaction document with items, payments, customer info, and tax data
SaleDetailJSON field containing the items array (line items with product, quantity, price, tax)
PaymentDetailJSON field containing payment method entries
CartDiscountDetailJSON field for cart-level discount snapshots
CostDetailJSON snapshot of inventory costs at time of sale creation
Session linkingSales are automatically linked to the cashier's open cash register session

Sale Status Flow

draft → submitted → reviewed → completed
→ cancelled
→ voided

Sale Types

  • sale — Standard sale
  • return — Return/credit note
  • exchange — Exchange transaction

API Endpoints

CRUD

MethodPathDescription
POST/salesCreate a sale
GET/salesList sales (paginated, filtered)
GET/sales/:idGet sale by ID
PATCH/sales/:idUpdate a sale
DELETE/sales/:idDelete a sale

Document Generation

MethodPathDescription
GET/sales/:id/pdfDownload PDF (binary)
GET/sales/:id/pdf/contentGet PDF as base64
GET/sales/:id/printHTML print preview
POST/sales/:id/public-linkCreate time-limited signed URL
GET/sales/:id/template-dataGet template rendering data

Discounts

MethodPathDescription
POST/sales/:id/items/discountApply line-level discount
DELETE/sales/:id/items/discountRemove line-level discount
POST/sales/:id/discountApply cart-level discount
DELETE/sales/:id/discountRemove cart-level discount

Bundles

MethodPathDescription
GET/sales/:id/bundles/evaluateEvaluate eligible bundles
POST/sales/:id/bundles/applyApply bundle to existing items
DELETE/sales/:id/bundles/removeRemove bundle application
POST/sales/:id/bundles/add-comboAdd combo bundle (atomic)

Key Behaviors

Sale Creation

  1. Resolves default currency from business if not provided
  2. Generates a document number (sequence)
  3. Validates product variant references
  4. Validates header total matches line item totals
  5. Groups items by location/product and fetches inventory costs
  6. Creates cost detail snapshot
  7. Auto-links to open cash register session (or validates explicit sessionId)
  8. Emits sale.create event

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 cartDiscountDetail column.
  • 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)

ParameterTypeDescription
businessIdUUIDFilter by business
statusstringFilter by sale status
documentNumberstringFilter by document number
serviceBookingIdUUIDFilter by linked service booking
createdAtFrom / createdAtToISO dateFilter by creation date range
saleDateFrom / saleDateToISO dateFilter by sale date range
searchstringSearch by taxId, taxName, taxAddress, locationName
size / pagenumberPagination (default: 10 per page)
orderBy / orderstringSort by taxId, taxName, taxAddress, or locationName

Bruno API Collection

All endpoints are available as Bruno requests in:

api-client/flowpos/collections/sales/

Design Decisions

  1. Service decomposition: The main SalesService delegates to SaleDiscountService, SaleBundleService, and SaleDocumentService to maintain SRP while keeping a single entry point for the controller.

  2. Repository injection token: Uses SALES_REPOSITORY Symbol token for proper port/adapter pattern, allowing the infrastructure implementation to be swapped.

  3. 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.

  4. 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.