package repository import ( "context" "errors" "fmt" "apskel-pos-be/internal/entities" "github.com/google/uuid" "gorm.io/gorm" ) type InventoryRepository interface { Create(ctx context.Context, inventory *entities.Inventory) error GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error) GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error) GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) Update(ctx context.Context, inventory *entities.Inventory) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error) Count(ctx context.Context, filters map[string]interface{}) (int64, error) AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) } type InventoryRepositoryImpl struct { db *gorm.DB } func NewInventoryRepositoryImpl(db *gorm.DB) *InventoryRepositoryImpl { return &InventoryRepositoryImpl{ db: db, } } func (r *InventoryRepositoryImpl) Create(ctx context.Context, inventory *entities.Inventory) error { return r.db.WithContext(ctx).Create(inventory).Error } func (r *InventoryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) { var inventory entities.Inventory err := r.db.WithContext(ctx).First(&inventory, "id = ?", id).Error if err != nil { return nil, err } return &inventory, nil } func (r *InventoryRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) { var inventory entities.Inventory err := r.db.WithContext(ctx). Preload("Product"). Preload("Product.Category"). Preload("Outlet"). First(&inventory, "id = ?", id).Error if err != nil { return nil, err } return &inventory, nil } func (r *InventoryRepositoryImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error) { var inventory entities.Inventory err := r.db.WithContext(ctx).Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error if err != nil { return nil, err } return &inventory, nil } func (r *InventoryRepositoryImpl) GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) { var inventory []*entities.Inventory err := r.db.WithContext(ctx). Preload("Product"). Preload("Product.Category"). Where("outlet_id = ?", outletID). Find(&inventory).Error return inventory, err } func (r *InventoryRepositoryImpl) GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error) { var inventory []*entities.Inventory err := r.db.WithContext(ctx). Preload("Outlet"). Where("product_id = ?", productID). Find(&inventory).Error return inventory, err } func (r *InventoryRepositoryImpl) GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) { var inventory []*entities.Inventory err := r.db.WithContext(ctx). Preload("Product"). Preload("Product.Category"). Where("outlet_id = ? AND quantity <= reorder_level", outletID). Find(&inventory).Error return inventory, err } func (r *InventoryRepositoryImpl) GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) { var inventory []*entities.Inventory err := r.db.WithContext(ctx). Preload("Product"). Preload("Product.Category"). Where("outlet_id = ? AND quantity = 0", outletID). Find(&inventory).Error return inventory, err } func (r *InventoryRepositoryImpl) Update(ctx context.Context, inventory *entities.Inventory) error { return r.db.WithContext(ctx).Save(inventory).Error } func (r *InventoryRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { return r.db.WithContext(ctx).Delete(&entities.Inventory{}, "id = ?", id).Error } func (r *InventoryRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error) { var inventory []*entities.Inventory var total int64 query := r.db.WithContext(ctx).Model(&entities.Inventory{}). Preload("Product"). Preload("Product.Category"). Preload("Outlet") for key, value := range filters { switch key { case "search": searchValue := "%" + value.(string) + "%" query = query.Joins("JOIN products ON inventory.product_id = products.id"). Where("products.name ILIKE ? OR products.sku ILIKE ?", searchValue, searchValue) case "low_stock": if value.(bool) { query = query.Where("quantity <= reorder_level") } case "zero_stock": if value.(bool) { query = query.Where("quantity = 0") } case "category_id": query = query.Joins("JOIN products ON inventory.product_id = products.id"). Where("products.category_id = ?", value) default: query = query.Where(key+" = ?", value) } } if err := query.Count(&total).Error; err != nil { return nil, 0, err } err := query.Limit(limit).Offset(offset).Find(&inventory).Error return inventory, total, err } func (r *InventoryRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { var count int64 query := r.db.WithContext(ctx).Model(&entities.Inventory{}) for key, value := range filters { switch key { case "search": searchValue := "%" + value.(string) + "%" query = query.Joins("JOIN products ON inventory.product_id = products.id"). Where("products.name ILIKE ? OR products.sku ILIKE ?", searchValue, searchValue) case "low_stock": if value.(bool) { query = query.Where("quantity <= reorder_level") } case "zero_stock": if value.(bool) { query = query.Where("quantity = 0") } case "category_id": query = query.Joins("JOIN products ON inventory.product_id = products.id"). Where("products.category_id = ?", value) default: query = query.Where(key+" = ?", value) } } err := query.Count(&count).Error return count, err } func (r *InventoryRepositoryImpl) AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) { var inventory entities.Inventory err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Try to find existing inventory if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // Inventory doesn't exist, create it with initial quantity inventory = entities.Inventory{ ProductID: productID, OutletID: outletID, Quantity: 0, ReorderLevel: 0, } if err := tx.Create(&inventory).Error; err != nil { return fmt.Errorf("failed to create inventory record: %w", err) } } else { return err } } inventory.UpdateQuantity(delta) return tx.Save(&inventory).Error }) if err != nil { return nil, err } return &inventory, nil } func (r *InventoryRepositoryImpl) SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error) { var inventory entities.Inventory err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { inventory = entities.Inventory{ ProductID: productID, OutletID: outletID, Quantity: quantity, ReorderLevel: 0, } if inventory.Quantity < 0 { inventory.Quantity = 0 } if err := tx.Create(&inventory).Error; err != nil { return fmt.Errorf("failed to create inventory record: %w", err) } return nil } return err } // Set new quantity inventory.Quantity = quantity if inventory.Quantity < 0 { inventory.Quantity = 0 } // Save updated inventory return tx.Save(&inventory).Error }) if err != nil { return nil, err } return &inventory, nil } func (r *InventoryRepositoryImpl) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error { return r.db.WithContext(ctx).Model(&entities.Inventory{}). Where("id = ?", id). Update("reorder_level", reorderLevel).Error } func (r *InventoryRepositoryImpl) BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error { return r.db.WithContext(ctx).CreateInBatches(inventoryItems, 100).Error } func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for productID, delta := range adjustments { var inventory entities.Inventory if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // Inventory doesn't exist, create it with initial quantity inventory = entities.Inventory{ ProductID: productID, OutletID: outletID, Quantity: 0, ReorderLevel: 0, } if err := tx.Create(&inventory).Error; err != nil { return fmt.Errorf("failed to create inventory record for product %s: %w", productID, err) } } else { return err } } inventory.UpdateQuantity(delta) if err := tx.Save(&inventory).Error; err != nil { return err } } return nil }) } func (r *InventoryRepositoryImpl) GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) { var totalValue float64 err := r.db.WithContext(ctx). Table("inventory"). Select("SUM(inventory.quantity * products.cost)"). Joins("JOIN products ON inventory.product_id = products.id"). Where("inventory.outlet_id = ?", outletID). Scan(&totalValue).Error return totalValue, err }