mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
unfinish: fifo system
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# Mesin Stok FIFO
|
||||
|
||||
Utilitas FIFO bersifat reusable dan dibagi menjadi dua lapis:
|
||||
|
||||
1. **Registry (`internal/utils/fifo`)** – mendeklarasikan tabel mana yang bersifat `Stockable` (sumber stok) atau `Usable` (pemakai stok). Setiap modul cukup menyebutkan nama tabel dan kolom wajib:
|
||||
- Stockable: `id`, `product_warehouse_id`, `total_qty`, `total_used_qty`, `created_at`
|
||||
- Usable: `id`, `product_warehouse_id`, `usage_qty`, `pending_qty`, `created_at`
|
||||
2. **Service (`internal/common/service/common.fifo.service.go`)** – memakai registry tersebut untuk:
|
||||
- Menambah stok baru (`Replenish`).
|
||||
- Menyinkronkan total pemakaian (`Consume`). Method ini idempotent: panggil dengan *total kuantitas yang diinginkan* (mis. saat create/update/delete). Service menghitung selisih terhadap `usage_qty + pending_qty`, kemudian otomatis mengalokasikan tambahan atau melepaskan selisihnya.
|
||||
- Membatalkan pemakaian (`ReleaseUsage`) yang mengembalikan stok lalu memicu alokasi ulang ke antrian pending.
|
||||
- Baik `Replenish` maupun pelepasan stok akan menjalankan `resolvePendingForWarehouse`, sehingga pending tertua langsung terisi ketika stok tersedia.
|
||||
|
||||
## Registrasi tabel
|
||||
|
||||
```go
|
||||
import (
|
||||
commonservice "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
fifoSvc := commonservice.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
fifoSvc.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("PURCHASE_DETAIL"),
|
||||
Table: "purchase_details",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
})
|
||||
|
||||
fifoSvc.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKey("RECORDING_STOCK"),
|
||||
Table: "recording_stocks",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Each registration optionally accepts an order clause or base scope (e.g. to exclude drafts).
|
||||
|
||||
Setiap registrasi bisa diberi klausa urutan atau scope dasar (mis. untuk mengecualikan draft).
|
||||
|
||||
## Menggunakan service di modul
|
||||
|
||||
1. **Saat stok masuk** (mis. purchase selesai): panggil `fifoSvc.Replenish(...)` dengan key stockable, id record, id product warehouse, dan kuantitas yang baru tersedia. Service akan:
|
||||
- Menambah `total_qty` pada tabel stockable,
|
||||
- Menambah `product_warehouses.quantity`,
|
||||
- Mencoba membersihkan `pending_qty` dari semua usable yang terdaftar (sesuai urutan FIFO).
|
||||
2. **Saat modul memakai stok** (recording, marketing, dsb.) panggil `fifoSvc.Consume(...)` dengan total qty terbaru.
|
||||
- Jika qty baru lebih besar, service mengambil stok FIFO dan menambah `usage_qty`; kekurangan dicatat sebagai `pending_qty`.
|
||||
- Jika qty baru lebih kecil, service otomatis menurunkan `pending_qty` lebih dulu, lalu melepaskan alokasi aktif (stok kembali ke gudang) dan langsung dipakai untuk mengisi pending milik entitas lain.
|
||||
- Hapus data? panggil `Consume` dengan qty 0 atau gunakan `ReleaseUsage`.
|
||||
3. **Jika dibatalkan penuh**: `fifoSvc.ReleaseUsage(...)` mengosongkan `usage_qty/pending_qty` dan menandai baris pivot sebagai `RELEASED`.
|
||||
|
||||
Tabel pivot (`stock_allocations`) menyimpan asal pemakaian secara presisi, sehingga audit trail dan rollback stok menjadi deterministik.
|
||||
@@ -0,0 +1,5 @@
|
||||
package fifo
|
||||
|
||||
const (
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user