package fifo import ( "errors" "fmt" "strings" "sync" "gorm.io/gorm" ) // QueryScope allows callers to inject custom query modifiers (preloads, filters, etc). type QueryScope func(*gorm.DB) *gorm.DB type StockableKey string type UsableKey string func (k StockableKey) String() string { return string(k) } func (k UsableKey) String() string { return string(k) } // StockableColumns describes the minimum columns required for a stock-bearing row. type StockableColumns struct { ID string ProductWarehouseID string TotalQuantity string TotalUsedQuantity string CreatedAt string } // UsableColumns describes the required columns for rows that consume stock. type UsableColumns struct { ID string ProductWarehouseID string UsageQuantity string PendingQuantity string CreatedAt string } // StockableConfig registers a table that introduces stock into the system (purchases, transfers, etc). type StockableConfig struct { Key StockableKey Table string Columns StockableColumns // OrderBy accepts raw column expressions, evaluated in-order (e.g. []string{"created_at ASC", "id ASC"}). OrderBy []string // Scope lets a module append base filters (e.g. exclude drafts). Scope QueryScope } // UsableConfig registers a table that consumes stock (recordings, adjustments, sales, etc). type UsableConfig struct { Key UsableKey Table string Columns UsableColumns OrderBy []string Scope QueryScope } var ( stockableRegistry = make(map[StockableKey]StockableConfig) usableRegistry = make(map[UsableKey]UsableConfig) registryMu sync.RWMutex ) // RegisterStockable stores the configuration so services can perform FIFO operations generically. func RegisterStockable(cfg StockableConfig) error { if err := validateStockableConfig(cfg); err != nil { return err } registryMu.Lock() defer registryMu.Unlock() key := StockableKey(strings.TrimSpace(cfg.Key.String())) if _, exists := stockableRegistry[key]; exists { return fmt.Errorf("stockable key %q already registered", key) } stockableRegistry[key] = cfg return nil } // RegisterUsable stores the configuration for stock-consuming tables. func RegisterUsable(cfg UsableConfig) error { if err := validateUsableConfig(cfg); err != nil { return err } registryMu.Lock() defer registryMu.Unlock() key := UsableKey(strings.TrimSpace(cfg.Key.String())) if _, exists := usableRegistry[key]; exists { return fmt.Errorf("usable key %q already registered", key) } usableRegistry[key] = cfg return nil } // Stockable returns the registered configuration for the key (if any). func Stockable(key StockableKey) (StockableConfig, bool) { registryMu.RLock() defer registryMu.RUnlock() cfg, ok := stockableRegistry[key] return cfg, ok } // Usable returns the registered configuration for the key (if any). func Usable(key UsableKey) (UsableConfig, bool) { registryMu.RLock() defer registryMu.RUnlock() cfg, ok := usableRegistry[key] return cfg, ok } // Stockables exposes a copy of the current registry (useful for iterating pending requests). func Stockables() map[StockableKey]StockableConfig { registryMu.RLock() defer registryMu.RUnlock() if len(stockableRegistry) == 0 { return nil } result := make(map[StockableKey]StockableConfig, len(stockableRegistry)) for key, cfg := range stockableRegistry { result[key] = cfg } return result } // Usables exposes a copy of the usable registry. func Usables() map[UsableKey]UsableConfig { registryMu.RLock() defer registryMu.RUnlock() if len(usableRegistry) == 0 { return nil } result := make(map[UsableKey]UsableConfig, len(usableRegistry)) for key, cfg := range usableRegistry { result[key] = cfg } return result } func validateStockableConfig(cfg StockableConfig) error { if strings.TrimSpace(cfg.Key.String()) == "" { return errors.New("stockable key is required") } if strings.TrimSpace(cfg.Table) == "" { return fmt.Errorf("table name is required for stockable %q", cfg.Key) } cols := cfg.Columns switch { case strings.TrimSpace(cols.ID) == "": return fmt.Errorf("column id is required for stockable %q", cfg.Key) case strings.TrimSpace(cols.ProductWarehouseID) == "": return fmt.Errorf("column product warehouse id is required for stockable %q", cfg.Key) case strings.TrimSpace(cols.TotalQuantity) == "": return fmt.Errorf("column total quantity is required for stockable %q", cfg.Key) case strings.TrimSpace(cols.TotalUsedQuantity) == "": return fmt.Errorf("column total used quantity is required for stockable %q", cfg.Key) case strings.TrimSpace(cols.CreatedAt) == "": return fmt.Errorf("column created_at is required for stockable %q", cfg.Key) } return nil } func validateUsableConfig(cfg UsableConfig) error { if strings.TrimSpace(cfg.Key.String()) == "" { return errors.New("usable key is required") } if strings.TrimSpace(cfg.Table) == "" { return fmt.Errorf("table name is required for usable %q", cfg.Key) } cols := cfg.Columns switch { case strings.TrimSpace(cols.ID) == "": return fmt.Errorf("column id is required for usable %q", cfg.Key) case strings.TrimSpace(cols.ProductWarehouseID) == "": return fmt.Errorf("column product warehouse id is required for usable %q", cfg.Key) case strings.TrimSpace(cols.UsageQuantity) == "": return fmt.Errorf("column usage quantity is required for usable %q", cfg.Key) case strings.TrimSpace(cols.PendingQuantity) == "": return fmt.Errorf("column pending quantity is required for usable %q", cfg.Key) case strings.TrimSpace(cols.CreatedAt) == "": return fmt.Errorf("column created_at is required for usable %q", cfg.Key) } return nil }