README docs DAILY_SALES_API.md

Daily Sales API

Daily sales / end-of-day report for POS clients. Returns the same JSON payload as legacy GET /api/v2/today-sales-v3 in web-application (createTodaySalesJsonV4).

Authentication: jwtAuth — JWT in Authorization: Bearer <token>. Tenant scope (customer_id, location_id) comes from the token.

Frontend handout (shorter): frontend-handout/07-DAILY-SALES.md


Route

Method Path Handler
GET /v1/daily-sales Daily sales report for the requested business day

Legacy / gateway paths (migration reference):

Legacy (web-application) Invoice service
GET /api/v2/today-sales-v3 GET /v1/daily-sales
GET /today-sales-v3 (TRACK gateway) GET /v1/daily-sales

web-pos: invoiceApiSliceGET {INVOICE_SERVICE_URL}/daily-sales (e.g. http://localhost:8080/v1/daily-sales).


Authentication

Authorization: Bearer <jwt>
Claim Aliases Required
customer_id customerId yes
location_id locationId yes

Optional JWT fields used in the response:

Claim Used for
currency Pending / late-closure row currency (default AED)
is_master_locations_enabled Product join scope (master vs location-scoped products)
allow_locations Enforced when overriding location via query
user_id / id Open-shift lookup when shift=1
name / username Actual-price module lookup

Missing customer_id or location_id400 Authentication must include customer_id and location_id.

Invalid JWT → 401.


GET /v1/daily-sales

Returns a plain JSON object (no { success, data } wrapper).

Query parameters

Param Type Default Description
date string today Single business day (DD-MM-YYYY). Legacy alias used by web-pos
from_date string Range start (DD-MM-YYYY). Overrides date when set
to_date string Range end (DD-MM-YYYY)
paid_at string Filter completed invoices by paid date (DD-MM-YYYY) instead of created_at window
brand_id number | string Single brand filter
brands string Comma-separated or JSON array of brand ids
close_by_user_id number Only invoices closed by this user
shift 1 | true Restrict to the authenticated user's open shift
shift_id number Restrict to a specific shift id
location_id number Override location (must be allowed by JWT allow_locations)
force_location_ids string | array Multi-location report (comma-separated or JSON array)
app_version string Affects modifier / item qty formatting
build_number number | string Used with app_version for qty typing

Date resolution

  • If from_date and/or to_date are provided, they define the range (swapped if from > to).
  • Else if date is provided, both ends of the range equal that day.
  • Else defaults to today (server calendar date at request time).
  • Invalid date format → 400 Invalid date parameter(s): … Use DD-MM-YYYY.

Business day window

  • Resolved from config rows time_zone + opening_hour per location.
  • Invoices are included when created_at falls between start_utc and end_utc of the business day (unless paid_at filter is used).
  • Missing timezone or opening hour → 400 Missing Time Zone or Business Hours.

Shift filter

  • When shift=1 / shift=true / shift_id is set but no matching open shift exists, returns a valid empty report (zeros, shift_filter_applied: true) — not an error.

There is no Joi validation on this route; unknown query keys are ignored.

Example requests

GET /v1/daily-sales?date=09-07-2026
Authorization: Bearer <jwt>
GET /v1/daily-sales?date=09-07-2026&close_by_user_id=4734&app_version=1.1.1&build_number=16
Authorization: Bearer <jwt>
GET /v1/daily-sales?from_date=01-07-2026&to_date=09-07-2026&brand_id=12
Authorization: Bearer <jwt>

Response envelope

Top-level object. Amounts are numbers (parsed from DB decimals). Timestamps:

Field Type Description
ts number Server millis at response time
time string Current local time string for the customer timezone (e.g. "2:30 PM")
date_range object { from, to } as YYYY-MM-DD

Sales totals

Field Type Description
totalAmount number Completed sales total (incl. late closure)
total_sales number Same as totalAmount
total_orders number Completed order count
total_payable number Sum of completed invoice payable
total_without_vat number total_payable - total_vat
gross_sales number Payable + discounts
total_vat number VAT on completed orders
avg_invoice_bill number Average completed ticket

Line items & categories

Field Type Description
items array Sold products (non-addon, non-refunded), aggregated by product
addons array Add-on lines
refund_items array Refunded lines
modifiers array Aggregated modifier lines
itemsCat array { type, total } per product type (+ modifiers, add-ons, refunds)

Item row (typical)

{
  "type": "Beverages",
  "name": "Latte",
  "qty": 12,
  "total": 180.5,
  "unit_id": 1,
  "is_addon": 0,
  "is_refunded": 0,
  "extra": []
}

Payments

Field Type Description
itemsPayment array Per payment method: { payment_method, total, transactions, tip_amount? }
Also includes synthetic rows: Unpaid Order, Discount Amount, Tip Amount, Points, Top-ups

Use total_payable (or totalAmount) for the payment table footer total in web-pos.

Order types

Field Type Description
order_type object Counts keyed by type (dine_in, drive_thru, takeaway, …) plus void_orders, refunded_items
order_types array { name, count, amount } sorted by amount
order_type_summary object Extended breakdown with share % (see order_type_summary.rows)

Users & shifts

Field Type Description
user_statistics object open_orders, completed_orders, created_by_users, closed_by_users — each { users[], total_amount, total_orders }
active_shift_id number | null Shift used when shift filter applied
shift_filter_applied boolean Whether shift scoping was requested

Wallet, cash, voids

Field Type Description
wallet_orders object { total_amount, total_count }
wallet_topups object { transaction_type: "debit", amount, count }
void_orders object { title, no_of_cancelled_order, no_of_items_refund, total, transaction[] }
withdraw_amount number Cash out total
credit_amount number Cash in total

Discounts & CRM

Field Type Description
discount_summary array { discount_name, count, amount }
discount_amount object { title, total, transactions }
total_customers object { count } — distinct CRM customers
total_discounts object { total_amount, total_applications }
order_discount_summary object Per-discount-name rollup
item_discount_summary object Per-line discount detail

Other

Field Type Description
dining_guests number Sum of dining_guests on completed invoices
item_transferred number Transferred line count
hourly array { hour, value, order_count } distribution
pending_orders array Open invoices summary
late_closure array Completed in period but created before opening
invoice_begin / invoice_end number | null First / last customer_invoice_id in scope
invoices_count number Invoice count in scope
location_id number Primary location
force_location_ids number[] Present when multi-location

Tip & loyalty

Field Type Description
tip_amount object { title: "Tip Amount", total, transactions }
points object { title: "Points", total, transactions } — burn points value
unpaid_order object { title: "Unpaid Order", total, transactions }

Errors

HTTP Condition Body shape
400 Missing JWT claims { success: false, code: 400, message: "…" }
400 Invalid date param Plain { status: false, message: "Invalid date parameter(s): …" } from report builder
400 Missing business hours Plain { message: "Missing Time Zone or Business Hours" }
401 Invalid / expired JWT { success: false, code: 401, message: "…" }
403 Location not in allow_locations { success: false, code: 403, message: "Access denied for this location" }
500 Unhandled server error { success: false, code: 500, message: "…" }

On success, the body is the report object directly (not the error envelope above).


Implementation architecture

dailySales.route.js
  → dailySalesController.getDailySales
  → dailySalesService.getDailySales
  → createTodaySalesReport.js          # aggregation + response (legacy parity)
  → DailySalesRepository               # Sequelize reads
       ├── Invoice + InvoiceItem
       ├── Payment
       └── late-closure Invoice queries
  → raw SQL (pool)
       ├── config (business hours)
       ├── wallet_transactions
       ├── withdrawal / cash_in
       └── CRM discount summary queries
Concern Path
Route src/routes/v1/dailySales.route.js
Controller src/controllers/dailySales.controller.js
Service src/services/dailySales.service.js
Report builder src/daily-sales/createTodaySalesReport.js
Sequelize repository src/data/repositories/DailySalesRepository.js
Scope → WHERE helpers src/daily-sales/utils/salesScopeBuilder.js
Decimal parsing src/daily-sales/utils/decimal.js
Location/date SQL fragments src/daily-sales/utils/locationDateSql.js
Legacy reference web-application/.../createTodaySalesJsonV4.js

Configuration & data dependencies

Requires MySQL tables used by web-application POS reporting:

  • invoices, invoice_items, payments, products
  • config (time_zone, opening_hour per customer/location)
  • crm_users, discounts, shifts
  • wallet_transactions, withdrawal, cash_in

Per-location config must have valid time_zone (e.g. +04:00) and opening_hour (e.g. 05:00).


Client migration checklist

  1. Point base URL to invoice service: NEXT_PUBLIC_INVOICE_SERVICE_URL / /v1.
  2. Replace GET /api/v2/today-sales-v3 with GET /v1/daily-sales.
  3. Keep query params (date, close_by_user_id, app_version, etc.) unchanged.
  4. Parse response as a top-level object (not wrapped in data).
  5. Use total_payable ?? totalAmount for payment footer totals.
  6. Map itemsCat + items / addons / modifiers / refund_items for category tables (see web-pos mapByCategory).