Files
backend_fiesta/models/order.go
abhishek c290e1729a Route offline sales by the branch named on each spreadsheet row
The offline-sales import required one workbook per outlet and a store
picked in the UI. A merchant running several branches had to download,
fill and upload a file per branch, and the picker defaulted to the
tenant's first outlet — so an admin who never touched it silently
credited the wrong store, which no validation could catch because the
file and the selection agreed with each other.

One workbook now covers every branch. getsaletemplate takes locationid=0
(the default) to span the tenant, stamping tenantid, locationid and the
store name onto every row, and that row's locationid is what decides
which branch a sale is deducted from. The INNER JOIN on tenantlocations
confines it to outlets the tenant owns, so a template can never disclose
another merchant's catalogue.

uploadofflinesales accordingly takes locationid on each bill. The
locationid on the request itself becomes a scope constraint rather than
a destination: left at 0 the bills go where their rows say, and set to a
branch it pins the upload there and refuses anything else. That is what
holds a store user to their own store — the pin comes from their session,
so editing the locationid column in the spreadsheet changes nothing.
Every branch referenced is checked against the tenant regardless.

Branch context and catalogue are resolved once per branch and reused; a
workbook covering six outlets would otherwise re-run both queries for
every bill in it.

Duplicate detection is now per branch. Bill numbers only have to be
unique within a store, since counter books at different outlets
routinely restart numbering at 1, and treating a shared number as a
repeat would have silently dropped a real sale.

Verified against tenant 1087, whose two branches both stock product
6998 at 100 units: a single upload of two bills moved 1097 to 97 and
1135 to 95 independently; the same bill number at both branches imported
as two separate orders; an upload pinned to 1097 imported its own bill
and refused the 1135 one; a row naming another tenant's outlet was
refused; and re-uploading the file deducted nothing. All five test
orders were cancelled afterwards and both branches confirmed back at 100.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:17:52 +05:30

573 lines
23 KiB
Go

