From ac5edb36e70606cd974e449d2cafd536d03c3508 Mon Sep 17 00:00:00 2001 From: ragilap Date: Mon, 12 Jan 2026 16:28:03 +0700 Subject: [PATCH] [FIX/BE] adjustment response --- .../repports/dto/repportDebtSupplier.dto.go | 10 +- .../repositories/debt_supplier.repository.go | 113 ++++++++++- .../repports/services/repport.service.go | 177 ++++++++++++++++-- .../validations/repport.validation.go | 2 +- 4 files changed, 277 insertions(+), 25 deletions(-) diff --git a/internal/modules/repports/dto/repportDebtSupplier.dto.go b/internal/modules/repports/dto/repportDebtSupplier.dto.go index 5dce055f..8699ca60 100644 --- a/internal/modules/repports/dto/repportDebtSupplier.dto.go +++ b/internal/modules/repports/dto/repportDebtSupplier.dto.go @@ -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 { @@ -31,7 +32,8 @@ type DebtSupplierTotalDTO struct { } type DebtSupplierDTO struct { - Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` - Rows []DebtSupplierRowDTO `json:"rows"` - Total DebtSupplierTotalDTO `json:"total"` + Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` + InitialBalance float64 `json:"initial_balance"` + Rows []DebtSupplierRowDTO `json:"rows"` + Total DebtSupplierTotalDTO `json:"total"` } diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index 84e9402d..3d415606 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -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 +} diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c7576e5f..5f3cbbad 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -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, + }) + } - if row.Aging > total.Aging { - total.Aging = row.Aging + 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 } - total.TotalPrice += row.TotalPrice - total.PaymentPrice += row.PaymentPrice - total.DebtPrice += row.DebtPrice } sortDesc := strings.EqualFold(params.SortOrder, "desc") - sort.SliceStable(rows, func(i, j int) bool { - if sortDesc { - return rows[i].PrDate > rows[j].PrDate + if sortDesc { + for i := len(combinedRows) - 1; i >= 0; i-- { + rows = append(rows, combinedRows[i].Row) } - return rows[i].PrDate < rows[j].PrDate - }) + } else { + for i := range combinedRows { + rows = append(rows, combinedRows[i].Row) + } + } var supplierDTORef *supplierDTO.SupplierRelationDTO if supplier.Id != 0 { @@ -741,9 +813,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu } result = append(result, dto.DebtSupplierDTO{ - Supplier: supplierDTORef, - Rows: rows, - Total: total, + 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 { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 6c80275f..5b60a31f 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -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"` }