Files
backend_fiesta/repositories/orderRepository.go
abhishek 3b60a90009 Derive stock and availability from the ledger, not stored fields
Stock shown in the console did not match the productstocks ledger, and two
product endpoints were failing outright. Every cause was on the read side or
in how the availability flag was maintained; the ledger writes themselves
(CreateOrder's "out" entry, cancellation's "in" entry) were already correct.

Read fixes, repositories/productRepository.go:

- GetProductStocks returned SQLSTATE 42803 on every call: bare a.tenantid /
  a.stocktype / a.status under GROUP BY a.productid. The per-ledger-row
  columns are now aggregated and the grouping covers the identity columns.

- FetchFilteredProducts filtered on an alias `e` that no query defines, so
  every /getallproducts call carrying a locationid failed with SQLSTATE 42P01
  instead of returning products.

- FetchFilteredProducts joined productlocations on productid alone and joined
  a (productid, locationid)-grouped stock subquery on productid alone, so a
  product carried by three outlets came back three times, each row showing
  another outlet's quantity and status. Both are now tenant-scoped subqueries
  collapsed to one row per product and scoped to the outlet when one is given.

- GetProductStocks and FetchFilteredProducts compared stocktype = 'in'
  case-sensitively. Production holds 'in' and 'IN' both, so uppercase receipts
  were silently dropped from the balance: one outlet reported 0 for a product
  holding 50, another reported 0 for twelve products holding 200-840.

- GetStockStatement summed opening over stockdate <= CURRENT_DATE, making it
  arithmetically identical to closing. The Inventory ledger showed the same
  number in both columns on every row, which reads as stock never moving.

Availability flag:

productlocations.status was maintained by two different rules — the order path
derived it from the balance, the receiving path set 'available' on any "in"
entry regardless of the resulting balance. A partial restock that left the
balance at or below zero marked a product sellable, and a flag set by an old
order never cleared for stock that arrived by a route the API did not own.

Both paths now derive the flag from the live balance through one rule:
SyncProductLocationStatus (receiving side) and syncProductLocationStatus
(order side, inside the caller's transaction). ReactivateProductLocations is
replaced by the former; the service no longer filters refs by stocktype, since
the direction of the movement is no longer what decides the flag. A row that
has already drifted now repairs itself on its next ledger entry.

Verified against the live database: all four stock endpoints return matching
balances, /getallproducts no longer duplicates rows, and the flag sync was
exercised in both directions inside a rolled-back transaction.

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

1596 lines
49 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
}
func (r *orderRepository) getSequenceno(tid int, prefix string) string {
type SeqResult struct {
Orderseqno string
}
var q1 string
// Formats the ID as tenantid-subprefix+seqno (e.g., 908-20245189)
switch prefix {
case "ORD":
q1 = `SELECT CONCAT(tenantid, '-',
CASE WHEN subprefix IS NULL OR CAST(subprefix AS TEXT) IN ('0', '0.0', '') THEN '' ELSE CAST(subprefix AS TEXT) END,
COALESCE(MAX(orderseqno) + 1, 1)) AS orderseqno
FROM ordersequences WHERE tenantid = ?
GROUP BY tenantid, subprefix`
case "INV":
q1 = `SELECT CONCAT(tenantid, '-',
CASE WHEN subprefix IS NULL OR CAST(subprefix AS TEXT) IN ('0', '0.0', '') THEN '' ELSE CAST(subprefix AS TEXT) END,
COALESCE(MAX(invoiceseqno) + 1, 1)) AS orderseqno
FROM ordersequences WHERE tenantid = ?
GROUP BY tenantid, subprefix`
}
var result SeqResult
r.db.Raw(q1, tid).Scan(&result)
// Fallback if no row exists in the database
if result.Orderseqno == "" {
return fmt.Sprintf("%d-1", tid)
}
return result.Orderseqno
}
func (r *orderRepository) updateSeqno(tid int, prefix string) error {
var field string
switch prefix {
case "ORD":
field = "orderseqno"
case "INV":
field = "invoiceseqno"
default:
return fmt.Errorf("invalid prefix: %s", prefix)
}
// 🛠️ Improved: Check if row exists, if not, create it
var count int64
r.db.Table("ordersequences").Where("tenantid = ?", tid).Count(&count)
if count == 0 {
newSeq := map[string]interface{}{
"tenantid": tid,
field: 1,
"subprefix": nil, // Use NULL so CONCAT ignores it or set default here
}
return r.db.Table("ordersequences").Create(&newSeq).Error
}
// If it exists, perform the update
return r.db.Table("ordersequences").
Where("tenantid = ?", tid).
Update(field, gorm.Expr(fmt.Sprintf("%s + 1", field))).Error
}
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
data.Orderid = r.getSequenceno(data.Tenantid, "ORD")
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 := r.updateSeqno(data.Tenantid, "ORD"); err != nil {
log.Println("updateSeqno error:", err)
}
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
}