Files
backend_fiesta/repositories/orderRepository.go
abhishek a11c4843ca Allocate order numbers atomically instead of read-then-increment
Order ids were duplicating in production: 160 distinct (tenant, orderid) pairs
are shared by more than one order, worst of them "1135-1" on 108 orders, and
every order tenant 1147 has ever placed is numbered "1147-1".

getSequenceno read MAX(seqno)+1 and updateSeqno incremented, both against
r.db rather than the order's transaction and separated by the whole order
insert. Two concurrent orders therefore read the same number before either
wrote, and an order that rolled back still consumed one. Three further
defects made it worse:

  - A NULL orderseqno made COALESCE(MAX(orderseqno) + 1, 1) evaluate
    NULL + 1 = NULL and fall through to a hardcoded "<tenantid>-1". The
    increment then computed NULL + 1 = NULL too, so the counter could never
    leave NULL and every subsequent order reused that same id.

  - Tenants with several ordersequences rows (tenant 1135 has ~25) hit a
    GROUP BY returning multiple rows, of which Scan kept the first
    arbitrarily, while the increment updated all of them.

  - A tenant with no row at all fell back to "<tenantid>-1" indefinitely,
    because nothing ever created one.

nextSequenceNo replaces both functions with a single UPDATE ... RETURNING run
inside the caller's transaction, so the counter row stays locked until the
order commits and concurrent orders queue rather than collide. A NULL seeds
from the tenant's existing order count — at least as high as any number
already issued, so recovery cannot reissue a used id — the counter is pinned
to the tenant's lowest sequenceid so reads and writes address one row, and a
missing row is created on first use.

Verified against production data in rolled-back transactions: tenant 1147
(NULL) now yields 1147-9, 1147-10, ...; tenant 1135 (NULL plus duplicate rows)
1135-356 onward; tenant 916 keeps its 916-2024115209 subprefix format; an
unknown tenant creates its row and starts at 1. Eight concurrent allocations
produced eight distinct ids. Two real orders through the API returned 1147-9
and 1147-10, then were cancelled with stock restoring to its baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 13:13:43 +05:30

1611 lines
51 KiB
Go