package models
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"gorm.io/gorm"
)
// FlexibleString handles JSON that could be either a number or a string,
// and ensures it can be saved/read from the database as a string.
type FlexibleString string
func (fs *FlexibleString) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return nil
}
// If it's a string in JSON
if b[0] == '"' {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
*fs = FlexibleString(s)
return nil
}
// If it's a number in JSON
var f float64
if err := json.Unmarshal(b, &f); err != nil {
return err
}
*fs = FlexibleString(strconv.FormatFloat(f, 'f', -1, 64))
return nil
}
func (fs FlexibleString) MarshalJSON() ([]byte, error) {
return json.Marshal(string(fs))
}
// 🔥 Database Logic: Handle reading from DB
func (fs *FlexibleString) Scan(value interface{}) error {
if value == nil {
*fs = ""
return nil
}
switch v := value.(type) {
case []byte:
*fs = FlexibleString(v)
case string:
*fs = FlexibleString(v)
case float64:
*fs = FlexibleString(strconv.FormatFloat(v, 'f', -1, 64))
case int64:
*fs = FlexibleString(strconv.FormatInt(v, 10))
default:
*fs = FlexibleString(fmt.Sprintf("%v", v))
}
return nil
}
// 🔥 Database Logic: Handle writing to DB
func (fs FlexibleString) Value() (driver.Value, error) {
return string(fs), nil
}
func (fs FlexibleString) String() string {
return string(fs)
}
type OrderInfo struct {
Orderheaderid int `json:"orderheaderid"`
Applocationid int `json:"applocationid"`
Applocation string `json:"applocation"`
Tenantid int `json:"tenantid"`
Partnerid int `json:"partnerid"`
Locationid int `json:"locationid"`
Categoryid int `json:"categoryid"`
Subcategoryid int `json:"subcategoryid"`
Moduleid int `json:"moduleid"`
Configid int `json:"configid"`
Orderid string `json:"orderid"`
Orderdate string `json:"orderdate"`
Deliverydate string `json:"deliverydate"`
Orderstatus string `json:"orderstatus"`
Deliverystatus string `json:"deliverystatus"`
Deliveryamt float64 `json:"deliveryamt"`
Itemcount int `json:"itemcount"`
Ordernotes string `json:"ordernotes"`
Kms FlexibleString `json:"kms"`
Actualkms FlexibleString `json:"actualkms"`
Pending string `json:"Pending"`
Processing string `json:"processing"`
Ready string `json:"ready"`
Cancelled string `json:"cancelled"`
Delivered string `json:"delivered"`
Assigntime string `json:"assigntime"`
Starttime string `json:"starttime"`
Arrivaltime string `json:"arrivaltime"`
Pickuptime string `json:"pickuptime"`
Deliverytime string `json:"deliverytime"`
Canceltime string `json:"canceltime"`
Deliverycharge float32 `json:"deliverycharge"`
Orderamount float32 `json:"orderamount"`
Customerid int `json:"customerid"`
Pickupcustomer string `json:"pickupcustomer"`
Pickupcontactno string `json:"pickupcontactno"`
Pickuplocationid int `json:"pickuplocationid"`
Pickupaddress string `json:"pickupaddress"`
Pickupsuburb string `json:"pickupsuburb"`
Pickupcity string `json:"pickupcity"`
Pickuplat FlexibleString `json:"pickuplat"`
Pickuplong FlexibleString `json:"pickuplong"`
Deliveryid int `json:"deliveryid"`
Deliverycustomerid int `json:"deliverycustomerid"`
Deliverycustomer string `json:"deliverycustomer"`
Deliverycontactno string `json:"deliverycontactno"`
Deliverylocationid int `json:"deliverylocationid"`
Deliveryaddress string `json:"deliveryaddress"`
Deliverysuburb string `json:"deliverysuburb"`
Droplat FlexibleString `json:"droplat"`
Droplon FlexibleString `json:"droplon"`
Deliverylat FlexibleString `json:"deliverylat"`
Deliverylong FlexibleString `json:"deliverylong"`
Deliverytype string `json:"deliverytype"`
Paymenttype int `json:"paymenttype"`
Tenantname string `json:"tenantname"`
Tenanttoken string `json:"tenanttoken"`
Tenantsuburb string `json:"tenantsuburb"`
Tenantcity string `json:"tenantcity"`
Tenantcontactno string `json:"tenantcontactno"`
Tenantpostcode string `json:"tenantpostcode"`
Locationname string `json:"locationname"`
Locationsuburb string `json:"locationsuburb"`
Locationcity string `json:"locationcity"`
Locationcontactno string `json:"locationcontactno"`
Rider string `json:"rider"`
Ridercontactno string `json:"ridercontactno"`
Riderkms FlexibleString `json:"riderkms"`
Smsdelivery int `json:"smsdelivery"`
Customertoken string `json:"customertoken"`
Ridertoken string `json:"ridertoken"`
}
type DeliveryQuery struct {
Tenantid int
Customerid int
Partnerid int
UserID int
Appuserid int
Applocationid int
Moduleid int
Locationid int
Configid int
Fromdate string
ToDate string
Status string
Pageno int
Pagesize int
Conn *gorm.DB
Keyword string `json:"keyword"`
}
type Ordersummary struct {
Total int `json:"total"`
Created int `json:"created"`
Pending int `json:"pending"`
Processing int `json:"processing"`
Delivered int `json:"delivered"`
Cancelled int `json:"cancelled"`
// Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
Applocationid string `json:"applocationid"`
}
type Ordersummarylocation struct {
Total int `json:"total"`
Created int `json:"created"`
Pending int `json:"pending"`
Processing int `json:"processing"`
Delivered int `json:"delivered"`
Cancelled int `json:"cancelled"`
Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
}
type Ordersummarydaily struct {
Total int `json:"total"`
Created int `json:"created"`
Pending int `json:"pending"`
Processing int `json:"processing"`
Delivered int `json:"delivered"`
Cancelled int `json:"cancelled"`
Tenantid int `json:"tenantid"`
Tenantname string `json:"tenantname"`
Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
}
type OrderInsight struct {
Applocationid int `json:"applocationid" gorm:"Primary_Key"`
Locationname string `json:"locationname"`
Ordermonths *Ordermonths `json:"ordermonths" gorm:"-"`
}
type OrderInsightv1 struct {
Locationid int `json:"locationid" gorm:"Primary_Key"`
Locationname string `json:"locationname"`
Ordermonths *Ordermonths `json:"ordermonths" gorm:"-"`
}
type Ordermonths struct {
Jan int `json:"jan"`
Feb int `json:"feb"`
Mar int `json:"mar"`
Apr int `json:"apr"`
May int `json:"may"`
Jun int `json:"jun"`
Jul int `json:"jul"`
Aug int `Json:"aug"`
Sep int `Json:"sep"`
Oct int `Json:"oct"`
Nov int `Json:"nov"`
Dece int `Json:"dece"`
// Applocationid int `json:"applocationid"`
}
type Orders struct {
Orderheaderid int `json:"orderheaderid" gorm:"Primary_Key"`
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Applocationid int `json:"applocationid"`
Moduleid int `json:"moduleid"`
Partnerid int `json:"partnerid"`
Configid int `json:"configid"`
Categoryid int `json:"categoryid"`
Subcategoryid int `json:"subcategoryid"`
Orderid string `json:"orderid"`
Orderdate string `json:"orderdate,omitempty"`
Deliverytime string `json:"deliverytime"`
Deliverytype string `json:"deliverytype"`
Orderstatus string `json:"orderstatus"`
Pending string `json:"pending"`
Processing string `json:"processing"`
Ready string `json:"ready"`
Delivered string `json:"delivered"`
Cancelled string `json:"cancelled"`
Customerid int `json:"customerid"`
Pickupaddress string `json:"pickupaddress"`
Pickuplat FlexibleString `json:"pickuplat"`
Pickuplong FlexibleString `json:"pickuplong"`
Pickupcustomer string `json:"pickupcustomer"`
Pickupcontactno string `json:"pickupcontactno"`
Pickuplocation string `json:"pickupsuburb"` // alias
Pickupcity string `json:"pickupcity"`
Deliveryid int `json:"deliverycustomerid"` // alias
Deliverycustomer string `json:"deliverycustomer"`
Deliverycontactno string `json:"deliverycontactno"`
Deliveryaddress string `json:"deliveryaddress"`
Deliverylocation string `json:"deliverylocation"`
Deliverycity string `json:"deliverycity"`
Deliverylocationid int `json:"deliverylocationid"`
Deliverylat FlexibleString `json:"deliverylat"`
Deliverylong FlexibleString `json:"deliverylong"`
Promotionid int `json:"promotionid"`
Promoname string `json:"promoname"`
Promoterms string `json:"promoterms"`
Promovalue int `json:"promovalue"`
Promoamount float32 `json:"promoamount"`
Orderamount float32 `json:"orderamount"`
Taxamount float32 `json:"taxamount"`
Ordercharges float32 `json:"ordercharges"`
Ordervalue float32 `json:"ordervalue"`
Itemcount int `json:"itemcount"`
Paymenttype int `json:"paymenttype"`
Paymentstatus int `json:"paymentstatus"`
Deliverycharge float32 `json:"deliverycharge"`
Ordernotes string `json:"ordernotes"`
Kms FlexibleString `json:"kms"`
Remarks string `json:"remarks"`
Tenantuserid int `json:"tenantuserid"`
Partneruserid int `json:"partneruserid"`
Smsdelivery int `json:"smsdelivery" gorm:"default:0"`
Items []OrderDetail `json:"items" gorm:"-"`
}
type OrderDetails struct {
Orderdetailid int `json:"orderdetailid" gorm:"primaryKey;autoIncrement"`
Orderheaderid int `json:"orderheaderid"`
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Productid int `json:"productid"`
Productname string `json:"productname"`
Productdescription string `json:"productdescription"`
Supplyqty float64 `json:"supplyqty"`
Balanceqty float64 `json:"balanceqty"`
Orderqty float64 `json:"orderqty"`
Price float64 `json:"price"`
Unitid int `json:"unitid"`
Unitname string `json:"unitname"`
Productaddonid int `json:"productaddonid"`
Addontypeid int `json:"addontypeid"`
Productmapid int `json:"productmapid"`
Productvariantid int `json:"productvariantid"`
Productaddondescription string `json:"productaddondescription"`
Discountid int `json:"discountid"`
Discountname string `json:"discountname"`
Discountcode string `json:"discountcode"`
Discountterms string `json:"discountterms"`
Discountpercentage float64 `json:"discountpercentage"`
Discountamount float64 `json:"discountamount"`
Landingamount float64 `json:"landingamount"`
Taxpercentage float64 `json:"taxpercentage"`
Taxamount float64 `json:"taxamount"`
Totaltaxamount float64 `json:"totaltaxamount"`
Productsumprice float64 `json:"productsumprice"`
Itemstatus string `json:"itemstatus"`
Delivered string `json:"delivered"`
Orderamount float64 `json:"Orderamount"`
Productimage string `json:"productimage"`
}
type OrderDetail struct {
Orderdetailid int `json:"orderdetailid" gorm:"primaryKey;autoIncrement"`
Orderheaderid int `json:"orderheaderid"`
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Productid int `json:"productid"`
Productname string `json:"productname"`
Productdescription string `json:"productdescription"`
Supplyqty float64 `json:"supplyqty"`
Balanceqty float64 `json:"balanceqty"`
Orderqty float64 `json:"orderqty"`
Price float64 `json:"price"`
Unitid int `json:"unitid"`
Unitname string `json:"unitname"`
Productaddonid int `json:"productaddonid"`
Addontypeid int `json:"addontypeid"`
Productmapid int `json:"productmapid"`
Productvariantid int `json:"productvariantid"`
Productaddondescription string `json:"productaddondescription"`
Discountid int `json:"discountid"`
Discountname string `json:"discountname"`
Discountcode string `json:"discountcode"`
Discountterms string `json:"discountterms"`
Discountpercentage float64 `json:"discountpercentage"`
Discountamount float64 `json:"discountamount"`
Landingamount float64 `json:"landingamount"`
Taxpercentage float64 `json:"taxpercentage"`
Taxamount float64 `json:"taxamount"`
Productsumprice float64 `json:"productsumprice"`
Itemstatus string `json:"itemstatus"`
Delivered string `json:"delivered"`
Orderamount float64 `json:"-" gorm:"-"`
Productimage string `json:"productimage" gorm:"-"` // only for JSON
}
type CustomerOrder struct {
Orderheaderid int `json:"orderheaderid" gorm:"Primary_Key"`
Applocationid int `json:"applocationid"`
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Partnerid int `json:"partnerid"`
Configid int `json:"configid"`
Categoryid int `json:"categoryid"`
Subcategoryid int `json:"subcategoryid"`
Moduleid int `json:"moduleid"`
Orderid string `json:"orderid"`
Orderstatus string `json:"orderstatus"`
Orderdate string `json:"orderdate,omitempty"`
Ordernotes string `json:"ordernotes"`
Itemcount int `json:"itemcount"`
Deliverytime string `json:"deliverytime"` // alias for delivered
Pending string `json:"pending"`
Processing string `json:"processing"`
Ready string `json:"ready"`
Delivered string `json:"delivered"` // original delivered column
Cancelled string `json:"cancelled"`
Deliverycharge float32 `json:"deliverycharge"`
Kms FlexibleString `json:"kms"`
Smsdelivery int `json:"smsdelivery"`
// Tenant Info
Tenantname string `json:"tenantname"`
Tenanttoken string `json:"tenanttoken"`
Tenantcontactno string `json:"tenantcontactno"`
Tenantpostcode string `json:"tenantpostcode"`
Tenantsuburb string `json:"tenantsuburb"`
Tenantcity string `json:"tenantcity"`
Registrationno string `json:"registrationno"`
// Location Info
Locationname string `json:"locationname"`
Locationcontactno string `json:"locationcontactno"`
Locationpostcode string `json:"locationpostcode"`
Locationsuburb string `json:"locationsuburb"`
Locationcity string `json:"locationcity"`
// App Location
Applocation string `json:"applocation"`
// Delivery Info (from `deliveries` table)
Deliverystatus string `json:"deliverystatus"`
DeliveryUserID int `json:"deliveryid"` // alias
Assigntime string `json:"assigntime"`
Starttime string `json:"starttime"`
Arrivaltime string `json:"arrivaltime"`
Pickuptime string `json:"pickuptime"`
Finaldeliverytime string `json:"finaldeliverytime"` // alias for f.deliverytime
Canceltime string `json:"canceltime"`
Riderkms FlexibleString `json:"riderkms"`
// Actualkms sql.NullFloat64 `json:"actualkms"`
// Actualkms sql.NullFloat64 `json:"actualkms"`
// Riderkms sql.NullFloat64 `json:"riderkms"`
// Deliveryamt sql.NullFloat64 `json:"deliveryamt"`
Droplat FlexibleString `json:"droplat"`
Droplon FlexibleString `json:"droplon"`
// Rider
Rider string `json:"rider"`
Orderamount float64 `json:"orderamount"`
Taxamount float64 `json:"taxamount"`
Totaltaxamount float64 `json:"totaltaxamount"`
Riderkms_v2 FlexibleString `json:"-" gorm:"column:riderkms"` // Extra safety
// Items if applicable
OrderDetails []OrderDetails `json:"orderdetails" gorm:"-"`
}
type Ordersequences struct {
Sequenceid int `json:"sequenceid" gorm:"Primary_Key"`
Tenantid int `json:"tenantid"`
Orderprefix string `json:"orderprefix" gorm:"default:ORD"`
Customerprefix string `json:"customerprefix" gorm:"default:CUS"`
Appointmentprefix string `json:"appointmentprefix" gorm:"default:ORD"`
Receiptprefix string `json:"receiptprefix" gorm:"default:REC"`
Paymentprefix string `json:"paymentprefix" gorm:"default:PAY"`
}
// ── Offline (in-store) sales import ───────────────────────────────────────────
//
// A sale rung up at the counter never passes through the app, so nothing
// deducts its stock. These types carry a spreadsheet of such sales into the
// same order path online orders use, so one ledger remains the single source
// of truth for stock and one revenue figure covers both channels.
// OfflineSaleItem is one spreadsheet row. Only Productid and Qtysold are
// required; the rest fall back to the product's own pricing when left blank.
// Productname is carried for verification against Productid, not for matching
// (see SaleTemplateRow for why a name can't be a key).
type OfflineSaleItem struct {
Productid int `json:"productid"`
Productname string `json:"productname"`
Qtysold float64 `json:"qtysold"`
Unitprice float64 `json:"unitprice"`
Discountamount float64 `json:"discountamount"`
Taxpercent float64 `json:"taxpercent"`
}
// OfflineSaleBill is one counter bill — the rows of a spreadsheet grouped by
// their branch and bill number.
//
// Locationid is the branch the bill was rung up at, taken from the spreadsheet
// row rather than from a store the operator picked in the UI. One workbook can
// therefore carry sales for every branch a merchant runs, and each bill's stock
// comes out of its own outlet. Bills are grouped per branch, so the same bill
// number at two outlets is two separate sales, not a duplicate.
//
// Billno is what makes a re-upload of the same file safe: it is recorded on the
// order and refused if already present for that branch.
type OfflineSaleBill struct {
Locationid int `json:"locationid"`
Billno string `json:"billno"`
Saledate string `json:"saledate"`
Paymentmode string `json:"paymentmode"`
Customername string `json:"customername"`
Customermobile string `json:"customermobile"`
Remarks string `json:"remarks"`
Items []OfflineSaleItem `json:"items"`
}
// OfflineSalesUpload is the request body.
//
// Locationid here is a scope constraint, not the destination. Left at 0 the
// bills go to whichever branch each one names, which is what a multi-branch
// owner uploads. Set to a branch it pins the whole upload to that outlet and
// any bill naming a different one is refused — that is how a store user is
// held to their own store no matter what the spreadsheet says.
//
// Every branch referenced is checked against Tenantid regardless, so no upload
// can reach an outlet the merchant does not own.
type OfflineSalesUpload struct {
Tenantid int `json:"tenantid"`
Locationid int `json:"locationid"`
Userid int `json:"userid"`
Bills []OfflineSaleBill `json:"bills"`
}
// Outcomes a single bill can have. A bill is all-or-nothing: it either commits
// with its stock movement or it leaves nothing behind.
const (
OfflineSaleImported = "imported"
OfflineSaleDuplicate = "duplicate"
OfflineSaleFailed = "failed"
)
// OfflineSaleResult reports one bill's fate. Bills are independent, so a file
// with one bad bill still imports the rest and names exactly what it skipped.
// The branch is echoed back because a single upload spans several, and "bill 7
// failed" is not actionable without knowing which store it belonged to.
type OfflineSaleResult struct {
Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
Billno string `json:"billno"`
Status string `json:"status"`
Orderid string `json:"orderid"`
Orderheaderid int `json:"orderheaderid"`
Itemcount int `json:"itemcount"`
Amount float64 `json:"amount"`
Message string `json:"message"`
}
type OfflineSalesUploadResponse struct {
Imported int `json:"imported"`
Duplicate int `json:"duplicate"`
Failed int `json:"failed"`
Totalamount float64 `json:"totalamount"`
Results []OfflineSaleResult `json:"results"`
}
type TenantRevenueSummary struct {
Tenantid int `json:"tenantid"`
Tenantname string `json:"tenantname"`
OverallRevenue float64 `json:"overallrevenue"`
LocationRevenue []LocationRevenueDetails `json:"locationrevenue"`
}
type LocationRevenueDetails struct {
Locationid int `json:"locationid"`
Locationname string `json:"locationname"`
Revenue float64 `json:"revenue"`
}
type SalesSummaryChartData struct {
Date string `json:"date"`
Revenue float64 `json:"revenue"`
Orders int `json:"orders"`
}
type SalesSummaryTopLocation struct {
Locationname string `json:"locationname"`
Revenue float64 `json:"revenue"`
}
type SalesSummaryResponse struct {
TotalRevenue float64 `json:"totalRevenue"`
TotalOrders int `json:"totalOrders"`
AverageOrderValue float64 `json:"averageOrderValue"`
ChartData []SalesSummaryChartData `json:"chartData"`
TopLocations []SalesSummaryTopLocation `json:"topLocations"`
}
type TimeSeriesData struct {
Label string `json:"label"`
Orders int `json:"orders"`
Revenue float64 `json:"revenue"`
Cancelled int `json:"cancelled"`
Delivered int `json:"delivered"`
Activeskus int `json:"activeskus"`
}