From 1ef32407f1a19e97d0138e86a550b97ea7228e4a Mon Sep 17 00:00:00 2001 From: giovanni Date: Fri, 5 Jun 2026 13:51:09 +0700 Subject: [PATCH] create api get depresiasi v2 --- .../controllers/repport.controller.go | 23 ++ .../dto/repportExpenseDepreciation.dto.go | 23 ++ internal/modules/repports/route.go | 1 + .../repports/services/repport.service.go | 216 ++++++++++++++++++ .../validations/repport.validation.go | 7 + 5 files changed, 270 insertions(+) diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 4fb0e167..3fb580ce 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -152,6 +152,29 @@ func (c *RepportController) GetExpenseDepreciation(ctx *fiber.Ctx) error { return ctx.Status(fiber.StatusOK).JSON(resp) } +func (c *RepportController) GetExpenseDepreciationV2(ctx *fiber.Ctx) error { + rows, meta, err := c.RepportService.GetExpenseDepreciationV2(ctx) + if err != nil { + return err + } + + resp := struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Meta dto.ExpenseDepreciationV2MetaDTO `json:"meta"` + Data []dto.ExpenseDepreciationV2RowDTO `json:"data"` + }{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expense depreciation report v2 successfully", + Meta: *meta, + Data: rows, + } + + return ctx.Status(fiber.StatusOK).JSON(resp) +} + func (c *RepportController) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) error { rows, meta, err := c.RepportService.GetExpenseDepreciationManualInputs(ctx) if err != nil { diff --git a/internal/modules/repports/dto/repportExpenseDepreciation.dto.go b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go index db6328cd..2df82a5f 100644 --- a/internal/modules/repports/dto/repportExpenseDepreciation.dto.go +++ b/internal/modules/repports/dto/repportExpenseDepreciation.dto.go @@ -40,6 +40,29 @@ type ExpenseDepreciationManualInputRowDTO struct { Note *string `json:"note"` } +type ExpenseDepreciationV2MetaDTO struct { + ProjectFlockID int64 `json:"project_flock_id"` + FarmName string `json:"farm_name"` + LocationID int64 `json:"location_id"` + Period string `json:"period"` + Limit int `json:"limit"` + TotalDays int `json:"total_days"` +} + +type ExpenseDepreciationV2RowDTO struct { + Date string `json:"date"` + DepreciationPercentEffective float64 `json:"depreciation_percent_effective"` + DepreciationValue float64 `json:"depreciation_value"` + PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"` + MultiplicationPercentage float64 `json:"multiplication_percentage"` + DayN int `json:"day_n"` + ChickinDate string `json:"chickin_date"` + TotalValuePulletAfterDepreciation float64 `json:"total_value_pullet_after_depreciation"` + StandardEffectiveDate string `json:"standard_effective_date,omitempty"` + TotalPopulation float64 `json:"total_population"` + Components any `json:"components"` +} + func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO { return ExpenseDepreciationFiltersDTO{ AreaID: area, diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 56faae35..24ff8c99 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -17,6 +17,7 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) route.Get("/expense/depreciation", ctrl.GetExpenseDepreciation) + route.Get("/expense/v2/depreciation", ctrl.GetExpenseDepreciationV2) route.Get("/expense/depreciation/manual-inputs", ctrl.GetExpenseDepreciationManualInputs) route.Put("/expense/depreciation/manual-inputs", ctrl.UpsertExpenseDepreciationManualInput) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 198dfa82..e2a405d1 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -43,6 +43,7 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) + GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationV2RowDTO, *dto.ExpenseDepreciationV2MetaDTO, error) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) @@ -355,6 +356,182 @@ func (s *repportService) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDe return rows[offset:end], meta, nil } +func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationV2RowDTO, *dto.ExpenseDepreciationV2MetaDTO, error) { + params, err := s.parseExpenseDepreciationV2Query(ctx) + if err != nil { + return nil, nil, err + } + if err := s.Validate.Struct(params); err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + if s.ExpenseDepreciationRepo == nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") + } + if s.HppCostRepo == nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "hpp cost repository is not configured") + } + if s.HppV2Svc == nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "hpp v2 service is not configured") + } + + location, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") + } + periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location) + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD") + } + + limit := params.Limit + if limit <= 0 { + limit = 10 + } + + farmID := uint(params.ProjectFlockID) + kandangIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx.Context(), farmID) + if err != nil { + return nil, nil, err + } + if len(kandangIDs) == 0 { + return nil, nil, fiber.NewError(fiber.StatusNotFound, "project flock has no kandangs") + } + + var farmName string + if err := s.db.WithContext(ctx.Context()). + Table("project_flocks"). + Select("flock_name"). + Where("id = ? AND deleted_at IS NULL", farmID). + Scan(&farmName).Error; err != nil { + return nil, nil, err + } + if farmName == "" { + return nil, nil, fiber.NewError(fiber.StatusNotFound, "project flock not found") + } + + rows := make([]dto.ExpenseDepreciationV2RowDTO, 0, limit) + actualDays := 0 + + for i := 0; i < limit; i++ { + dayDate := periodDate.AddDate(0, 0, i) + dayStr := dayDate.Format("2006-01-02") + + var totalDepreciationValue float64 + var totalPulletCostDayN float64 + var totalPopulation float64 + var allKandangComponents []depreciationKandangComponent + + for _, kandangID := range kandangIDs { + breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate) + if err != nil { + return nil, nil, err + } + if breakdown == nil { + continue + } + + depreciationComponent := hppV2FindDepreciationComponent(breakdown) + if depreciationComponent == nil { + continue + } + + for _, part := range depreciationComponent.Parts { + if part.Total <= 0 { + continue + } + + houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType) + component := depreciationKandangComponent{ + ProjectFlockKandangID: breakdown.ProjectFlockKandangID, + KandangID: breakdown.KandangID, + KandangName: breakdown.KandangName, + SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"), + HouseType: houseType, + DayN: hppV2DetailInt(part.Details, "schedule_day"), + DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"), + MultiplicationPercentage: hppV2DetailFloat(part.Details, "multiplication_percentage"), + PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"), + DepreciationValue: part.Total, + TotalValuePulletAfterDepreciation: hppV2DetailFloat(part.Details, "total_value_pullet_after_depreciation"), + DepreciationSource: part.Code, + OriginDate: hppV2DetailString(part.Details, "origin_date"), + ChickinDate: hppV2DetailString(part.Details, "origin_date"), + StandardEffectiveDate: hppV2DetailString(part.Details, "standard_effective_date"), + Population: hppV2DetailFloat(part.Details, "kandang_population"), + } + + if component.HouseType == "" { + component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type")) + } + + if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil { + component.TransferID = ref.ID + component.TransferDate = ref.Date + component.TransferQty = ref.Qty + } + + if part.Code == "manual_cutover" { + if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 { + component.StartScheduleDay = &startDay + } + component.CutoverDate = hppV2DetailString(part.Details, "cutover_date") + if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 { + component.ManualInputID = &manualID + } + if component.ManualInputID == nil { + if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 { + manualID := ref.ID + component.ManualInputID = &manualID + } + } + } + + totalPulletCostDayN += component.PulletCostDayN + totalDepreciationValue += component.DepreciationValue + totalPopulation += component.Population + allKandangComponents = append(allKandangComponents, component) + } + } + + effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN) + + components := depreciationFarmComponents{ + KandangCount: len(allKandangComponents), + TotalPopulation: totalPopulation, + Kandang: allKandangComponents, + } + componentsJSON, _ := json.Marshal(components) + + multiplicationPercentage, dayN, chickinDate, standardEffectiveDate := depreciationSnapshotInfo(parseSnapshotComponents(componentsJSON)) + + rows = append(rows, dto.ExpenseDepreciationV2RowDTO{ + Date: dayStr, + DepreciationPercentEffective: effectivePercent, + DepreciationValue: totalDepreciationValue, + PulletCostDayNTotal: totalPulletCostDayN, + MultiplicationPercentage: multiplicationPercentage, + DayN: dayN, + ChickinDate: chickinDate, + TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue, + StandardEffectiveDate: standardEffectiveDate, + TotalPopulation: totalPopulation, + Components: parseSnapshotComponents(componentsJSON), + }) + actualDays++ + } + + meta := &dto.ExpenseDepreciationV2MetaDTO{ + ProjectFlockID: params.ProjectFlockID, + FarmName: farmName, + LocationID: params.LocationID, + Period: params.Period, + Limit: limit, + TotalDays: actualDays, + } + + return rows, meta, nil +} + func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) { params, filters, err := s.parseExpenseDepreciationQuery(ctx) if err != nil { @@ -3025,6 +3202,45 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat return params, filters, nil } +func (s *repportService) parseExpenseDepreciationV2Query(ctx *fiber.Ctx) (*validation.ExpenseDepreciationV2Query, error) { + limit := ctx.QueryInt("limit", 10) + if limit < 1 { + limit = 10 + } + period := strings.TrimSpace(ctx.Query("period", "")) + locationID := ctx.QueryInt("location_id", 0) + projectFlockID := ctx.QueryInt("project_flock_id", 0) + + locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, err + } + + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + return nil, fiber.NewError(fiber.StatusForbidden, "no location access") + } + found := false + for _, id := range allowed { + if id == int64(locationID) { + found = true + break + } + } + if !found { + return nil, fiber.NewError(fiber.StatusForbidden, "location not in scope") + } + } + + return &validation.ExpenseDepreciationV2Query{ + Limit: limit, + Period: period, + LocationID: int64(locationID), + ProjectFlockID: int64(projectFlockID), + }, 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 c2d06c12..93a2d3f3 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -93,6 +93,13 @@ type ExpenseDepreciationQuery struct { LocationIDs []int64 `query:"-"` } +type ExpenseDepreciationV2Query struct { + Limit int `query:"limit" validate:"omitempty,min=1,max=90"` + Period string `query:"period" validate:"required,datetime=2006-01-02"` + LocationID int64 `query:"location_id" validate:"required,gt=0"` + ProjectFlockID int64 `query:"project_flock_id" validate:"required,gt=0"` +} + type ExpenseDepreciationManualInputUpsert struct { ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"` TotalCost float64 `json:"total_cost" validate:"required,gte=0"`