Compare commits
2 Commits
5d9879657b
...
c2f7d2481c
| Author | SHA1 | Date | |
|---|---|---|---|
| c2f7d2481c | |||
| f57127444f |
@@ -124,11 +124,16 @@ type Locationproducts struct {
|
||||
Productcombo int `json:"productcombo" gorm:"default:0"`
|
||||
Variants int `json:"variants" gorm:"default:0"`
|
||||
Quantity int `json:"quantity"`
|
||||
Retailprice float64 `json:"retailprice,omitempty"`
|
||||
Diffprice float64 `json:"diffprice,omitempty"`
|
||||
Diffpercent float64 `json:"diffpercent,omitempty"`
|
||||
Othercost float64 `json:"othercost,omitempty"`
|
||||
Approve int `json:"approve" gorm:"default:0"`
|
||||
// Price is the per-store selling price from productlocations.price — the one
|
||||
// CreateProductLocation upserts. Read-only here: it comes from the joined
|
||||
// productlocations row, not from products. Without it a store could set a
|
||||
// price and never read it back, so the UI always showed the master price.
|
||||
Price float64 `json:"price" gorm:"->"`
|
||||
Retailprice float64 `json:"retailprice,omitempty"`
|
||||
Diffprice float64 `json:"diffprice,omitempty"`
|
||||
Diffpercent float64 `json:"diffpercent,omitempty"`
|
||||
Othercost float64 `json:"othercost,omitempty"`
|
||||
Approve int `json:"approve" gorm:"default:0"`
|
||||
// Productstatus string `json:"productstatus" gorm:"default:available"`
|
||||
Status string `json:"status" gorm:"default:outofstock"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"nearle/models"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -1130,6 +1131,49 @@ func (r *orderRepository) CreateOrder(data models.Orders) (models.Orders, error)
|
||||
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
|
||||
|
||||
@@ -405,7 +405,11 @@ func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategor
|
||||
|
||||
params := []interface{}{tenantID, locationID}
|
||||
|
||||
query := `SELECT a.*, b.productlocationid, b.status,
|
||||
// b.price is the per-store selling price. It has to be selected explicitly:
|
||||
// a.* only covers products (whose price column is retailprice, the master
|
||||
// price), so without this the catalogue could never read back a price set
|
||||
// for this outlet via CreateProductLocation.
|
||||
query := `SELECT a.*, b.productlocationid, b.status, b.price,
|
||||
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' THEN c.quantity ELSE 0 END), 0) AS total_in,
|
||||
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'OUT' THEN c.quantity ELSE 0 END), 0) AS total_out,
|
||||
COALESCE(SUM(CASE WHEN UPPER(c.stocktype) = 'IN' THEN c.quantity ELSE 0 END) -
|
||||
@@ -427,7 +431,7 @@ func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategor
|
||||
|
||||
query += ` GROUP BY a.productid, a.productname, a.productimage, a.categoryid, a.subcategoryid,
|
||||
a.productunit, a.productcost, a.taxpercent, a.taxamount, a.retailprice,
|
||||
b.tenantid, b.locationid, b.productlocationid, b.status
|
||||
b.tenantid, b.locationid, b.productlocationid, b.status, b.price
|
||||
ORDER BY a.productid DESC LIMIT ? OFFSET ?`
|
||||
|
||||
params = append(params, pagesize, offset)
|
||||
|
||||
Reference in New Issue
Block a user