Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-334-Report-closing-hpp-expedisi

This commit is contained in:
ragilap
2025-12-16 14:49:53 +07:00
32 changed files with 2199 additions and 238 deletions
@@ -17,6 +17,14 @@ type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error)
FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error)
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
}
type ClosingRepositoryImpl struct {
@@ -271,3 +279,370 @@ JOIN warehouses w ON w.id = pw.warehouse_id
WHERE pw.project_flock_kandang_id IN ?
`
)
type SapronakIncomingRow struct {
ProductID uint
ProductName string
Flag string
Qty float64
Value float64
DefaultPrice float64
}
type SapronakUsageRow struct {
ProductID uint
ProductName string
Flag string
Qty float64
DefaultPrice float64
}
type SapronakDetailRow struct {
ProductID uint
ProductName string
Flag string
Date *time.Time
Reference string
QtyIn float64
QtyOut float64
Price float64
}
func (r *ClosingRepositoryImpl) withCtx(ctx context.Context) *gorm.DB { return r.DB().WithContext(ctx) }
func applyJoins(db *gorm.DB, joins ...string) *gorm.DB {
for _, j := range joins {
if strings.TrimSpace(j) != "" {
db = db.Joins(j)
}
}
return db
}
func sapronakFlags(flags ...utils.FlagType) []string {
out := make([]string, len(flags))
for i, f := range flags {
out[i] = string(f)
}
return out
}
var (
sapronakFlagsAll = sapronakFlags(utils.FlagDOC, utils.FlagPakan, utils.FlagOVK, utils.FlagPullet)
sapronakFlagsUsage = sapronakFlags(utils.FlagPakan, utils.FlagOVK)
sapronakFlagsChickin = sapronakFlags(utils.FlagDOC, utils.FlagPullet)
)
func groupSapronakDetails(rows []SapronakDetailRow) map[uint][]SapronakDetailRow {
m := make(map[uint][]SapronakDetailRow)
for _, row := range rows {
m[row.ProductID] = append(m[row.ProductID], row)
}
return m
}
func scanAndGroupDetails(db *gorm.DB) (map[uint][]SapronakDetailRow, error) {
rows := make([]SapronakDetailRow, 0)
if err := db.Scan(&rows).Error; err != nil {
return nil, err
}
return groupSapronakDetails(rows), nil
}
// =========================
// Usage (summary + details)
// =========================
func (r *ClosingRepositoryImpl) usageQuery(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
where string,
args ...any,
) *gorm.DB {
db := r.withCtx(ctx).Table(table).Select(`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(SUM(usage_qty), 0) AS qty,
COALESCE(p.product_price, 0) AS default_price
`)
db = applyJoins(db, joins...)
return db.
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where(where, args...)
}
func (r *ClosingRepositoryImpl) fetchSapronakUsage(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
where string,
args ...any,
) ([]SapronakUsageRow, error) {
rows := make([]SapronakUsageRow, 0)
db := r.usageQuery(ctx, table, pwJoinCond, joins, where, args...)
if err := db.Group("pw.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) detailQuery(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
selectSQL string,
where string,
args ...any,
) *gorm.DB {
db := r.withCtx(ctx).
Table(table).
Joins("JOIN product_warehouses pw ON " + pwJoinCond).
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct)
db = applyJoins(db, joins...)
return db.Select(selectSQL).Where(where, args...)
}
func (r *ClosingRepositoryImpl) fetchSapronakDetails(
ctx context.Context,
table string,
pwJoinCond string,
joins []string,
selectSQL string,
where string,
args ...any,
) (map[uint][]SapronakDetailRow, error) {
return scanAndGroupDetails(r.detailQuery(ctx, table, pwJoinCond, joins, selectSQL, where, args...))
}
func (r *ClosingRepositoryImpl) FetchSapronakUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
if pfkID == 0 {
return nil, nil
}
return r.fetchSapronakUsage(
ctx,
"recording_stocks rs",
"pw.id = rs.product_warehouse_id",
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"},
"r.project_flock_kandangs_id = ? AND f.name IN ?",
pfkID,
sapronakFlagsUsage,
)
}
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsage(ctx context.Context, pfkID uint) ([]SapronakUsageRow, error) {
if pfkID == 0 {
return []SapronakUsageRow{}, nil
}
return r.fetchSapronakUsage(
ctx,
"project_chickins pc",
"pw.id = pc.product_warehouse_id",
nil,
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
pfkID,
sapronakFlagsChickin,
)
}
func (r *ClosingRepositoryImpl) FetchSapronakUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
return r.fetchSapronakDetails(
ctx,
"recording_stocks rs",
"pw.id = rs.product_warehouse_id",
[]string{"JOIN recordings r ON r.id = rs.recording_id AND r.deleted_at IS NULL"}, // penting: supaya alias r valid
`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
r.record_datetime AS date,
CAST(r.id AS TEXT) AS reference,
0 AS qty_in,
COALESCE(rs.usage_qty,0) AS qty_out,
COALESCE(p.product_price,0) AS price
`,
"r.project_flock_kandangs_id = ? AND f.name IN ?",
pfkID,
sapronakFlagsUsage,
)
}
func (r *ClosingRepositoryImpl) FetchSapronakChickinUsageDetails(ctx context.Context, pfkID uint) (map[uint][]SapronakDetailRow, error) {
return r.fetchSapronakDetails(
ctx,
"project_chickins pc",
"pw.id = pc.product_warehouse_id",
nil,
`
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pc.chick_in_date AS date,
CAST(pc.id AS TEXT) AS reference,
0 AS qty_in,
COALESCE(pc.usage_qty,0) AS qty_out,
COALESCE(p.product_price,0) AS price
`,
"pc.project_flock_kandang_id = ? AND pc.usage_qty > 0 AND f.name IN ?",
pfkID,
sapronakFlagsChickin,
)
}
func (r *ClosingRepositoryImpl) incomingPurchaseBase(ctx context.Context, kandangID uint) *gorm.DB {
return r.withCtx(ctx).
Table("purchase_items AS pi").
Joins("JOIN purchases po ON po.id = pi.purchase_id AND po.deleted_at IS NULL").
Joins("JOIN products p ON p.id = pi.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN warehouses w ON w.id = pi.warehouse_id").
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Where("pi.received_date IS NOT NULL")
}
func (r *ClosingRepositoryImpl) FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error) {
rows := make([]SapronakIncomingRow, 0)
db := r.incomingPurchaseBase(ctx, kandangID).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
COALESCE(SUM(pi.total_qty), 0) AS qty,
COALESCE(SUM(pi.total_qty * pi.price), 0) AS value,
COALESCE(p.product_price, 0) AS default_price
`)
if err := db.Group("pi.product_id, p.name, f.name, p.product_price").Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error) {
return scanAndGroupDetails(
r.incomingPurchaseBase(ctx, kandangID).Select(`
pi.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
pi.received_date AS date,
COALESCE(po.po_number, '') AS reference,
COALESCE(pi.total_qty,0) AS qty_in,
0 AS qty_out,
COALESCE(pi.price,0) AS price
`),
)
}
type stockLogSapronakRow struct {
ID uint `gorm:"column:id"`
ProductID uint `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
Flag string `gorm:"column:flag"`
CreatedAt *time.Time `gorm:"column:created_at"`
Increase float64 `gorm:"column:increase"`
Decrease float64 `gorm:"column:decrease"`
Price float64 `gorm:"column:price"`
MovementNumber string `gorm:"column:movement_number"`
}
func (r *ClosingRepositoryImpl) fetchStockLogs(ctx context.Context, kandangID uint, logType any, withMovement bool) ([]stockLogSapronakRow, error) {
rows := make([]stockLogSapronakRow, 0)
movementSelect := "'' AS movement_number"
joins := []string{}
if withMovement {
movementSelect = "COALESCE(st.movement_number,'') AS movement_number"
joins = append(joins, "JOIN stock_transfers st ON st.id = sl.loggable_id")
}
db := r.withCtx(ctx).
Table("stock_logs sl").
Select(`
sl.id AS id,
pw.product_id AS product_id,
p.name AS product_name,
f.name AS flag,
sl.created_at AS created_at,
COALESCE(sl.increase,0) AS increase,
COALESCE(sl.decrease,0) AS decrease,
COALESCE(p.product_price,0) AS price,
` + movementSelect + `
`).
Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id").
Joins("JOIN products p ON p.id = pw.product_id").
Joins("JOIN flags f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("JOIN warehouses w ON w.id = pw.warehouse_id")
db = applyJoins(db, joins...)
if err := db.
Where("sl.loggable_type = ?", logType).
Where("w.kandang_id = ?", kandangID).
Where("f.name IN ?", sapronakFlagsAll).
Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow) string) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow) {
in := make(map[uint][]SapronakDetailRow)
out := make(map[uint][]SapronakDetailRow)
for _, row := range rows {
base := SapronakDetailRow{
ProductID: row.ProductID,
ProductName: row.ProductName,
Flag: row.Flag,
Date: row.CreatedAt,
Reference: refFn(row),
Price: row.Price,
}
if row.Increase > 0 {
d := base
d.QtyIn = row.Increase
in[row.ProductID] = append(in[row.ProductID], d)
}
if row.Decrease > 0 {
d := base
d.QtyOut = row.Decrease
out[row.ProductID] = append(out[row.ProductID], d)
}
}
return in, out
}
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
if err != nil {
return nil, nil, err
}
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
return in, out, nil
}
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
if err != nil {
return nil, nil, err
}
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string {
if ref := strings.TrimSpace(row.MovementNumber); ref != "" {
return ref
}
return fmt.Sprintf("TRF-%d", row.ID)
})
return in, out, nil
}