package repositories
import (
"fmt"
"log"
"nearle/models"
"sort"
"strings"
"time"
"gorm.io/gorm"
)
type OrderRepository interface {
GetTenantOrders(q models.DeliveryQuery) ([]models.OrderInfo, error)
GetTenantLocationOrders(input models.DeliveryQuery) ([]models.OrderInfo, error)
GetPartnerOrders(stat, fdate, tdate string, pid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error)
GetCustomerOrders(stat, fdate, tdate string, cid, mid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error)
GetAdminOrders(stat, fdate, tdate string, aid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error)
GetUserOrders(stat, fdate, tdate string, uid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error)
GetAllOrders(stat, fdate, tdate string, pageno, pagesize int, keyword string) ([]models.OrderInfo, error)
GetOrderSummary(tid, pid, cid, aid int, fdate, tdate string) ([]models.Ordersummarydaily, error)
GetLocationOrderSummary(tenantID int) ([]models.Ordersummarylocation, error)
GetOrderInsights(tenantID int) ([]models.OrderInsightv1, error)
GetOrderDetails(orderHeaderID int) ([]models.OrderDetails, error)
UpdateOrder(order *models.Orders) error
CreateOrder(order models.Orders) (models.Orders, error)
GetCustomerOrdersv3(customerID, tenantID, moduleID, fromDate, toDate, orderStatus, keyword string, pageSize, offset int) ([]models.CustomerOrder, error)
GetRevenueSummary(tid, lid int, fdate, tdate string) (*models.TenantRevenueSummary, error)
GetSalesSummary(tid, lid int, fdate, tdate string) (*models.SalesSummaryResponse, error)
GetTimeSeries(tenantID, locationID int, granularity, fromDate, toDate string) ([]models.TimeSeriesData, error)
}
type orderRepository struct {
db *gorm.DB
}
func NewOrderRepository(db *gorm.DB) OrderRepository {
return &orderRepository{db: db}
}
const (
base = `SELECT DISTINCT a.orderheaderid, a.applocationid, h.locationname AS applocation, a.tenantid, a.locationid, a.partnerid, a.configid, a.categoryid, a.subcategoryid, a.moduleid,
a.orderid, a.orderstatus, a.orderdate, a.ordernotes, a.itemcount, a.deliverytime AS deliverydate,
a.pending, a.processing, a.ready, a.delivered AS completed, a.cancelled,
a.deliverycharge, a.kms,
a.customerid, a.pickuplocationid, a.pickupaddress, a.pickuplat, a.pickuplong,
a.pickupcustomer, a.pickupcontactno, a.pickuplocation as pickupsuburb, a.pickupcity,
a.deliveryid AS deliverycustomerid, a.deliveryaddress, a.deliverylat, a.deliverylong, a.deliverytype,
a.deliverycustomer,a.deliverycontactno,a.deliverylocation as deliverysuburb, a.deliverycity, a.paymenttype, a.smsdelivery, b.customertoken,
c.tenantname, c.tenanttoken, c.primarycontact AS tenantcontactno, c.postcode AS tenantpostcode, c.suburb AS tenantsuburb, c.city AS tenantcity,
d.locationname, d.contactno AS locationcontactno, d.postcode AS locationpostcode, d.suburb AS locationsuburb, d.city AS locationcity
FROM orders a
INNER JOIN customers b ON a.customerid = b.customerid
INNER JOIN tenants c ON a.tenantid = c.tenantid
INNER JOIN tenantlocations d ON a.locationid = d.locationid
INNER JOIN app_location h ON a.applocationid = h.applocationid
INNER JOIN app_locationconfig i ON a.applocationid = i.applocationid`
orderdetails = `SELECT DISTINCT a.orderheaderid, a.applocationid,
a.tenantid, a.locationid, a.partnerid, a.configid, a.categoryid, a.subcategoryid, a.moduleid,
a.orderid, a.orderstatus, a.orderdate, a.ordernotes, a.itemcount, a.deliverytime AS deliverydate,
a.pending, a.processing, a.ready, a.delivered AS completed, a.cancelled,
a.deliverycharge, a.kms,
a.customerid, a.pickupaddress, a.pickuplat, a.pickuplong,
a.pickupcustomer, a.pickupcontactno, a.pickuplocation as pickupsuburb, a.pickupcity,
a.deliveryid AS deliverycustomerid, a.deliveryaddress, a.deliverylat, a.deliverylong, a.deliverytype,
a.deliverycustomer,a.deliverycontactno,a.deliverylocation as deliverysuburb, a.deliverycity,a.paymenttype, a.smsdelivery, a.orderamount,
b.tenantname, b.tenanttoken, b.primarycontact AS tenantcontactno, b.postcode AS tenantpostcode, b.suburb AS tenantsuburb,b.city AS tenantcity,
c.locationname, c.contactno AS locationcontactno, c.postcode AS locationpostcode, c.suburb AS locationsuburb, c.city AS locationcity,
d.locationname AS applocation
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
INNER JOIN app_locationconfig e ON d.applocationid = e.applocationid`
)
func (r *orderRepository) GetTenantOrders(input models.DeliveryQuery) ([]models.OrderInfo, error) {
var data []models.OrderInfo
var query string
var params []interface{}
offset := (input.Pageno - 1) * input.Pagesize
baseQuery := base + ` WHERE a.tenantid = ?`
params = append(params, input.Tenantid)
if input.Status == "ongoing" {
query = baseQuery + ` AND a.orderstatus IN ('pending','processing','ready')`
} else {
query = baseQuery + ` AND a.orderstatus = ?`
params = append(params, input.Status)
}
query += ` AND a.deliverytime::date BETWEEN ? AND ?`
params = append(params, input.Fromdate, input.ToDate)
if input.Keyword != "" {
query += ` AND (
a.pickupcustomer LIKE ? OR
c.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + input.Keyword + "%"
params = append(params, like, like, like, like, like, like)
}
if input.Configid != 0 {
query += ` AND a.configid = ?`
params = append(params, input.Configid)
}
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, input.Pagesize, offset)
fmt.Println("Executing:", query)
res := r.db.Raw(query, params...).Find(&data)
return data, res.Error
}
func (r *orderRepository) GetPartnerOrders(stat, fdate, tdate string, pid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error) {
var data []models.OrderInfo
if pageno <= 0 {
pageno = 1
}
if pagesize <= 0 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
fmt.Println("Getting partner order details")
query := orderdetails + ` WHERE a.partnerid = ?`
params := []interface{}{pid}
if fdate != "" && tdate != "" {
query += ` AND a.deliverytime::date BETWEEN ? AND ?`
params = append(params, fdate, tdate)
}
if stat != "" {
query += ` AND a.orderstatus = ?`
params = append(params, stat)
}
if keyword != "" {
query += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
params = append(params, like, like, like, like, like, like)
}
var total int64
countQuery := `
SELECT COUNT(DISTINCT a.orderheaderid)
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
INNER JOIN app_locationconfig e ON d.applocationid = e.applocationid
LEFT JOIN deliveries f ON a.orderheaderid = f.orderheaderid
LEFT JOIN app_users g ON f.userid = g.userid
WHERE a.partnerid = ?`
countParams := []interface{}{pid}
if fdate != "" && tdate != "" {
countQuery += ` AND a.deliverytime::date BETWEEN ? AND ?`
countParams = append(countParams, fdate, tdate)
}
if stat != "" {
countQuery += ` AND a.orderstatus = ?`
countParams = append(countParams, stat)
}
if keyword != "" {
countQuery += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
countParams = append(countParams, like, like, like, like, like, like)
}
r.db.Raw(countQuery, countParams...).Scan(&total)
if int64(offset) >= total {
offset = 0
pageno = 1
}
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset)
fmt.Println("QUERY:", query)
fmt.Println("PARAMS:", params)
r.db.Raw(query, params...).Find(&data)
fmt.Println("RESULT COUNT:", len(data))
return data, nil
}
func (r *orderRepository) GetCustomerOrders(stat, fdate, tdate string, cid, mid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error) {
var data []models.OrderInfo
if pageno <= 0 {
pageno = 1
}
if pagesize <= 0 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
fmt.Println("Getting customer order details")
query := orderdetails + ` WHERE a.customerid = ?`
params := []interface{}{cid}
if mid != 0 {
query += ` AND a.moduleid = ?`
params = append(params, mid)
}
if fdate != "" && tdate != "" {
query += ` AND a.orderdate::date BETWEEN ? AND ?`
params = append(params, fdate, tdate)
}
if stat != "" {
query += ` AND a.orderstatus = ?`
params = append(params, stat)
}
if keyword != "" {
query += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
params = append(params, like, like, like, like, like, like)
}
var total int64
countQuery := `
SELECT COUNT(DISTINCT a.orderheaderid)
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
INNER JOIN app_locationconfig e ON d.applocationid = e.applocationid
WHERE a.customerid = ?`
countParams := []interface{}{cid}
if mid != 0 {
countQuery += ` AND a.moduleid = ?`
countParams = append(countParams, mid)
}
if fdate != "" && tdate != "" {
countQuery += ` AND a.orderdate::date BETWEEN ? AND ?`
countParams = append(countParams, fdate, tdate)
}
if stat != "" {
countQuery += ` AND a.orderstatus = ?`
countParams = append(countParams, stat)
}
if keyword != "" {
countQuery += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
countParams = append(countParams, like, like, like, like, like, like)
}
r.db.Raw(countQuery, countParams...).Scan(&total)
if int64(offset) >= total {
offset = 0
pageno = 1
}
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset)
fmt.Println("QUERY:", query)
fmt.Println("PARAMS:", params)
res := r.db.Raw(query, params...).Find(&data)
if res.Error != nil {
fmt.Println("ERROR:", res.Error)
}
fmt.Println("RESULT COUNT:", len(data))
return data, nil
}
func (r *orderRepository) GetAdminOrders(stat, fdate, tdate string, aid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error) {
var data []models.OrderInfo
if pageno <= 0 {
pageno = 1
}
if pagesize <= 0 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
fmt.Println("Getting admin order details")
query := orderdetails + ` WHERE 1=1`
params := []interface{}{}
if aid != 0 {
query += ` AND a.applocationid = ?`
params = append(params, aid)
}
if fdate != "" && tdate != "" {
query += ` AND a.deliverytime::date BETWEEN ? AND ?`
params = append(params, fdate, tdate)
}
if stat != "" {
query += ` AND a.orderstatus = ?`
params = append(params, stat)
}
if keyword != "" {
query += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
params = append(params, like, like, like, like, like, like)
}
var total int64
countQuery := `
SELECT COUNT(DISTINCT a.orderheaderid)
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
INNER JOIN app_locationconfig e ON d.applocationid = e.applocationid
LEFT JOIN deliveries f ON a.orderheaderid = f.orderheaderid
LEFT JOIN app_users g ON f.userid = g.userid
WHERE 1=1
`
countParams := []interface{}{}
if aid != 0 {
countQuery += ` AND a.applocationid = ?`
countParams = append(countParams, aid)
}
if fdate != "" && tdate != "" {
countQuery += ` AND a.deliverytime::date BETWEEN ? AND ?`
countParams = append(countParams, fdate, tdate)
}
if stat != "" {
countQuery += ` AND a.orderstatus = ?`
countParams = append(countParams, stat)
}
if keyword != "" {
countQuery += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
countParams = append(countParams, like, like, like, like, like, like)
}
r.db.Raw(countQuery, countParams...).Scan(&total)
if int64(offset) >= total {
offset = 0
pageno = 1
}
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset)
fmt.Println("QUERY:", query)
fmt.Println("PARAMS:", params)
r.db.Raw(query, params...).Find(&data)
fmt.Println("RESULT COUNT:", len(data))
return data, nil
}
func (r *orderRepository) GetUserOrders(stat, fdate, tdate string, uid, pageno, pagesize int, keyword string) ([]models.OrderInfo, error) {
var data []models.OrderInfo
if pageno <= 0 {
pageno = 1
}
if pagesize <= 0 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
fmt.Println("Getting user order details")
query := orderdetails + ` WHERE e.status = 'Active' AND e.userid = ?`
params := []interface{}{uid}
if fdate != "" && tdate != "" {
query += ` AND a.deliverytime::date BETWEEN ? AND ?`
params = append(params, fdate, tdate)
}
if stat != "" {
query += ` AND a.orderstatus = ?`
params = append(params, stat)
}
if keyword != "" {
query += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
params = append(params, like, like, like, like, like, like)
}
var total int64
countQuery := `
SELECT COUNT(DISTINCT a.orderheaderid)
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
INNER JOIN app_locationconfig e ON d.applocationid = e.applocationid
LEFT JOIN deliveries f ON a.orderheaderid = f.orderheaderid
LEFT JOIN app_users g ON f.userid = g.userid
WHERE e.status = 'Active' AND e.userid = ?
`
countParams := []interface{}{uid}
if fdate != "" && tdate != "" {
countQuery += ` AND a.deliverytime::date BETWEEN ? AND ?`
countParams = append(countParams, fdate, tdate)
}
if stat != "" {
countQuery += ` AND a.orderstatus = ?`
countParams = append(countParams, stat)
}
if keyword != "" {
countQuery += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
countParams = append(countParams, like, like, like, like, like, like)
}
r.db.Raw(countQuery, countParams...).Scan(&total)
if int64(offset) >= total {
offset = 0
pageno = 1
}
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset)
fmt.Println("QUERY:", query)
fmt.Println("PARAMS:", params)
r.db.Raw(query, params...).Find(&data)
fmt.Println("RESULT COUNT:", len(data))
return data, nil
}
func (r *orderRepository) GetAllOrders(stat, fdate, tdate string, pageno, pagesize int, keyword string) ([]models.OrderInfo, error) {
var data []models.OrderInfo
if pageno <= 0 {
pageno = 1
}
if pagesize <= 0 {
pagesize = 10
}
offset := (pageno - 1) * pagesize
fmt.Println("Getting all orders")
query := orderdetails + ` WHERE 1=1`
params := []interface{}{}
if fdate != "" && tdate != "" {
query += ` AND a.deliverytime::date BETWEEN ? AND ?`
params = append(params, fdate, tdate)
}
if stat != "" {
query += ` AND a.orderstatus = ?`
params = append(params, stat)
}
if keyword != "" {
query += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
params = append(params, like, like, like, like, like, like)
}
var total int64
countQuery := `
SELECT COUNT(DISTINCT a.orderheaderid)
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
INNER JOIN app_locationconfig e ON d.applocationid = e.applocationid
LEFT JOIN deliveries f ON a.orderheaderid = f.orderheaderid
LEFT JOIN app_users g ON f.userid = g.userid
WHERE 1=1
`
countParams := []interface{}{}
if fdate != "" && tdate != "" {
countQuery += ` AND a.deliverytime::date BETWEEN ? AND ?`
countParams = append(countParams, fdate, tdate)
}
if stat != "" {
countQuery += ` AND a.orderstatus = ?`
countParams = append(countParams, stat)
}
if keyword != "" {
countQuery += ` AND (
a.pickupcustomer LIKE ? OR
b.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
like := "%" + keyword + "%"
countParams = append(countParams, like, like, like, like, like, like)
}
r.db.Raw(countQuery, countParams...).Scan(&total)
if int64(offset) >= total {
offset = 0
pageno = 1
}
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, pagesize, offset)
fmt.Println("QUERY:", query)
fmt.Println("PARAMS:", params)
r.db.Raw(query, params...).Find(&data)
fmt.Println("RESULT COUNT:", len(data))
return data, nil
}
func (r *orderRepository) GetOrderSummary(tid, pid, cid, lid int, fdate, tdate string) ([]models.Ordersummarydaily, error) {
var data []models.Ordersummarydaily
// Base SELECT
const base = `
SELECT
COUNT(*) AS total,
SUM(CASE WHEN o.orderstatus = 'created' THEN 1 ELSE 0 END) AS created,
SUM(CASE WHEN o.orderstatus = 'pending' THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN o.orderstatus = 'processing' THEN 1 ELSE 0 END) AS processing,
SUM(CASE WHEN o.orderstatus = 'delivered' THEN 1 ELSE 0 END) AS delivered,
SUM(CASE WHEN o.orderstatus = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
t.tenantid,
t.tenantname
FROM orders o
INNER JOIN tenants t ON o.tenantid = t.tenantid
`
var params []interface{}
var q1 string
// Apply filters (at least one scoping id is required — enforced by the caller)
switch {
case tid != 0:
q1 = base + " WHERE o.configid = 1 AND o.tenantid = ?"
params = append(params, tid)
case pid != 0:
q1 = base + " WHERE o.configid = 1 AND o.partnerid = ?"
params = append(params, pid)
case cid != 0:
q1 = base + " WHERE o.configid = 1 AND o.customerid = ?"
params = append(params, cid)
case lid != 0:
q1 = base + " WHERE o.configid = 1 AND o.locationid = ?"
params = append(params, lid)
default:
return nil, fmt.Errorf("at least one of tenantid, partnerid, customerid or locationid is required")
}
// Date filter
if fdate != "" && tdate != "" {
q1 += " AND o.orderdate::date BETWEEN ? AND ?"
params = append(params, fdate, tdate)
}
// Group by tenant
q1 += " GROUP BY t.tenantid, t.tenantname"
// Debug
fmt.Println("Executing GetOrderSummary query:", q1)
if err := r.db.Raw(q1, params...).Scan(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func (r *orderRepository) GetLocationOrderSummary(tenantID int) ([]models.Ordersummarylocation, error) {
var data []models.Ordersummarylocation
var params []interface{}
q1 := `
SELECT
l.locationid,
l.locationname,
COALESCE(COUNT(o.orderid), 0) AS total,
COALESCE(SUM(CASE WHEN o.orderstatus = 'created' THEN 1 ELSE 0 END), 0) AS created,
COALESCE(SUM(CASE WHEN o.orderstatus = 'pending' THEN 1 ELSE 0 END), 0) AS pending,
COALESCE(SUM(CASE WHEN o.orderstatus = 'processing' THEN 1 ELSE 0 END), 0) AS processing,
COALESCE(SUM(CASE WHEN o.orderstatus = 'delivered' THEN 1 ELSE 0 END), 0) AS delivered,
COALESCE(SUM(CASE WHEN o.orderstatus = 'cancelled' THEN 1 ELSE 0 END), 0) AS cancelled
FROM tenantlocations l
LEFT JOIN orders o
ON l.locationid = o.locationid
AND l.tenantid = o.tenantid
AND o.configid = 1
`
if tenantID != 0 {
q1 += " WHERE l.tenantid = ?"
params = append(params, tenantID)
}
q1 += " GROUP BY l.locationid, l.locationname ORDER BY l.locationid"
fmt.Println("Executing query:", q1)
if err := r.db.Raw(q1, params...).Scan(&data).Error; err != nil {
return nil, err
}
return data, nil
}
func (r *orderRepository) GetRevenueSummary(tid, lid int, fdate, tdate string) (*models.TenantRevenueSummary, error) {
var summary models.TenantRevenueSummary
// 1. If tid is 0 and lid is not 0, lookup the tenantid from tenantlocations
if tid == 0 && lid != 0 {
var tenantLoc struct {
Tenantid int
}
if err := r.db.Table("tenantlocations").Select("tenantid").Where("locationid = ?", lid).Scan(&tenantLoc).Error; err != nil {
return nil, err
}
tid = tenantLoc.Tenantid
}
if tid == 0 {
return nil, fmt.Errorf("tenant ID is required or could not be determined")
}
summary.Tenantid = tid
// 2. Fetch the tenant name
var tenant struct {
Tenantname string
}
if err := r.db.Table("tenants").Select("tenantname").Where("tenantid = ?", tid).Scan(&tenant).Error; err != nil {
return nil, err
}
summary.Tenantname = tenant.Tenantname
// 3. Fetch overall revenue for this tenant
overallQuery := `
SELECT COALESCE(SUM(orderamount), 0) AS overall_revenue
FROM orders
WHERE tenantid = ? AND orderstatus = 'delivered' AND configid = 1
`
var overallParams []interface{}
overallParams = append(overallParams, tid)
if lid != 0 {
overallQuery += " AND locationid = ?"
overallParams = append(overallParams, lid)
}
if fdate != "" && tdate != "" {
overallQuery += " AND orderdate::date BETWEEN ? AND ?"
overallParams = append(overallParams, fdate, tdate)
}
var overallRev float64
if err := r.db.Raw(overallQuery, overallParams...).Scan(&overallRev).Error; err != nil {
return nil, err
}
summary.OverallRevenue = overallRev
// 4. Fetch revenue details by location
locationQuery := `
SELECT
l.locationid,
l.locationname,
COALESCE(SUM(o.orderamount), 0) AS revenue
FROM tenantlocations l
LEFT JOIN orders o
ON l.locationid = o.locationid
AND o.orderstatus = 'delivered'
AND o.configid = 1
`
var locParams []interface{}
if fdate != "" && tdate != "" {
locationQuery += " AND o.orderdate::date BETWEEN ? AND ?"
locParams = append(locParams, fdate, tdate)
}
locationQuery += " WHERE l.tenantid = ?"
locParams = append(locParams, tid)
if lid != 0 {
locationQuery += " AND l.locationid = ?"
locParams = append(locParams, lid)
}
locationQuery += " GROUP BY l.locationid, l.locationname ORDER BY l.locationid"
var locRevenues []models.LocationRevenueDetails
if err := r.db.Raw(locationQuery, locParams...).Scan(&locRevenues).Error; err != nil {
return nil, err
}
if locRevenues == nil {
locRevenues = []models.LocationRevenueDetails{}
}
summary.LocationRevenue = locRevenues
return &summary, nil
}
func (r *orderRepository) GetDistinctLocations() ([]models.OrderInsight, error) {
var locations []models.OrderInsight
q1 := `
SELECT DISTINCT a.applocationid, b.locationname
FROM orders a
INNER JOIN app_location b ON a.applocationid = b.applocationid
WHERE b.status = 'Active'`
if err := r.db.Raw(q1).Scan(&locations).Error; err != nil {
return nil, err
}
return locations, nil
}
func (r *orderRepository) GetSalesSummary(tid, lid int, fdate, tdate string) (*models.SalesSummaryResponse, error) {
var summary models.SalesSummaryResponse
whereClause := "tenantid = ? AND orderstatus IN ('delivered', 'completed') AND configid = 1"
var params []interface{}
params = append(params, tid)
if lid != 0 {
whereClause += " AND locationid = ?"
params = append(params, lid)
}
if fdate != "" && tdate != "" {
whereClause += " AND orderdate::date BETWEEN ? AND ?"
params = append(params, fdate, tdate)
}
totalsQuery := fmt.Sprintf(`
SELECT
COALESCE(SUM(COALESCE(ordervalue, 0) + COALESCE(orderamount, 0) + COALESCE(deliveryamt, 0)), 0) AS total_revenue,
COUNT(orderheaderid) AS total_orders
FROM orders
WHERE %s`, whereClause)
var result struct {
TotalRevenue float64
TotalOrders int
}
if err := r.db.Raw(totalsQuery, params...).Scan(&result).Error; err != nil {
return nil, err
}
summary.TotalRevenue = result.TotalRevenue
summary.TotalOrders = result.TotalOrders
if summary.TotalOrders > 0 {
summary.AverageOrderValue = summary.TotalRevenue / float64(summary.TotalOrders)
}
chartQuery := fmt.Sprintf(`
SELECT
CAST(orderdate AS DATE) AS date,
COALESCE(SUM(COALESCE(ordervalue, 0) + COALESCE(orderamount, 0) + COALESCE(deliveryamt, 0)), 0) AS revenue,
COUNT(orderheaderid) AS orders
FROM orders
WHERE %s
GROUP BY CAST(orderdate AS DATE)
ORDER BY date ASC`, whereClause)
var chartData []models.SalesSummaryChartData
if err := r.db.Raw(chartQuery, params...).Scan(&chartData).Error; err != nil {
return nil, err
}
if chartData == nil {
chartData = []models.SalesSummaryChartData{}
}
summary.ChartData = chartData
var topLocations []models.SalesSummaryTopLocation
if lid == 0 {
topWhere := "o.tenantid = ? AND o.orderstatus IN ('delivered', 'completed') AND o.configid = 1"
var topParams []interface{}
topParams = append(topParams, tid)
if fdate != "" && tdate != "" {
topWhere += " AND o.orderdate::date BETWEEN ? AND ?"
topParams = append(topParams, fdate, tdate)
}
cleanTopLocQuery := fmt.Sprintf(`
SELECT
COALESCE(l.locationname, 'Unknown') AS locationname,
COALESCE(SUM(COALESCE(o.ordervalue, 0) + COALESCE(o.orderamount, 0) + COALESCE(o.deliveryamt, 0)), 0) AS revenue
FROM orders o
LEFT JOIN tenantlocations l ON o.locationid = l.locationid
WHERE %s
GROUP BY l.locationid, l.locationname
ORDER BY revenue DESC
LIMIT 5`, topWhere)
if err := r.db.Raw(cleanTopLocQuery, topParams...).Scan(&topLocations).Error; err != nil {
return nil, err
}
}
if topLocations == nil {
topLocations = []models.SalesSummaryTopLocation{}
}
summary.TopLocations = topLocations
return &summary, nil
}
func (r *orderRepository) GetMonthlyOrders(applocationid string) (*models.Ordermonths, error) {
var orderMonths models.Ordermonths
q2 := `
SELECT
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 1 THEN 1 ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 2 THEN 1 ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 3 THEN 1 ELSE 0 END), 0) AS mar,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 4 THEN 1 ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 5 THEN 1 ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 6 THEN 1 ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 7 THEN 1 ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 8 THEN 1 ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 9 THEN 1 ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 10 THEN 1 ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 11 THEN 1 ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 12 THEN 1 ELSE 0 END), 0) AS dece
FROM orders a
WHERE a.applocationid = ?
AND EXTRACT(YEAR FROM a.orderdate) = EXTRACT(YEAR FROM CURRENT_DATE)`
if err := r.db.Raw(q2, applocationid).Scan(&orderMonths).Error; err != nil {
return nil, err
}
return &orderMonths, nil
}
func (r *orderRepository) GetOrderInsights(tenantID int) ([]models.OrderInsightv1, error) {
var locations []models.OrderInsightv1
var params []interface{}
// ✅ Query 1: Get all locations (even without orders)
q1 := `SELECT b.locationid, b.locationname
FROM tenantlocations b
WHERE b.status = 'Active'`
if tenantID != 0 {
q1 += " AND b.tenantid = ?"
params = append(params, tenantID)
}
if err := r.db.Raw(q1, params...).Scan(&locations).Error; err != nil {
return nil, err
}
// ✅ Query 2: Monthly order counts per location
q2 := `SELECT
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 1 THEN 1 ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 2 THEN 1 ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 3 THEN 1 ELSE 0 END), 0) AS mar,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 4 THEN 1 ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 5 THEN 1 ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 6 THEN 1 ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 7 THEN 1 ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 8 THEN 1 ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 9 THEN 1 ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 10 THEN 1 ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 11 THEN 1 ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM a.orderdate) = 12 THEN 1 ELSE 0 END), 0) AS dece
FROM orders a
WHERE a.locationid = ? AND EXTRACT(YEAR FROM a.orderdate) = EXTRACT(YEAR FROM CURRENT_DATE)`
if tenantID != 0 {
q2 += " AND a.tenantid = ?"
}
// ✅ Attach monthly order counts for each location
for i := range locations {
var orderMonths models.Ordermonths
if tenantID != 0 {
if err := r.db.Raw(q2, locations[i].Locationid, tenantID).Scan(&orderMonths).Error; err != nil {
return nil, err
}
} else {
if err := r.db.Raw(q2, locations[i].Locationid).Scan(&orderMonths).Error; err != nil {
return nil, err
}
}
locations[i].Ordermonths = &orderMonths
}
return locations, nil
}
func (r *orderRepository) GetOrderDetails(orderHeaderID int) ([]models.OrderDetails, error) {
var details []models.OrderDetails
query := `
SELECT a.orderdetailid, a.orderheaderid, a.tenantid, a.locationid, a.productid, a.productname, a.productdescription, a.supplyqty, a.balanceqty,
a.orderqty, a.price, a.unitid, a.unitname, a.productaddonid, a.addontypeid, a.productmapid, a.productvariantid, a.productaddondescription,
a.discountid, a.discountname, a.discountcode, a.discountterms, a.discountpercentage, a.discountamount, a.landingamount, a.taxpercentage,
a.taxamount, a.productsumprice, a.itemstatus, a.delivered, COALESCE(b.orderamount, 0) as orderamount, COALESCE(b.taxamount, 0) as totaltaxamount,
c.productimage
FROM orderdetails a
LEFT JOIN orders b ON b.orderheaderid = a.orderheaderid
LEFT JOIN products c ON a.productid = c.productid
WHERE a.orderheaderid = ?
`
if err := r.db.Raw(query, orderHeaderID).Scan(&details).Error; err != nil {
return nil, err
}
return details, nil
}
// syncProductLocationStatus re-derives productlocations.status for one
// product at one outlet from the live productstocks balance — "available"
// above zero, "outofstock" at or below it. It runs inside the caller's
// transaction so the flag is committed together with the ledger entry that
// moved it, and it is the same rule productRepository.SyncProductLocationStatus
// applies on the receiving side; both paths agreeing is what stops the flag
// drifting away from the ledger over time.
//
// Status errors are deliberately swallowed: an order must not fail because a
// display flag could not be refreshed, and the next ledger entry re-derives it.
func syncProductLocationStatus(tx *gorm.DB, tenantid, locationid, productid int) {
if tenantid <= 0 || locationid <= 0 || productid <= 0 {
return
}
if err := tx.Exec(`
UPDATE productlocations
SET status = CASE WHEN (
SELECT COALESCE(
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END), 0)
FROM productstocks
WHERE productid = ? AND tenantid = ? AND locationid = ?
) > 0 THEN 'available' ELSE 'outofstock' END
WHERE tenantid = ? AND locationid = ? AND productid = ?`,
productid, tenantid, locationid,
tenantid, locationid, productid).Error; err != nil {
log.Println("syncProductLocationStatus:", err)
}
}
func (r *orderRepository) UpdateOrder(order *models.Orders) error {
tx := r.db.Begin()
// Handle stock restoration on order cancellation
var existingOrder models.Orders
if err := tx.Where("orderheaderid = ?", order.Orderheaderid).First(&existingOrder).Error; err == nil {
newStatus := strings.ToLower(strings.TrimSpace(order.Orderstatus))
oldStatus := strings.ToLower(strings.TrimSpace(existingOrder.Orderstatus))
if newStatus == "cancelled" && oldStatus != "cancelled" {
var items []models.OrderDetail
if err := tx.Table("orderdetails").Where("orderheaderid = ?", order.Orderheaderid).Find(&items).Error; err == nil {
for _, item := range items {
itemLocID := item.Locationid
if itemLocID == 0 {
itemLocID = existingOrder.Locationid
}
qty := int(item.Orderqty)
if qty <= 0 {
qty = 1
}
restoredStock := models.Productstock{
Tenantid: existingOrder.Tenantid,
Stockdate: time.Now(),
Locationid: itemLocID,
Productid: item.Productid,
Quantity: qty,
Stocktype: "in",
Status: "Active",
}
if err := tx.Table("productstocks").Create(&restoredStock).Error; err != nil {
tx.Rollback()
return err
}
// Re-derive availability from the restored balance rather
// than assuming "available": cancelling one line of a
// heavily oversold product can leave it still at or below
// zero, in which case it must stay flagged outofstock.
syncProductLocationStatus(tx, existingOrder.Tenantid, itemLocID, item.Productid)
}
}
}
}
if err := tx.Where("orderheaderid = ?", order.Orderheaderid).Updates(order).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
// nextSequenceNo claims the next order (or invoice) number for a tenant and
// returns it formatted as tenantid-subprefix+seqno (e.g. 916-2024115209).
//
// It runs inside the caller's transaction and both reads and increments the
// counter in a single UPDATE ... RETURNING, which is what makes the number
// unique. The previous implementation split this into getSequenceno (read
// MAX+1) and updateSeqno (increment), both on r.db rather than the order's
// transaction, so two concurrent orders read the same value before either
// wrote, and a rolled-back order still consumed a number.
//
// Three further defects that produced duplicate ids in production:
//
// - A NULL orderseqno made COALESCE(MAX(orderseqno) + 1, 1) evaluate
// NULL + 1 = NULL, falling through to a hardcoded "<tenantid>-1"; the
// increment then computed NULL + 1 = NULL as well, so the counter could
// never leave NULL. Every order such a tenant ever placed was numbered
// "<tenantid>-1" — 108 orders share "1135-1" today. A NULL is now seeded
// from the tenant's existing order count, which is at least as high as any
// number already handed out, so recovery never reissues a used id.
//
// - Tenants with more than one ordersequences row (tenant 1135 has ~25) hit
// a GROUP BY that returned several rows, of which Scan silently kept the
// first, while the increment updated every row. The counter is now pinned
// to the tenant's lowest sequenceid, so reads and writes address the same
// row whatever duplicates exist.
//
// - A tenant with no row at all fell back to "<tenantid>-1" forever, since
// nothing created one. The row is now created on first use.
func nextSequenceNo(tx *gorm.DB, tid int, prefix string) (string, error) {
var field string
switch prefix {
case "ORD":
field = "orderseqno"
case "INV":
field = "invoiceseqno"
default:
return "", fmt.Errorf("invalid prefix: %s", prefix)
}
// field is not user input — it comes from the switch above.
formatted := fmt.Sprintf(`CONCAT(tenantid, '-',
CASE WHEN subprefix IS NULL OR CAST(subprefix AS TEXT) IN ('0', '0.0', '')
THEN '' ELSE CAST(subprefix AS TEXT) END,
%s)`, field)
var seq string
err := tx.Raw(fmt.Sprintf(`
UPDATE ordersequences
SET %s = COALESCE(%s, (SELECT COUNT(*) FROM orders WHERE tenantid = ?)) + 1,
updated = NOW()
WHERE sequenceid = (SELECT MIN(sequenceid) FROM ordersequences WHERE tenantid = ?)
RETURNING %s`, field, field, formatted), tid, tid).Scan(&seq).Error
if err != nil {
return "", err
}
if seq != "" {
return seq, nil
}
// No counter row for this tenant yet — create one, seeded past whatever
// their existing orders already used.
err = tx.Raw(fmt.Sprintf(`
INSERT INTO ordersequences (tenantid, %s, created, updated)
VALUES (?, (SELECT COUNT(*) FROM orders WHERE tenantid = ?) + 1, NOW(), NOW())
RETURNING %s`, field, formatted), tid, tid).Scan(&seq).Error
if err != nil {
return "", err
}
if seq == "" {
return "", fmt.Errorf("could not allocate %s sequence for tenant %d", prefix, tid)
}
return seq, nil
}
func (r *orderRepository) CreateOrder(data models.Orders) (models.Orders, error) {
tx := r.db.Begin()
locID := data.Locationid
if locID == 0 {
locID = data.Applocationid
}
// 🛠️ Step 0: Lock every (tenantid, locationid, productid) row this order
// touches before checking availability. Without this, two concurrent
// orders for the same product can both read "stock available" before
// either commits its deduction, oversell the item, and drive stock
// negative. Locking productlocations — the row the stock computation is
// already keyed against — serializes conflicting orders instead.
//
// Locks are acquired in a fixed (productid, locationid) order so that
// two orders sharing overlapping products always contend for them in
// the same sequence, avoiding a lock-ordering deadlock between the two
// transactions (as opposed to just making each individually block).
type lockTarget struct {
productid int
locationid int
}
seen := make(map[lockTarget]bool)
locks := make([]lockTarget, 0, len(data.Items))
for _, item := range data.Items {
itemLocID := item.Locationid
if itemLocID == 0 {
itemLocID = locID
}
lt := lockTarget{productid: item.Productid, locationid: itemLocID}
if !seen[lt] {
seen[lt] = true
locks = append(locks, lt)
}
}
sort.Slice(locks, func(a, b int) bool {
if locks[a].productid != locks[b].productid {
return locks[a].productid < locks[b].productid
}
return locks[a].locationid < locks[b].locationid
})
for _, lt := range locks {
var locked int
lockQuery := `SELECT productlocationid FROM productlocations WHERE tenantid = ? AND locationid = ? AND productid = ? FOR UPDATE`
if err := tx.Raw(lockQuery, data.Tenantid, lt.locationid, lt.productid).Scan(&locked).Error; err != nil {
tx.Rollback()
return models.Orders{}, fmt.Errorf("failed to lock stock for product %d: %w", lt.productid, err)
}
}
// 🛠️ Step 1: Pre-validate stock availability for all items before placing order
for _, item := range data.Items {
itemLocID := item.Locationid
if itemLocID == 0 {
itemLocID = locID
}
requestedQty := int(item.Orderqty)
if requestedQty <= 0 {
requestedQty = 1
}
var availableStock int
stockQuery := `
SELECT COALESCE(
SUM(CASE WHEN LOWER(stocktype) = 'in' THEN quantity ELSE 0 END) -
SUM(CASE WHEN LOWER(stocktype) = 'out' THEN quantity ELSE 0 END),
0
)
FROM productstocks
WHERE productid = ? AND tenantid = ? AND locationid = ?
`
if err := tx.Raw(stockQuery, item.Productid, data.Tenantid, itemLocID).Scan(&availableStock).Error; err != nil {
tx.Rollback()
return models.Orders{}, fmt.Errorf("failed to verify stock for product %d: %w", item.Productid, err)
}
// If stock tracking exists and available stock is less than requested quantity, block order placement
if availableStock < requestedQty {
tx.Rollback()
pName := item.Productname
if pName == "" {
pName = fmt.Sprintf("ID %d", item.Productid)
}
return models.Orders{}, fmt.Errorf("insufficient stock for product '%s': requested %d, available %d", pName, requestedQty, availableStock)
}
}
// 🛠️ Step 2: Create Order Header
// Claimed inside tx so the row lock on the counter holds until commit:
// concurrent orders queue for it instead of reading the same number, and a
// rollback releases it rather than burning it.
orderid, err := nextSequenceNo(tx, data.Tenantid, "ORD")
if err != nil {
tx.Rollback()
return models.Orders{}, fmt.Errorf("failed to allocate order number: %w", err)
}
data.Orderid = orderid
if err := tx.Create(&data).Error; err != nil {
tx.Rollback()
return models.Orders{}, err
}
// 🛠️ Step 3: Insert Order Details & Record "out" stock deduction
for _, item := range data.Items {
item.Orderheaderid = data.Orderheaderid
item.Tenantid = data.Tenantid
itemLocID := item.Locationid
if itemLocID == 0 {
itemLocID = locID
}
item.Locationid = itemLocID
if err := tx.Table("orderdetails").Create(&item).Error; err != nil {
tx.Rollback()
return models.Orders{}, err
}
qty := int(item.Orderqty)
if qty <= 0 {
qty = 1
}
stock := models.Productstock{
Tenantid: data.Tenantid,
Stockdate: time.Now(),
Locationid: itemLocID,
Productid: item.Productid,
Quantity: qty,
Stocktype: "out",
Status: "Active",
}
if err := tx.Table("productstocks").Create(&stock).Error; err != nil {
tx.Rollback()
return models.Orders{}, err
}
// Re-derive the location's availability flag from the ledger balance
// this "out" entry just produced.
syncProductLocationStatus(tx, data.Tenantid, itemLocID, item.Productid)
}
if err := tx.Commit().Error; err != nil {
return models.Orders{}, err
}
var order models.Orders
if err := r.db.Where("orderheaderid = ?", data.Orderheaderid).First(&order).Error; err != nil {
return models.Orders{}, err
}
var items []models.OrderDetail
if err := r.db.Table("orderdetails").Where("orderheaderid = ?", data.Orderheaderid).Find(&items).Error; err == nil {
order.Items = items
}
return order, nil
}
func (r *orderRepository) getOrderDetailsByHeaderID(orderHeaderID int) ([]models.OrderDetails, float64, float64, error) {
var details []models.OrderDetails
var orderAmount float64
var totalTaxAmount float64
query := `
SELECT a.orderdetailid,a.orderheaderid,a.tenantid,a.locationid,a.productid,a.productname,a.productdescription,a.supplyqty,a.balanceqty,a.orderqty,a.price,a.unitid,
a.unitname,a.productaddonid,a.addontypeid,a.productmapid,a.productvariantid,a.productaddondescription,a.discountid,a.discountname,a.discountcode,a.discountterms,
a.discountpercentage,a.discountamount,a.landingamount,a.taxpercentage,a.taxamount,a.productsumprice,a.itemstatus,a.delivered,COALESCE(b.orderamount, 0) as orderamount,
COALESCE(b.taxamount, 0) as totaltaxamount,c.productimage
FROM orderdetails a
LEFT JOIN orders b ON b.orderheaderid = a.orderheaderid
LEFT JOIN products c ON a.productid = c.productid
WHERE a.orderheaderid = ?`
err := r.db.Raw(query, orderHeaderID).Scan(&details).Error
if err != nil {
return nil, 0, 0, err
}
if len(details) > 0 {
orderAmount = details[0].Orderamount
totalTaxAmount = details[0].Totaltaxamount
}
return details, orderAmount, totalTaxAmount, nil
}
func (r *orderRepository) GetCustomerOrdersv3(customerID, tenantID, moduleID, fromDate, toDate, orderStatus, keyword string, pageSize, offset int) ([]models.CustomerOrder, error) {
orders := make([]models.CustomerOrder, 0)
baseQuery := `
SELECT a.orderheaderid, a.applocationid, a.tenantid, a.locationid, a.partnerid, a.configid,
a.categoryid, a.subcategoryid, a.moduleid, a.orderid, a.orderstatus, a.orderdate,
a.ordernotes, a.itemcount, a.deliverytime, a.pending, a.processing, a.ready,
a.delivered, a.cancelled, a.deliverycharge, a.kms, a.customerid, a.pickupaddress,
a.pickuplat, a.pickuplong, a.pickupcustomer, a.pickupcontactno, a.pickuplocation AS pickupsuburb,
a.pickupcity, a.deliveryid AS deliverycustomerid, a.deliveryaddress, a.deliverylat,
a.deliverylong, a.deliverytype, a.deliverycustomer, a.deliverycontactno,
a.deliverylocation AS deliverysuburb, a.deliverycity, a.paymenttype, a.smsdelivery,
a.taxamount,
b.tenantname, b.tenanttoken, b.primarycontact AS tenantcontactno,
b.postcode AS tenantpostcode, b.suburb AS tenantsuburb, b.city AS tenantcity, b.registrationno,
c.locationname, c.contactno AS locationcontactno, c.postcode AS locationpostcode,
c.suburb AS locationsuburb, c.city AS locationcity,
d.locationname AS applocation
FROM orders a
INNER JOIN tenants b ON a.tenantid = b.tenantid
INNER JOIN tenantlocations c ON a.locationid = c.locationid
INNER JOIN app_location d ON a.applocationid = d.applocationid
WHERE 1=1
`
params := []interface{}{}
if customerID != "" {
baseQuery += " AND a.customerid = ?"
params = append(params, customerID)
}
if tenantID != "" && tenantID != "0" {
baseQuery += " AND a.tenantid = ?"
params = append(params, tenantID)
}
if moduleID != "" {
baseQuery += " AND a.moduleid = ?"
params = append(params, moduleID)
}
if fromDate != "" && toDate != "" {
baseQuery += " AND DATE(a.orderdate) BETWEEN ? AND ?"
params = append(params, fromDate, toDate)
}
if orderStatus != "" {
baseQuery += " AND a.orderstatus = ?"
params = append(params, orderStatus)
}
if keyword != "" {
baseQuery += " AND EXISTS (SELECT 1 FROM orderdetails od WHERE od.orderheaderid = a.orderheaderid AND od.productname LIKE ?)"
params = append(params, "%"+keyword+"%")
}
baseQuery += " ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?"
params = append(params, pageSize, offset)
if err := r.db.Raw(baseQuery, params...).Scan(&orders).Error; err != nil {
return nil, err
}
// 🚀 NO LOOPING YET — Now fetch all details in one query
return r.attachOrderDetails(orders)
}
func (r *orderRepository) attachOrderDetails(orders []models.CustomerOrder) ([]models.CustomerOrder, error) {
if len(orders) == 0 {
return orders, nil
}
orderIDs := make([]int, 0, len(orders))
for _, o := range orders {
orderIDs = append(orderIDs, o.Orderheaderid)
}
var details []models.OrderDetails
query := `
SELECT a.*, p.productimage
FROM orderdetails a
LEFT JOIN products p ON a.productid = p.productid
WHERE a.orderheaderid IN ?
`
if err := r.db.Raw(query, orderIDs).Scan(&details).Error; err != nil {
return nil, err
}
// map orderheaderid → []details
detailMap := map[int][]models.OrderDetails{}
for _, d := range details {
detailMap[d.Orderheaderid] = append(detailMap[d.Orderheaderid], d)
}
for i := range orders {
orders[i].OrderDetails = detailMap[orders[i].Orderheaderid]
if len(orders[i].OrderDetails) > 0 {
orders[i].Orderamount = orders[i].OrderDetails[0].Orderamount
orders[i].Totaltaxamount = orders[i].OrderDetails[0].Totaltaxamount
}
}
return orders, nil
}
func (r *orderRepository) GetTenantLocationOrders(input models.DeliveryQuery) ([]models.OrderInfo, error) {
var data []models.OrderInfo
var params []interface{}
offset := (input.Pageno - 1) * input.Pagesize
// Start building SQL
query := base + `
WHERE a.tenantid = ?
AND a.locationid = ?
`
params = append(params, input.Tenantid, input.Locationid)
// Status filter
if input.Status == "ongoing" {
query += ` AND a.orderstatus IN ('pending','processing','ready')`
} else if input.Status != "" {
query += ` AND a.orderstatus = ?`
params = append(params, input.Status)
}
// Applocation filter
if input.Applocationid != 0 {
query += ` AND a.applocationid = ?`
params = append(params, input.Applocationid)
}
// Config filter
if input.Configid != 0 {
query += ` AND a.configid = ?`
params = append(params, input.Configid)
}
// Date filter
query += ` AND DATE(a.deliverytime) BETWEEN ? AND ?`
params = append(params, input.Fromdate, input.ToDate)
// Keyword filter
if input.Keyword != "" {
like := "%" + input.Keyword + "%"
query += ` AND (
a.pickupcustomer LIKE ? OR
c.tenantname LIKE ? OR
a.deliverycustomer LIKE ? OR
a.pickupcontactno LIKE ? OR
a.deliverycontactno LIKE ? OR
a.orderid LIKE ?
)`
params = append(params, like, like, like, like, like, like)
}
// Sorting + pagination
query += ` ORDER BY a.orderheaderid DESC LIMIT ? OFFSET ?`
params = append(params, input.Pagesize, offset)
fmt.Println("Executing Tenant+Location SQL:", query)
result := r.db.Raw(query, params...).Find(&data)
return data, result.Error
}
func (r *orderRepository) GetTimeSeries(tenantID, locationID int, granularity, fromDate, toDate string) ([]models.TimeSeriesData, error) {
var data []models.TimeSeriesData
var dateFunc, dateFuncO2 string
switch granularity {
case "month":
dateFunc = "TO_CHAR(o.orderdate::date, 'YYYY-MM')"
dateFuncO2 = "TO_CHAR(o2.orderdate::date, 'YYYY-MM')"
case "year":
dateFunc = "TO_CHAR(o.orderdate::date, 'YYYY')"
dateFuncO2 = "TO_CHAR(o2.orderdate::date, 'YYYY')"
case "day":
fallthrough
default:
dateFunc = "TO_CHAR(o.orderdate::date, 'YYYY-MM-DD')"
dateFuncO2 = "TO_CHAR(o2.orderdate::date, 'YYYY-MM-DD')"
}
var locFilter, dateFilter string
var params []interface{}
// subquery params
params = append(params, tenantID)
if locationID != 0 {
locFilter = " AND o2.locationid = ?"
params = append(params, locationID)
}
if fromDate != "" && toDate != "" {
dateFilter = " AND o2.orderdate::date BETWEEN ? AND ?"
params = append(params, fromDate, toDate)
}
// main query params
params = append(params, tenantID)
if locationID != 0 {
params = append(params, locationID)
}
if fromDate != "" && toDate != "" {
params = append(params, fromDate, toDate)
}
query := fmt.Sprintf(`
SELECT
%s AS label,
COUNT(o.orderheaderid) AS orders,
COALESCE(SUM(o.orderamount), 0) AS revenue,
COALESCE(SUM(CASE WHEN o.orderstatus = 'cancelled' THEN 1 ELSE 0 END), 0) AS cancelled,
COALESCE(SUM(CASE WHEN o.orderstatus IN ('delivered', 'completed') THEN 1 ELSE 0 END), 0) AS delivered,
COALESCE(MAX(sku_counts.activeskus), 0) AS activeskus
FROM orders o
LEFT JOIN (
SELECT %s AS label, COUNT(DISTINCT d.productid) AS activeskus
FROM orders o2
JOIN orderdetails d ON o2.orderheaderid = d.orderheaderid
WHERE o2.tenantid = ? AND o2.configid = 1
%s
%s
GROUP BY %s
) sku_counts ON %s = sku_counts.label
WHERE o.tenantid = ?
AND o.configid = 1
`, dateFunc, dateFuncO2, locFilter, dateFilter, dateFuncO2, dateFunc)
if locationID != 0 {
query += " AND o.locationid = ?"
}
if fromDate != "" && toDate != "" {
query += " AND o.orderdate::date BETWEEN ? AND ?"
}
query += fmt.Sprintf(" GROUP BY %s ORDER BY %s", dateFunc, dateFunc)
if err := r.db.Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
}
if data == nil {
data = []models.TimeSeriesData{}
}
return data, nil
}