mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22bf66dbb9 | |||
| 540434e33b | |||
| 0ebad48348 | |||
| 9ab4e1a6ef | |||
| 0a900986e7 | |||
| 217f35b250 | |||
| b3887b8d08 | |||
| 2ddfa57aed |
@@ -96,6 +96,7 @@ type HppV2FarmDepreciationSnapshotRow struct {
|
||||
DepreciationPercentEffective float64
|
||||
DepreciationValue float64
|
||||
PulletCostDayNTotal float64
|
||||
Components []byte
|
||||
}
|
||||
|
||||
type HppV2CostRepository interface {
|
||||
@@ -404,7 +405,7 @@ func (r *HppV2RepositoryImpl) GetFarmDepreciationSnapshotByProjectFlockIDAndPeri
|
||||
var row HppV2FarmDepreciationSnapshotRow
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("farm_depreciation_snapshots").
|
||||
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total").
|
||||
Select("id, project_flock_id, period_date, depreciation_percent_effective, depreciation_value, pullet_cost_day_n_total, components").
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Where("period_date = DATE(?)", periodDate).
|
||||
Limit(1).
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
@@ -1472,6 +1473,18 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
||||
depreciationPercent = (appliedDepreciation / appliedPulletCostDayN) * 100
|
||||
}
|
||||
|
||||
details := map[string]any{
|
||||
"basis_total": snapshot.DepreciationValue,
|
||||
"pullet_cost_day_n": appliedPulletCostDayN,
|
||||
"depreciation_percent": depreciationPercent,
|
||||
"snapshot_id": snapshot.ID,
|
||||
"snapshot_period_date": formatDateOnly(snapshot.PeriodDate),
|
||||
"snapshot_project_flock": snapshot.ProjectFlockID,
|
||||
}
|
||||
for key, value := range farmDepreciationSnapshotMetadata(snapshot.Components, projectFlockKandangId) {
|
||||
details[key] = value
|
||||
}
|
||||
|
||||
return &HppV2ComponentPart{
|
||||
Code: hppV2PartDepreciationFarmSnapshot,
|
||||
Title: "Farm Snapshot",
|
||||
@@ -1483,14 +1496,7 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
||||
Denominator: denominator,
|
||||
Ratio: ratio,
|
||||
},
|
||||
Details: map[string]any{
|
||||
"basis_total": snapshot.DepreciationValue,
|
||||
"pullet_cost_day_n": appliedPulletCostDayN,
|
||||
"depreciation_percent": depreciationPercent,
|
||||
"snapshot_id": snapshot.ID,
|
||||
"snapshot_period_date": formatDateOnly(snapshot.PeriodDate),
|
||||
"snapshot_project_flock": snapshot.ProjectFlockID,
|
||||
},
|
||||
Details: details,
|
||||
References: []HppV2Reference{
|
||||
{
|
||||
Type: "farm_depreciation_snapshot",
|
||||
@@ -1504,6 +1510,84 @@ func (s *hppV2Service) buildFarmSnapshotDepreciationPart(
|
||||
}, nil
|
||||
}
|
||||
|
||||
type farmDepreciationSnapshotComponents struct {
|
||||
Kandang []farmDepreciationSnapshotKandangComponent `json:"kandang"`
|
||||
}
|
||||
|
||||
type farmDepreciationSnapshotKandangComponent struct {
|
||||
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
|
||||
DayN int `json:"day_n"`
|
||||
MultiplicationPercent float64 `json:"multiplication_percentage"`
|
||||
ChickinDate string `json:"chickin_date"`
|
||||
OriginDate string `json:"origin_date"`
|
||||
StandardEffectiveDate string `json:"standard_effective_date"`
|
||||
Population float64 `json:"population"`
|
||||
}
|
||||
|
||||
func farmDepreciationSnapshotMetadata(raw []byte, projectFlockKandangID uint) map[string]any {
|
||||
result := make(map[string]any)
|
||||
if len(raw) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
var components farmDepreciationSnapshotComponents
|
||||
if err := json.Unmarshal(raw, &components); err != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
var fallback *farmDepreciationSnapshotKandangComponent
|
||||
for i := range components.Kandang {
|
||||
component := &components.Kandang[i]
|
||||
if !component.hasDepreciationMetadata() {
|
||||
continue
|
||||
}
|
||||
if component.ProjectFlockKandangID == projectFlockKandangID {
|
||||
return component.snapshotDetails()
|
||||
}
|
||||
if fallback == nil {
|
||||
fallback = component
|
||||
}
|
||||
}
|
||||
if fallback != nil {
|
||||
return fallback.snapshotDetails()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c farmDepreciationSnapshotKandangComponent) hasDepreciationMetadata() bool {
|
||||
return c.DayN > 0 ||
|
||||
c.MultiplicationPercent > 0 ||
|
||||
c.ChickinDate != "" ||
|
||||
c.OriginDate != "" ||
|
||||
c.StandardEffectiveDate != "" ||
|
||||
c.Population > 0
|
||||
}
|
||||
|
||||
func (c farmDepreciationSnapshotKandangComponent) snapshotDetails() map[string]any {
|
||||
chickinDate := c.ChickinDate
|
||||
if chickinDate == "" {
|
||||
chickinDate = c.OriginDate
|
||||
}
|
||||
|
||||
details := map[string]any{
|
||||
"schedule_day": c.DayN,
|
||||
"multiplication_percentage": c.MultiplicationPercent,
|
||||
}
|
||||
if chickinDate != "" {
|
||||
details["origin_date"] = chickinDate
|
||||
details["chickin_date"] = chickinDate
|
||||
}
|
||||
if c.StandardEffectiveDate != "" {
|
||||
details["standard_effective_date"] = c.StandardEffectiveDate
|
||||
}
|
||||
if c.Population > 0 {
|
||||
details["kandang_population"] = c.Population
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
func (s *hppV2Service) buildNormalTransferDepreciationPart(
|
||||
contextRow *commonRepo.HppV2ProjectFlockKandangContext,
|
||||
transferInput *commonRepo.HppV2LatestTransferInputRow,
|
||||
|
||||
@@ -825,6 +825,28 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
|
||||
DepreciationPercentEffective: 10,
|
||||
DepreciationValue: 1000,
|
||||
PulletCostDayNTotal: 10000,
|
||||
Components: []byte(`{
|
||||
"kandang_count": 2,
|
||||
"total_population": 1000,
|
||||
"kandang": [
|
||||
{
|
||||
"project_flock_kandang_id": 71,
|
||||
"day_n": 5,
|
||||
"multiplication_percentage": 0.95,
|
||||
"chickin_date": "2026-01-02",
|
||||
"standard_effective_date": "2026-06-01",
|
||||
"population": 800
|
||||
},
|
||||
{
|
||||
"project_flock_kandang_id": 70,
|
||||
"day_n": 7,
|
||||
"multiplication_percentage": 0.93,
|
||||
"chickin_date": "2026-01-01",
|
||||
"standard_effective_date": "2026-06-02",
|
||||
"population": 200
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
},
|
||||
eggProductionByPFK: map[uint]struct {
|
||||
@@ -873,6 +895,21 @@ func TestHppV2CalculateHppBreakdown_UsesFarmSnapshotDepreciationProratedByEggPro
|
||||
if depreciation.Parts[0].Details["snapshot_id"] != uint(901) {
|
||||
t.Fatalf("expected snapshot id 901, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["schedule_day"] != 7 {
|
||||
t.Fatalf("expected snapshot schedule_day 7, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["multiplication_percentage"] != 0.93 {
|
||||
t.Fatalf("expected snapshot multiplication_percentage 0.93, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["chickin_date"] != "2026-01-01" {
|
||||
t.Fatalf("expected snapshot chickin_date 2026-01-01, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["standard_effective_date"] != "2026-06-02" {
|
||||
t.Fatalf("expected snapshot standard_effective_date 2026-06-02, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
if depreciation.Parts[0].Details["kandang_population"] != float64(200) {
|
||||
t.Fatalf("expected snapshot kandang_population 200, got %+v", depreciation.Parts[0].Details)
|
||||
}
|
||||
}
|
||||
|
||||
func stubKey(ids []uint, flags []string) string {
|
||||
|
||||
@@ -72,9 +72,9 @@ func (s kandangGroupService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
||||
}
|
||||
|
||||
if params.OrderBy == "desc" || params.OrderBy == "" {
|
||||
db = db.Order(fmt.Sprintf("kandang_groups.%s DESC", params.SortBy))
|
||||
db = db.Order(fmt.Sprintf("kandang_groups.%s DESC, kandang_groups.id ASC", params.SortBy))
|
||||
} else {
|
||||
db = db.Order(fmt.Sprintf("kandang_groups.%s ASC", params.SortBy))
|
||||
db = db.Order(fmt.Sprintf("kandang_groups.%s ASC, kandang_groups.id ASC", params.SortBy))
|
||||
}
|
||||
|
||||
return db
|
||||
|
||||
@@ -20,6 +20,6 @@ type Query struct {
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"updated_at"`
|
||||
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"desc"`
|
||||
SortBy string `query:"sort_by" validate:"omitempty,max=50,oneof=name created_at updated_at" default:"name"`
|
||||
OrderBy string `query:"order_by" validate:"omitempty,oneof=asc desc" default:"asc"`
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ type ExpenseDepreciationV2MetaDTO struct {
|
||||
}
|
||||
|
||||
type ExpenseDepreciationV2RowDTO struct {
|
||||
Date string `json:"date"`
|
||||
Date string `json:"date"`
|
||||
DepreciationPercentEffective float64 `json:"depreciation_percent_effective"`
|
||||
DepreciationValue float64 `json:"depreciation_value"`
|
||||
PulletCostDayNTotal float64 `json:"pullet_cost_day_n_total"`
|
||||
@@ -60,7 +60,6 @@ type ExpenseDepreciationV2RowDTO struct {
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpenseDepreciationRowDTOComponentsJSONContract(t *testing.T) {
|
||||
v1 := ExpenseDepreciationRowDTO{
|
||||
ProjectFlockID: 1,
|
||||
FarmName: "Farm A",
|
||||
Period: "2026-06-05",
|
||||
Components: map[string]any{"kandang_count": 1},
|
||||
}
|
||||
rawV1, err := json.Marshal(v1)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal v1 dto: %v", err)
|
||||
}
|
||||
|
||||
var decodedV1 map[string]any
|
||||
if err := json.Unmarshal(rawV1, &decodedV1); err != nil {
|
||||
t.Fatalf("unmarshal v1 dto: %v", err)
|
||||
}
|
||||
if _, ok := decodedV1["components"]; !ok {
|
||||
t.Fatalf("expected v1 components to be present, got %s", string(rawV1))
|
||||
}
|
||||
|
||||
v2 := ExpenseDepreciationV2RowDTO{
|
||||
Date: "2026-06-05",
|
||||
DepreciationPercentEffective: 10,
|
||||
DepreciationValue: 100,
|
||||
PulletCostDayNTotal: 1000,
|
||||
MultiplicationPercentage: 0.9,
|
||||
DayN: 2,
|
||||
ChickinDate: "2026-01-01",
|
||||
TotalValuePulletAfterDepreciation: 900,
|
||||
TotalPopulation: 100,
|
||||
}
|
||||
rawV2, err := json.Marshal(v2)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal v2 dto: %v", err)
|
||||
}
|
||||
|
||||
var decodedV2 map[string]any
|
||||
if err := json.Unmarshal(rawV2, &decodedV2); err != nil {
|
||||
t.Fatalf("unmarshal v2 dto: %v", err)
|
||||
}
|
||||
if _, ok := decodedV2["components"]; ok {
|
||||
t.Fatalf("expected v2 components to be omitted, got %s", string(rawV2))
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func ToMarketingReportItems(mdps []entity.MarketingDeliveryProduct, hppByDeliver
|
||||
realizationDate = *mdp.DeliveryDate
|
||||
}
|
||||
|
||||
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
|
||||
totalWeightKg := mdp.TotalWeight
|
||||
salesAmount := totalWeightKg * mdp.UnitPrice
|
||||
|
||||
var hpp float64
|
||||
|
||||
@@ -90,6 +90,12 @@ func (m *expenseDepreciationRepoMock) DeleteSnapshotsFromDate(_ context.Context,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *expenseDepreciationRepoMock) DeleteSnapshotsByFarmIDs(_ context.Context, farmIDs []uint) error {
|
||||
m.deleteCalled = true
|
||||
m.deleteFarmIDs = append([]uint{}, farmIDs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *expenseDepreciationRepoMock) GetLatestManualInputsByFarms(_ context.Context, _ []int64, _ []int64, _ []int64) ([]repportRepo.FarmDepreciationManualInputRow, error) {
|
||||
return append([]repportRepo.FarmDepreciationManualInputRow{}, m.manualInputs...), nil
|
||||
}
|
||||
|
||||
@@ -423,7 +423,10 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
||||
var totalDepreciationValue float64
|
||||
var totalPulletCostDayN float64
|
||||
var totalPopulation float64
|
||||
var allKandangComponents []depreciationKandangComponent
|
||||
var multiplicationPercentage float64
|
||||
var dayN int
|
||||
var chickinDate string
|
||||
var standardEffectiveDate string
|
||||
|
||||
for _, kandangID := range kandangIDs {
|
||||
breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &dayDate)
|
||||
@@ -444,72 +447,33 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
||||
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"),
|
||||
partPulletCostDayN := hppV2DetailFloat(part.Details, "pullet_cost_day_n")
|
||||
partPopulation := hppV2DetailFloat(part.Details, "kandang_population")
|
||||
partDayN := hppV2DetailInt(part.Details, "schedule_day")
|
||||
partMultiplicationPercentage := hppV2DetailFloat(part.Details, "multiplication_percentage")
|
||||
partChickinDate := hppV2DetailString(part.Details, "chickin_date")
|
||||
if partChickinDate == "" {
|
||||
partChickinDate = hppV2DetailString(part.Details, "origin_date")
|
||||
}
|
||||
|
||||
if component.HouseType == "" {
|
||||
component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type"))
|
||||
}
|
||||
totalPulletCostDayN += partPulletCostDayN
|
||||
totalDepreciationValue += part.Total
|
||||
totalPopulation += partPopulation
|
||||
|
||||
if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil {
|
||||
component.TransferID = ref.ID
|
||||
component.TransferDate = ref.Date
|
||||
component.TransferQty = ref.Qty
|
||||
if dayN == 0 && multiplicationPercentage == 0 && chickinDate == "" &&
|
||||
(partDayN > 0 || partMultiplicationPercentage > 0 || partChickinDate != "") {
|
||||
dayN = partDayN
|
||||
multiplicationPercentage = partMultiplicationPercentage
|
||||
chickinDate = partChickinDate
|
||||
standardEffectiveDate = hppV2DetailString(part.Details, "standard_effective_date")
|
||||
}
|
||||
|
||||
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,
|
||||
Date: dayStr,
|
||||
DepreciationPercentEffective: effectivePercent,
|
||||
DepreciationValue: totalDepreciationValue,
|
||||
PulletCostDayNTotal: totalPulletCostDayN,
|
||||
@@ -519,7 +483,6 @@ func (s *repportService) GetExpenseDepreciationV2(ctx *fiber.Ctx) ([]dto.Expense
|
||||
TotalValuePulletAfterDepreciation: totalPulletCostDayN - totalDepreciationValue,
|
||||
StandardEffectiveDate: standardEffectiveDate,
|
||||
TotalPopulation: totalPopulation,
|
||||
Components: parseSnapshotComponents(componentsJSON),
|
||||
})
|
||||
actualDays++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user