Discovery Module
Public-facing kiosk/catalog API for product browsing and quote requests. All endpoints are unauthenticated (@IsPublic()).
Architecture
discovery/
├── domain/
│ └── discovery-repository.domain.ts # Port interface + domain types
├── application/
│ └── discovery.service.ts # Use cases (orchestration)
├── infrastructure/
│ └── discovery.repository.ts # Kysely adapter (implements port)
└── interfaces/
├── discovery.controller.ts # HTTP routes
└── dtos/
├── discovery-products-query.dto.ts
├── create-quote.dto.ts
└── required-location-id.query.ts
Dependency flow: Controller → Service → Domain Interface ← Repository (adapter)
Domain Concepts
| Concept | Description |
|---|---|
| Discovery Config | Per-location settings: enabled flag, low-stock threshold, polling interval |
| Catalog Product | Variant-level item with price, availability status, and label |
| Availability Status | in_stock (qty > threshold), low_stock (0 < qty ≤ threshold), out_of_stock |
| Availability Matrix | Per-location stock breakdown for a product (current location listed first) |
| Kiosk Quote | Customer-initiated quote request from a public kiosk terminal |
API Endpoints
All endpoints are public (no auth required).
| Method | Path | Description |
|---|---|---|
GET | /discovery/config?locationId= | Get discovery config for a location |
GET | /discovery/products?locationId=&... | Paginated catalog with filters |
GET | /discovery/products/:productId?locationId= | Product detail + availability matrix |
POST | /discovery/quotes | Create kiosk quote (rate-limited: 5/hr/IP) |
Query Parameters — Catalog Listing
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
locationId | UUID | Yes | — | Scopes availability to this location |
search | string | No | — | Search name, SKU, description |
categoryId | UUID | No | — | Filter by category |
collectionId | UUID | No | — | Filter by merchandising collection |
inStockOnly | boolean | No | — | Only items with stock > 0 |
onSaleOnly | boolean | No | — | Only items marked on sale |
page | number | No | 1 | Page number (1-based) |
limit | number | No | 24 | Items per page (max 100) |
Quote Request Body
{
"locationId": "uuid",
"customerName": "John Doe",
"customerEmail": "john@example.com",
"customerPhone": "+50212345678",
"notes": "Optional notes",
"items": [
{
"productId": "uuid",
"productName": "Product Name",
"quantity": 2,
"locationId": "uuid (optional)",
"unitPrice": 25.99
}
]
}
At least one of customerEmail or customerPhone is required (enforced by QuotesService).
Design Decisions
-
Variant-level catalog: Each product variant appears as a separate catalog item (one row per size/color), not grouped by parent product.
-
Availability thresholds: Configured per-location via
discoveryConfigtable. Defaults: low-stock threshold = 5, polling interval = 60s. -
Quote delegation: Quote creation delegates to
QuotesService.createQuoteFromKiosk()— the discovery module does not own quote persistence. -
Rate limiting: The quote endpoint uses NestJS Throttler (5 requests/hour/IP) to prevent abuse from public kiosks.
-
Current location first: In the availability matrix, the requesting location appears first, followed by other locations sorted alphabetically.