Skip to main content

Quotes Module

Overview​

The Quotes module manages price quotations for retail businesses. It supports creating quotes for customers (both staff-initiated and kiosk-originated), managing their lifecycle through various statuses, applying discounts and bundles, generating PDFs, and converting accepted quotes into sales.

Architecture​

quotes/
├── quotes.module.ts # NestJS module (DI wiring)
├── domain/
│ └── quotes-repository.domain.ts # Repository port, types, injection token
├── application/
│ └── quotes.service.ts # Business logic & use cases
├── infrastructure/
│ └── quotes.repository.ts # Kysely DB adapter
└── interfaces/
├── quotes.controller.ts # HTTP routes
├── dtos/
│ ├── create-quote-staff.dto.ts # Create quote request
│ ├── update-quote.dto.ts # Update quote request
│ ├── apply-quote-discount.dto.ts # Discount apply/remove DTOs
│ └── quote-bundle.dto.ts # Bundle apply/remove/combo DTOs
└── query/
└── paginate-quotes.query.ts # Pagination & filter query

Dependency flow: interfaces → application → domain ← infrastructure

The repository is bound via a QUOTES_REPOSITORY Symbol token in the module, and the service depends on the IQuotesRepository interface (not the concrete class).

Domain Concepts​

Quote​

A price quotation document with:

FieldDescription
businessIdOwning business (multi-tenancy scope)
locationIdOriginating location
originSTAFF (authenticated) or KIOSK (public)
statusdraft, sent, accepted, rejected, expired, requested, converted
documentNumberAuto-generated sequential number (staff quotes only)
quoteDetailJSONB containing { items: QuoteDetailItem[] }
costDetailJSONB with inventory cost snapshot
cartDiscountDetailJSONB with cart-level discount info
totalAmount / totalBaseAmountComputed totals in transaction and base currency
expiresAtOptional expiration date
Customer fieldscustomerName, customerId, customerEmail, customerPhone
Tax fieldstaxId, taxName, taxAddress, taxpayerType

QuoteDetailItem​

Each line item in quoteDetail.items:

interface QuoteDetailItem {
productId: string;
productName: string;
quantity: number;
unitPrice: number;
amount: number;
baseAmount: number;
taxes: TaxBreakdown[];
discountDetail?: LineDiscountDetail;
bundleApplicationId?: string;
bundleDetail?: LineBundleDetail;
bundle?: number; // bundle discount amount
baseBundle?: number; // bundle discount in base currency
}

Quote Status Lifecycle​

DRAFT → SENT → ACCEPTED → CONVERTED (to sale)
↘ REJECTED
↘ EXPIRED
REQUESTED (kiosk) → DRAFT (staff picks up)
  • CONVERTED is set automatically by convertQuoteToSale() — clients cannot set this status directly.

Use Cases​

Use CaseMethodEndpoint
Create staff quotePOST /quotesFull tax enrichment, document numbering, cost snapshot
Create kiosk quoteInternalMinimal data, status REQUESTED, no doc number
List quotesGET /quotesPaginated with search, filter, sort
Get quoteGET /quotes/:idEnriches bundle fields on read
Update quotePATCH /quotes/:idRe-enriches items, recalculates totals, cascade-cleans bundles
Delete quoteDELETE /quotes/:idBlocks deletion of converted quotes
Apply line discountPOST /quotes/:id/items/discountStacked discount on item by index
Remove line discountDELETE /quotes/:id/items/discountRemove specific or all line discounts
Apply cart discountPOST /quotes/:id/discountCart-level discount
Remove cart discountDELETE /quotes/:id/discountRemove specific or all cart discounts
Generate PDFGET /quotes/:id/pdfDownload as attachment
PDF as base64GET /quotes/:id/pdf/contentFor embedding/email
Print previewGET /quotes/:id/printHTML for browser print
Public linkPOST /quotes/:id/public-linkSigned URL for sharing
Template dataGET /quotes/:id/template-dataMerge fields for email templates
Convert to salePOST /quotes/:id/convert-to-saleCreates draft sale from accepted quote
Evaluate bundlesGET /quotes/:id/bundles/evaluateCheck eligible bundles
Apply bundlePOST /quotes/:id/bundles/applyApply bundle discount to items
Remove bundleDELETE /quotes/:id/bundles/removeRemove bundle, restore prices
Add comboPOST /quotes/:id/bundles/add-comboAdd combo items with bundle

Key Design Decisions​

Tax Enrichment on Create/Update​

When items are provided, the service fetches product tax configurations from the database and recomputes amounts using LineItemTaxService. This ensures tax-inclusive pricing is always correct regardless of what the client sends.

Cost Snapshot​

Every create/update with items computes a cost detail snapshot using current inventory WAC (weighted average cost). This is stored in costDetail for margin analysis.

Bundle Cascade Cleanup​

When items are removed during an update, the service detects if any bundle's member items were removed and automatically:

  1. Calls bundlesService.removeBundle() to clean up the application record
  2. Restores original prices on remaining items from that bundle

Quote-to-Sale Conversion​

convertQuoteToSale maps quote data to a sale structure:

  • Stores lot intent on quote lines (inventoryDetails + variantId) without reserving or decrementing stock
  • Re-checks those lots at convert time against tenant, location, product, variant, and AVAILABLE status
  • If a quoted lot is missing, sold, at another location, or short on qty, convert fills the remainder from the next AVAILABLE lots at the line location (FEFO). Convert still fails if nothing can cover the line
  • Serial-tracked products require selected lot ids; batch lines may omit them and FEFO-pick at sale submit
  • Creates a DRAFT sale via SalesService.createSale() with the resolved lots copied from inventory_detail
  • Carries over items, discounts, customer/tax data, and currency
  • Re-enriches each line's tax breakdown from current product tax config so FEL certification has items[].taxes
  • Resolves currency from stored data or falls back to business default
  • Marks the quote as CONVERTED (preventing further edits or deletion)
  • The PWA then opens /forms/sale/:id without hydrating from the convert POST body. SalePage GETs the sale (same path as Search → select), marks it as an existing document (isUpdate), and lands on Line items so Save and the quoted cart are available immediately.
  • Sale submit is what consumes stock (existing FEFO / explicit-lot path). Reservation is not in this pass.

Multi-Currency Support​

Quotes store exchangeRate, currencyId, currencyCode, and a currency JSONB object. All amounts are tracked in both transaction currency (totalAmount) and base currency (totalBaseAmount).

Dependencies​

ModulePurpose
BundlesModuleBundle evaluation, application, removal
DiscountsModuleLine and cart discount building
DocumentCalculationsModuleTax computation, total validation
InventoriesModuleCost data for cost snapshot
InventoryDetailsModuleLot re-check on quote-to-sale convert
PdfModulePDF generation, print preview, template data
SalesModuleQuote-to-sale conversion