diff --git a/repositories/orderRepository.go b/repositories/orderRepository.go index 90149d1..8f6c5bc 100644 --- a/repositories/orderRepository.go +++ b/repositories/orderRepository.go @@ -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