Shop It Docs
Developer ResourcesPOS (In-Store Sales)

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 + RoleGuard on all endpoints
  • Base URL: /api/admin/pos
  • Swagger tag: POS
  • Response envelope: ResponseDto<T> ({ message, data, meta? })
  • HTTP status: 200 OK for all successful endpoints (including POST)

2. Route Summary

MethodPathPermissionDescription
GET/api/admin/pos/customers/lookupPos_READLook up customer by email
POST/api/admin/pos/customersPos_CREATECreate walk-in customer
POST/api/admin/pos/ordersPos_CREATECreate POS order
POST/api/admin/pos/orders/:orderId/paymentPos_RECORD_PAYMENTRecord POS payment
POST/api/admin/pos/orders/:orderId/payment-failurePos_UPDATERecord payment failure
POST/api/admin/pos/orders/:orderId/pickup-confirmPos_UPDATEConfirm in-store pickup
POST/api/admin/pos/emi-inquiryPos_CREATESubmit EMI inquiry
GET/api/admin/pos/orders/exportPos_READExport POS orders (flat, date-ranged)
GET/api/admin/pos/ordersPos_READList POS orders (paginated)
GET/api/admin/pos/analyticsPos_READPOS revenue analytics

Important: GET /api/admin/pos/orders/export is declared before GET /api/admin/pos/orders in the controller to prevent the :orderId route parameter from capturing the literal "export" path segment.

3. Endpoint Details

3.1 GET /api/admin/pos/customers/lookup

AspectDetails
PermissionPos_READ
Query paramsemail: string (required)

Response data:

{
  exists: true;
  customerId: string;   // UUID
  name: string;
  email: string;
  phone: string | null;
  image: string | null;
} | null
  • Returns null when no customer is found for that email. There is no 404null is the signal for "not found, prompt to create".
  • Email lookup is case-insensitive (lowercased + trimmed internally).

3.2 POST /api/admin/pos/customers

AspectDetails
PermissionPos_CREATE
Rate limit10 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

AspectDetails
PermissionPos_CREATE
Headeridempotency-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 from product.mrp in DB
  • totalSp = sum(unitSp * quantity) — taken from product.sp in DB
  • discount = 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_FOUNDcustomerId not found
  • 404 ORDER_PRODUCT_NOT_FOUND — any productId not found
  • 400 ORDER_PRODUCT_NOT_AVAILABLE — product status != "published" or isSellable = false

Created order has: orderSource = "pos_sale", paymentMethod = "pos", orderStatus = "payment_pending".

3.4 POST /api/admin/pos/orders/:orderId/payment

AspectDetails
PermissionPos_RECORD_PAYMENT
Headeridempotency-key (string, required — must be different from the order creation key)
Path paramorderId: 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):

  1. Idempotency check on async_requests.
  2. Fetch order — validate orderSource = "pos_sale", paymentMethod = "pos", paymentStatus = "pending".
  3. Validate gateway is in POS_GATEWAYS.
  4. transactionRef = dto.transactionReference ?? uuidv7().
  5. PaymentService.createPaymentRecord(orderId, "order", customerId, amountNpr, gateway) → returns { id: paymentId }.
  6. PaymentService.markPaymentCompleted(paymentId, transactionRef, {}) — catches Postgres 23505POS_DUPLICATE_PAYMENT_REFERENCE.
  7. Emit PAYMENT_EVENTS.COMPLETED via EventEmitter2OrderPaymentListener handles downstream (order → paid, fulfillment → received).
  8. Insert async_requests record with onConflictDoNothing.

Errors:

  • 404 ORDER_NOT_FOUNDorderId not found
  • 400 POS_ORDER_NOT_POS_SALE — order is not a POS order
  • 400 POS_ORDER_NOT_PAYMENT_PENDING — order paymentStatus is not "pending"
  • 400 POS_INVALID_GATEWAY — gateway not in POS_GATEWAYS
  • 400 POS_DUPLICATE_PAYMENT_REFERENCEtransactionReference violates unique constraint (Postgres 23505)

3.5 POST /api/admin/pos/orders/:orderId/payment-failure

AspectDetails
PermissionPos_UPDATE
Path paramorderId: number

Request body:

{ reason: string; }   // minLength: 1, maxLength: 1000

Response 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_FOUND
  • 400 POS_ORDER_NOT_POS_SALE

3.6 POST /api/admin/pos/orders/:orderId/pickup-confirm

AspectDetails
PermissionPos_UPDATE
Path paramorderId: 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_FOUND
  • 400 POS_ORDER_NOT_POS_SALE
  • 400 POS_PICKUP_NOT_IN_STOREfulfillmentStatus is not "received" or "ready"

3.7 POST /api/admin/pos/emi-inquiry

AspectDetails
PermissionPos_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

AspectDetails
PermissionPos_READ

Query params:

{ startDate: string; endDate: string; }  // ISO date strings, both required

Response data: PosOrderListItemDto[] (flat array, no pagination)

  • Max date range: 90 days. Error 400 ORDER_DATE_RANGE_TOO_LARGE if exceeded.
  • Results ordered by createdAt DESC.
  • Each item shape: same as PosOrderListItemDto (see 3.9).

3.9 GET /api/admin/pos/orders

AspectDetails
PermissionPos_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

AspectDetails
PermissionPos_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

CodeHTTPTrigger
POS_DUPLICATE_EMAIL409Walk-in customer creation with already-registered email
POS_DUPLICATE_PHONE409Walk-in customer creation with already-registered phone
POS_CUSTOMER_NOT_FOUND404Order creation with unknown customerId
POS_ORDER_NOT_POS_SALE400Payment/failure/pickup on non-POS order
POS_ORDER_NOT_PAYMENT_PENDING400Payment recording on order not in pending payment status
POS_INVALID_GATEWAY400Payment with gateway not in ["pos_cash","pos_fonepay","pos_esewa","pos_card"]
POS_DUPLICATE_PAYMENT_REFERENCE400Postgres 23505 on transactionReference unique constraint
POS_PICKUP_NOT_IN_STORE400Pickup-confirm on order not in "received" or "ready" fulfillment status
ORDER_NOT_FOUND404orderId not found
ORDER_PRODUCT_NOT_FOUND404productId not found during order creation
ORDER_PRODUCT_NOT_AVAILABLE400Product not published or not sellable
ORDER_DATE_RANGE_TOO_LARGE400Export 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