mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
fix perhitunga sapronak
This commit is contained in:
@@ -153,6 +153,20 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
normalizeCutOverToken := func(raw string) string {
|
||||
normalized := strings.ToUpper(strings.TrimSpace(raw))
|
||||
normalized = strings.ReplaceAll(normalized, "-", "")
|
||||
normalized = strings.ReplaceAll(normalized, " ", "")
|
||||
return normalized
|
||||
}
|
||||
containsCutOver := func(values ...string) bool {
|
||||
for _, value := range values {
|
||||
if strings.Contains(normalizeCutOverToken(value), "CUTOVER") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
filter := normalizeFlag(flag)
|
||||
|
||||
byFlag := map[string]**SapronakCategoryDTO{}
|
||||
@@ -258,6 +272,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
UnitPrice: item.Harga,
|
||||
Notes: "-",
|
||||
}
|
||||
isCutOver := containsCutOver(baseRow.ProductCategory, baseRow.Description, item.ProductName)
|
||||
|
||||
row := getOrCreateRow(productKey, baseRow)
|
||||
|
||||
@@ -289,11 +304,21 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
}
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
row.TotalAmount += item.QtyKeluar * price
|
||||
case "adjustment keluar", "mutasi keluar", "penjualan":
|
||||
case "adjustment keluar":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
}
|
||||
if row.UnitPrice == 0 {
|
||||
row.UnitPrice = price
|
||||
}
|
||||
if isCutOver {
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
row.TotalAmount += item.QtyKeluar * price
|
||||
continue
|
||||
}
|
||||
row.QtyOut += item.QtyKeluar
|
||||
case "mutasi keluar", "penjualan":
|
||||
row.QtyOut += item.QtyKeluar
|
||||
if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" {
|
||||
ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi))
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package dto
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestToSapronakProjectAggregatedFromReportMovesCutOverAdjustmentOutToQtyUsed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
groupFlag string
|
||||
filter string
|
||||
productFlag string
|
||||
}{
|
||||
{
|
||||
name: "pakan cut-over",
|
||||
groupFlag: "PAKAN",
|
||||
filter: "PAKAN",
|
||||
productFlag: "PAKAN CUT-OVER",
|
||||
},
|
||||
{
|
||||
name: "ovk cut over",
|
||||
groupFlag: "OVK",
|
||||
filter: "OVK",
|
||||
productFlag: "OVK CUT OVER",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
report := &SapronakReportDTO{
|
||||
Groups: []SapronakGroupDTO{
|
||||
{
|
||||
Flag: tc.groupFlag,
|
||||
Items: []SapronakDetailDTO{
|
||||
{
|
||||
ProductID: 1,
|
||||
ProductName: "CUTOVER ITEM",
|
||||
NoReferensi: "ADJ-CUT-01",
|
||||
JenisTransaksi: "Adjustment Keluar",
|
||||
QtyKeluar: 5,
|
||||
Harga: 15000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := ToSapronakProjectAggregatedFromReport(
|
||||
report,
|
||||
tc.filter,
|
||||
map[uint][]string{
|
||||
1: {tc.productFlag},
|
||||
},
|
||||
)
|
||||
|
||||
var cat *SapronakCategoryDTO
|
||||
if tc.groupFlag == "PAKAN" {
|
||||
cat = result.Pakan
|
||||
} else {
|
||||
cat = result.Ovk
|
||||
}
|
||||
if cat == nil {
|
||||
t.Fatalf("expected category payload for %s", tc.groupFlag)
|
||||
}
|
||||
if len(cat.Rows) != 1 {
|
||||
t.Fatalf("expected 1 row, got %d", len(cat.Rows))
|
||||
}
|
||||
|
||||
row := cat.Rows[0]
|
||||
if row.QtyOut != 0 {
|
||||
t.Fatalf("expected qty_out 0 for cut-over adjustment, got %.2f", row.QtyOut)
|
||||
}
|
||||
if row.QtyUsed != 5 {
|
||||
t.Fatalf("expected qty_used 5 for cut-over adjustment, got %.2f", row.QtyUsed)
|
||||
}
|
||||
if row.UnitPrice != 15000 {
|
||||
t.Fatalf("expected unit_price 15000, got %.2f", row.UnitPrice)
|
||||
}
|
||||
if row.TotalAmount != 75000 {
|
||||
t.Fatalf("expected total_amount 75000, got %.2f", row.TotalAmount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToSapronakProjectAggregatedFromReportKeepsNonCutOverAdjustmentOutInQtyOut(t *testing.T) {
|
||||
report := &SapronakReportDTO{
|
||||
Groups: []SapronakGroupDTO{
|
||||
{
|
||||
Flag: "PAKAN",
|
||||
Items: []SapronakDetailDTO{
|
||||
{
|
||||
ProductID: 7,
|
||||
ProductName: "PAKAN REGULER",
|
||||
NoReferensi: "ADJ-REG-01",
|
||||
JenisTransaksi: "Adjustment Keluar",
|
||||
QtyKeluar: 3,
|
||||
Harga: 12000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := ToSapronakProjectAggregatedFromReport(
|
||||
report,
|
||||
"PAKAN",
|
||||
map[uint][]string{
|
||||
7: {"PAKAN"},
|
||||
},
|
||||
)
|
||||
if result.Pakan == nil || len(result.Pakan.Rows) != 1 {
|
||||
t.Fatalf("expected 1 pakan row, got %+v", result.Pakan)
|
||||
}
|
||||
|
||||
row := result.Pakan.Rows[0]
|
||||
if row.QtyOut != 3 {
|
||||
t.Fatalf("expected qty_out 3 for non cut-over adjustment, got %.2f", row.QtyOut)
|
||||
}
|
||||
if row.QtyUsed != 0 {
|
||||
t.Fatalf("expected qty_used 0 for non cut-over adjustment, got %.2f", row.QtyUsed)
|
||||
}
|
||||
if row.TotalAmount != 0 {
|
||||
t.Fatalf("expected total_amount 0 for non cut-over adjustment, got %.2f", row.TotalAmount)
|
||||
}
|
||||
}
|
||||
@@ -1576,11 +1576,20 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow)
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint, start, end *time.Time) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
poByWarehouse := r.DB().
|
||||
Table("purchase_items pi").
|
||||
Select("DISTINCT ON (pi.product_warehouse_id) pi.product_warehouse_id, po.po_number, pi.received_date").
|
||||
Joins("JOIN purchases po ON po.id = pi.purchase_id").
|
||||
Where("pi.received_date IS NOT NULL").
|
||||
Order("pi.product_warehouse_id, pi.received_date ASC")
|
||||
Table("(?) AS ranked_po",
|
||||
r.DB().
|
||||
Table("purchase_items pi").
|
||||
Select(`
|
||||
pi.product_warehouse_id,
|
||||
po.po_number,
|
||||
pi.received_date,
|
||||
ROW_NUMBER() OVER (PARTITION BY pi.product_warehouse_id ORDER BY pi.received_date ASC, pi.id ASC) AS rn
|
||||
`).
|
||||
Joins("JOIN purchases po ON po.id = pi.purchase_id").
|
||||
Where("pi.received_date IS NOT NULL"),
|
||||
).
|
||||
Select("ranked_po.product_warehouse_id, ranked_po.po_number, ranked_po.received_date").
|
||||
Where("ranked_po.rn = 1")
|
||||
|
||||
incomingQuery := r.withCtx(ctx).
|
||||
Table("adjustment_stocks AS ast").
|
||||
@@ -1589,10 +1598,10 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
ast.created_at AS date,
|
||||
CONCAT('ADJ-', ast.id) AS reference,
|
||||
'ADJ-' || CAST(ast.id AS TEXT) AS reference,
|
||||
COALESCE(ast.total_qty, 0) AS qty_in,
|
||||
0 AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
COALESCE(ast.price, p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
@@ -1615,10 +1624,18 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date,
|
||||
COALESCE(po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference,
|
||||
COALESCE(
|
||||
po.po_number,
|
||||
st.movement_number,
|
||||
lt.transfer_number,
|
||||
pfp_po.po_number,
|
||||
CASE WHEN ast_in.id IS NOT NULL THEN 'ADJ-' || CAST(ast_in.id AS TEXT) END,
|
||||
CASE WHEN ast.id IS NOT NULL THEN 'ADJ-' || CAST(ast.id AS TEXT) END,
|
||||
CASE WHEN pc.id IS NOT NULL THEN 'CHICKIN-' || CAST(pc.id AS TEXT) END
|
||||
) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
COALESCE(pi.price, ast_in.price, ast.price, p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()).
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
||||
@@ -1639,7 +1656,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)).
|
||||
Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price")
|
||||
Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, pi.price, ast_in.price, ast.price, p.product_price")
|
||||
outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
|
||||
outgoingQuery = applyDateRange(outgoingQuery, dateExpr, start, end)
|
||||
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||
|
||||
@@ -89,6 +89,63 @@ func TestFetchSapronakIncomingIncludesAttributedFarmPurchasesAndHistoricalWareho
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -134,6 +191,7 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
|
||||
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,
|
||||
@@ -152,7 +210,13 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
|
||||
)`,
|
||||
`CREATE TABLE project_chickins (
|
||||
id INTEGER PRIMARY KEY,
|
||||
project_flock_kandang_id INTEGER NOT NULL
|
||||
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,
|
||||
@@ -179,6 +243,16 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
|
||||
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,
|
||||
@@ -193,6 +267,7 @@ func setupClosingRepositoryTestDB(t *testing.T) *gorm.DB {
|
||||
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
|
||||
)`,
|
||||
|
||||
@@ -573,17 +573,21 @@ func (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var minChickin *time.Time
|
||||
var firstChickin struct {
|
||||
ChickInDate time.Time `gorm:"column:chick_in_date"`
|
||||
}
|
||||
if err := db.Table("project_chickins").
|
||||
Select("MIN(chick_in_date)").
|
||||
Where("project_flock_kandang_id = ?", pfk.Id).
|
||||
Scan(&minChickin).Error; err != nil {
|
||||
Select("chick_in_date").
|
||||
Where("project_flock_kandang_id = ? AND chick_in_date IS NOT NULL", pfk.Id).
|
||||
Order("chick_in_date ASC").
|
||||
Limit(1).
|
||||
Scan(&firstChickin).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
start := pfk.CreatedAt
|
||||
if minChickin != nil && !minChickin.IsZero() {
|
||||
start = *minChickin
|
||||
if !firstChickin.ChickInDate.IsZero() {
|
||||
start = firstChickin.ChickInDate
|
||||
}
|
||||
startDate := dateOnlyUTC(start)
|
||||
|
||||
@@ -596,26 +600,34 @@ func (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID
|
||||
return &startDate, endDate, nil
|
||||
}
|
||||
|
||||
var minCreated time.Time
|
||||
var firstPFK entity.ProjectFlockKandang
|
||||
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||
Select("MIN(created_at)").
|
||||
Select("created_at").
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Scan(&minCreated).Error; err != nil {
|
||||
Order("created_at ASC").
|
||||
First(&firstPFK).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var minChickin *time.Time
|
||||
var firstChickin struct {
|
||||
ChickInDate time.Time `gorm:"column:chick_in_date"`
|
||||
}
|
||||
if err := db.Table("project_chickins pc").
|
||||
Select("MIN(pc.chick_in_date)").
|
||||
Select("pc.chick_in_date").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = pc.project_flock_kandang_id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Scan(&minChickin).Error; err != nil {
|
||||
Where("pfk.project_flock_id = ? AND pc.chick_in_date IS NOT NULL", projectFlockID).
|
||||
Order("pc.chick_in_date ASC").
|
||||
Limit(1).
|
||||
Scan(&firstChickin).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
start := minCreated
|
||||
if minChickin != nil && !minChickin.IsZero() {
|
||||
start = *minChickin
|
||||
start := firstPFK.CreatedAt
|
||||
if !firstChickin.ChickInDate.IsZero() {
|
||||
start = firstChickin.ChickInDate
|
||||
}
|
||||
startDate := dateOnlyUTC(start)
|
||||
|
||||
@@ -627,15 +639,19 @@ func (s closingService) getSapronakDateRange(ctx context.Context, projectFlockID
|
||||
return nil, nil, err
|
||||
}
|
||||
if openCount == 0 {
|
||||
var maxClosed *time.Time
|
||||
var latestClosed entity.ProjectFlockKandang
|
||||
if err := db.Model(&entity.ProjectFlockKandang{}).
|
||||
Select("MAX(closed_at)").
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Scan(&maxClosed).Error; err != nil {
|
||||
Select("closed_at").
|
||||
Where("project_flock_id = ? AND closed_at IS NOT NULL", projectFlockID).
|
||||
Order("closed_at DESC").
|
||||
First(&latestClosed).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &startDate, nil, nil
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
if maxClosed != nil && !maxClosed.IsZero() {
|
||||
d := dateOnlyUTC(*maxClosed)
|
||||
if latestClosed.ClosedAt != nil && !latestClosed.ClosedAt.IsZero() {
|
||||
d := dateOnlyUTC(*latestClosed.ClosedAt)
|
||||
endDate = &d
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestGetSapronakDateRange_ProjectWithoutChickin_DoesNotError(t *testing.T) {
|
||||
db := setupClosingServiceTestDB(t)
|
||||
repo := repository.NewClosingRepository(db)
|
||||
svc := closingService{Repository: repo}
|
||||
|
||||
createdAt := time.Date(2026, 4, 15, 7, 0, 0, 0, time.UTC)
|
||||
if err := db.Exec(`INSERT INTO project_flock_kandangs (id, project_flock_id, created_at, closed_at) VALUES (66, 47, ?, NULL)`, createdAt).Error; err != nil {
|
||||
t.Fatalf("failed seeding project_flock_kandangs: %v", err)
|
||||
}
|
||||
|
||||
start, end, err := svc.getSapronakDateRange(context.Background(), 47, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if start == nil {
|
||||
t.Fatalf("expected non-nil start date")
|
||||
}
|
||||
expected := dateOnlyUTC(createdAt)
|
||||
if !start.Equal(expected) {
|
||||
t.Fatalf("expected start %s, got %s", expected.Format(time.RFC3339), start.Format(time.RFC3339))
|
||||
}
|
||||
if end != nil {
|
||||
t.Fatalf("expected nil end date for open kandang, got %v", end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSapronakDateRange_KandangWithoutChickin_DoesNotError(t *testing.T) {
|
||||
db := setupClosingServiceTestDB(t)
|
||||
repo := repository.NewClosingRepository(db)
|
||||
svc := closingService{Repository: repo}
|
||||
|
||||
createdAt := time.Date(2026, 4, 15, 7, 0, 0, 0, time.UTC)
|
||||
if err := db.Exec(`INSERT INTO project_flock_kandangs (id, project_flock_id, created_at, closed_at) VALUES (66, 47, ?, NULL)`, createdAt).Error; err != nil {
|
||||
t.Fatalf("failed seeding project_flock_kandangs: %v", err)
|
||||
}
|
||||
|
||||
pfkID := uint(66)
|
||||
start, end, err := svc.getSapronakDateRange(context.Background(), 47, &pfkID)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if start == nil {
|
||||
t.Fatalf("expected non-nil start date")
|
||||
}
|
||||
expected := dateOnlyUTC(createdAt)
|
||||
if !start.Equal(expected) {
|
||||
t.Fatalf("expected start %s, got %s", expected.Format(time.RFC3339), start.Format(time.RFC3339))
|
||||
}
|
||||
if end != nil {
|
||||
t.Fatalf("expected nil end date for open kandang, got %v", end)
|
||||
}
|
||||
}
|
||||
|
||||
func setupClosingServiceTestDB(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)
|
||||
}
|
||||
|
||||
stmts := []string{
|
||||
`CREATE TABLE project_flock_kandangs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
project_flock_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
closed_at TIMESTAMP NULL
|
||||
)`,
|
||||
`CREATE TABLE project_chickins (
|
||||
id INTEGER PRIMARY KEY,
|
||||
project_flock_kandang_id INTEGER NOT NULL,
|
||||
chick_in_date TIMESTAMP NULL
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range stmts {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed preparing schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
@@ -371,7 +371,9 @@ func buildSapronakDetails(
|
||||
addRows(result.Incoming, incomingRows, "Pembelian", true)
|
||||
addRows(result.Usage, usageRows, "Pemakaian", false)
|
||||
addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true)
|
||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false)
|
||||
// Outgoing adjustment rows here are sourced from stock allocation
|
||||
// consume flow (adjustment_stocks.usage_qty), so treat them as usage.
|
||||
addRows(result.AdjOutgoing, adjOutgoingRows, "Pemakaian", false)
|
||||
addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true)
|
||||
addRows(result.TransferOut, transferOutRows, "Mutasi Keluar", false)
|
||||
addRows(result.SalesOut, salesOutRows, "Penjualan", false)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||
)
|
||||
|
||||
func TestBuildSapronakDetailsMapsAdjustmentOutgoingAsUsage(t *testing.T) {
|
||||
res := buildSapronakDetails(
|
||||
map[uint][]repository.SapronakDetailRow{},
|
||||
map[uint][]repository.SapronakDetailRow{},
|
||||
map[uint][]repository.SapronakDetailRow{},
|
||||
map[uint][]repository.SapronakDetailRow{
|
||||
17: {
|
||||
{
|
||||
ProductID: 17,
|
||||
ProductName: "PAKAN GROWING CRUMBLE 8603 MALINDO",
|
||||
Flag: "PAKAN",
|
||||
QtyOut: 9000,
|
||||
Price: 6450,
|
||||
},
|
||||
},
|
||||
},
|
||||
map[uint][]repository.SapronakDetailRow{},
|
||||
map[uint][]repository.SapronakDetailRow{},
|
||||
map[uint][]repository.SapronakDetailRow{},
|
||||
)
|
||||
|
||||
rows := res.AdjOutgoing[17]
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("expected 1 adjustment outgoing row, got %d", len(rows))
|
||||
}
|
||||
|
||||
row := rows[0]
|
||||
if row.JenisTransaksi != "Pemakaian" {
|
||||
t.Fatalf("expected jenis_transaksi Pemakaian, got %q", row.JenisTransaksi)
|
||||
}
|
||||
if row.QtyKeluar != 9000 {
|
||||
t.Fatalf("expected qty_keluar 9000, got %.3f", row.QtyKeluar)
|
||||
}
|
||||
if row.Nilai != 58050000 {
|
||||
t.Fatalf("expected nilai 58050000, got %.3f", row.Nilai)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user