From f57127444f0e6faac387844575ceefa5aab61f44 Mon Sep 17 00:00:00 2001 From: abhishek Date: Mon, 27 Jul 2026 18:48:10 +0530 Subject: [PATCH] Lock productlocations rows before checking stock in CreateOrder The order-placement stock check (read available qty, then insert an "out" deduction) had no row lock, so two concurrent orders for the same product could both pass the availability check before either committed its deduction, overselling the item. Locks each ordered product's productlocations row with SELECT ... FOR UPDATE up front, in a fixed (productid, locationid) order across all items so overlapping concurrent orders contend for locks in the same sequence instead of deadlocking. --- repositories/orderRepository.go | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) 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