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:

  • Creates a DRAFT sale via SalesService.createSale()
  • Carries over items, discounts, customer/tax data, and currency
  • Resolves currency from stored data or falls back to business default
  • Marks the quote as CONVERTED (preventing further edits or deletion)

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
PdfModulePDF generation, print preview, template data
SalesModuleQuote-to-sale conversion