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"`
|
Productcombo int `json:"productcombo" gorm:"default:0"`
|
||||||
Variants int `json:"variants" gorm:"default:0"`
|
Variants int `json:"variants" gorm:"default:0"`
|
||||||
Quantity int `json:"quantity"`
|
Quantity int `json:"quantity"`
|
||||||
Retailprice float64 `json:"retailprice,omitempty"`
|
// Price is the per-store selling price from productlocations.price — the one
|
||||||
Diffprice float64 `json:"diffprice,omitempty"`
|
// CreateProductLocation upserts. Read-only here: it comes from the joined
|
||||||
Diffpercent float64 `json:"diffpercent,omitempty"`
|
// productlocations row, not from products. Without it a store could set a
|
||||||
Othercost float64 `json:"othercost,omitempty"`
|
// price and never read it back, so the UI always showed the master price.
|
||||||
Approve int `json:"approve" gorm:"default:0"`
|
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"`
|
// Productstatus string `json:"productstatus" gorm:"default:available"`
|
||||||
Status string `json:"status" gorm:"default:outofstock"`
|
Status string `json:"status" gorm:"default:outofstock"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"nearle/models"
|
"nearle/models"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1130,6 +1131,49 @@ func (r *orderRepository) CreateOrder(data models.Orders) (models.Orders, error)
|
|||||||
locID = data.Applocationid
|
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
|
// 🛠️ Step 1: Pre-validate stock availability for all items before placing order
|
||||||
for _, item := range data.Items {
|
for _, item := range data.Items {
|
||||||
itemLocID := item.Locationid
|
itemLocID := item.Locationid
|
||||||
|
|||||||
@@ -405,7 +405,11 @@ func (r *productRepository) GetLocationProducts(tenantID, locationID, subcategor
|
|||||||
|
|
||||||
params := []interface{}{tenantID, locationID}
|
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) = '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) = '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) -
|
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,
|
query += ` GROUP BY a.productid, a.productname, a.productimage, a.categoryid, a.subcategoryid,
|
||||||
a.productunit, a.productcost, a.taxpercent, a.taxamount, a.retailprice,
|
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 ?`
|
ORDER BY a.productid DESC LIMIT ? OFFSET ?`
|
||||||
|
|
||||||
params = append(params, pagesize, offset)
|
params = append(params, pagesize, offset)
|
||||||
|
|||||||
Reference in New Issue
Block a user