The order-placement stock check (read available qty, then insert an "out" deduction) had no row lock, so two concurrent orders for the same product could both pass the availability check before either committed its deduction, overselling the item. Locks each ordered product's productlocations row with SELECT ... FOR UPDATE up front, in a fixed (productid, locationid) order across all items so overlapping concurrent orders contend for locks in the same sequence instead of deadlocking.
1580 lines
49 KiB
Go
1580 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Update productlocation status back to available if stock is restored
|
|
tx.Table("productlocations").
|
|
Where("productid = ? AND tenantid = ? AND locationid = ?", item.Productid, existingOrder.Tenantid, itemLocID).
|
|
Update("status", "available")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Update productlocation status to 'outofstock' if remaining stock drops to 0 or below
|
|
var remainingStock int
|
|
remStockQuery := `
|
|
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(remStockQuery, item.Productid, data.Tenantid, itemLocID).Scan(&remainingStock).Error; err == nil {
|
|
if remainingStock <= 0 {
|
|
tx.Table("productlocations").
|
|
Where("productid = ? AND tenantid = ? AND locationid = ?", item.Productid, data.Tenantid, itemLocID).
|
|
Update("status", "outofstock")
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|