Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'

[FIX/BE] adjustment response

See merge request mbugroup/lti-api!162
This commit is contained in:
Hafizh A. Y.
2026-01-13 01:55:59 +00:00
4 changed files with 277 additions and 25 deletions
@@ -9,8 +9,8 @@ import (
type DebtSupplierRowDTO struct {
PrNumber string `json:"pr_number"`
PoNumber string `json:"po_number"`
PrDate string `json:"pr_date"`
PoDate string `json:"po_date"`
ReceivedDate string `json:"received_date"`
Aging int `json:"aging"`
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
@@ -21,6 +21,7 @@ type DebtSupplierRowDTO struct {
DebtPrice float64 `json:"debt_price"`
Status string `json:"status"`
TravelNumber string `json:"travel_number"`
Balance float64 `json:"balance"`
}
type DebtSupplierTotalDTO struct {
@@ -32,6 +33,7 @@ type DebtSupplierTotalDTO struct {
type DebtSupplierDTO struct {
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
InitialBalance float64 `json:"initial_balance"`
Rows []DebtSupplierRowDTO `json:"rows"`
Total DebtSupplierTotalDTO `json:"total"`
}
@@ -15,7 +15,10 @@ import (
type DebtSupplierRepository interface {
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
}
type debtSupplierRepositoryImpl struct {
@@ -28,10 +31,10 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
func resolveDebtSupplierDateColumn(filterBy string) string {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "receive_date":
return "purchases.receive_date"
case "po_date":
return "purchases.po_date"
case "pr_date":
return "purchases.created_at"
case "do_date", "received_date", "":
return "purchase_items.received_date"
default:
@@ -157,6 +160,39 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context
return purchases, nil
}
func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) {
if len(supplierIDs) == 0 {
return []entity.Payment{}, nil
}
db := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs)
if strings.TrimSpace(filters.StartDate) != "" {
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
db = db.Where("DATE(payment_date) >= ?", dateFrom)
}
}
if strings.TrimSpace(filters.EndDate) != "" {
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
db = db.Where("DATE(payment_date) <= ?", dateTo)
}
}
var payments []entity.Payment
if err := db.
Order("payment_date ASC, id ASC").
Find(&payments).Error; err != nil {
return nil, err
}
return payments, nil
}
func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) {
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
@@ -219,3 +255,76 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
return result, nil
}
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
return map[uint]float64{}, nil
}
dateFrom, err := utils.ParseDateString(filters.StartDate)
if err != nil {
return map[uint]float64{}, nil
}
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
type purchaseTotalRow struct {
SupplierID uint `gorm:"column:supplier_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]purchaseTotalRow, 0)
if err := r.db.WithContext(ctx).
Table("purchases").
Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
Where("purchases.supplier_id IN ?", supplierIDs).
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
Group("purchases.supplier_id").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, row := range rows {
result[row.SupplierID] = row.Total
}
return result, nil
}
func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
return map[uint]float64{}, nil
}
dateFrom, err := utils.ParseDateString(filters.StartDate)
if err != nil {
return map[uint]float64{}, nil
}
type paymentTotalRow struct {
SupplierID uint `gorm:"column:supplier_id"`
Total float64 `gorm:"column:total"`
}
rows := make([]paymentTotalRow, 0)
if err := r.db.WithContext(ctx).
Model(&entity.Payment{}).
Select("party_id AS supplier_id, SUM(nominal) AS total").
Where("party_type = ?", string(utils.PaymentPartySupplier)).
Where("direction = ?", "OUT").
Where("party_id IN ?", supplierIDs).
Where("DATE(payment_date) < ?", dateFrom).
Group("party_id").
Scan(&rows).Error; err != nil {
return nil, err
}
result := make(map[uint]float64, len(rows))
for _, row := range rows {
result[row.SupplierID] = row.Total
}
return result, nil
}
@@ -642,8 +642,8 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
}
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
if params.FilterBy == "" {
params.FilterBy = "do_date"
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
params.FilterBy = "received_date"
}
if err := s.Validate.Struct(params); err != nil {
@@ -675,6 +675,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return nil, 0, err
}
payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
references := make([]string, 0)
seenRefs := make(map[string]struct{})
@@ -697,6 +702,21 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
return nil, 0, err
}
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
for _, payment := range payments {
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
}
initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
@@ -710,29 +730,81 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
continue
}
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
items := purchasesBySupplier[supplierID]
rows := make([]dto.DebtSupplierRowDTO, 0, len(items))
paymentItems := paymentsBySupplier[supplierID]
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
total := dto.DebtSupplierTotalDTO{}
type debtSupplierRowItem struct {
Row dto.DebtSupplierRowDTO
SortTime time.Time
Order int
DeltaBalance float64
CountTotals bool
}
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
for _, purchase := range items {
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
rows = append(rows, row)
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
SortTime: sortTime,
Order: 0,
DeltaBalance: -row.TotalPrice,
CountTotals: true,
})
}
for _, payment := range paymentItems {
row := buildDebtSupplierPaymentRow(payment, location)
sortTime := payment.PaymentDate.In(location)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
SortTime: sortTime,
Order: 1,
DeltaBalance: payment.Nominal,
CountTotals: false,
})
}
sort.SliceStable(combinedRows, func(i, j int) bool {
if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) {
return combinedRows[i].Order < combinedRows[j].Order
}
return combinedRows[i].SortTime.Before(combinedRows[j].SortTime)
})
balance := initialBalance
for i := range combinedRows {
balance += combinedRows[i].DeltaBalance
combinedRows[i].Row.Balance = balance
if combinedRows[i].CountTotals {
row := combinedRows[i].Row
if row.Aging > total.Aging {
total.Aging = row.Aging
}
total.TotalPrice += row.TotalPrice
total.PaymentPrice += row.PaymentPrice
total.DebtPrice += row.DebtPrice
} else {
combinedRows[i].Row.DebtPrice = balance
}
}
sortDesc := strings.EqualFold(params.SortOrder, "desc")
sort.SliceStable(rows, func(i, j int) bool {
if sortDesc {
return rows[i].PrDate > rows[j].PrDate
for i := len(combinedRows) - 1; i >= 0; i-- {
rows = append(rows, combinedRows[i].Row)
}
} else {
for i := range combinedRows {
rows = append(rows, combinedRows[i].Row)
}
}
return rows[i].PrDate < rows[j].PrDate
})
var supplierDTORef *supplierDTO.SupplierRelationDTO
if supplier.Id != 0 {
@@ -742,6 +814,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
result = append(result, dto.DebtSupplierDTO{
Supplier: supplierDTORef,
InitialBalance: initialBalance,
Rows: rows,
Total: total,
})
@@ -769,6 +842,7 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
totalPrice := 0.0
travelNumber := "-"
receivedDate := ""
var area *areaDTO.AreaRelationDTO
var warehouse *warehouseDTO.WarehouseRelationDTO
@@ -787,8 +861,19 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
}
}
earliestReceived := time.Time{}
for _, item := range purchase.Items {
totalPrice += item.TotalPrice
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
received := item.ReceivedDate.In(loc)
if earliestReceived.IsZero() || received.Before(earliestReceived) {
earliestReceived = received
}
}
if !earliestReceived.IsZero() {
receivedDate = earliestReceived.Format("2006-01-02")
}
}
@@ -820,8 +905,8 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
return dto.DebtSupplierRowDTO{
PrNumber: prNumber,
PoNumber: poNumber,
PrDate: prDate.Format("2006-01-02"),
PoDate: poDate,
ReceivedDate: receivedDate,
Aging: aging,
Area: area,
Warehouse: warehouse,
@@ -835,6 +920,62 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
}
}
func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO {
referenceNumber := ""
if payment.ReferenceNumber != nil {
referenceNumber = *payment.ReferenceNumber
}
prNumber := payment.PaymentCode
if strings.TrimSpace(prNumber) == "" {
prNumber = referenceNumber
}
return dto.DebtSupplierRowDTO{
PrNumber: prNumber,
PoNumber: referenceNumber,
PoDate: "-",
ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"),
Aging: 0,
Area: nil,
Warehouse: nil,
DueDate: "-",
DueStatus: "-",
TotalPrice: 0,
PaymentPrice: payment.Nominal,
DebtPrice: 0,
Status: "Pembayaran",
TravelNumber: "-",
}
}
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "po_date":
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
return purchase.PoDate.In(loc)
}
case "pr_date":
return purchase.CreatedAt.In(loc)
default:
earliest := time.Time{}
for _, item := range purchase.Items {
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
received := item.ReceivedDate.In(loc)
if earliest.IsZero() || received.Before(earliest) {
earliest = received
}
}
if !earliest.IsZero() {
return earliest
}
}
return purchase.CreatedAt.In(loc)
}
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
params, filters, err := s.parseHppPerKandangQuery(ctx)
if err != nil {
@@ -49,7 +49,7 @@ type DebtSupplierQuery struct {
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=do_date po_date pr_date"`
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"`
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
}