Files
lti-api/internal/modules/closings/repositories/closing.repository_test.go
T
Giovanni Gabriel Septriadi a15fd1b174 Merge branch 'fix/sapronak-cal' into 'development'
[FIX][BE]: fix perhitunga sapronak

See merge request mbugroup/lti-api!439
2026-04-22 12:36:16 +00:00

285 lines
9.7 KiB
Go

package repository
import (
"context"
"fmt"
"testing"
"time"
"github.com/glebarez/sqlite"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
func TestSapronakIncomingPurchaseQueryPartsUsesAttributedPurchasesWhenProjectFlockKandangIDsProvided(t *testing.T) {
sql, args := sapronakIncomingPurchaseQueryParts(SapronakQueryParams{
WarehouseIDs: []uint{46},
ProjectFlockKandangIDs: []uint{101},
})
if sql != sapronakIncomingPurchasesScopedSQL() {
t.Fatalf("expected scoped purchase SQL, got %q", sql)
}
if len(args) != 11 {
t.Fatalf("expected 11 argument groups, got %d", len(args))
}
}
func TestFetchSapronakIncomingIncludesAttributedFarmPurchasesAndHistoricalWarehouseFallback(t *testing.T) {
db := setupClosingRepositoryTestDB(t)
repo := NewClosingRepository(db)
ctx := context.Background()
receivedAt := time.Date(2026, 4, 1, 4, 0, 0, 0, time.UTC)
statements := []string{
`INSERT INTO warehouses (id, kandang_id) VALUES (1, NULL), (2, 59), (3, 88)`,
`INSERT INTO product_categories (id, code) VALUES (1, 'OBT'), (2, 'RAW')`,
`INSERT INTO products (id, name, product_category_id, product_price) VALUES
(10, 'MEFISTO @1 LITER', 1, 261700),
(20, 'PAKAN GROWING CRUMBLE MALINDO', 2, 15000)`,
`INSERT INTO flags (id, flagable_id, flagable_type, name) VALUES
(1, 10, 'products', 'OVK'),
(2, 10, 'products', 'OBAT')`,
`INSERT INTO purchases (id, po_number, deleted_at) VALUES (1, 'PO-LTI-0005', NULL)`,
`INSERT INTO recordings (id, project_flock_kandangs_id, deleted_at) VALUES (11, 101, NULL), (12, 999, NULL)`,
`INSERT INTO recording_stocks (id, recording_id, product_warehouse_id, usage_qty, project_flock_kandang_id) VALUES (21, 11, 501, 150, 101), (22, 12, 502, 10, 999)`,
`INSERT INTO purchase_items (id, purchase_id, product_id, warehouse_id, project_flock_kandang_id, total_qty, price, received_date) VALUES
(1, 1, 10, 1, NULL, 100, 261700, '` + receivedAt.Format(time.RFC3339) + `'),
(2, 1, 20, 1, NULL, 50, 15000, '` + receivedAt.Format(time.RFC3339) + `'),
(3, 1, 20, 2, NULL, 25, 12000, '` + receivedAt.Format(time.RFC3339) + `'),
(4, 1, 10, 3, 999, 10, 261700, '` + receivedAt.Format(time.RFC3339) + `'),
(5, 1, 20, 1, NULL, 40, 15000, '` + receivedAt.Format(time.RFC3339) + `')`,
fmt.Sprintf(`INSERT INTO stock_allocations (id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty, allocation_purpose, status) VALUES
(1, 701, '%s', 1, '%s', 21, 100, 'CONSUME', 'ACTIVE'),
(2, 702, '%s', 2, '%s', 21, 50, 'CONSUME', 'ACTIVE'),
(3, 703, '%s', 5, '%s', 22, 40, 'CONSUME', 'ACTIVE')`,
fifo.StockableKeyPurchaseItems.String(),
fifo.UsableKeyRecordingStock.String(),
fifo.StockableKeyPurchaseItems.String(),
fifo.UsableKeyRecordingStock.String(),
fifo.StockableKeyPurchaseItems.String(),
fifo.UsableKeyRecordingStock.String(),
),
}
for _, stmt := range statements {
if err := db.Exec(stmt).Error; err != nil {
t.Fatalf("failed seeding schema: %v", err)
}
}
rows, err := repo.FetchSapronakIncoming(ctx, 101, 59, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(rows) != 2 {
t.Fatalf("expected 2 sapronak rows, got %d", len(rows))
}
byProduct := make(map[uint]SapronakIncomingRow, len(rows))
for _, row := range rows {
byProduct[row.ProductID] = row
}
if got := byProduct[10]; got.ProductID == 0 || got.Flag != "OVK" || got.Qty != 100 {
t.Fatalf("expected OVK farm purchase qty 100 for product 10, got %+v", got)
}
if got := byProduct[20]; got.ProductID == 0 || got.Flag != "PAKAN" || got.Qty != 75 {
t.Fatalf("expected PAKAN total qty 75 including farm allocated qty 50 and kandang receipt qty 25, got %+v", got)
}
}
func TestFetchSapronakAdjustmentsUsesAdjustmentReferenceAndPrice(t *testing.T) {
db := setupClosingRepositoryTestDB(t)
repo := NewClosingRepository(db)
ctx := context.Background()
statements := []string{
`INSERT INTO warehouses (id, kandang_id) VALUES (5, 5)`,
`INSERT INTO product_categories (id, code) VALUES (1, 'OBT')`,
`INSERT INTO products (id, name, product_category_id, product_price) VALUES (17, 'OVK CUT-OVER', 1, 1)`,
`INSERT INTO flags (id, flagable_id, flagable_type, name) VALUES (1, 17, 'products', 'OVK')`,
`INSERT INTO product_warehouses (id, product_id, warehouse_id, project_flock_kandang_id) VALUES (1365, 17, 5, 66)`,
`INSERT INTO adjustment_stocks (id, product_warehouse_id, total_qty, usage_qty, price) VALUES
(1139, 1365, 1, 0, 298594487),
(1140, 1365, 0, 1, 298594487)`,
fmt.Sprintf(`INSERT INTO stock_allocations (id, product_warehouse_id, stockable_type, stockable_id, usable_type, usable_id, qty, allocation_purpose, status) VALUES
(25990, 1365, '%s', 1139, '%s', 1140, 1, 'CONSUME', 'ACTIVE')`,
fifo.StockableKeyAdjustmentIn.String(),
fifo.UsableKeyAdjustmentOut.String(),
),
}
for _, stmt := range statements {
if err := db.Exec(stmt).Error; err != nil {
t.Fatalf("failed seeding schema: %v", err)
}
}
incoming, outgoing, err := repo.FetchSapronakAdjustments(ctx, 5, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
incomingRows := incoming[17]
if len(incomingRows) != 1 {
t.Fatalf("expected 1 incoming row for product 17, got %d", len(incomingRows))
}
if incomingRows[0].Reference != "ADJ-1139" {
t.Fatalf("expected incoming reference ADJ-1139, got %q", incomingRows[0].Reference)
}
if incomingRows[0].Price != 298594487 {
t.Fatalf("expected incoming price 298594487 from adjustment_stocks.price, got %.3f", incomingRows[0].Price)
}
outgoingRows := outgoing[17]
if len(outgoingRows) != 1 {
t.Fatalf("expected 1 outgoing row for product 17, got %d", len(outgoingRows))
}
if outgoingRows[0].Reference != "ADJ-1139" {
t.Fatalf("expected outgoing reference ADJ-1139, got %q", outgoingRows[0].Reference)
}
if outgoingRows[0].Reference == "CHICKIN-" {
t.Fatalf("expected outgoing reference to avoid CHICKIN- placeholder")
}
if outgoingRows[0].Price != 298594487 {
t.Fatalf("expected outgoing price 298594487 from adjustment_stocks.price, got %.3f", outgoingRows[0].Price)
}
}
func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
if err != nil {
t.Fatalf("failed opening sqlite db: %v", err)
}
statements := []string{
`CREATE TABLE warehouses (
id INTEGER PRIMARY KEY,
kandang_id INTEGER NULL
)`,
`CREATE TABLE product_categories (
id INTEGER PRIMARY KEY,
code TEXT NOT NULL
)`,
`CREATE TABLE uoms (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)`,
`CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
product_category_id INTEGER NULL,
uom_id INTEGER NULL,
product_price NUMERIC(15,3) NOT NULL DEFAULT 0
)`,
`CREATE TABLE flags (
id INTEGER PRIMARY KEY,
flagable_id INTEGER NOT NULL,
flagable_type TEXT NOT NULL,
name TEXT NOT NULL
)`,
`CREATE TABLE purchases (
id INTEGER PRIMARY KEY,
po_number TEXT NULL,
notes TEXT NULL,
deleted_at TIMESTAMP NULL
)`,
`CREATE TABLE purchase_items (
id INTEGER PRIMARY KEY,
purchase_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
product_warehouse_id INTEGER NULL,
project_flock_kandang_id INTEGER NULL,
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
price NUMERIC(15,3) NOT NULL DEFAULT 0,
received_date TIMESTAMP NULL
)`,
`CREATE TABLE recordings (
id INTEGER PRIMARY KEY,
project_flock_kandangs_id INTEGER NOT NULL,
deleted_at TIMESTAMP NULL
)`,
`CREATE TABLE recording_stocks (
id INTEGER PRIMARY KEY,
recording_id INTEGER NOT NULL,
product_warehouse_id INTEGER NOT NULL,
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
project_flock_kandang_id INTEGER NULL
)`,
`CREATE TABLE project_chickins (
id INTEGER PRIMARY KEY,
project_flock_kandang_id INTEGER NOT NULL,
chick_in_date TIMESTAMP NULL
)`,
`CREATE TABLE project_flock_populations (
id INTEGER PRIMARY KEY,
project_chickin_id INTEGER NULL,
product_warehouse_id INTEGER NULL
)`,
`CREATE TABLE stock_allocations (
id INTEGER PRIMARY KEY,
product_warehouse_id INTEGER NOT NULL,
stockable_type TEXT NOT NULL,
stockable_id INTEGER NOT NULL,
usable_type TEXT NOT NULL,
usable_id INTEGER NOT NULL,
qty NUMERIC(15,3) NOT NULL DEFAULT 0,
allocation_purpose TEXT NOT NULL,
status TEXT NOT NULL
)`,
`CREATE TABLE product_warehouses (
id INTEGER PRIMARY KEY,
product_id INTEGER NOT NULL,
warehouse_id INTEGER NOT NULL,
project_flock_kandang_id INTEGER NULL
)`,
`CREATE TABLE stock_transfers (
id INTEGER PRIMARY KEY,
from_warehouse_id INTEGER NULL,
to_warehouse_id INTEGER NULL,
transfer_date TIMESTAMP NULL,
movement_number TEXT NULL,
reason TEXT NULL
)`,
`CREATE TABLE laying_transfers (
id INTEGER PRIMARY KEY,
transfer_date TIMESTAMP NULL,
transfer_number TEXT NULL
)`,
`CREATE TABLE laying_transfer_targets (
id INTEGER PRIMARY KEY,
laying_transfer_id INTEGER NOT NULL,
product_warehouse_id INTEGER NULL
)`,
`CREATE TABLE stock_transfer_details (
id INTEGER PRIMARY KEY,
stock_transfer_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
dest_product_warehouse_id INTEGER NULL,
source_product_warehouse_id INTEGER NULL,
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0
)`,
`CREATE TABLE adjustment_stocks (
id INTEGER PRIMARY KEY,
product_warehouse_id INTEGER NOT NULL,
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
usage_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
price NUMERIC(15,3) NOT NULL DEFAULT 0,
adj_number TEXT NULL,
created_at TIMESTAMP NULL
)`,
}
for _, stmt := range statements {
if err := db.Exec(stmt).Error; err != nil {
t.Fatalf("failed preparing schema: %v", err)
}
}
return db
}