fix perhitunga sapronak

This commit is contained in:
giovanni
2026-04-21 19:30:03 +07:00
parent 1e34a0e7b2
commit ded8be198a
8 changed files with 433 additions and 35 deletions
@@ -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)
}
}