POS Module API Reference
Complete API reference for all POS admin endpoints — customer lookup, order creation, payment recording, fulfillment, analytics, and export.
Audience: Admin/POS terminal frontend developers. Scope: All
POST|GET /api/admin/pos/*endpoints.
POS Module - API Reference
1. Quick Metadata
- Module:
POS - Auth:
JwtAuthGuard + RoleGuardon all endpoints - Base URL:
/api/admin/pos - Swagger tag:
POS - Response envelope:
ResponseDto<T>({ message, data, meta? }) - HTTP status:
200 OKfor all successful endpoints (including POST)
2. Route Summary
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /api/admin/pos/customers/lookup | Pos_READ | Look up customer by email |
POST | /api/admin/pos/customers | Pos_CREATE | Create walk-in customer |
POST | /api/admin/pos/orders | Pos_CREATE | Create POS order |
POST | /api/admin/pos/orders/:orderId/payment | Pos_RECORD_PAYMENT | Record POS payment |
POST | /api/admin/pos/orders/:orderId/payment-failure | Pos_UPDATE | Record payment failure |
POST | /api/admin/pos/orders/:orderId/pickup-confirm | Pos_UPDATE | Confirm in-store pickup |
POST | /api/admin/pos/emi-inquiry | Pos_CREATE | Submit EMI inquiry |
GET | /api/admin/pos/orders/export | Pos_READ | Export POS orders (flat, date-ranged) |
GET | /api/admin/pos/orders | Pos_READ | List POS orders (paginated) |
GET | /api/admin/pos/analytics | Pos_READ | POS revenue analytics |
Important:
GET /api/admin/pos/orders/exportis declared beforeGET /api/admin/pos/ordersin the controller to prevent the:orderIdroute parameter from capturing the literal"export"path segment.
3. Endpoint Details
3.1 GET /api/admin/pos/customers/lookup
| Aspect | Details |
|---|---|
| Permission | Pos_READ |
| Query params | email: string (required) |
Response data:
{
exists: true;
customerId: string; // UUID
name: string;
email: string;
phone: string | null;
image: string | null;
} | null- Returns
nullwhen no customer is found for that email. There is no404—nullis the signal for "not found, prompt to create". - Email lookup is case-insensitive (lowercased + trimmed internally).
3.2 POST /api/admin/pos/customers
| Aspect | Details |
|---|---|
| Permission | Pos_CREATE |
| Rate limit | 10 req / 3600s per admin user (keyStrategy: "user") |
Request body:
{
name: string; // maxLength: 100, required
email: string; // valid email, required
phone?: string; // maxLength: 20, optional
}Response data: same shape as customer lookup (PosCustomerLookupResponseDto).
Errors:
409 POS_DUPLICATE_EMAIL— customer with that email already exists (use lookup instead)409 POS_DUPLICATE_PHONE— customer with that phone already exists
Side effects: Sends a password-reset email to the new customer (so they can set their own password).
Account created with emailVerified: false, phoneVerified: false, random temp password (64-hex chars).
3.3 POST /api/admin/pos/orders
| Aspect | Details |
|---|---|
| Permission | Pos_CREATE |
| Header | idempotency-key (string, required — UUID recommended) |
Request body:
{
customerId: string; // UUID, required — must be existing customer
items: Array<{
productId: number; // integer >= 1
quantity: number; // integer >= 1
}>;
couponDiscount?: number; // integer >= 0, applied discount in NPR
shippingFee?: number; // integer >= 0, NPR
receiverName?: string; // optional
receiverContactNumber?: string; // optional
shippingDistrict?: string;
shippingAddressLine1?: string;
shippingAddressLine2?: string;
shippingCity?: string;
shippingState?: string;
shippingPostalCode?: string;
shippingLandmark?: string;
shippingLatitude?: number;
shippingLongitude?: number;
}Response data: AsyncRequestResponseDto
{
requestId: string; // e.g. "pos_create_order_<uuid7hex>"
status: "completed"; // always completed synchronously
id: number; // orderId
createdAt: Date;
}Idempotency: Replaying the same idempotency-key returns the cached AsyncRequestResponseDto with the same requestId and id.
Price calculation:
totalMrp = sum(unitMrp * quantity)— taken fromproduct.mrpin DBtotalSp = sum(unitSp * quantity)— taken fromproduct.spin DBdiscount = max(0, totalMrp - totalSp)finalPayable = max(0, totalSp - couponDiscount + shippingFee)
Order type resolution:
- All items physical →
orderType = "physical" - Any item digital + others →
orderType = "mixed" - All items digital →
orderType = "digital"
Errors:
404 POS_CUSTOMER_NOT_FOUND—customerIdnot found404 ORDER_PRODUCT_NOT_FOUND— anyproductIdnot found400 ORDER_PRODUCT_NOT_AVAILABLE— productstatus != "published"orisSellable = false
Created order has: orderSource = "pos_sale", paymentMethod = "pos", orderStatus = "payment_pending".
3.4 POST /api/admin/pos/orders/:orderId/payment
| Aspect | Details |
|---|---|
| Permission | Pos_RECORD_PAYMENT |
| Header | idempotency-key (string, required — must be different from the order creation key) |
| Path param | orderId: number |
Request body:
{
gateway: "pos_cash" | "pos_fonepay" | "pos_esewa" | "pos_card";
amountNpr: number; // integer >= 1
transactionReference?: string; // maxLength: 255. Auto-generated (uuidv7) if omitted.
}Response data:
{
paymentId: number;
orderId: number;
}Idempotency: Replaying the same key returns the cached { paymentId, orderId } without re-processing.
Payment flow (sequential):
- Idempotency check on
async_requests. - Fetch order — validate
orderSource = "pos_sale",paymentMethod = "pos",paymentStatus = "pending". - Validate gateway is in
POS_GATEWAYS. transactionRef = dto.transactionReference ?? uuidv7().PaymentService.createPaymentRecord(orderId, "order", customerId, amountNpr, gateway)→ returns{ id: paymentId }.PaymentService.markPaymentCompleted(paymentId, transactionRef, {})— catches Postgres23505→POS_DUPLICATE_PAYMENT_REFERENCE.- Emit
PAYMENT_EVENTS.COMPLETEDviaEventEmitter2→OrderPaymentListenerhandles downstream (order →paid, fulfillment →received). - Insert
async_requestsrecord withonConflictDoNothing.
Errors:
404 ORDER_NOT_FOUND—orderIdnot found400 POS_ORDER_NOT_POS_SALE— order is not a POS order400 POS_ORDER_NOT_PAYMENT_PENDING— orderpaymentStatusis not"pending"400 POS_INVALID_GATEWAY— gateway not inPOS_GATEWAYS400 POS_DUPLICATE_PAYMENT_REFERENCE—transactionReferenceviolates unique constraint (Postgres23505)
3.5 POST /api/admin/pos/orders/:orderId/payment-failure
| Aspect | Details |
|---|---|
| Permission | Pos_UPDATE |
| Path param | orderId: number |
Request body:
{ reason: string; } // minLength: 1, maxLength: 1000Response data: null
Effect: Adds admin note "[POS Payment Failure] <reason>" via OrderAdminService.addNote(). Order status is unchanged. Staff can retry payment afterwards.
Errors:
404 ORDER_NOT_FOUND400 POS_ORDER_NOT_POS_SALE
3.6 POST /api/admin/pos/orders/:orderId/pickup-confirm
| Aspect | Details |
|---|---|
| Permission | Pos_UPDATE |
| Path param | orderId: number |
No request body.
Response data: AsyncRequestResponseDto
Idempotent: already "picked_up" → returns immediately without error.
Fulfillment fast-track logic:
fulfillmentStatus = "received"→ advances through"ready"then"picked_up"(2 steps)fulfillmentStatus = "ready"→ advances to"picked_up"(1 step)fulfillmentStatus = "picked_up"→ no-op, returns{ requestId: "pos_pickup_already_<orderId>", status: "completed", id: orderId }- Other statuses (e.g.,
"delivered","in_transit") → error
Errors:
404 ORDER_NOT_FOUND400 POS_ORDER_NOT_POS_SALE400 POS_PICKUP_NOT_IN_STORE—fulfillmentStatusis not"received"or"ready"
3.7 POST /api/admin/pos/emi-inquiry
| Aspect | Details |
|---|---|
| Permission | Pos_CREATE |
Request body: EmiInquirySubmitDto (same as customer EMI inquiry — refer to EMI module docs).
Response data: EmiInquirySubmitResponseDto
Delegates to EmiCustomerService.submitInquiry() — no POS-specific logic.
3.8 GET /api/admin/pos/orders/export
| Aspect | Details |
|---|---|
| Permission | Pos_READ |
Query params:
{ startDate: string; endDate: string; } // ISO date strings, both requiredResponse data: PosOrderListItemDto[] (flat array, no pagination)
- Max date range: 90 days. Error
400 ORDER_DATE_RANGE_TOO_LARGEif exceeded. - Results ordered by
createdAt DESC. - Each item shape: same as
PosOrderListItemDto(see 3.9).
3.9 GET /api/admin/pos/orders
| Aspect | Details |
|---|---|
| Permission | Pos_READ |
Query params:
{
page?: number; // default: 1
limit?: number; // default: 20, max: 100
startDate?: string; // ISO date string
endDate?: string; // ISO date string
}Response: paginated ResponseDto<PosOrderListItemDto[]> with meta { count, page, size }.
Each item shape:
{
id: number;
orderNumber: string;
orderStatus: string;
paymentStatus: string;
fulfillmentStatus: string | null;
finalPayable: number; // NPR integer
paymentMethod: string | null;
createdAt: Date;
customer: {
id: string | null; // customerId (UUID) or null
name: string;
email: string;
};
}3.10 GET /api/admin/pos/analytics
| Aspect | Details |
|---|---|
| Permission | Pos_READ |
Query params:
{
startDate?: string; // ISO date string
endDate?: string; // ISO date string
district?: string; // Filter by shipping district (optional)
}Response data:
{
totalRevenue: number; // NPR integer, only completed orders
orderCount: number; // only completed orders
gatewayBreakdown: Array<{
gateway: string | null;
revenue: number;
count: number;
}>;
districtBreakdown: Array<{
districtName: string | null;
revenue: number;
orderCount: number;
}>;
}Only includes orders where paymentStatus = "completed". Pending/failed orders are excluded from revenue.
When district is provided, all queries are filtered to that district. districtBreakdown always returns per-district totals regardless of the filter — useful for checking cross-district distribution even when viewing a single district's aggregate.
4. Error Codes Reference
| Code | HTTP | Trigger |
|---|---|---|
POS_DUPLICATE_EMAIL | 409 | Walk-in customer creation with already-registered email |
POS_DUPLICATE_PHONE | 409 | Walk-in customer creation with already-registered phone |
POS_CUSTOMER_NOT_FOUND | 404 | Order creation with unknown customerId |
POS_ORDER_NOT_POS_SALE | 400 | Payment/failure/pickup on non-POS order |
POS_ORDER_NOT_PAYMENT_PENDING | 400 | Payment recording on order not in pending payment status |
POS_INVALID_GATEWAY | 400 | Payment with gateway not in ["pos_cash","pos_fonepay","pos_esewa","pos_card"] |
POS_DUPLICATE_PAYMENT_REFERENCE | 400 | Postgres 23505 on transactionReference unique constraint |
POS_PICKUP_NOT_IN_STORE | 400 | Pickup-confirm on order not in "received" or "ready" fulfillment status |
ORDER_NOT_FOUND | 404 | orderId not found |
ORDER_PRODUCT_NOT_FOUND | 404 | productId not found during order creation |
ORDER_PRODUCT_NOT_AVAILABLE | 400 | Product not published or not sellable |
ORDER_DATE_RANGE_TOO_LARGE | 400 | Export date range exceeds 90 days |
Time fields in this module are stored as timezone-aware values and should be handled as ISO-8601 instants by API consumers.
See Also
- Module Overview: See POS - Overview for user flows, permissions, and state model.
- Backend Reference: See POS - Backend Documentation Section 5 (POS Payment Flow) for the full event sequence.
POS Module Overview
In-store sales terminal for admin staff — customer lookup, order creation, payment recording, and fulfillment management.
POS Module Backend Documentation
Backend architecture, services, data model, payment event flow, idempotency, and inter-module integration for the In-Store Sales (POS) module.