create api get depresiasi v2

This commit is contained in:
giovanni
2026-06-05 13:51:09 +07:00
parent 6d2b6a0cb8
commit 1ef32407f1
5 changed files with 270 additions and 0 deletions
@@ -152,6 +152,29 @@ func (c *RepportController) GetExpenseDepreciation(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp) 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 { func (c *RepportController) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) error {
rows, meta, err := c.RepportService.GetExpenseDepreciationManualInputs(ctx) rows, meta, err := c.RepportService.GetExpenseDepreciationManualInputs(ctx)
if err != nil { if err != nil {
@@ -40,6 +40,29 @@ type ExpenseDepreciationManualInputRowDTO struct {
Note *string `json:"note"` 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 { func NewExpenseDepreciationFiltersDTO(area, location, projectFlockID, period string) ExpenseDepreciationFiltersDTO {
return ExpenseDepreciationFiltersDTO{ return ExpenseDepreciationFiltersDTO{
AreaID: area, AreaID: area,
+1
View File
@@ -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", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
route.Get("/expense/depreciation", ctrl.GetExpenseDepreciation) route.Get("/expense/depreciation", ctrl.GetExpenseDepreciation)
route.Get("/expense/v2/depreciation", ctrl.GetExpenseDepreciationV2)
route.Get("/expense/depreciation/manual-inputs", ctrl.GetExpenseDepreciationManualInputs) route.Get("/expense/depreciation/manual-inputs", ctrl.GetExpenseDepreciationManualInputs)
route.Put("/expense/depreciation/manual-inputs", ctrl.UpsertExpenseDepreciationManualInput) route.Put("/expense/depreciation/manual-inputs", ctrl.UpsertExpenseDepreciationManualInput)
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
@@ -43,6 +43,7 @@ import (
type RepportService interface { type RepportService interface {
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, 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) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error)
UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error)
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, 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 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) { func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) {
params, filters, err := s.parseExpenseDepreciationQuery(ctx) params, filters, err := s.parseExpenseDepreciationQuery(ctx)
if err != nil { if err != nil {
@@ -3025,6 +3202,45 @@ func (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validat
return params, filters, nil 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) { func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
if raw == "" { if raw == "" {
@@ -93,6 +93,13 @@ type ExpenseDepreciationQuery struct {
LocationIDs []int64 `query:"-"` 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 { type ExpenseDepreciationManualInputUpsert struct {
ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"` ProjectFlockID uint `json:"project_flock_id" validate:"required,gt=0"`
TotalCost float64 `json:"total_cost" validate:"required,gte=0"` TotalCost float64 `json:"total_cost" validate:"required,gte=0"`