From f0b4fe916c918494689c08b8fc0a9f71b71ad5dc Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 12 Jan 2026 20:00:49 +0700 Subject: [PATCH 01/12] FEAT[BE] ;: inisiate customer payment report route and related DTOs --- internal/middleware/permissions.go | 6 +- .../chickins/services/chickin.service.go | 1 - .../controllers/repport.controller.go | 27 ++++++++ .../dto/repportCustomerPayment.dto.go | 63 +++++++++++++++++++ internal/modules/repports/route.go | 2 +- 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 internal/modules/repports/dto/repportCustomerPayment.dto.go diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 10741bff..9450d228 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -1,8 +1,9 @@ package middleware -const( +const ( P_DashboardGetAll = "lti.dashboard.list" ) + // project-flock const ( P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" @@ -50,6 +51,7 @@ const ( P_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list" P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" P_ReportProductionResultGetAll = "lti.repport.production_result.list" + P_ReportCustomerPaymentGetAll = "lti.repport.customerpayment.list" ) const ( @@ -150,7 +152,7 @@ const ( P_ProductsCreateOne = "lti.master.products.create" P_ProductsUpdateOne = "lti.master.products.update" P_ProductsDeleteOne = "lti.master.products.delete" - + P_SuppliersGetAll = "lti.master.suppliers.list" P_SuppliersGetOne = "lti.master.suppliers.detail" P_SuppliersCreateOne = "lti.master.suppliers.create" diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index eabe596c..84e98f2d 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -584,7 +584,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -// autoAddFlagToProduct adds target flag to product if not already present (idempotent) func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error { if s.ProductRepo == nil { return nil diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 22ff4acf..d89effa3 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -282,6 +282,33 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { }) } +func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { + page := ctx.QueryInt("page", 1) + limit := ctx.QueryInt("limit", 10) + + if page < 1 || limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + // TODO: Implement service call + data := []dto.CustomerPaymentReportItem{} + totalResults := int64(0) + + return ctx.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get customer payment report successfully", + Meta: response.Meta{ + Page: page, + Limit: limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(limit))), + TotalResults: totalResults, + }, + Data: data, + }) +} + func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go new file mode 100644 index 00000000..e0938b51 --- /dev/null +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -0,0 +1,63 @@ +package dto + +import ( + "time" +) + +// CustomerPaymentReportCustomer represents customer information in the report +type CustomerPaymentReportCustomer struct { + ID uint `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + AccountNumber string `json:"account_number"` + Balance float64 `json:"balance"` + Address string `json:"address"` +} + +// CustomerPaymentReportRow represents each transaction row +type CustomerPaymentReportRow struct { + ID uint `json:"id"` + DoDate time.Time `json:"do_date"` + RealizationDate time.Time `json:"realization_date"` + AgingDay int `json:"aging_day"` + Reference string `json:"reference"` + VehiclePlate []string `json:"vehicle_plate"` + Qty float64 `json:"qty"` + Weight float64 `json:"weight"` + AverageWeight float64 `json:"average_weight"` + Price float64 `json:"price"` + CreditNote float64 `json:"credit_note"` + FinalPrice float64 `json:"final_price"` + PPN float64 `json:"ppn"` + Total float64 `json:"total"` + Payment float64 `json:"payment"` + AccountsReceivable float64 `json:"accounts_receivable"` + Notes string `json:"notes"` + PickupInfo string `json:"pickup_info"` + SalesMarketing string `json:"sales_marketing"` +} + +// CustomerPaymentReportSummary represents summary calculations per customer +type CustomerPaymentReportSummary struct { + TotalQty float64 `json:"total_qty"` + TotalWeight float64 `json:"total_weight"` + TotalInitialAmount float64 `json:"total_initial_amount"` + TotalCreditNote float64 `json:"total_credit_note"` + TotalFinalAmount float64 `json:"total_final_amount"` + TotalPPN float64 `json:"total_ppn"` + TotalGrandAmount float64 `json:"total_grand_amount"` + TotalPayment float64 `json:"total_payment"` + TotalAccountsReceivable float64 `json:"total_accounts_receivable"` +} + +// CustomerPaymentReportItem represents data grouped by customer +type CustomerPaymentReportItem struct { + Customer CustomerPaymentReportCustomer `json:"customer"` + Rows []CustomerPaymentReportRow `json:"rows"` + Summary CustomerPaymentReportSummary `json:"summary"` +} + +// CustomerPaymentReportResponse represents the complete response +type CustomerPaymentReportResponse struct { + Data []CustomerPaymentReportItem `json:"data"` +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 0a0cf8a3..2f5eceec 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -21,5 +21,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang) route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult) - + route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment) } From bba2dec8c645a4ed420f621b9ee28e57a80636c7 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 13 Jan 2026 09:52:25 +0700 Subject: [PATCH 02/12] FEAT[BE] :update route --- internal/modules/repports/controllers/repport.controller.go | 5 ++++- internal/modules/repports/route.go | 2 +- internal/modules/repports/services/repport.service.go | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index d89effa3..613c4e00 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -289,8 +289,11 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { if page < 1 || limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } + + + + - // TODO: Implement service call data := []dto.CustomerPaymentReportItem{} totalResults := int64(0) diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 2f5eceec..3f803677 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -21,5 +21,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang) route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult) - route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment) + route.Get("/customer-payment", ctrl.GetCustomerPayment) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c7576e5f..e54d8674 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -40,6 +40,7 @@ type RepportService interface { GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) + GetCustomerPayment(ctx *fiber.Ctx) (int, error) } type repportService struct { @@ -1232,6 +1233,11 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return params, filters, nil } +func (s *repportService) GetCustomerPayment(c *fiber.Ctx) (int, error) { + + return 0, nil +} + func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { From 7f1d796b650a1a5fbd2fecd9c49b10130012a43d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 13 Jan 2026 22:50:58 +0700 Subject: [PATCH 03/12] fix[BE]: correct total price calculation in delivery and sales order services --- internal/modules/marketing/services/deliveryorder.service.go | 4 ++-- internal/modules/marketing/services/salesorder.service.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index a1f4e1dd..a521e5bc 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -249,7 +249,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery // Hitung total_weight dan total_price otomatis totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty + totalPrice := requestedProduct.UnitPrice * totalWeight deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -363,7 +363,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO // Hitung total_weight dan total_price otomatis totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty + totalPrice := requestedProduct.UnitPrice * totalWeight deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index d57b323e..e73184dd 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -294,7 +294,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u // Hitung total_weight dan total_price otomatis totalWeight := rp.Qty * rp.AvgWeight - totalPrice := rp.UnitPrice * rp.Qty + totalPrice := rp.UnitPrice * totalWeight updateBody := map[string]any{ "product_warehouse_id": rp.ProductWarehouseId, @@ -594,7 +594,7 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont // Hitung total_weight dan total_price otomatis totalWeight := rp.Qty * rp.AvgWeight - totalPrice := rp.UnitPrice * rp.Qty + totalPrice := rp.UnitPrice * totalWeight marketingProduct := &entity.MarketingProduct{ MarketingId: marketingId, From f6e872c0aa2c026a1fee649635f71d72a9f41d54 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 11:46:39 +0700 Subject: [PATCH 04/12] feat[BE]: implement customer payment report retrieval with pagination and filtering --- .../controllers/repport.controller.go | 84 ++++--- .../dto/repportCustomerPayment.dto.go | 62 +++--- internal/modules/repports/module.go | 3 +- .../customer_payment.repository.go | 205 ++++++++++++++++++ .../repports/services/repport.service.go | 171 ++++++++++++++- .../validations/repport.validation.go | 8 + 6 files changed, 460 insertions(+), 73 deletions(-) create mode 100644 internal/modules/repports/repositories/customer_payment.repository.go diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index d9701305..577c1b1b 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -242,6 +242,60 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error { return ctx.Status(fiber.StatusOK).JSON(resp) } +func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { + var customerID *uint + if customerIDStr := ctx.Query("customer_id"); customerIDStr != "" { + if id, err := strconv.ParseUint(customerIDStr, 10, 32); err == nil { + cid := uint(id) + customerID = &cid + } + } + + query := &validation.CustomerPaymentQuery{ + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + CustomerID: customerID, + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), + } + + // Validate pagination + if customerID == nil && (query.Page < 1 || query.Limit < 1) { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query) + if err != nil { + return err + } + + // If single customer mode, return without pagination + if customerID != nil { + return ctx.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get customer payment report successfully", + Data: result, + }) + } + + // Multiple customers mode with pagination + return ctx.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get customer payment report successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + }) +} + func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { idParam := ctx.Params("idProjectFlockKandang") if idParam == "" { @@ -283,36 +337,6 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { }) } -func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { - page := ctx.QueryInt("page", 1) - limit := ctx.QueryInt("limit", 10) - - if page < 1 || limit < 1 { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") - } - - - - - - data := []dto.CustomerPaymentReportItem{} - totalResults := int64(0) - - return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.CustomerPaymentReportItem]{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get customer payment report successfully", - Meta: response.Meta{ - Page: page, - Limit: limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(limit))), - TotalResults: totalResults, - }, - Data: data, - }) -} - func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index e0938b51..2f200379 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -2,42 +2,33 @@ package dto import ( "time" + + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" ) -// CustomerPaymentReportCustomer represents customer information in the report -type CustomerPaymentReportCustomer struct { - ID uint `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - AccountNumber string `json:"account_number"` - Balance float64 `json:"balance"` - Address string `json:"address"` -} - -// CustomerPaymentReportRow represents each transaction row type CustomerPaymentReportRow struct { - ID uint `json:"id"` - DoDate time.Time `json:"do_date"` - RealizationDate time.Time `json:"realization_date"` - AgingDay int `json:"aging_day"` - Reference string `json:"reference"` - VehiclePlate []string `json:"vehicle_plate"` - Qty float64 `json:"qty"` - Weight float64 `json:"weight"` - AverageWeight float64 `json:"average_weight"` - Price float64 `json:"price"` - CreditNote float64 `json:"credit_note"` - FinalPrice float64 `json:"final_price"` - PPN float64 `json:"ppn"` - Total float64 `json:"total"` - Payment float64 `json:"payment"` - AccountsReceivable float64 `json:"accounts_receivable"` - Notes string `json:"notes"` - PickupInfo string `json:"pickup_info"` - SalesMarketing string `json:"sales_marketing"` + TransactionType string `json:"transaction_type"` + TransactionID int64 `json:"transaction_id"` + TransDate time.Time `json:"trans_date"` + DeliveryDate *time.Time `json:"delivery_date"` + Reference string `json:"reference"` + VehicleNumbers string `json:"vehicle_numbers"` + Qty float64 `json:"qty"` + Weight float64 `json:"weight"` + AverageWeight float64 `json:"average_weight"` + Price float64 `json:"price"` + CreditNote float64 `json:"credit_note"` + FinalPrice float64 `json:"final_price"` + PPN float64 `json:"ppn"` + TotalPrice float64 `json:"total_price"` + PaymentAmount float64 `json:"payment_amount"` + AccountsReceivable float64 `json:"accounts_receivable"` + AgingDay int `json:"aging_day"` + Status string `json:"status"` + PickupInfo string `json:"pickup_info"` + SalesPerson string `json:"sales_person"` } -// CustomerPaymentReportSummary represents summary calculations per customer type CustomerPaymentReportSummary struct { TotalQty float64 `json:"total_qty"` TotalWeight float64 `json:"total_weight"` @@ -50,14 +41,13 @@ type CustomerPaymentReportSummary struct { TotalAccountsReceivable float64 `json:"total_accounts_receivable"` } -// CustomerPaymentReportItem represents data grouped by customer type CustomerPaymentReportItem struct { - Customer CustomerPaymentReportCustomer `json:"customer"` - Rows []CustomerPaymentReportRow `json:"rows"` - Summary CustomerPaymentReportSummary `json:"summary"` + Customer customerDTO.CustomerRelationDTO `json:"customer"` + InitialBalance float64 `json:"initial_balance"` + Rows []CustomerPaymentReportRow `json:"rows"` + Summary CustomerPaymentReportSummary `json:"summary"` } -// CustomerPaymentReportResponse represents the complete response type CustomerPaymentReportResponse struct { Data []CustomerPaymentReportItem `json:"data"` } diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 61f37d4d..b0432316 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -34,10 +34,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * debtSupplierRepository := repportRepo.NewDebtSupplierRepository(db) hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db) + customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository) + repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository) userService := sUser.NewUserService(userRepository, validate) RepportRoutes(router, userService, repportService) diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go new file mode 100644 index 00000000..1d4ffd28 --- /dev/null +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -0,0 +1,205 @@ +package repositories + +import ( + "context" + "time" + + "gorm.io/gorm" +) + +type CustomerPaymentTransaction struct { + TransactionType string `gorm:"column:transaction_type"` + TransactionID int64 `gorm:"column:transaction_id"` + CustomerID int64 `gorm:"column:customer_id"` + TransDate time.Time `gorm:"column:trans_date"` + DeliveryDate *time.Time `gorm:"column:delivery_date"` + Reference string `gorm:"column:reference"` + VehicleNumbers string `gorm:"column:vehicle_numbers"` + Qty float64 `gorm:"column:qty"` + Weight float64 `gorm:"column:weight"` + AverageWeight float64 `gorm:"column:average_weight"` + Price float64 `gorm:"column:price"` + CreditNote float64 `gorm:"column:credit_note"` + FinalPrice float64 `gorm:"column:final_price"` + PPN float64 `gorm:"column:ppn"` + TotalPrice float64 `gorm:"column:total_price"` + PaymentAmount float64 `gorm:"column:payment_amount"` + PickupInfo string `gorm:"column:pickup_info"` + SalesPerson string `gorm:"column:sales_person"` +} + +type CustomerPaymentRepository interface { + GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) + GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) + GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) +} + +type customerPaymentRepositoryImpl struct { + db *gorm.DB +} + +func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository { + return &customerPaymentRepositoryImpl{db: db} +} + +func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) { + // Build SALES subquery + salesQuery := r.db.WithContext(ctx). + Table("marketings m"). + Select(` + 'SALES' AS transaction_type, + m.id::BIGINT AS transaction_id, + c.id::BIGINT AS customer_id, + m.so_date::DATE AS trans_date, + MAX(mdp.delivery_date)::DATE AS delivery_date, + m.so_number AS reference, + COALESCE(STRING_AGG(DISTINCT mdp.vehicle_number, ', ') FILTER (WHERE mdp.vehicle_number IS NOT NULL AND mdp.vehicle_number != ''), '') AS vehicle_numbers, + COALESCE(SUM(COALESCE(mp.qty, 0)), 0)::NUMERIC(15,3) AS qty, + COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0)::NUMERIC(15,3) AS weight, + CASE WHEN COALESCE(SUM(COALESCE(mp.qty, 0)), 0) > 0 + THEN (COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0) / COALESCE(SUM(COALESCE(mp.qty, 0)), 0))::NUMERIC(15,3) + ELSE 0::NUMERIC(15,3) + END AS average_weight, + COALESCE(AVG(COALESCE(mdp.unit_price, mp.unit_price, 0)), 0)::NUMERIC(15,3) AS price, + 0::NUMERIC(15,3) AS credit_note, + COALESCE(SUM(COALESCE(mdp.total_price, mp.total_price)), 0)::NUMERIC(15,3) AS final_price, + 0::NUMERIC(15,3) AS ppn, + COALESCE(SUM(COALESCE(mdp.total_price, mp.total_price)), 0)::NUMERIC(15,3) AS total_price, + 0::NUMERIC(15,3) AS payment_amount, + COALESCE(STRING_AGG(DISTINCT w.name, ', ') FILTER (WHERE w.name IS NOT NULL), '') AS pickup_info, + MAX(u.name) AS sales_person + `). + Joins("INNER JOIN customers c ON c.id = m.customer_id"). + Joins("LEFT JOIN marketing_products mp ON mp.marketing_id = m.id"). + Joins("LEFT JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id"). + Joins("LEFT JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("LEFT JOIN users u ON u.id = m.sales_person_id"). + Where("m.deleted_at IS NULL"). + Where("c.deleted_at IS NULL") + + if customerID != nil { + salesQuery = salesQuery.Where("c.id = ?", *customerID) + } + + salesQuery = salesQuery.Group("m.id, c.id, m.so_date, m.so_number") + + // Build PAYMENT subquery + paymentQuery := r.db.WithContext(ctx). + Table("payments p"). + Select(` + 'PAYMENT' AS transaction_type, + p.id::BIGINT AS transaction_id, + c.id::BIGINT AS customer_id, + p.payment_date::DATE AS trans_date, + NULL AS delivery_date, + COALESCE(p.reference_number, p.payment_code) AS reference, + '-' AS vehicle_numbers, + 0::NUMERIC(15,3) AS qty, + 0::NUMERIC(15,3) AS weight, + 0::NUMERIC(15,3) AS average_weight, + 0::NUMERIC(15,3) AS price, + 0::NUMERIC(15,3) AS credit_note, + 0::NUMERIC(15,3) AS final_price, + 0::NUMERIC(15,3) AS ppn, + 0::NUMERIC(15,3) AS total_price, + p.nominal::NUMERIC(15,3) AS payment_amount, + '-' AS pickup_info, + '-' AS sales_person + `). + Joins("INNER JOIN customers c ON c.id = p.party_id"). + Where("p.party_type = ?", "CUSTOMER"). + Where("p.direction = ?", "IN"). + Where("p.transaction_type = ?", "PENJUALAN"). + Where("p.deleted_at IS NULL"). + Where("c.deleted_at IS NULL") + + if customerID != nil { + paymentQuery = paymentQuery.Where("c.id = ?", *customerID) + } + + // Combine with UNION ALL and execute + var results []CustomerPaymentTransaction + err := r.db.WithContext(ctx). + Raw("? UNION ALL ? ORDER BY customer_id, trans_date, transaction_type DESC, transaction_id", + salesQuery, + paymentQuery, + ). + Scan(&results). + Error + + if err != nil { + return nil, err + } + + return results, nil +} + +func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) { + var result struct { + Nominal float64 + } + + err := r.db.WithContext(ctx). + Table("payments"). + Select("COALESCE(SUM(nominal), 0) as nominal"). + Where("party_type = ?", "CUSTOMER"). + Where("party_id = ?", customerID). + Where("transaction_type = ?", "SALDO_AWAL"). + Where("deleted_at IS NULL"). + Scan(&result). + Error + + if err != nil { + return 0, err + } + + return result.Nominal, nil +} + +func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) { + // Subquery to get all distinct customer IDs with transactions + subQuery := r.db.WithContext(ctx). + Table("(" + + "SELECT DISTINCT c.id as customer_id FROM marketings m " + + "INNER JOIN customers c ON c.id = m.customer_id " + + "WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL " + + "UNION " + + "SELECT DISTINCT c.id as customer_id FROM payments p " + + "INNER JOIN customers c ON c.id = p.party_id " + + "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " + + "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" + + ") as customer_ids") + + // Count total customers + var total int64 + if err := subQuery.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Get paginated customer IDs + var customerIDs []uint + err := r.db.WithContext(ctx). + Table("("+ + "SELECT DISTINCT c.id as customer_id FROM marketings m "+ + "INNER JOIN customers c ON c.id = m.customer_id "+ + "WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL "+ + "UNION "+ + "SELECT DISTINCT c.id as customer_id FROM payments p "+ + "INNER JOIN customers c ON c.id = p.party_id "+ + "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+ + "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+ + ") as customer_ids"). + Select("customer_id"). + Order("customer_id ASC"). + Limit(limit). + Offset(offset). + Pluck("customer_id", &customerIDs). + Error + + if err != nil { + return nil, 0, err + } + + return customerIDs, total, nil +} diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index a36d4d21..89acca09 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -19,6 +19,7 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" @@ -40,12 +41,13 @@ type RepportService interface { GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) - GetCustomerPayment(ctx *fiber.Ctx) (int, error) + GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) } type repportService struct { Log *logrus.Logger Validate *validator.Validate + DB *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository @@ -56,6 +58,7 @@ type repportService struct { DebtSupplierRepo repportRepo.DebtSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository + CustomerPaymentRepo repportRepo.CustomerPaymentRepository } type HppCostAggregate struct { @@ -68,6 +71,7 @@ type HppCostAggregate struct { } func NewRepportService( + db *gorm.DB, validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, @@ -79,10 +83,12 @@ func NewRepportService( debtSupplierRepo repportRepo.DebtSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, + customerPaymentRepo repportRepo.CustomerPaymentRepository, ) RepportService { return &repportService{ Log: utils.Log, Validate: validate, + DB: db, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, @@ -93,6 +99,7 @@ func NewRepportService( DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, + CustomerPaymentRepo: customerPaymentRepo, } } @@ -308,6 +315,163 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation. return weeklyResults, totalWeeks, nil } +func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + // Determine customer IDs to process + var customerIDs []uint + var totalCustomers int64 + + if params.CustomerID != nil { + // Single customer mode + customerIDs = []uint{*params.CustomerID} + totalCustomers = 1 + } else { + // Multiple customers mode with pagination + page := params.Page + limit := params.Limit + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 10 + } + + offset := (page - 1) * limit + + var err error + customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset) + if err != nil { + return nil, 0, err + } + + if len(customerIDs) == 0 { + return []dto.CustomerPaymentReportItem{}, 0, nil + } + } + + // Process each customer + var result []dto.CustomerPaymentReportItem + for _, customerID := range customerIDs { + item, err := s.processCustomerPayment(ctx.Context(), customerID) + if err != nil { + return nil, 0, err + } + result = append(result, item) + } + + return result, totalCustomers, nil +} + +func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) { + // Get customer info + customer := entity.Customer{} + if err := s.DB.WithContext(ctx). + Where("id = ?", customerID). + First(&customer).Error; err != nil { + return dto.CustomerPaymentReportItem{}, err + } + + // Get initial balance + initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID) + if err != nil { + return dto.CustomerPaymentReportItem{}, err + } + + // Get transactions for this customer + cid := customerID + transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid) + if err != nil { + return dto.CustomerPaymentReportItem{}, err + } + + // Process transactions and calculate running balance + rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions)) + runningBalance := initialBalance + + for _, tx := range transactions { + row := dto.CustomerPaymentReportRow{ + TransactionType: tx.TransactionType, + TransactionID: tx.TransactionID, + TransDate: tx.TransDate, + DeliveryDate: tx.DeliveryDate, + Reference: tx.Reference, + VehicleNumbers: tx.VehicleNumbers, + Qty: tx.Qty, + Weight: tx.Weight, + AverageWeight: tx.AverageWeight, + Price: tx.Price, + CreditNote: tx.CreditNote, + FinalPrice: tx.FinalPrice, + PPN: tx.PPN, + TotalPrice: tx.TotalPrice, + PaymentAmount: tx.PaymentAmount, + PickupInfo: tx.PickupInfo, + SalesPerson: tx.SalesPerson, + } + + // Calculate running balance + if tx.TransactionType == "SALES" { + runningBalance -= tx.TotalPrice + // Status will be calculated later (requires looking ahead) + row.Status = "" + row.AgingDay = 0 // Will be calculated later + } else if tx.TransactionType == "PAYMENT" { + runningBalance += tx.PaymentAmount + row.Status = "" + row.AgingDay = 0 + } + + row.AccountsReceivable = runningBalance + rows = append(rows, row) + } + + // Calculate summary + summary := s.calculateSummary(rows, initialBalance) + + // Build customer DTO + customerDTO := customerDTO.CustomerRelationDTO{ + Id: customer.Id, + Name: customer.Name, + Type: customer.Type, + AccountNumber: customer.AccountNumber, + Balance: customer.Balance, + } + + return dto.CustomerPaymentReportItem{ + Customer: customerDTO, + InitialBalance: initialBalance, + Rows: rows, + Summary: summary, + }, nil +} + +func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, initialBalance float64) dto.CustomerPaymentReportSummary { + summary := dto.CustomerPaymentReportSummary{} + + for _, row := range rows { + summary.TotalQty += row.Qty + summary.TotalWeight += row.Weight + summary.TotalCreditNote += row.CreditNote + summary.TotalPPN += row.PPN + + if row.TransactionType == "SALES" { + summary.TotalInitialAmount += row.TotalPrice + summary.TotalFinalAmount += row.FinalPrice + summary.TotalGrandAmount += row.TotalPrice + } else if row.TransactionType == "PAYMENT" { + summary.TotalPayment += row.PaymentAmount + } + } + + // Final AR = initial balance + total sales - total payment + summary.TotalAccountsReceivable = initialBalance + summary.TotalGrandAmount - summary.TotalPayment + + return summary +} + func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO { result := dto.ProductionResultDTO{ CreatedAt: record.CreatedAt, @@ -1374,11 +1538,6 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return params, filters, nil } -func (s *repportService) GetCustomerPayment(c *fiber.Ctx) (int, error) { - - return 0, nil -} - func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 5dde8f51..c79dd90d 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -71,3 +71,11 @@ type ProductionResultQuery struct { Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"` } + +type CustomerPaymentQuery struct { + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + CustomerID *uint `query:"customer_id" validate:"omitempty,gt=0"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` +} From 7daa509cd053d334de49766713e5d7652802b35a Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 14:06:34 +0700 Subject: [PATCH 05/12] feat[BE]: update customer payment report to support multiple customer IDs and nullable aging days --- .../controllers/repport.controller.go | 33 +++--- .../dto/repportCustomerPayment.dto.go | 2 +- .../customer_payment.repository.go | 1 + .../repports/services/repport.service.go | 101 ++++++++++++++---- .../validations/repport.validation.go | 10 +- 5 files changed, 109 insertions(+), 38 deletions(-) diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 577c1b1b..f83f0902 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -243,25 +243,30 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error { } func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { - var customerID *uint - if customerIDStr := ctx.Query("customer_id"); customerIDStr != "" { - if id, err := strconv.ParseUint(customerIDStr, 10, 32); err == nil { - cid := uint(id) - customerID = &cid + var customerIDs []uint + if customerIDsStr := ctx.Query("customer_ids"); customerIDsStr != "" { + ids := strings.Split(customerIDsStr, ",") + for _, idStr := range ids { + idStr = strings.TrimSpace(idStr) + if idStr != "" { + if id, err := strconv.ParseUint(idStr, 10, 32); err == nil { + customerIDs = append(customerIDs, uint(id)) + } + } } } query := &validation.CustomerPaymentQuery{ - Page: ctx.QueryInt("page", 1), - Limit: ctx.QueryInt("limit", 10), - CustomerID: customerID, - StartDate: ctx.Query("start_date", ""), - EndDate: ctx.Query("end_date", ""), + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + CustomerIDs: customerIDs, + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), } // Validate pagination - if customerID == nil && (query.Page < 1 || query.Limit < 1) { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + if len(customerIDs) == 0 && (query.Page < 1 || query.Limit < 1) { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0 when customer_ids is not provided") } result, totalResults, err := c.RepportService.GetCustomerPayment(ctx, query) @@ -269,8 +274,8 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error { return err } - // If single customer mode, return without pagination - if customerID != nil { + // If single customer mode (only 1 customer ID), return without pagination + if len(customerIDs) == 1 { return ctx.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 2f200379..439eed42 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -23,7 +23,7 @@ type CustomerPaymentReportRow struct { TotalPrice float64 `json:"total_price"` PaymentAmount float64 `json:"payment_amount"` AccountsReceivable float64 `json:"accounts_receivable"` - AgingDay int `json:"aging_day"` + AgingDay *int `json:"aging_day"` Status string `json:"status"` PickupInfo string `json:"pickup_info"` SalesPerson string `json:"sales_person"` diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 1d4ffd28..49e9424c 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -2,6 +2,7 @@ package repositories import ( "context" + "time" "gorm.io/gorm" diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 89acca09..c6f18002 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -324,10 +324,14 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C var customerIDs []uint var totalCustomers int64 - if params.CustomerID != nil { - // Single customer mode - customerIDs = []uint{*params.CustomerID} - totalCustomers = 1 + if len(params.CustomerIDs) > 0 { + // Specific customer IDs mode (no pagination) + customerIDs = params.CustomerIDs + totalCustomers = int64(len(customerIDs)) + + if len(customerIDs) == 0 { + return []dto.CustomerPaymentReportItem{}, 0, nil + } } else { // Multiple customers mode with pagination page := params.Page @@ -366,7 +370,6 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C } func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) { - // Get customer info customer := entity.Customer{} if err := s.DB.WithContext(ctx). Where("id = ?", customerID). @@ -374,24 +377,21 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID return dto.CustomerPaymentReportItem{}, err } - // Get initial balance initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID) if err != nil { return dto.CustomerPaymentReportItem{}, err } - // Get transactions for this customer cid := customerID transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid) if err != nil { return dto.CustomerPaymentReportItem{}, err } - // Process transactions and calculate running balance rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions)) runningBalance := initialBalance - for _, tx := range transactions { + for i, tx := range transactions { row := dto.CustomerPaymentReportRow{ TransactionType: tx.TransactionType, TransactionID: tx.TransactionID, @@ -412,26 +412,48 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID SalesPerson: tx.SalesPerson, } - // Calculate running balance + previousBalance := runningBalance + if tx.TransactionType == "SALES" { runningBalance -= tx.TotalPrice - // Status will be calculated later (requires looking ahead) - row.Status = "" - row.AgingDay = 0 // Will be calculated later + status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance) + row.Status = status + + if status == "LUNAS" { + if previousBalance >= tx.TotalPrice { + days := 0 + row.AgingDay = &days + } else if paymentDate != nil { + // Aging = payment_date - trans_date (SO date) + days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) + if days < 0 { + days = 0 + } + row.AgingDay = &days + } else { + days := 0 + row.AgingDay = &days + } + } else { + // Aging = current_date - trans_date (SO date) + days := int(time.Since(tx.TransDate).Hours() / 24) + if days < 0 { + days = 0 + } + row.AgingDay = &days + } } else if tx.TransactionType == "PAYMENT" { runningBalance += tx.PaymentAmount row.Status = "" - row.AgingDay = 0 + row.AgingDay = nil } row.AccountsReceivable = runningBalance rows = append(rows, row) } - // Calculate summary summary := s.calculateSummary(rows, initialBalance) - // Build customer DTO customerDTO := customerDTO.CustomerRelationDTO{ Id: customer.Id, Name: customer.Name, @@ -466,12 +488,55 @@ func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, i } } - // Final AR = initial balance + total sales - total payment - summary.TotalAccountsReceivable = initialBalance + summary.TotalGrandAmount - summary.TotalPayment + // Formula: Total AR = Initial Balance - Total Sales + Total Payment + // - Initial balance: positive (customer deposit) + // - Sales: reduces balance (customer debt) + // - Payment: increases balance (customer pays) + summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment return summary } +func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) { + currentSales := transactions[currentIndex] + + // Status Logic: + // 1. LUNAS: previousBalance >= salesAmount (paid from deposit) + // 2. LUNAS: future payments make AR >= 0 (eventually paid) + // 3. DIBAYAR SEBAGIAN: has payment but not enough + // 4. BELUM LUNAS: no payment at all + + if previousBalance >= currentSales.TotalPrice { + return "LUNAS", nil + } + + hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice + + futureBalance := currentBalance + hasPayment := false + var paymentDateThatMadeItLunas *time.Time + + for i := currentIndex + 1; i < len(transactions); i++ { + if transactions[i].TransactionType == "PAYMENT" { + futureBalance += transactions[i].PaymentAmount + hasPayment = true + + if futureBalance >= 0 { + paymentDateThatMadeItLunas = &transactions[i].TransDate + return "LUNAS", paymentDateThatMadeItLunas + } + } else if transactions[i].TransactionType == "SALES" { + futureBalance -= transactions[i].TotalPrice + } + } + + if hasPayment || hasPartialPaymentFromBalance { + return "DIBAYAR SEBAGIAN", nil + } + + return "BELUM LUNAS", nil +} + func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO { result := dto.ProductionResultDTO{ CreatedAt: record.CreatedAt, diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index c79dd90d..68bfee90 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -73,9 +73,9 @@ type ProductionResultQuery struct { } type CustomerPaymentQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - CustomerID *uint `query:"customer_id" validate:"omitempty,gt=0"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + CustomerIDs []uint `query:"customer_ids" 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"` } From 804ff45dbd1bc6e2ccf4c324853834efc702c31c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 15:15:29 +0700 Subject: [PATCH 06/12] feat[BE]: enhance customer payment report with vehicle numbers and pickup info, add date filtering --- .../master/customers/dto/customer.dto.go | 1 + .../dto/repportCustomerPayment.dto.go | 110 ++++++++++++++---- .../customer_payment.repository.go | 6 +- .../repports/services/repport.service.go | 102 ++++++++-------- 4 files changed, 141 insertions(+), 78 deletions(-) diff --git a/internal/modules/master/customers/dto/customer.dto.go b/internal/modules/master/customers/dto/customer.dto.go index 444c6768..592f14cd 100644 --- a/internal/modules/master/customers/dto/customer.dto.go +++ b/internal/modules/master/customers/dto/customer.dto.go @@ -52,6 +52,7 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO { Name: e.Name, Type: e.Type, AccountNumber: e.AccountNumber, + Balance: e.Balance, Pic: pic, } } diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 439eed42..3f6b7a2d 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -1,32 +1,35 @@ package dto import ( + "strings" "time" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" + repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" ) type CustomerPaymentReportRow struct { - TransactionType string `json:"transaction_type"` - TransactionID int64 `json:"transaction_id"` - TransDate time.Time `json:"trans_date"` - DeliveryDate *time.Time `json:"delivery_date"` - Reference string `json:"reference"` - VehicleNumbers string `json:"vehicle_numbers"` - Qty float64 `json:"qty"` - Weight float64 `json:"weight"` - AverageWeight float64 `json:"average_weight"` - Price float64 `json:"price"` - CreditNote float64 `json:"credit_note"` - FinalPrice float64 `json:"final_price"` - PPN float64 `json:"ppn"` - TotalPrice float64 `json:"total_price"` - PaymentAmount float64 `json:"payment_amount"` - AccountsReceivable float64 `json:"accounts_receivable"` - AgingDay *int `json:"aging_day"` - Status string `json:"status"` - PickupInfo string `json:"pickup_info"` - SalesPerson string `json:"sales_person"` + TransactionType string `json:"transaction_type"` + TransactionID int64 `json:"transaction_id"` + TransDate time.Time `json:"trans_date"` + DeliveryDate *time.Time `json:"delivery_date"` + Reference string `json:"reference"` + + Qty float64 `json:"qty"` + Weight float64 `json:"weight"` + AverageWeight float64 `json:"average_weight"` + Price float64 `json:"price"` + CreditNote float64 `json:"credit_note"` + FinalPrice float64 `json:"final_price"` + PPN float64 `json:"ppn"` + TotalPrice float64 `json:"total_price"` + PaymentAmount float64 `json:"payment_amount"` + AccountsReceivable float64 `json:"accounts_receivable"` + AgingDay *int `json:"aging_day"` + Status string `json:"status"` + VehicleNumbers []string `json:"vehicle_numbers"` + PickupInfo []string `json:"pickup_info"` + SalesPerson string `json:"sales_person"` } type CustomerPaymentReportSummary struct { @@ -51,3 +54,70 @@ type CustomerPaymentReportItem struct { type CustomerPaymentReportResponse struct { Data []CustomerPaymentReportItem `json:"data"` } + +func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) CustomerPaymentReportRow { + return CustomerPaymentReportRow{ + TransactionType: tx.TransactionType, + TransactionID: tx.TransactionID, + TransDate: tx.TransDate, + DeliveryDate: tx.DeliveryDate, + Reference: tx.Reference, + Qty: tx.Qty, + Weight: tx.Weight, + AverageWeight: tx.AverageWeight, + Price: tx.Price, + CreditNote: tx.CreditNote, + FinalPrice: tx.FinalPrice, + PPN: tx.PPN, + TotalPrice: tx.TotalPrice, + PaymentAmount: tx.PaymentAmount, + VehicleNumbers: parseStringSlice(tx.VehicleNumbers), + PickupInfo: parseStringSlice(tx.PickupInfo), + SalesPerson: tx.SalesPerson, + } +} + +func parseStringSlice(str string) []string { + str = strings.TrimSpace(str) + if str == "" || str == "-" { + return []string{} + } + + parts := strings.Split(str, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + result = append(result, part) + } + } + + return result +} + +func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary { + summary := CustomerPaymentReportSummary{} + + for _, row := range rows { + summary.TotalQty += row.Qty + summary.TotalWeight += row.Weight + summary.TotalCreditNote += row.CreditNote + summary.TotalPPN += row.PPN + + if row.TransactionType == "SALES" { + summary.TotalInitialAmount += row.TotalPrice + summary.TotalFinalAmount += row.FinalPrice + summary.TotalGrandAmount += row.TotalPrice + } else if row.TransactionType == "PAYMENT" { + summary.TotalPayment += row.PaymentAmount + } + } + + // Formula: Total AR = Initial Balance - Total Sales + Total Payment + // - Initial balance: positive (customer deposit) + // - Sales: reduces balance (customer debt) + // - Payment: increases balance (customer pays) + summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment + + return summary +} diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 49e9424c..5a39b127 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -54,7 +54,8 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte m.so_date::DATE AS trans_date, MAX(mdp.delivery_date)::DATE AS delivery_date, m.so_number AS reference, - COALESCE(STRING_AGG(DISTINCT mdp.vehicle_number, ', ') FILTER (WHERE mdp.vehicle_number IS NOT NULL AND mdp.vehicle_number != ''), '') AS vehicle_numbers, + COALESCE(STRING_AGG(DISTINCT mdp.vehicle_number, ', ') FILTER (WHERE mdp.vehicle_number IS NOT NULL), '') AS vehicle_numbers, + COALESCE(SUM(COALESCE(mp.qty, 0)), 0)::NUMERIC(15,3) AS qty, COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0)::NUMERIC(15,3) AS weight, CASE WHEN COALESCE(SUM(COALESCE(mp.qty, 0)), 0) > 0 @@ -77,7 +78,8 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id"). Joins("LEFT JOIN users u ON u.id = m.sales_person_id"). Where("m.deleted_at IS NULL"). - Where("c.deleted_at IS NULL") + Where("c.deleted_at IS NULL"). + Where("mdp.delivery_date IS NOT NULL") if customerID != nil { salesQuery = salesQuery.Where("c.id = ?", *customerID) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c6f18002..1dba2114 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -359,7 +359,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C // Process each customer var result []dto.CustomerPaymentReportItem for _, customerID := range customerIDs { - item, err := s.processCustomerPayment(ctx.Context(), customerID) + item, err := s.processCustomerPayment(ctx.Context(), customerID, params) if err != nil { return nil, 0, err } @@ -369,7 +369,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C return result, totalCustomers, nil } -func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint) (dto.CustomerPaymentReportItem, error) { +func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) { customer := entity.Customer{} if err := s.DB.WithContext(ctx). Where("id = ?", customerID). @@ -392,28 +392,11 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID runningBalance := initialBalance for i, tx := range transactions { - row := dto.CustomerPaymentReportRow{ - TransactionType: tx.TransactionType, - TransactionID: tx.TransactionID, - TransDate: tx.TransDate, - DeliveryDate: tx.DeliveryDate, - Reference: tx.Reference, - VehicleNumbers: tx.VehicleNumbers, - Qty: tx.Qty, - Weight: tx.Weight, - AverageWeight: tx.AverageWeight, - Price: tx.Price, - CreditNote: tx.CreditNote, - FinalPrice: tx.FinalPrice, - PPN: tx.PPN, - TotalPrice: tx.TotalPrice, - PaymentAmount: tx.PaymentAmount, - PickupInfo: tx.PickupInfo, - SalesPerson: tx.SalesPerson, - } previousBalance := runningBalance + row := dto.ToCustomerPaymentReportRow(tx) + if tx.TransactionType == "SALES" { runningBalance -= tx.TotalPrice status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance) @@ -452,51 +435,58 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID rows = append(rows, row) } - summary := s.calculateSummary(rows, initialBalance) + if params.StartDate != "" || params.EndDate != "" { + filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows)) + location, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return dto.CustomerPaymentReportItem{}, err + } - customerDTO := customerDTO.CustomerRelationDTO{ - Id: customer.Id, - Name: customer.Name, - Type: customer.Type, - AccountNumber: customer.AccountNumber, - Balance: customer.Balance, + var startDate, endDate *time.Time + if params.StartDate != "" { + parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location) + if err != nil { + return dto.CustomerPaymentReportItem{}, err + } + startDate = &parsed + } + if params.EndDate != "" { + parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location) + if err != nil { + return dto.CustomerPaymentReportItem{}, err + } + // End date should be inclusive, so set to end of day + endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location) + endDate = &endOfDay + } + + for _, row := range rows { + transDate := row.TransDate.In(location) + + // Check if transaction date is within range + if startDate != nil && transDate.Before(*startDate) { + continue + } + if endDate != nil && transDate.After(*endDate) { + continue + } + + filteredRows = append(filteredRows, row) + } + + rows = filteredRows } + summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance) + return dto.CustomerPaymentReportItem{ - Customer: customerDTO, + Customer: customerDTO.ToCustomerRelationDTO(customer), InitialBalance: initialBalance, Rows: rows, Summary: summary, }, nil } -func (s *repportService) calculateSummary(rows []dto.CustomerPaymentReportRow, initialBalance float64) dto.CustomerPaymentReportSummary { - summary := dto.CustomerPaymentReportSummary{} - - for _, row := range rows { - summary.TotalQty += row.Qty - summary.TotalWeight += row.Weight - summary.TotalCreditNote += row.CreditNote - summary.TotalPPN += row.PPN - - if row.TransactionType == "SALES" { - summary.TotalInitialAmount += row.TotalPrice - summary.TotalFinalAmount += row.FinalPrice - summary.TotalGrandAmount += row.TotalPrice - } else if row.TransactionType == "PAYMENT" { - summary.TotalPayment += row.PaymentAmount - } - } - - // Formula: Total AR = Initial Balance - Total Sales + Total Payment - // - Initial balance: positive (customer deposit) - // - Sales: reduces balance (customer debt) - // - Payment: increases balance (customer pays) - summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment - - return summary -} - func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) { currentSales := transactions[currentIndex] From aeb5433346edd647972287c528e89c015340a3ec Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 20:00:44 +0700 Subject: [PATCH 07/12] feat[BE]: refine customer payment report structure by removing unused fields and enhancing query logic for better performance --- .../dto/repportCustomerPayment.dto.go | 15 +--- internal/modules/repports/module.go | 4 +- .../customer_payment.repository.go | 69 ++++++++----------- .../repports/services/repport.service.go | 20 +++--- 4 files changed, 40 insertions(+), 68 deletions(-) diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 3f6b7a2d..cdac5029 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -19,9 +19,7 @@ type CustomerPaymentReportRow struct { Weight float64 `json:"weight"` AverageWeight float64 `json:"average_weight"` Price float64 `json:"price"` - CreditNote float64 `json:"credit_note"` FinalPrice float64 `json:"final_price"` - PPN float64 `json:"ppn"` TotalPrice float64 `json:"total_price"` PaymentAmount float64 `json:"payment_amount"` AccountsReceivable float64 `json:"accounts_receivable"` @@ -35,10 +33,7 @@ type CustomerPaymentReportRow struct { type CustomerPaymentReportSummary struct { TotalQty float64 `json:"total_qty"` TotalWeight float64 `json:"total_weight"` - TotalInitialAmount float64 `json:"total_initial_amount"` - TotalCreditNote float64 `json:"total_credit_note"` TotalFinalAmount float64 `json:"total_final_amount"` - TotalPPN float64 `json:"total_ppn"` TotalGrandAmount float64 `json:"total_grand_amount"` TotalPayment float64 `json:"total_payment"` TotalAccountsReceivable float64 `json:"total_accounts_receivable"` @@ -66,9 +61,7 @@ func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) Custo Weight: tx.Weight, AverageWeight: tx.AverageWeight, Price: tx.Price, - CreditNote: tx.CreditNote, FinalPrice: tx.FinalPrice, - PPN: tx.PPN, TotalPrice: tx.TotalPrice, PaymentAmount: tx.PaymentAmount, VehicleNumbers: parseStringSlice(tx.VehicleNumbers), @@ -101,11 +94,8 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal for _, row := range rows { summary.TotalQty += row.Qty summary.TotalWeight += row.Weight - summary.TotalCreditNote += row.CreditNote - summary.TotalPPN += row.PPN if row.TransactionType == "SALES" { - summary.TotalInitialAmount += row.TotalPrice summary.TotalFinalAmount += row.FinalPrice summary.TotalGrandAmount += row.TotalPrice } else if row.TransactionType == "PAYMENT" { @@ -113,10 +103,7 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal } } - // Formula: Total AR = Initial Balance - Total Sales + Total Payment - // - Initial balance: positive (customer deposit) - // - Sales: reduces balance (customer debt) - // - Payment: increases balance (customer pays) + // Total AR = Initial Balance - Total Sales + Total Payment summary.TotalAccountsReceivable = initialBalance - summary.TotalGrandAmount + summary.TotalPayment return summary diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index b0432316..d081306c 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -12,6 +12,7 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" @@ -35,10 +36,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db) productionResultRepository := repportRepo.NewProductionResultRepository(db) customerPaymentRepository := repportRepo.NewCustomerPaymentRepository(db) + customerRepository := customerRepo.NewCustomerRepository(db) userRepository := rUser.NewUserRepository(db) approvalSvc := approvalService.NewApprovalService(approvalRepository) - repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository) + repportService := sRepport.NewRepportService(db, validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, debtSupplierRepository, hppPerKandangRepository, productionResultRepository, customerPaymentRepository, customerRepository) userService := sUser.NewUserService(userRepository, validate) RepportRoutes(router, userService, repportService) diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 5a39b127..8a5747aa 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -20,9 +20,7 @@ type CustomerPaymentTransaction struct { Weight float64 `gorm:"column:weight"` AverageWeight float64 `gorm:"column:average_weight"` Price float64 `gorm:"column:price"` - CreditNote float64 `gorm:"column:credit_note"` FinalPrice float64 `gorm:"column:final_price"` - PPN float64 `gorm:"column:ppn"` TotalPrice float64 `gorm:"column:total_price"` PaymentAmount float64 `gorm:"column:payment_amount"` PickupInfo string `gorm:"column:pickup_info"` @@ -44,50 +42,41 @@ func NewCustomerPaymentRepository(db *gorm.DB) CustomerPaymentRepository { } func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) { - // Build SALES subquery salesQuery := r.db.WithContext(ctx). - Table("marketings m"). + Table("marketing_delivery_products mdp"). Select(` 'SALES' AS transaction_type, - m.id::BIGINT AS transaction_id, + mdp.id::BIGINT AS transaction_id, c.id::BIGINT AS customer_id, m.so_date::DATE AS trans_date, - MAX(mdp.delivery_date)::DATE AS delivery_date, - m.so_number AS reference, - COALESCE(STRING_AGG(DISTINCT mdp.vehicle_number, ', ') FILTER (WHERE mdp.vehicle_number IS NOT NULL), '') AS vehicle_numbers, + mdp.delivery_date::DATE AS delivery_date, + m.so_number || '-' || TO_CHAR(mdp.delivery_date, 'YYYYMMDD') || '-' || CAST(pw.warehouse_id AS VARCHAR) AS reference, + COALESCE(mdp.vehicle_number, '') AS vehicle_numbers, - COALESCE(SUM(COALESCE(mp.qty, 0)), 0)::NUMERIC(15,3) AS qty, - COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0)::NUMERIC(15,3) AS weight, - CASE WHEN COALESCE(SUM(COALESCE(mp.qty, 0)), 0) > 0 - THEN (COALESCE(SUM(COALESCE(mdp.total_weight, mp.total_weight, 0)), 0) / COALESCE(SUM(COALESCE(mp.qty, 0)), 0))::NUMERIC(15,3) - ELSE 0::NUMERIC(15,3) - END AS average_weight, - COALESCE(AVG(COALESCE(mdp.unit_price, mp.unit_price, 0)), 0)::NUMERIC(15,3) AS price, - 0::NUMERIC(15,3) AS credit_note, - COALESCE(SUM(COALESCE(mdp.total_price, mp.total_price)), 0)::NUMERIC(15,3) AS final_price, - 0::NUMERIC(15,3) AS ppn, - COALESCE(SUM(COALESCE(mdp.total_price, mp.total_price)), 0)::NUMERIC(15,3) AS total_price, + COALESCE(mdp.usage_qty, 0)::NUMERIC(15,3) AS qty, + COALESCE(mdp.total_weight, 0)::NUMERIC(15,3) AS weight, + COALESCE(mdp.avg_weight, 0)::NUMERIC(15,3) AS average_weight, + COALESCE(mdp.unit_price, 0)::NUMERIC(15,3) AS price, + COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS final_price, + COALESCE(mdp.total_price, 0)::NUMERIC(15,3) AS total_price, 0::NUMERIC(15,3) AS payment_amount, - COALESCE(STRING_AGG(DISTINCT w.name, ', ') FILTER (WHERE w.name IS NOT NULL), '') AS pickup_info, - MAX(u.name) AS sales_person + w.name AS pickup_info, + u.name AS sales_person `). + Joins("INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). + Joins("INNER JOIN marketings m ON m.id = mp.marketing_id"). Joins("INNER JOIN customers c ON c.id = m.customer_id"). - Joins("LEFT JOIN marketing_products mp ON mp.marketing_id = m.id"). - Joins("LEFT JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id"). - Joins("LEFT JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). - Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id"). - Joins("LEFT JOIN users u ON u.id = m.sales_person_id"). + Joins("INNER JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id"). + Joins("INNER JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("INNER JOIN users u ON u.id = m.sales_person_id"). + Where("mdp.delivery_date IS NOT NULL"). Where("m.deleted_at IS NULL"). - Where("c.deleted_at IS NULL"). - Where("mdp.delivery_date IS NOT NULL") + Where("c.deleted_at IS NULL") if customerID != nil { salesQuery = salesQuery.Where("c.id = ?", *customerID) } - salesQuery = salesQuery.Group("m.id, c.id, m.so_date, m.so_number") - - // Build PAYMENT subquery paymentQuery := r.db.WithContext(ctx). Table("payments p"). Select(` @@ -102,9 +91,7 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte 0::NUMERIC(15,3) AS weight, 0::NUMERIC(15,3) AS average_weight, 0::NUMERIC(15,3) AS price, - 0::NUMERIC(15,3) AS credit_note, 0::NUMERIC(15,3) AS final_price, - 0::NUMERIC(15,3) AS ppn, 0::NUMERIC(15,3) AS total_price, p.nominal::NUMERIC(15,3) AS payment_amount, '-' AS pickup_info, @@ -121,7 +108,6 @@ func (r *customerPaymentRepositoryImpl) GetCustomerPaymentTransactions(ctx conte paymentQuery = paymentQuery.Where("c.id = ?", *customerID) } - // Combine with UNION ALL and execute var results []CustomerPaymentTransaction err := r.db.WithContext(ctx). Raw("? UNION ALL ? ORDER BY customer_id, trans_date, transaction_type DESC, transaction_id", @@ -161,12 +147,13 @@ func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context. } func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) { - // Subquery to get all distinct customer IDs with transactions subQuery := r.db.WithContext(ctx). Table("(" + - "SELECT DISTINCT c.id as customer_id FROM marketings m " + + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " + + "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " + + "INNER JOIN marketings m ON m.id = mp.marketing_id " + "INNER JOIN customers c ON c.id = m.customer_id " + - "WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL " + + "WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " + "UNION " + "SELECT DISTINCT c.id as customer_id FROM payments p " + "INNER JOIN customers c ON c.id = p.party_id " + @@ -174,19 +161,19 @@ func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx conte "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" + ") as customer_ids") - // Count total customers var total int64 if err := subQuery.Count(&total).Error; err != nil { return nil, 0, err } - // Get paginated customer IDs var customerIDs []uint err := r.db.WithContext(ctx). Table("("+ - "SELECT DISTINCT c.id as customer_id FROM marketings m "+ + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+ + "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+ + "INNER JOIN marketings m ON m.id = mp.marketing_id "+ "INNER JOIN customers c ON c.id = m.customer_id "+ - "WHERE m.deleted_at IS NULL AND c.deleted_at IS NULL "+ + "WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+ "UNION "+ "SELECT DISTINCT c.id as customer_id FROM payments p "+ "INNER JOIN customers c ON c.id = p.party_id "+ diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 1dba2114..2c102418 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -20,6 +20,7 @@ import ( marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" + customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" @@ -59,6 +60,7 @@ type repportService struct { HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository CustomerPaymentRepo repportRepo.CustomerPaymentRepository + CustomerRepo customerRepo.CustomerRepository } type HppCostAggregate struct { @@ -84,6 +86,7 @@ func NewRepportService( hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, customerPaymentRepo repportRepo.CustomerPaymentRepository, + customerRepo customerRepo.CustomerRepository, ) RepportService { return &repportService{ Log: utils.Log, @@ -100,6 +103,7 @@ func NewRepportService( HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, CustomerPaymentRepo: customerPaymentRepo, + CustomerRepo: customerRepo, } } @@ -356,7 +360,6 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C } } - // Process each customer var result []dto.CustomerPaymentReportItem for _, customerID := range customerIDs { item, err := s.processCustomerPayment(ctx.Context(), customerID, params) @@ -370,10 +373,9 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C } func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) { - customer := entity.Customer{} - if err := s.DB.WithContext(ctx). - Where("id = ?", customerID). - First(&customer).Error; err != nil { + + customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil) + if err != nil { return dto.CustomerPaymentReportItem{}, err } @@ -407,7 +409,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID days := 0 row.AgingDay = &days } else if paymentDate != nil { - // Aging = payment_date - trans_date (SO date) days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) if days < 0 { days = 0 @@ -418,7 +419,6 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID row.AgingDay = &days } } else { - // Aging = current_date - trans_date (SO date) days := int(time.Since(tx.TransDate).Hours() / 24) if days < 0 { days = 0 @@ -455,22 +455,18 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID if err != nil { return dto.CustomerPaymentReportItem{}, err } - // End date should be inclusive, so set to end of day endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location) endDate = &endOfDay } for _, row := range rows { transDate := row.TransDate.In(location) - - // Check if transaction date is within range if startDate != nil && transDate.Before(*startDate) { continue } if endDate != nil && transDate.After(*endDate) { continue } - filteredRows = append(filteredRows, row) } @@ -480,7 +476,7 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance) return dto.CustomerPaymentReportItem{ - Customer: customerDTO.ToCustomerRelationDTO(customer), + Customer: customerDTO.ToCustomerRelationDTO(*customer), InitialBalance: initialBalance, Rows: rows, Summary: summary, From c6dc94a4e11c5b94fe0c50344191a82fa78afe5c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 20:06:41 +0700 Subject: [PATCH 08/12] feat[BE]: add permission requirement for customer payment report route --- internal/modules/repports/route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 3f803677..2f5eceec 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -21,5 +21,5 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/debt-supplier", m.RequirePermissions(m.P_ReportDebtSupplierGetAll), ctrl.GetDebtSupplier) route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll), ctrl.GetHppPerKandang) route.Get("/production-result/:idProjectFlockKandang", m.RequirePermissions(m.P_ReportProductionResultGetAll), ctrl.GetProductionResult) - route.Get("/customer-payment", ctrl.GetCustomerPayment) + route.Get("/customer-payment", m.RequirePermissions(m.P_ReportCustomerPaymentGetAll), ctrl.GetCustomerPayment) } From c316a6d7a93cd4619bcf0f7ff961fb05a1de5c22 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 15 Jan 2026 10:41:44 +0700 Subject: [PATCH 09/12] feat[BE]: add address field to CustomerRelationDTO and refactor payment report functions for improved clarity and structure --- .../master/customers/dto/customer.dto.go | 2 + .../dto/repportCustomerPayment.dto.go | 42 ++++++++++++------- .../repports/services/repport.service.go | 10 +---- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/internal/modules/master/customers/dto/customer.dto.go b/internal/modules/master/customers/dto/customer.dto.go index 592f14cd..eceafa39 100644 --- a/internal/modules/master/customers/dto/customer.dto.go +++ b/internal/modules/master/customers/dto/customer.dto.go @@ -14,6 +14,7 @@ type CustomerRelationDTO struct { Name string `json:"name"` Type string `json:"type"` AccountNumber string `json:"account_number"` + Address string `json:"address,omitempty"` Balance float64 `json:"balance"` Pic *userDTO.UserRelationDTO `json:"pic,omitempty"` } @@ -52,6 +53,7 @@ func ToCustomerRelationDTO(e entity.Customer) CustomerRelationDTO { Name: e.Name, Type: e.Type, AccountNumber: e.AccountNumber, + Address: e.Address, Balance: e.Balance, Pic: pic, } diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index cdac5029..5a8a69ac 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -4,6 +4,7 @@ import ( "strings" "time" + "gitlab.com/mbugroup/lti-api.git/internal/entities" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" ) @@ -70,25 +71,16 @@ func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) Custo } } -func parseStringSlice(str string) []string { - str = strings.TrimSpace(str) - if str == "" || str == "-" { - return []string{} +func ToCustomerPaymentReportItem(customer entities.Customer, initialBalance float64, rows []CustomerPaymentReportRow, summary CustomerPaymentReportSummary) CustomerPaymentReportItem { + return CustomerPaymentReportItem{ + Customer: customerDTO.ToCustomerRelationDTO(customer), + InitialBalance: initialBalance, + Rows: rows, + Summary: summary, } - - parts := strings.Split(str, ",") - result := make([]string, 0, len(parts)) - for _, part := range parts { - part = strings.TrimSpace(part) - if part != "" { - result = append(result, part) - } - } - - return result } -func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary { +func ToCustomerPaymentReportSummary(rows []CustomerPaymentReportRow, initialBalance float64) CustomerPaymentReportSummary { summary := CustomerPaymentReportSummary{} for _, row := range rows { @@ -108,3 +100,21 @@ func CalculateCustomerPaymentSummary(rows []CustomerPaymentReportRow, initialBal return summary } + +func parseStringSlice(str string) []string { + str = strings.TrimSpace(str) + if str == "" || str == "-" { + return []string{} + } + + parts := strings.Split(str, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part != "" { + result = append(result, part) + } + } + + return result +} diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 4e2104e8..1b721d64 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -19,7 +19,6 @@ import ( expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" - customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" @@ -473,14 +472,9 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID rows = filteredRows } - summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance) + summary := dto.ToCustomerPaymentReportSummary(rows, initialBalance) - return dto.CustomerPaymentReportItem{ - Customer: customerDTO.ToCustomerRelationDTO(*customer), - InitialBalance: initialBalance, - Rows: rows, - Summary: summary, - }, nil + return dto.ToCustomerPaymentReportItem(*customer, initialBalance, rows, summary), nil } func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) { From 8792161c02c437e32a43f28d83b961c224172408 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 15 Jan 2026 10:58:00 +0700 Subject: [PATCH 10/12] feat[BE]: rename Price field to UnitPrice in CustomerPaymentReportRow for clarity --- internal/modules/repports/dto/repportCustomerPayment.dto.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/modules/repports/dto/repportCustomerPayment.dto.go b/internal/modules/repports/dto/repportCustomerPayment.dto.go index 5a8a69ac..99862349 100644 --- a/internal/modules/repports/dto/repportCustomerPayment.dto.go +++ b/internal/modules/repports/dto/repportCustomerPayment.dto.go @@ -19,7 +19,7 @@ type CustomerPaymentReportRow struct { Qty float64 `json:"qty"` Weight float64 `json:"weight"` AverageWeight float64 `json:"average_weight"` - Price float64 `json:"price"` + UnitPrice float64 `json:"unit_price"` FinalPrice float64 `json:"final_price"` TotalPrice float64 `json:"total_price"` PaymentAmount float64 `json:"payment_amount"` @@ -61,7 +61,7 @@ func ToCustomerPaymentReportRow(tx repportRepo.CustomerPaymentTransaction) Custo Qty: tx.Qty, Weight: tx.Weight, AverageWeight: tx.AverageWeight, - Price: tx.Price, + UnitPrice: tx.Price, FinalPrice: tx.FinalPrice, TotalPrice: tx.TotalPrice, PaymentAmount: tx.PaymentAmount, From 89293a843e566d852938716790de14dd18994a27 Mon Sep 17 00:00:00 2001 From: giovanni Date: Thu, 15 Jan 2026 16:07:45 +0700 Subject: [PATCH 11/12] adjust api hpp kandang --- .../modules/repports/dto/repportHpp.dto.go | 58 +++---- .../hpp_per_kandang.repository.go | 160 +++++++++++++----- .../repports/services/repport.service.go | 108 +++++++++--- 3 files changed, 227 insertions(+), 99 deletions(-) diff --git a/internal/modules/repports/dto/repportHpp.dto.go b/internal/modules/repports/dto/repportHpp.dto.go index 63c5dce9..f790244c 100644 --- a/internal/modules/repports/dto/repportHpp.dto.go +++ b/internal/modules/repports/dto/repportHpp.dto.go @@ -25,14 +25,12 @@ type HppPerKandangResponseData struct { } type HppPerKandangRowDTO struct { - ID int `json:"id"` - Kandang HppPerKandangRowKandangDTO `json:"kandang"` - WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"` - RemainingChickenBirds int64 `json:"remaining_chicken_birds"` - RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"` - AvgWeightKg float64 `json:"avg_weight_kg"` - EggProductionPieces int64 `json:"egg_production_pieces"` - EggProductionKg float64 `json:"egg_production_kg"` + ID int `json:"id"` + Kandang HppPerKandangRowKandangDTO `json:"kandang"` + WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"` + AvgWeightKg float64 `json:"avg_weight_kg"` + EggProductionPieces int64 `json:"egg_production_pieces"` + EggProductionKg float64 `json:"egg_production_kg"` // FeedCostRp float64 `json:"feed_cost_rp"` // OvkCostRp float64 `json:"ovk_cost_rp"` EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"` @@ -80,34 +78,28 @@ type HppPerKandangSummaryDTO struct { } type HppPerKandangSummaryWeightRangeDTO struct { - ID int `json:"id"` - WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"` - Label string `json:"label"` - RemainingChickenBirds int64 `json:"remaining_chicken_birds"` - RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"` - AvgWeightKg float64 `json:"avg_weight_kg"` - EggProductionPieces int64 `json:"egg_production_pieces"` - EggProductionKg float64 `json:"egg_production_kg"` - EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"` - EggValueRp int64 `json:"egg_value_rp"` - FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"` - DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"` - AverageDocPriceRp float64 `json:"average_doc_price_rp"` - HppRp float64 `json:"hpp_rp"` - RemainingValueRp int64 `json:"remaining_value_rp"` + ID int `json:"id"` + WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"` + Label string `json:"label"` + AvgWeightKg float64 `json:"avg_weight_kg"` + EggProductionPieces int64 `json:"egg_production_pieces"` + EggProductionKg float64 `json:"egg_production_kg"` + EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"` + EggValueRp int64 `json:"egg_value_rp"` + FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"` + DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"` + AverageDocPriceRp float64 `json:"average_doc_price_rp"` + HppRp float64 `json:"hpp_rp"` + RemainingValueRp int64 `json:"remaining_value_rp"` } type HppPerKandangSummaryTotalDTO struct { - TotalRemainingChickenBirds int64 `json:"total_remaining_chicken_birds"` - TotalRemainingChickenWeightKg float64 `json:"total_remaining_chicken_weight_kg"` - AverageWeightKg float64 `json:"average_weight_kg"` - TotalRemainingValueRp int64 `json:"total_remaining_value_rp"` - TotalEggProductionPieces int64 `json:"total_egg_production_pieces"` - TotalEggProductionKg float64 `json:"total_egg_production_kg"` - AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"` - TotalEggValueRp int64 `json:"total_egg_value_rp"` - TotalHppRp float64 `json:"total_hpp_rp"` - TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"` + AverageWeightKg float64 `json:"average_weight_kg"` + TotalEggProductionPieces int64 `json:"total_egg_production_pieces"` + TotalEggProductionKg float64 `json:"total_egg_production_kg"` + AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"` + TotalEggValueRp int64 `json:"total_egg_value_rp"` + TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"` } func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO { diff --git a/internal/modules/repports/repositories/hpp_per_kandang.repository.go b/internal/modules/repports/repositories/hpp_per_kandang.repository.go index 4bd9aab4..64676ca8 100644 --- a/internal/modules/repports/repositories/hpp_per_kandang.repository.go +++ b/internal/modules/repports/repositories/hpp_per_kandang.repository.go @@ -11,6 +11,7 @@ import ( ) type HppPerKandangRow struct { + ProjectFlockKandangID uint KandangID uint KandangName string KandangStatus string @@ -18,6 +19,7 @@ type HppPerKandangRow struct { LocationName string PicID uint PicName string + RecordingCount int64 RemainingChickenBirds float64 RemainingChickenWeight float64 EggProductionWeightKg float64 @@ -44,7 +46,8 @@ type HppPerKandangSupplierRow struct { type HppPerKandangRepository interface { GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) - GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) + GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) + GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) } type hppPerKandangRepository struct { @@ -58,9 +61,31 @@ func NewHppPerKandangRepository(db *gorm.DB) HppPerKandangRepository { func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) { var rows []HppPerKandangRow - query := r.db.WithContext(ctx). + latestApproval := r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.action"). + Joins(` + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = ? + GROUP BY approvable_id + ) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`, + string(utils.ApprovalWorkflowRecording), + ) + + validRecordings := r.db.WithContext(ctx). Table("recordings AS r"). + Select("r.id, r.project_flock_kandangs_id, r.total_chick_qty"). + Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval). + Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). + Where("r.deleted_at IS NULL"). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)) + + query := r.db.WithContext(ctx). + Table("project_flocks AS pf"). Select(` + pfk.id AS project_flock_kandang_id, k.id AS kandang_id, k.name AS kandang_name, k.status AS kandang_status, @@ -68,22 +93,21 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en loc.name AS location_name, pic.id AS pic_id, pic.name AS pic_name, - COALESCE(MAX(r.total_chick_qty), 0) AS remaining_chicken_birds, - COALESCE(SUM(rbw.total_weight), 0) AS remaining_chicken_weight, - COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg, - COALESCE(SUM(re.qty), 0) AS egg_production_pieces`). - Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). + COALESCE(COUNT(vr.id), 0) AS recording_count, + COALESCE(MAX(vr.total_chick_qty), 0) AS remaining_chicken_birds, + 0 AS remaining_chicken_weight, + 0 AS egg_production_weight_kg, + 0 AS egg_production_pieces`). + Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN users AS pic ON pic.id = k.pic_id"). - Joins("LEFT JOIN recording_bws AS rbw ON rbw.recording_id = r.id"). - Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id"). - Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). - Where("r.deleted_at IS NULL") + Joins("LEFT JOIN (?) AS vr ON vr.project_flock_kandangs_id = pfk.id", validRecordings). + Where("pfk.closed_at IS NULL") query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs) - query = query.Group("k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name"). + query = query.Group("pfk.id, k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name"). Order("k.id ASC") if err := query.Scan(&rows).Error; err != nil { @@ -93,41 +117,44 @@ func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, en return rows, nil } -func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) { +func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) { var rows []HppPerKandangCostRow - recordingPfk := r.db.WithContext(ctx). - Table("recordings AS r"). - Select("DISTINCT pfk.id"). - Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). - Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). - Joins("JOIN locations AS loc ON loc.id = k.location_id"). - Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). - Where("r.deleted_at IS NULL") - recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs) - purchaseStockableKey := fifo.StockableKeyPurchaseItems.String() transferStockableKey := fifo.StockableKeyStockTransferIn.String() + latestApproval := r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.action"). + Joins(` + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = ? + GROUP BY approvable_id + ) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`, + string(utils.ApprovalWorkflowRecording), + ) + query := r.db.WithContext(ctx). Table("recordings AS r"). Select(` k.id AS kandang_id, COALESCE(SUM(CASE WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) + WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0) ELSE 0 END), 0) AS feed_cost, COALESCE(SUM(CASE WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) - WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) + WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.total_qty, 0) * COALESCE(tpi.price, 0) ELSE 0 END), 0) AS ovk_cost`, utils.FlagPakan, transferStockableKey, utils.FlagPakan, utils.FlagOVK, transferStockableKey, utils.FlagOVK). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). - Joins("JOIN locations AS loc ON loc.id = k.location_id"). + Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval). Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id"). Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive). Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey). @@ -136,11 +163,10 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id"). Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct). - Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})). - Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). - Where("r.deleted_at IS NULL") - - query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs) + Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs). + // Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). + Where("r.deleted_at IS NULL"). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)) query = query.Group("k.id").Order("k.id ASC") @@ -172,9 +198,8 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id"). Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id"). Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id"). - Where("pc.project_flock_kandang_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})). + Where("pc.project_flock_kandang_id IN ?", projectFlockKandangIDs). Group("pfk.kandang_id, s.id, s.name, s.alias") - docQuery = applyLocationFilters(docQuery, areaIDs, locationIDs, kandangIDs) if err := docQuery.Scan(&docRows).Error; err != nil { return nil, nil, err @@ -254,9 +279,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id"). Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub). Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub). - Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})). + Where("pfk.id IN (?)", projectFlockKandangIDs). Group("k.id") - budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs) + // budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs) if err := budgetQuery.Scan(&budgetRows).Error; err != nil { return nil, nil, err @@ -288,9 +313,9 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, Joins("JOIN locations AS loc ON loc.id = k.location_id"). Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id"). Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id"). - Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})). + Where("pfk.id IN (?)", projectFlockKandangIDs). Group("k.id") - expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs) + // expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs) if err := expenseQuery.Scan(&expenseRows).Error; err != nil { return nil, nil, err @@ -323,10 +348,10 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id"). Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct). Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}). - Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})). - Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). + Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs). + // Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). Where("r.deleted_at IS NULL") - feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs) + // feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs) if err := feedQuery.Scan(&feedSuppliers).Error; err != nil { return nil, nil, err @@ -347,6 +372,61 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, return rows, supplierRows, nil } +func (r *hppPerKandangRepository) GetEggProductionByProjectFlockKandangIDs(ctx context.Context, start, end time.Time, projectFlockKandangIDs []uint) (map[uint]HppPerKandangRow, error) { + if len(projectFlockKandangIDs) == 0 { + return map[uint]HppPerKandangRow{}, nil + } + + latestApproval := r.db.WithContext(ctx). + Table("approvals AS a"). + Select("a.approvable_id, a.action"). + Joins(` + JOIN ( + SELECT approvable_id, MAX(action_at) AS latest_action_at + FROM approvals + WHERE approvable_type = ? + GROUP BY approvable_id + ) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`, + string(utils.ApprovalWorkflowRecording), + ) + + type eggRow struct { + ProjectFlockKandangID uint + EggProductionWeightKg float64 + EggProductionPieces float64 + } + + eggRows := make([]eggRow, 0) + query := r.db.WithContext(ctx). + Table("recordings AS r"). + Select(` + r.project_flock_kandangs_id AS project_flock_kandang_id, + COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg, + COALESCE(SUM(re.qty), 0) AS egg_production_pieces`). + Joins("LEFT JOIN (?) AS la ON la.approvable_id = r.id", latestApproval). + Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id"). + Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs). + // Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end). + Where("r.deleted_at IS NULL"). + Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). + Group("r.project_flock_kandangs_id") + + if err := query.Scan(&eggRows).Error; err != nil { + return nil, err + } + + result := make(map[uint]HppPerKandangRow, len(eggRows)) + for _, row := range eggRows { + result[row.ProjectFlockKandangID] = HppPerKandangRow{ + ProjectFlockKandangID: row.ProjectFlockKandangID, + EggProductionWeightKg: row.EggProductionWeightKg, + EggProductionPieces: row.EggProductionPieces, + } + } + + return result, nil +} + func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB { if len(areaIDs) > 0 { query = query.Where("loc.area_id IN ?", areaIDs) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index c4883b72..9be8d8c4 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -972,10 +972,37 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes if err != nil { return nil, nil, err } - costRows, supplierRows, err := s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs) - if err != nil { - return nil, nil, err + + validPfkIDs := make([]uint, 0, len(repoRows)) + pfkIndex := make(map[uint]int, len(repoRows)) + for idx := range repoRows { + row := repoRows[idx] + pfkIndex[row.ProjectFlockKandangID] = idx + if row.RecordingCount > 0 { + validPfkIDs = append(validPfkIDs, row.ProjectFlockKandangID) + } } + + costRows := make([]repportRepo.HppPerKandangCostRow, 0) + supplierRows := make([]repportRepo.HppPerKandangSupplierRow, 0) + if len(validPfkIDs) > 0 { + costRows, supplierRows, err = s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, validPfkIDs) + if err != nil { + return nil, nil, err + } + + eggMap, err := s.HppPerKandangRepo.GetEggProductionByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs) + if err != nil { + return nil, nil, err + } + for pfkID, egg := range eggMap { + if rowIdx, ok := pfkIndex[pfkID]; ok { + repoRows[rowIdx].EggProductionWeightKg = egg.EggProductionWeightKg + repoRows[rowIdx].EggProductionPieces = egg.EggProductionPieces + } + } + } + costMap := make(map[uint]HppCostAggregate, len(costRows)) for _, row := range costRows { costMap[row.KandangID] = HppCostAggregate{ @@ -1028,9 +1055,13 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes Max float64 } type weightRangeAggregate struct { - Summary *dto.HppPerKandangSummaryWeightRangeDTO - EggHppSum float64 - EggHppCount int + Summary *dto.HppPerKandangSummaryWeightRangeDTO + RemainingBirds int64 + RemainingWeightKg float64 + EggHppSum float64 + EggHppCount int + FeedSuppliers map[int64]dto.HppPerKandangSupplierDTO + DocSuppliers map[int64]dto.HppPerKandangSupplierDTO } dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows)) @@ -1049,6 +1080,10 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes var totalEggHppCount int for _, row := range repoRows { + if !params.ShowUnrecorded && row.RecordingCount == 0 { + continue + } + birdsFloat := row.RemainingChickenBirds if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) { birdsFloat = 0 @@ -1067,9 +1102,16 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes } avgWeight := 0.0 - if birdsFloat > 0 { - avgWeight = weightFloat / birdsFloat + if eggPiecesFloat > 0 { + avgWeight = eggWeightFloat / eggPiecesFloat } + if params.WeightMin != nil && avgWeight < *params.WeightMin { + continue + } + if params.WeightMax != nil && avgWeight > *params.WeightMax { + continue + } + weightMin := math.Floor(avgWeight*10) / 10 if weightMin < 0 { weightMin = 0 @@ -1116,9 +1158,7 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes WeightMin: weightMin, WeightMax: weightMax, }, - RemainingChickenBirds: rowBirds, - RemainingChickenWeightKg: weightFloat, - AvgWeightKg: avgWeight, + AvgWeightKg: avgWeight, // FeedCostRp: costEntry.FeedCost, // OvkCostRp: costEntry.OvkCost, DocSuppliers: docSupplierMap[row.KandangID], @@ -1126,10 +1166,10 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes EggProductionPieces: rowEggPieces, EggProductionKg: eggWeightFloat, AverageDocPriceRp: avgDocPrice, - HppRp: hppRp, - EggHppRpPerKg: eggHpp, - RemainingValueRp: rowRemainingValue, - EggValueRp: rowEggValue, + // HppRp: hppRp, + EggHppRpPerKg: eggHpp, + RemainingValueRp: rowRemainingValue, + EggValueRp: rowEggValue, }) totalBirds += rowBirds @@ -1161,13 +1201,25 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes }, Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax), }, + FeedSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO), + DocSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO), } perRangeMap[rangeKey] = rangeAgg } rangeSummary := rangeAgg.Summary - rangeSummary.RemainingChickenBirds += rowBirds - rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight + rangeAgg.RemainingBirds += rowBirds + rangeAgg.RemainingWeightKg += row.RemainingChickenWeight + for _, supplier := range feedSupplierMap[row.KandangID] { + if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok { + rangeAgg.FeedSuppliers[supplier.ID] = supplier + } + } + for _, supplier := range docSupplierMap[row.KandangID] { + if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok { + rangeAgg.DocSuppliers[supplier.ID] = supplier + } + } rangeSummary.EggProductionPieces += rowEggPieces rangeSummary.EggProductionKg += eggWeightFloat rangeSummary.RemainingValueRp += rowRemainingValue @@ -1194,22 +1246,27 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes agg := perRangeMap[key] entry := agg.Summary entry.ID = idx + 1 - if entry.RemainingChickenBirds > 0 { - entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds) + if agg.RemainingBirds > 0 { + entry.AvgWeightKg = agg.RemainingWeightKg / float64(agg.RemainingBirds) } if agg.EggHppCount > 0 { entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount) } + entry.FeedSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.FeedSuppliers)) + for _, supplier := range agg.FeedSuppliers { + entry.FeedSuppliers = append(entry.FeedSuppliers, supplier) + } + entry.DocSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.DocSuppliers)) + for _, supplier := range agg.DocSuppliers { + entry.DocSuppliers = append(entry.DocSuppliers, supplier) + } perRangeSummary = append(perRangeSummary, *entry) } totalSummary := dto.HppPerKandangSummaryTotalDTO{ - TotalRemainingChickenBirds: totalBirds, - TotalRemainingChickenWeightKg: totalWeight, - TotalEggProductionPieces: totalEggPieces, - TotalEggProductionKg: totalEggKg, - TotalRemainingValueRp: totalRemainingValueRp, - TotalEggValueRp: totalEggValueRp, + TotalEggProductionPieces: totalEggPieces, + TotalEggProductionKg: totalEggKg, + TotalEggValueRp: totalEggValueRp, } if totalBirds > 0 { totalSummary.AverageWeightKg = totalWeight / float64(totalBirds) @@ -1218,7 +1275,6 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount) } if totalHppCount > 0 { - totalSummary.TotalHppRp = totalHppSum / float64(totalHppCount) } if totalDocPriceCount > 0 { totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount) From 62ccc2e5d6f28da75bdb5c61a832022c5e22b4b0 Mon Sep 17 00:00:00 2001 From: giovanni Date: Thu, 15 Jan 2026 16:48:37 +0700 Subject: [PATCH 12/12] adjust avg weight --- .../modules/repports/services/repport.service.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 9be8d8c4..738bd34d 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1058,6 +1058,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes Summary *dto.HppPerKandangSummaryWeightRangeDTO RemainingBirds int64 RemainingWeightKg float64 + AvgWeightSum float64 + AvgWeightCount int64 EggHppSum float64 EggHppCount int FeedSuppliers map[int64]dto.HppPerKandangSupplierDTO @@ -1078,6 +1080,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes var totalDocPriceCount int var totalEggHppSum float64 var totalEggHppCount int + var totalAvgWeightSum float64 + var totalAvgWeightCount int64 for _, row := range repoRows { if !params.ShowUnrecorded && row.RecordingCount == 0 { @@ -1178,6 +1182,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes totalEggKg += eggWeightFloat totalRemainingValueRp += rowRemainingValue totalEggValueRp += rowEggValue + totalAvgWeightSum += avgWeight + totalAvgWeightCount++ if weightFloat > 0 { totalHppSum += hppRp totalHppCount++ @@ -1210,6 +1216,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes rangeSummary := rangeAgg.Summary rangeAgg.RemainingBirds += rowBirds rangeAgg.RemainingWeightKg += row.RemainingChickenWeight + rangeAgg.AvgWeightSum += avgWeight + rangeAgg.AvgWeightCount++ for _, supplier := range feedSupplierMap[row.KandangID] { if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok { rangeAgg.FeedSuppliers[supplier.ID] = supplier @@ -1246,8 +1254,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes agg := perRangeMap[key] entry := agg.Summary entry.ID = idx + 1 - if agg.RemainingBirds > 0 { - entry.AvgWeightKg = agg.RemainingWeightKg / float64(agg.RemainingBirds) + if agg.AvgWeightCount > 0 { + entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount) } if agg.EggHppCount > 0 { entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount) @@ -1269,7 +1277,9 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes TotalEggValueRp: totalEggValueRp, } if totalBirds > 0 { - totalSummary.AverageWeightKg = totalWeight / float64(totalBirds) + } + if totalAvgWeightCount > 0 { + totalSummary.AverageWeightKg = totalAvgWeightSum / float64(totalAvgWeightCount) } if totalEggHppCount > 0 { totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)