package service import ( "context" "fmt" "testing" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" filemanager "wucher/internal/domain/file_manager" fleetstatus "wucher/internal/domain/fleet_status" "wucher/internal/domain/helicopter" helicopterusage "wucher/internal/domain/helicopter_usage" "wucher/internal/repository/mysql" ) // TestFleetStatusUpdateAcceptsNegativeSummaryAndBareSchedule guards the full service path // behind PATCH /fleet-status/update/{id}: negative summary values + an A/F schedule with // empty type/due/ext must persist (incl. syncAutomaticAOG, which the repo-level test skips). func TestFleetStatusUpdateAcceptsNegativeSummaryAndBareSchedule(t *testing.T) { dsn := fmt.Sprintf("file:fleet_update_repro_%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{NowFunc: func() time.Time { return time.Now().UTC() }}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate( &filemanager.Folder{}, &filemanager.File{}, &filemanager.Attachment{}, &helicopter.Helicopter{}, &fleetstatus.FleetStatus{}, &fleetstatus.MaintenanceSchedule{}, &fleetstatus.FleetStatusFile{}, &helicopterusage.HelicopterUsage{}, ); err != nil { t.Fatalf("migrate: %v", err) } ctx := context.Background() heliRepo := mysql.NewHelicopterRepository(db) heli := &helicopter.Helicopter{Designation: "H1", Identifier: "PK-H1", Type: "Twin", NR1: true, NR2: true} if err := heliRepo.Create(ctx, heli); err != nil { t.Fatalf("create heli: %v", err) } fsRepo := mysql.NewFleetStatusRepository(db) fs := &fleetstatus.FleetStatus{HelicopterID: heli.ID, Status: fleetstatus.StatusActive} if err := fsRepo.Create(ctx, fs); err != nil { t.Fatalf("create fs: %v", err) } usageSvc := NewHelicopterUsageService(mysql.NewHelicopterUsageRepository(db)) svc := NewFleetStatusService(fsRepo, nil, NewHelicopterService(heliRepo), nil).WithUsage(usageSvc) // Handler builds this row from the patch: full-replace schedules with one A/F entry. fs.MaintenanceSchedules = []fleetstatus.MaintenanceSchedule{ {FleetStatusID: fs.ID, InspectionType: "A/F", Type: "", Due: "", Ext: ""}, } fs.UpdatedBy = heli.ID // any actor if err := svc.Update(ctx, fs); err != nil { t.Fatalf("REPRO: FleetStatusService.Update FAILED: %v", err) } t.Log("FleetStatusService.Update OK (incl syncAutomaticAOG)") f := func(v float64) *float64 { return &v } if err := svc.ReviseSummary(ctx, heli.ID, helicopterusage.ManualSummaryInput{ AirframeHours: f(1234), AirframeCycles: f(-1), Engine1Hours: f(1234), Engine1GpcNgN1: f(23.23), Engine1PtcNfN2: f(23.59), Engine1Ccc: f(-8), Engine2Hours: f(234), Engine2GpcNgN1: f(123), Engine2PtcNfN2: f(1230), Engine2Ccc: f(123), Landing: f(1), FlightReport: f(1), HookRelease: f(1), RotorBrakeCycle: f(1), }, heli.ID); err != nil { t.Fatalf("REPRO: ReviseSummary FAILED: %v", err) } got, _ := usageSvc.GetByHelicopterID(ctx, heli.ID) t.Logf("ReviseSummary OK: cycles=%v ccc1=%v ccc2=%v", got.TotalAirframeCycles, got.TotalEngine1Ccc, got.TotalEngine2Ccc) // The fleet GET reads the summary via GetSummaryByHelicopterID — verify it reflects the patch. summary, err := svc.GetSummaryByHelicopterID(ctx, heli.ID) if err != nil { t.Fatalf("GetSummaryByHelicopterID: %v", err) } if summary.Airframe.Hours == nil || *summary.Airframe.Hours != 1234 { t.Fatalf("summary airframe hours = %v, want 1234 (patch not reflected)", summary.Airframe.Hours) } if summary.Engine1.Hours == nil || *summary.Engine1.Hours != 1234 { t.Fatalf("summary engine1 hours = %v, want 1234", summary.Engine1.Hours) } // Second PATCH with a NEW value — the "ga terganti" (value doesn't change) scenario. if err := svc.ReviseSummary(ctx, heli.ID, helicopterusage.ManualSummaryInput{AirframeHours: f(9999)}, heli.ID); err != nil { t.Fatalf("second ReviseSummary: %v", err) } summary2, _ := svc.GetSummaryByHelicopterID(ctx, heli.ID) if summary2.Airframe.Hours == nil || *summary2.Airframe.Hours != 9999 { t.Fatalf("second patch: airframe hours = %v, want 9999 (VALUE DID NOT CHANGE)", summary2.Airframe.Hours) } t.Logf("both patches reflected in GET summary: first af.hours=1234, second=%v", *summary2.Airframe.Hours) }