package repositories import ( "fmt" "strings" "nearle/models" ) // Reading counter sales back out. // // The ingest side of this package only ever writes. Without these, a bill that // reached pos_orders was invisible to every screen in the product — the data // was safe and unreachable, which is its own kind of lost. // // Every query is scoped to one locationid. That is the authorisation boundary: // a caller who omits it gets an error rather than a page through somebody // else's takings. // posSalesWhere builds the shared filter, so the list, the detail and the // summary can never disagree about what "this outlet in this range" means. func posSalesWhere(f models.PosSalesFilter) (string, []interface{}) { where := "locationid = ?" params := []interface{}{f.Locationid} // Matched on businessdate — the day the sale was rung, not the day it // reached us. A till that was offline overnight uploads yesterday's bills // this morning and they belong to yesterday. if f.Fromdate != "" && f.Todate != "" { where += " AND businessdate BETWEEN ? AND ?" params = append(params, f.Fromdate, f.Todate) } else if f.Fromdate != "" { where += " AND businessdate >= ?" params = append(params, f.Fromdate) } else if f.Todate != "" { where += " AND businessdate <= ?" params = append(params, f.Todate) } if t := strings.TrimSpace(f.Terminalid); t != "" { where += " AND terminalid = ?" params = append(params, t) } if c := strings.TrimSpace(f.Cashiername); c != "" { where += " AND cashiername = ?" params = append(params, c) } if p := strings.TrimSpace(f.Paymentmode); p != "" { where += " AND LOWER(paymentmode) = ?" params = append(params, strings.ToLower(p)) } return where, params } // Sales returns a page of bills, newest first, with the total count. // // Line items are deliberately not included: a page of fifty bills would drag // several hundred rows behind it, and a list screen shows none of them. Use // SaleDetail for one bill. func (r *posRepository) Sales(f models.PosSalesFilter) (*models.PosSalesPage, error) { if f.Locationid <= 0 { return nil, fmt.Errorf("locationid is required") } if f.Pagesize <= 0 || f.Pagesize > 500 { f.Pagesize = 50 } if f.Pageno < 0 { f.Pageno = 0 } where, params := posSalesWhere(f) var total int64 if err := r.db.Raw( fmt.Sprintf(`SELECT COUNT(*) FROM pos_orders WHERE %s`, where), params..., ).Scan(&total).Error; err != nil { return nil, err } bills := make([]models.PosOrders, 0) // Ordered by billedat rather than by id: a batch uploaded after an outage // arrives out of order, and a list sorted by arrival would interleave // yesterday's bills through today's. query := fmt.Sprintf( `SELECT * FROM pos_orders WHERE %s ORDER BY billedat DESC, posorderid DESC LIMIT ? OFFSET ?`, where) if err := r.db.Raw(query, append(params, f.Pagesize, f.Pageno*f.Pagesize)..., ).Scan(&bills).Error; err != nil { return nil, err } return &models.PosSalesPage{ Total: total, Pageno: f.Pageno, Pagesize: f.Pagesize, Bills: bills, }, nil } // SaleDetail returns one bill with its lines. // // Accepts either the terminal's own order UUID or this backend's posorderid, // because a support call starts from whichever the caller happens to be looking // at — a receipt carries the invoice number, a log carries the UUID. func (r *posRepository) SaleDetail(locationID int, reference string) (*models.PosOrders, error) { if locationID <= 0 { return nil, fmt.Errorf("locationid is required") } reference = strings.TrimSpace(reference) if reference == "" { return nil, fmt.Errorf("an order id, invoice number or posorderid is required") } var bill models.PosOrders err := r.db.Raw(` SELECT * FROM pos_orders WHERE locationid = ? AND (terminalorderid = ? OR invoicenumber = ? OR CAST(posorderid AS TEXT) = ?) LIMIT 1`, locationID, reference, reference, reference, ).Scan(&bill).Error if err != nil { return nil, err } if bill.Posorderid == 0 { return nil, nil } items := make([]models.PosOrderItems, 0) if err := r.db.Raw( `SELECT * FROM pos_order_items WHERE posorderid = ? ORDER BY posorderitemid`, bill.Posorderid, ).Scan(&items).Error; err != nil { return nil, err } bill.Items = items return &bill, nil } // SalesSummary totals a range, broken out the three ways somebody actually // asks for: by tender, by day, and by till. func (r *posRepository) SalesSummary(f models.PosSalesFilter) (*models.PosSalesSummary, error) { if f.Locationid <= 0 { return nil, fmt.Errorf("locationid is required") } where, params := posSalesWhere(f) summary := &models.PosSalesSummary{ Locationid: f.Locationid, Fromdate: f.Fromdate, Todate: f.Todate, Bypaymentmode: make([]models.PosPaymentTotal, 0), Byday: make([]models.PosDayTotal, 0), Byterminal: make([]models.PosTerminalTotal, 0), } var head struct { Billcount int Itemcount int Grosssales float64 Taxcollected float64 Discount float64 Roundoff float64 } if err := r.db.Raw(fmt.Sprintf(` SELECT COUNT(*) AS billcount, COALESCE(SUM(itemcount), 0) AS itemcount, COALESCE(SUM(total), 0) AS grosssales, COALESCE(SUM(taxamount), 0) AS taxcollected, COALESCE(SUM(discount), 0) AS discount, COALESCE(SUM(roundoff), 0) AS roundoff FROM pos_orders WHERE %s`, where), params...).Scan(&head).Error; err != nil { return nil, err } summary.Billcount = head.Billcount summary.Itemcount = head.Itemcount summary.Grosssales = head.Grosssales summary.Taxcollected = head.Taxcollected summary.Discount = head.Discount summary.Roundoff = head.Roundoff if head.Billcount > 0 { summary.Averagebill = head.Grosssales / float64(head.Billcount) } if err := r.db.Raw(fmt.Sprintf(` SELECT COALESCE(paymentmode,'') AS paymentmode, COUNT(*) AS billcount, COALESCE(SUM(total),0) AS amount FROM pos_orders WHERE %s GROUP BY paymentmode ORDER BY amount DESC`, where), params...).Scan(&summary.Bypaymentmode).Error; err != nil { return nil, err } if err := r.db.Raw(fmt.Sprintf(` SELECT businessdate, COUNT(*) AS billcount, COALESCE(SUM(total),0) AS amount FROM pos_orders WHERE %s GROUP BY businessdate ORDER BY businessdate`, where), params...).Scan(&summary.Byday).Error; err != nil { return nil, err } if err := r.db.Raw(fmt.Sprintf(` SELECT COALESCE(terminalid,'') AS terminalid, COUNT(*) AS billcount, COALESCE(SUM(total),0) AS amount FROM pos_orders WHERE %s GROUP BY terminalid ORDER BY amount DESC`, where), params...).Scan(&summary.Byterminal).Error; err != nil { return nil, err } return summary, nil }