mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat[BE]: implement customer payment report retrieval with pagination and filtering
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user