Compare commits
3 Commits
e48fb4cf57
...
d3a7466f4c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3a7466f4c | ||
|
|
af55325a5b | ||
|
|
5bd1f52d41 |
124
MOBILE_ORDER_VERIFICATION.md
Normal file
124
MOBILE_ORDER_VERIFICATION.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Order Creation — Mobile Developer Verification Guide
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
We found that orders placed through the app were being saved with **zero line
|
||||||
|
items** — the order header (tenant, location, customer) saved fine, but the
|
||||||
|
`items` array was silently getting dropped somewhere between the app and the
|
||||||
|
database. Because the stock check only runs over whatever's in `items`, an
|
||||||
|
order with no items also skipped stock validation entirely.
|
||||||
|
|
||||||
|
The backend now tolerates a few different request shapes and has a stock
|
||||||
|
check in place, but the app's actual request needs to be verified against
|
||||||
|
what's below to confirm it lines up.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://fiesta.nearle.app/live/api/v1/mob/orders/createorder
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request shape — items MUST be inside `orders`, not a sibling of it
|
||||||
|
|
||||||
|
**Correct:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orders": {
|
||||||
|
"tenantid": 1135,
|
||||||
|
"locationid": 1166,
|
||||||
|
"customerid": 42,
|
||||||
|
"items": [
|
||||||
|
{ "productid": 7060, "orderqty": 2, "price": 45.0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Also accepted (flat, no wrapper):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tenantid": 1135,
|
||||||
|
"locationid": 1166,
|
||||||
|
"customerid": 42,
|
||||||
|
"items": [
|
||||||
|
{ "productid": 7060, "orderqty": 2, "price": 45.0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**This shape used to silently lose the items — avoid it:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"orders": { "tenantid": 1135, "locationid": 1166 },
|
||||||
|
"items": [ { "productid": 7060, "orderqty": 2 } ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`items` as a sibling of `orders` (not nested inside it) is now handled as a
|
||||||
|
fallback server-side too, but don't rely on the fallback — put `items` inside
|
||||||
|
`orders` to match the primary/documented shape.
|
||||||
|
|
||||||
|
## Required fields per item
|
||||||
|
|
||||||
|
| field | type | required | notes |
|
||||||
|
|-------------|--------|----------|-------------------------------------------|
|
||||||
|
| `productid` | int | yes | must be a real product for the tenant |
|
||||||
|
| `orderqty` | number | yes | quantity being ordered |
|
||||||
|
| `price` | number | recommended | unit price at time of order |
|
||||||
|
| `locationid`| int | no | defaults to the order's own `locationid` if omitted |
|
||||||
|
|
||||||
|
## Expected responses — verify your app handles all of these
|
||||||
|
|
||||||
|
| Scenario | HTTP code | Body (key fields) |
|
||||||
|
|---|---|---|
|
||||||
|
| Order succeeds | `200` | `"status": true`, `"details": { "orderheaderid": ..., "items": [...] }` |
|
||||||
|
| No `tenantid` at all | `409` | `"status": false`, `"message": "Tenant ID is required"` |
|
||||||
|
| `items` missing/empty | `400` | `"status": false`, `"message": "Order must contain at least one item"` |
|
||||||
|
| Requested qty > available stock | `409` | `"status": false`, `"message": "insufficient stock for product '<name>': requested X, available Y"` |
|
||||||
|
|
||||||
|
**Important:** a `409` with "insufficient stock" is not a network/server
|
||||||
|
error — it's the correct, expected response when a customer tries to order
|
||||||
|
more than what's in stock at that store. The app should catch this
|
||||||
|
specifically (check the message text, or treat any `409` from this endpoint
|
||||||
|
as a stock problem) and show the customer a clear "not enough stock" message
|
||||||
|
rather than a generic error screen.
|
||||||
|
|
||||||
|
## How to verify end-to-end yourselves
|
||||||
|
|
||||||
|
1. Pick a real `tenantid` + `locationid` + `productid` combo you know has
|
||||||
|
stock (ask backend/ops for current numbers, or check via the merchant
|
||||||
|
web app's inventory view).
|
||||||
|
2. Place a normal order for 1 unit through the app. Confirm it returns `200`
|
||||||
|
and the response's `details.items` array is non-empty.
|
||||||
|
3. Place an order for a quantity larger than what's currently in stock for
|
||||||
|
that product/location. Confirm you get a `409` with an "insufficient
|
||||||
|
stock" message, and that the app surfaces this to the user instead of
|
||||||
|
silently failing or showing a generic error.
|
||||||
|
4. Cancel a successful order and confirm a follow-up stock check reflects
|
||||||
|
the restored quantity (ask backend to check, or place the same
|
||||||
|
over-quantity order again afterward — it should now succeed if
|
||||||
|
cancellation restored enough stock).
|
||||||
|
|
||||||
|
## Checking stock before the customer even taps "order"
|
||||||
|
|
||||||
|
`GET /live/api/v1/mob/products/getproductbyvariant` now accepts an optional
|
||||||
|
`locationid` query param:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /live/api/v1/mob/products/getproductbyvariant?tenantid=1135&variantid=44&locationid=1166
|
||||||
|
```
|
||||||
|
|
||||||
|
When `locationid` is passed, each returned product now carries two extra
|
||||||
|
live fields:
|
||||||
|
|
||||||
|
| field | meaning |
|
||||||
|
|---|---|
|
||||||
|
| `productstock` | live available quantity at that store — same SUM(in)-SUM(out) formula the order stock check uses |
|
||||||
|
| `locationstatus` | that store's status for this product, e.g. `"outofstock"` or `"available"`/`"Active"` |
|
||||||
|
|
||||||
|
**If `locationid` is omitted, both fields come back empty/zero** — this is
|
||||||
|
the old behavior preserved for backward compatibility, not new stock data.
|
||||||
|
Start passing `locationid` (the store the customer is browsing) to get real
|
||||||
|
numbers, and use it to show "out of stock" / gray out the add-to-cart button
|
||||||
|
*before* the customer tries to order, instead of only finding out from the
|
||||||
|
`409` response above.
|
||||||
@@ -303,6 +303,24 @@ func (ctl *OrderController) CreateOrderv3(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🛠️ Strategy 3: some clients send the header under "orders" but the line
|
||||||
|
// items as a SIBLING top-level "items" array rather than nested inside
|
||||||
|
// it. Strategy 2's OrderWrapper only has an "orders" field, so
|
||||||
|
// encoding/json silently drops that sibling key — the order header
|
||||||
|
// parses fine but data.Items ends up empty, which used to let the order
|
||||||
|
// go through with zero items and skip the stock check entirely (the
|
||||||
|
// pre-validation loop below iterates over data.Items). Pick it up here
|
||||||
|
// if strategies 1/2 left Items empty.
|
||||||
|
if len(data.Items) == 0 {
|
||||||
|
type ItemsWrapper struct {
|
||||||
|
Items []models.OrderDetail `json:"items"`
|
||||||
|
}
|
||||||
|
var itemsWrapper ItemsWrapper
|
||||||
|
if err := c.BodyParser(&itemsWrapper); err == nil && len(itemsWrapper.Items) > 0 {
|
||||||
|
data.Items = itemsWrapper.Items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Double check we have the required ID
|
// Double check we have the required ID
|
||||||
if data.Tenantid == 0 {
|
if data.Tenantid == 0 {
|
||||||
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
return c.Status(http.StatusConflict).JSON(fiber.Map{
|
||||||
@@ -312,6 +330,18 @@ func (ctl *OrderController) CreateOrderv3(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An order with no line items has nothing to check stock against — the
|
||||||
|
// pre-validation loop in CreateOrder simply wouldn't run, silently
|
||||||
|
// creating a phantom header-only order that never deducted stock.
|
||||||
|
// Reject it outright instead.
|
||||||
|
if len(data.Items) == 0 {
|
||||||
|
return c.Status(http.StatusBadRequest).JSON(fiber.Map{
|
||||||
|
"code": http.StatusBadRequest,
|
||||||
|
"message": "Order must contain at least one item",
|
||||||
|
"status": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(data.Orderdate) == "" {
|
if strings.TrimSpace(data.Orderdate) == "" {
|
||||||
data.Orderdate = time.Now().Format("2006-01-02 15:04:05")
|
data.Orderdate = time.Now().Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,8 +367,9 @@ func (ctl *ProductController) GetProductByVariant(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
tenantID, _ := strconv.Atoi(c.Query("tenantid"))
|
tenantID, _ := strconv.Atoi(c.Query("tenantid"))
|
||||||
variantid, _ := strconv.Atoi(c.Query("variantid"))
|
variantid, _ := strconv.Atoi(c.Query("variantid"))
|
||||||
|
locationID, _ := strconv.Atoi(c.Query("locationid"))
|
||||||
|
|
||||||
result, err := ctl.productService.GetProductByVariant(tenantID, variantid)
|
result, err := ctl.productService.GetProductByVariant(tenantID, variantid, locationID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,13 @@ type Products struct {
|
|||||||
Othercost float64 `json:"othercost,omitempty"`
|
Othercost float64 `json:"othercost,omitempty"`
|
||||||
Approve int `json:"approve"`
|
Approve int `json:"approve"`
|
||||||
Productstatus string `json:"productstatus" `
|
Productstatus string `json:"productstatus" `
|
||||||
|
// Populated only by queries scoped to a specific location (e.g.
|
||||||
|
// GetProductByVariant when locationid is passed): Productstock becomes
|
||||||
|
// the live SUM(in)-SUM(out) balance from productstocks — the same
|
||||||
|
// formula CreateOrder's stock check uses — and Locationstatus mirrors
|
||||||
|
// productlocations.status ("outofstock"/"available") for that store.
|
||||||
|
// Left at zero values for callers that don't scope to a location.
|
||||||
|
Locationstatus string `json:"locationstatus,omitempty" gorm:"->"`
|
||||||
// Status string `json:"status" gorm:"default:InActive"`
|
// Status string `json:"status" gorm:"default:InActive"`
|
||||||
// Status string `json:"status" gorm:"-"`
|
// Status string `json:"status" gorm:"-"`
|
||||||
}
|
}
|
||||||
@@ -306,6 +313,15 @@ type Productlocations struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductLocationRef identifies a single (tenant, location, product) row in
|
||||||
|
// productlocations — used to reactivate it after new stock arrives, the
|
||||||
|
// per-location counterpart to CreateOrder's outofstock flag.
|
||||||
|
type ProductLocationRef struct {
|
||||||
|
Tenantid int
|
||||||
|
Locationid int
|
||||||
|
Productid int
|
||||||
|
}
|
||||||
|
|
||||||
type ProductSubcategory struct {
|
type ProductSubcategory struct {
|
||||||
Subcatid int `json:"subcatid"`
|
Subcatid int `json:"subcatid"`
|
||||||
Categoryid int `json:"categoryid"`
|
Categoryid int `json:"categoryid"`
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type ProductRepository interface {
|
|||||||
GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error)
|
GetProductStocks(tenantID, locationID string) ([]models.Productstocks, error)
|
||||||
CreateProductStock(stocks []models.Productstock) error
|
CreateProductStock(stocks []models.Productstock) error
|
||||||
UpdateProductStatus(productIDs []int, status string) error
|
UpdateProductStatus(productIDs []int, status string) error
|
||||||
|
ReactivateProductLocations(refs []models.ProductLocationRef) error
|
||||||
CreateProduct(product models.Products) error
|
CreateProduct(product models.Products) error
|
||||||
UpdateProduct(product models.Products) error
|
UpdateProduct(product models.Products) error
|
||||||
DeleteProduct(productID int) error
|
DeleteProduct(productID int) error
|
||||||
@@ -29,7 +30,7 @@ type ProductRepository interface {
|
|||||||
GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error)
|
GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error)
|
||||||
GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error)
|
GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error)
|
||||||
FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID int, keyword, productStatus, approve string, pageno, pagesize int) ([]models.Tenantproducts, error)
|
FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID int, keyword, productStatus, approve string, pageno, pagesize int) ([]models.Tenantproducts, error)
|
||||||
GetProductByVariant(tenantid, variantid int) ([]models.Products, error)
|
GetProductByVariant(tenantid, variantid, locationid int) ([]models.Products, error)
|
||||||
GetSubcategories(categoryID int) ([]models.Subcategory, error)
|
GetSubcategories(categoryID int) ([]models.Subcategory, error)
|
||||||
GetProducts(params models.ProductFilter) ([]models.Products, error)
|
GetProducts(params models.ProductFilter) ([]models.Products, error)
|
||||||
GetTenantInfo(tenantID, applocationID int) (map[string]interface{}, error)
|
GetTenantInfo(tenantID, applocationID int) (map[string]interface{}, error)
|
||||||
@@ -263,6 +264,23 @@ func (r *productRepository) CreateProductStock(stocks []models.Productstock) err
|
|||||||
return r.db.Table("productstocks").Create(&stocks).Error
|
return r.db.Table("productstocks").Create(&stocks).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReactivateProductLocations flips productlocations.status back to
|
||||||
|
// "available" for each ref — the counterpart to CreateOrder flagging a
|
||||||
|
// location "outofstock" when its stock hits zero. Without this, a store
|
||||||
|
// that runs out and then restocks via an approved stock request stays
|
||||||
|
// flagged outofstock forever, since receiving stock only ever added to the
|
||||||
|
// productstocks ledger and never touched this per-location flag.
|
||||||
|
func (r *productRepository) ReactivateProductLocations(refs []models.ProductLocationRef) error {
|
||||||
|
for _, ref := range refs {
|
||||||
|
if err := r.db.Table("productlocations").
|
||||||
|
Where("tenantid = ? AND locationid = ? AND productid = ?", ref.Tenantid, ref.Locationid, ref.Productid).
|
||||||
|
Update("status", "available").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *productRepository) UpdateProductStatus(productIDs []int, status string) error {
|
func (r *productRepository) UpdateProductStatus(productIDs []int, status string) error {
|
||||||
return r.db.Table("products").
|
return r.db.Table("products").
|
||||||
Where("productid IN ?", productIDs).
|
Where("productid IN ?", productIDs).
|
||||||
@@ -567,10 +585,16 @@ func (r *productRepository) FetchFilteredProducts(
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *productRepository) GetProductByVariant(tenantid, variantid int) ([]models.Products, error) {
|
func (r *productRepository) GetProductByVariant(tenantid, variantid, locationid int) ([]models.Products, error) {
|
||||||
|
|
||||||
var data []models.Products
|
var data []models.Products
|
||||||
|
|
||||||
|
// productstock is a correlated subquery (not a JOIN+GROUP BY) so it can
|
||||||
|
// coexist with `p.*` without having to enumerate every products column.
|
||||||
|
// When locationid is 0 (caller didn't scope to a store), both the
|
||||||
|
// subquery and the productlocations join simply match nothing, so
|
||||||
|
// Productstock/Locationstatus come back zero-valued — same response
|
||||||
|
// shape as before this field existed, not an error.
|
||||||
err := r.db.
|
err := r.db.
|
||||||
Table("products p").
|
Table("products p").
|
||||||
Select(`
|
Select(`
|
||||||
@@ -578,11 +602,19 @@ func (r *productRepository) GetProductByVariant(tenantid, variantid int) ([]mode
|
|||||||
c.categoryname,
|
c.categoryname,
|
||||||
d.subcatname AS subcategoryname,
|
d.subcatname AS subcategoryname,
|
||||||
COALESCE(pd.discountvalue, 0) AS discountvalue,
|
COALESCE(pd.discountvalue, 0) AS discountvalue,
|
||||||
pd.discountid
|
pd.discountid,
|
||||||
`).
|
pl.status AS locationstatus,
|
||||||
|
COALESCE((
|
||||||
|
SELECT SUM(CASE WHEN LOWER(ps.stocktype) = 'in' THEN ps.quantity ELSE 0 END) -
|
||||||
|
SUM(CASE WHEN LOWER(ps.stocktype) = 'out' THEN ps.quantity ELSE 0 END)
|
||||||
|
FROM productstocks ps
|
||||||
|
WHERE ps.productid = p.productid AND ps.tenantid = p.tenantid AND ps.locationid = ?
|
||||||
|
), 0) AS productstock
|
||||||
|
`, locationid).
|
||||||
Joins("LEFT JOIN productcategories c ON p.categoryid = c.categoryid").
|
Joins("LEFT JOIN productcategories c ON p.categoryid = c.categoryid").
|
||||||
Joins("LEFT JOIN productsubcategories d ON p.subcategoryid = d.subcatid").
|
Joins("LEFT JOIN productsubcategories d ON p.subcategoryid = d.subcatid").
|
||||||
Joins("LEFT JOIN productdiscounts pd ON pd.productid = p.productid").
|
Joins("LEFT JOIN productdiscounts pd ON pd.productid = p.productid").
|
||||||
|
Joins("LEFT JOIN productlocations pl ON pl.productid = p.productid AND pl.tenantid = p.tenantid AND pl.locationid = ?", locationid).
|
||||||
Where("p.tenantid = ? AND p.variants = ?", tenantid, variantid).
|
Where("p.tenantid = ? AND p.variants = ?", tenantid, variantid).
|
||||||
Order("p.productid DESC").
|
Order("p.productid DESC").
|
||||||
Scan(&data).Error
|
Scan(&data).Error
|
||||||
|
|||||||
@@ -350,8 +350,15 @@ func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) err
|
|||||||
var user models.Tenantuser
|
var user models.Tenantuser
|
||||||
tx := r.db.Begin()
|
tx := r.db.Begin()
|
||||||
|
|
||||||
// Set status BEFORE insert
|
// Default to Active if the caller didn't specify — matches
|
||||||
data.Status = "InActive"
|
// tenantlocations' own gorm default and the primary-location behavior
|
||||||
|
// from tenant onboarding. Forcing InActive here used to also block the
|
||||||
|
// spawned manager login (AppLogin checks account status before it ever
|
||||||
|
// gets to the "no password set" branch), so a new store's login could
|
||||||
|
// never reach the password-setup screen.
|
||||||
|
if data.Status == "" {
|
||||||
|
data.Status = "Active"
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: Insert into tenantlocations
|
// Step 1: Insert into tenantlocations
|
||||||
if err := tx.Create(&data).Error; err != nil {
|
if err := tx.Create(&data).Error; err != nil {
|
||||||
@@ -374,7 +381,7 @@ func (r *tenantRepository) CreateTenantLocation(data models.Tenantlocations) err
|
|||||||
user.Locationid = data.Locationid
|
user.Locationid = data.Locationid
|
||||||
user.Applocationid = data.Applocationid
|
user.Applocationid = data.Applocationid
|
||||||
user.Configid = 1
|
user.Configid = 1
|
||||||
user.Status = "InActive"
|
user.Status = data.Status
|
||||||
user.Roleid = 0
|
user.Roleid = 0
|
||||||
user.Authmode = 0
|
user.Authmode = 0
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"nearle/models"
|
"nearle/models"
|
||||||
"nearle/repositories"
|
"nearle/repositories"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ type ProductService interface {
|
|||||||
GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error)
|
GetLocationProducts(tenantID, locationID, subcategoryID, pageno, pagesize int, keyword string) ([]models.Locationproducts, error)
|
||||||
GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error)
|
GetLocationProductSummary(tenantID, locationID int) ([]models.ProductSummary, error)
|
||||||
FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID int, keyword, productStatus, approve string, pageno, pagesize int) ([]models.Tenantproducts, error)
|
FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID int, keyword, productStatus, approve string, pageno, pagesize int) ([]models.Tenantproducts, error)
|
||||||
GetProductByVariant(tenantid, variantid int) ([]models.Products, error)
|
GetProductByVariant(tenantid, variantid, locationid int) ([]models.Products, error)
|
||||||
GetProductsBySubcategory(params models.ProductFilter) (map[string]interface{}, error)
|
GetProductsBySubcategory(params models.ProductFilter) (map[string]interface{}, error)
|
||||||
UpdateProductLocation(input models.Productlocations) error
|
UpdateProductLocation(input models.Productlocations) error
|
||||||
CreateProductLocation(input []models.Productlocations) error
|
CreateProductLocation(input []models.Productlocations) error
|
||||||
@@ -78,11 +79,22 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error
|
|||||||
|
|
||||||
idMap := make(map[int]struct{})
|
idMap := make(map[int]struct{})
|
||||||
var productIDs []int
|
var productIDs []int
|
||||||
for _, s := range stocks {
|
locMap := make(map[models.ProductLocationRef]struct{})
|
||||||
if s.Productid > 0 {
|
var locRefs []models.ProductLocationRef
|
||||||
if _, exists := idMap[s.Productid]; !exists {
|
for _, stk := range stocks {
|
||||||
idMap[s.Productid] = struct{}{}
|
if stk.Productid > 0 {
|
||||||
productIDs = append(productIDs, s.Productid)
|
if _, exists := idMap[stk.Productid]; !exists {
|
||||||
|
idMap[stk.Productid] = struct{}{}
|
||||||
|
productIDs = append(productIDs, stk.Productid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only "in" entries mean stock actually arrived — an "out" entry
|
||||||
|
// (a sale) should never flip a location back to available.
|
||||||
|
if stk.Productid > 0 && stk.Locationid > 0 && stk.Tenantid > 0 && strings.EqualFold(stk.Stocktype, "in") {
|
||||||
|
ref := models.ProductLocationRef{Tenantid: stk.Tenantid, Locationid: stk.Locationid, Productid: stk.Productid}
|
||||||
|
if _, exists := locMap[ref]; !exists {
|
||||||
|
locMap[ref] = struct{}{}
|
||||||
|
locRefs = append(locRefs, ref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +105,12 @@ func (s *productService) CreateProductStock(stocks []models.Productstock) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(locRefs) > 0 {
|
||||||
|
if err := s.repo.ReactivateProductLocations(locRefs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +147,11 @@ func (s *productService) FetchFilteredProducts(categoryID, subcategoryID, produc
|
|||||||
return s.repo.FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID, keyword, productStatus, approve, pageno, pagesize)
|
return s.repo.FetchFilteredProducts(categoryID, subcategoryID, productID, applocationID, tenantID, locationID, keyword, productStatus, approve, pageno, pagesize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *productService) GetProductByVariant(tenantid, variantid int) ([]models.Products, error) {
|
func (s *productService) GetProductByVariant(tenantid, variantid, locationid int) ([]models.Products, error) {
|
||||||
|
|
||||||
var data []models.Products
|
var data []models.Products
|
||||||
|
|
||||||
result, err := s.repo.GetProductByVariant(tenantid, variantid)
|
result, err := s.repo.GetProductByVariant(tenantid, variantid, locationid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in New Issue
Block a user