package service import ( "bytes" "context" "strings" "time" beforeflightinspection "wucher/internal/domain/before_flight_inspection" "wucher/internal/domain/flight" flightinspection "wucher/internal/domain/flight_inspection" flightinspectionfilechecklist "wucher/internal/domain/flight_inspection_file_checklist" flightprepcheck "wucher/internal/domain/flight_prep_check" helicopterfile "wucher/internal/domain/helicopter_file" reserveac "wucher/internal/domain/reserve_ac" "wucher/internal/shared/pkg/uuidv7" requestdto "wucher/internal/transport/http/dto/request" ) // ReserveAcUpdateError carries structured validation/update failures from the // application service layer back to the HTTP handler. type ReserveAcUpdateError struct { Status int Title string Detail string Pointer string } func (e *ReserveAcUpdateError) Error() string { if e == nil { return "" } return e.Detail } type ReserveAcUpdateInput struct { ID []byte ActorUserID []byte AircraftID *string FlightID *string Inspection *requestdto.ReserveAcInspectionUpdateAttributes Flight reserveAcFlightService Inspect flightinspection.FlightInspectionService Before beforeflightinspection.Service Prep flightprepcheck.Service HeliFile helicopterfile.Service FileChecklist flightinspectionfilechecklist.Service } type reserveAcFlightService interface { GetByID(ctx context.Context, id []byte) (*flight.Flight, error) GetByReserveAcID(ctx context.Context, reserveAcID []byte) (*flight.Flight, error) Update(ctx context.Context, row *flight.Flight) error } type ReserveAcCreateInput struct { HelicopterID []byte FlightID []byte InspectionDate time.Time Before *requestdto.ReserveAcBeforeInspectionAttributes Prepare *requestdto.ReserveAcPrepareInspectionAttributes ActorUserID []byte Flight reserveAcFlightService Inspect flightinspection.FlightInspectionService BeforeSvc beforeflightinspection.Service Prep flightprepcheck.Service HeliFile helicopterfile.Service FileChecklist flightinspectionfilechecklist.Service } func (s *ReserveAcService) UpdateDetailed(ctx context.Context, in ReserveAcUpdateInput) (*reserveac.ReserveAc, error) { if s == nil || s.repo == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "reserve ac service is not configured"} } if len(in.ID) != 16 { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "id is invalid UUID", Pointer: "/data/id"} } existing, err := s.repo.GetByID(ctx, in.ID) if err != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: err.Error()} } if existing == nil { return nil, &ReserveAcUpdateError{Status: 404, Title: "Not found", Detail: "reserve_ac not found"} } if in.FlightID != nil { if in.Flight == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "flight service is not configured"} } flightValue := strings.TrimSpace(*in.FlightID) flightID, parseErr := uuidv7.ParseString(flightValue) if parseErr != nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "flight_id is invalid UUID", Pointer: "/data/attributes/flight_id"} } targetFlight, flightErr := in.Flight.GetByID(ctx, flightID) if flightErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: flightErr.Error()} } if targetFlight == nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "flight_id does not exist", Pointer: "/data/attributes/flight_id"} } if len(targetFlight.ReserveAcID) == 16 && !bytes.Equal(targetFlight.ReserveAcID, existing.ID) { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "flight already has reserve_ac assigned", Pointer: "/data/attributes/flight_id"} } currentFlight, currentErr := in.Flight.GetByReserveAcID(ctx, existing.ID) if currentErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: currentErr.Error()} } if currentFlight != nil && !bytes.Equal(currentFlight.ID, targetFlight.ID) { currentFlight.ReserveAcID = nil currentFlight.UpdatedBy = in.ActorUserID if updateErr := in.Flight.Update(ctx, currentFlight); updateErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: updateErr.Error()} } } if len(targetFlight.ReserveAcID) != 16 || !bytes.Equal(targetFlight.ReserveAcID, existing.ID) { targetFlight.ReserveAcID = append([]byte(nil), existing.ID...) targetFlight.UpdatedBy = in.ActorUserID if updateErr := in.Flight.Update(ctx, targetFlight); updateErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: updateErr.Error()} } } } if in.AircraftID != nil && strings.TrimSpace(*in.AircraftID) != "" { helicopterValue := strings.TrimSpace(*in.AircraftID) helicopterID, parseErr := uuidv7.ParseString(helicopterValue) if parseErr != nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "helicopter_id is invalid UUID", Pointer: "/data/attributes/helicopter_id"} } exists, existsErr := s.repo.AircraftExists(ctx, helicopterID) if existsErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: existsErr.Error()} } if !exists { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "helicopter_id does not exist", Pointer: "/data/attributes/helicopter_id"} } existing.AircraftID = helicopterID } if in.Inspection != nil { if in.Inspect == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "flight inspection service is not configured"} } if in.Before == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "before flight inspection service is not configured"} } if in.Prep == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "flight prep check service is not configured"} } if in.HeliFile == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "helicopter file service is not configured"} } if in.FileChecklist == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Update failed", Detail: "flight inspection file checklist service is not configured"} } inspectionRow, getErr := in.Inspect.GetByID(ctx, existing.InspectionID) if getErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: getErr.Error()} } if inspectionRow == nil { return nil, &ReserveAcUpdateError{Status: 404, Title: "Not found", Detail: "flight inspection not found"} } if in.Inspection.InspectionDate != nil { inspectionDate, parseErr := time.Parse("2006-01-02", strings.TrimSpace(*in.Inspection.InspectionDate)) if parseErr != nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "inspection.inspection_date must be YYYY-MM-DD", Pointer: "/data/attributes/inspection/inspection_date"} } inspectionRow.InspectionDate = inspectionDate.UTC() inspectionRow.UpdatedBy = in.ActorUserID if updateErr := in.Inspect.Update(ctx, inspectionRow); updateErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: updateErr.Error()} } } if in.Inspection.Before != nil { beforeReq := &beforeflightinspection.UpsertRequest{ OilEngineNR1Checked: in.Inspection.Before.OilEngineNR1Checked, OilEngineNR2Checked: in.Inspection.Before.OilEngineNR2Checked, HydraulicLHChecked: in.Inspection.Before.HydraulicLHChecked, HydraulicRHChecked: in.Inspection.Before.HydraulicRHChecked, OilTransmissionMGBChecked: in.Inspection.Before.OilTransmissionMGBChecked, OilTransmissionIGBChecked: in.Inspection.Before.OilTransmissionIGBChecked, OilTransmissionTGBChecked: in.Inspection.Before.OilTransmissionTGBChecked, FuelAmount: in.Inspection.Before.FuelAmount, FuelUnit: in.Inspection.Before.FuelUnit, FuelAddedAmount: in.Inspection.Before.FuelAddedAmount, Note: in.Inspection.Before.Note, } if _, upsertErr := in.Before.Upsert(ctx, existing.InspectionID, beforeReq); upsertErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: upsertErr.Error()} } if in.Inspection.Before.FileChecklist != nil { if err := s.upsertInspectionFileChecklistSection(ctx, in, existing.InspectionID, existing.AircraftID, helicopterfile.SectionBeforeFirstFlightInspection, in.Inspection.Before.FileChecklist, "/data/attributes/inspection/before/file_checklist", existing.ID); err != nil { return nil, err } } } if in.Inspection.Prepare != nil { if _, upsertErr := in.Prep.UpsertByFlightInspection(ctx, existing.InspectionID, &flightprepcheck.UpsertRequest{ NOTAMBriefing: in.Inspection.Prepare.NotamBriefing, WeatherBriefing: in.Inspection.Prepare.WeatherBriefing, OperationalFlightPlan: in.Inspection.Prepare.OperationalFlightPlan, }); upsertErr != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: upsertErr.Error()} } if in.Inspection.Prepare.FileChecklist != nil { if err := s.upsertInspectionFileChecklistSection(ctx, in, existing.InspectionID, existing.AircraftID, helicopterfile.SectionFlightPreparationAndWB, in.Inspection.Prepare.FileChecklist, "/data/attributes/inspection/prepare/file_checklist", existing.ID); err != nil { return nil, err } } } } existing.UpdatedBy = in.ActorUserID existing.Status = s.calculateReserveAcStatus(ctx, in, existing.InspectionID, existing.AircraftID) if err := s.Update(ctx, existing); err != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Update failed", Detail: err.Error()} } return existing, nil } func (s *ReserveAcService) CreateDetailed(ctx context.Context, in ReserveAcCreateInput) (*reserveac.ReserveAc, error) { if s == nil || s.repo == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "reserve ac service is not configured"} } if len(in.HelicopterID) != 16 { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "helicopter_id is invalid UUID", Pointer: "/data/attributes/helicopter_id"} } if len(in.FlightID) != 16 { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "flight_id is invalid UUID", Pointer: "/data/attributes/flight_id"} } if in.Before == nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "inspection.before is required", Pointer: "/data/attributes/inspection/before"} } if in.Prepare == nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "inspection.prepare is required", Pointer: "/data/attributes/inspection/prepare"} } if in.Flight == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "flight service is not configured"} } if in.Inspect == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "flight inspection service is not configured"} } if in.BeforeSvc == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "before flight inspection service is not configured"} } if in.Prep == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "flight prep check service is not configured"} } if in.HeliFile == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "helicopter file service is not configured"} } if in.FileChecklist == nil { return nil, &ReserveAcUpdateError{Status: 500, Title: "Create failed", Detail: "flight inspection file checklist service is not configured"} } flightRow, err := in.Flight.GetByID(ctx, in.FlightID) if err != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } if flightRow == nil { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "flight_id does not exist", Pointer: "/data/attributes/flight_id"} } if len(flightRow.ReserveAcID) == 16 { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "flight already has reserve_ac assigned", Pointer: "/data/attributes/flight_id"} } exists, err := s.repo.AircraftExists(ctx, in.HelicopterID) if err != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } if !exists { return nil, &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "helicopter_id does not exist", Pointer: "/data/attributes/helicopter_id"} } inspection := &flightinspection.FlightInspection{ InspectionDate: in.InspectionDate.UTC(), CreatedBy: in.ActorUserID, UpdatedBy: in.ActorUserID, } if err := in.Inspect.CreateDraft(ctx, inspection); err != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } row := &reserveac.ReserveAc{ Status: reserveac.StatusPending, AircraftID: append([]byte(nil), in.HelicopterID...), InspectionID: append([]byte(nil), inspection.ID...), CreatedBy: in.ActorUserID, UpdatedBy: in.ActorUserID, } if err := s.Create(ctx, row); err != nil { return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } beforeReq := &beforeflightinspection.UpsertRequest{ OilEngineNR1Checked: in.Before.OilEngineNR1Checked, OilEngineNR2Checked: in.Before.OilEngineNR2Checked, HydraulicLHChecked: in.Before.HydraulicLHChecked, HydraulicRHChecked: in.Before.HydraulicRHChecked, OilTransmissionMGBChecked: in.Before.OilTransmissionMGBChecked, OilTransmissionIGBChecked: in.Before.OilTransmissionIGBChecked, OilTransmissionTGBChecked: in.Before.OilTransmissionTGBChecked, FuelAmount: in.Before.FuelAmount, FuelUnit: in.Before.FuelUnit, FuelAddedAmount: in.Before.FuelAddedAmount, Note: in.Before.Note, } if _, err := in.BeforeSvc.Upsert(ctx, inspection.ID, beforeReq); err != nil { _ = s.Delete(ctx, row.ID, in.ActorUserID) return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } if _, err := in.Prep.InitializeChecks(ctx, inspection.ID); err != nil { _ = s.Delete(ctx, row.ID, in.ActorUserID) return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } if _, err := in.Prep.UpsertByFlightInspection(ctx, inspection.ID, &flightprepcheck.UpsertRequest{ NOTAMBriefing: in.Prepare.NotamBriefing, WeatherBriefing: in.Prepare.WeatherBriefing, OperationalFlightPlan: in.Prepare.OperationalFlightPlan, }); err != nil { _ = s.Delete(ctx, row.ID, in.ActorUserID) return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } if in.Before.FileChecklist != nil { if err := s.upsertInspectionFileChecklistSection(ctx, ReserveAcUpdateInput{ ActorUserID: in.ActorUserID, HeliFile: in.HeliFile, FileChecklist: in.FileChecklist, }, inspection.ID, in.HelicopterID, helicopterfile.SectionBeforeFirstFlightInspection, in.Before.FileChecklist, "/data/attributes/inspection/before/file_checklist", row.ID); err != nil { return nil, err } } if in.Prepare.FileChecklist != nil { if err := s.upsertInspectionFileChecklistSection(ctx, ReserveAcUpdateInput{ ActorUserID: in.ActorUserID, HeliFile: in.HeliFile, FileChecklist: in.FileChecklist, }, inspection.ID, in.HelicopterID, helicopterfile.SectionFlightPreparationAndWB, in.Prepare.FileChecklist, "/data/attributes/inspection/prepare/file_checklist", row.ID); err != nil { return nil, err } } flightRow.ReserveAcID = append([]byte(nil), row.ID...) flightRow.UpdatedBy = in.ActorUserID if err := in.Flight.Update(ctx, flightRow); err != nil { _ = s.Delete(ctx, row.ID, in.ActorUserID) return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } row.Status = s.calculateReserveAcStatus(ctx, ReserveAcUpdateInput{ Inspect: in.Inspect, Before: in.BeforeSvc, Prep: in.Prep, HeliFile: in.HeliFile, FileChecklist: in.FileChecklist, }, inspection.ID, in.HelicopterID) if err := s.Update(ctx, row); err != nil { _ = s.Delete(ctx, row.ID, in.ActorUserID) return nil, &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } return row, nil } func (s *ReserveAcService) calculateReserveAcStatus(ctx context.Context, in ReserveAcUpdateInput, inspectionID []byte, helicopterID []byte) string { if len(inspectionID) != 16 || len(helicopterID) != 16 { return reserveac.StatusPending } if in.Inspect == nil || in.Before == nil || in.Prep == nil || in.HeliFile == nil || in.FileChecklist == nil { return reserveac.StatusPending } inspectionRow, err := in.Inspect.GetByID(ctx, inspectionID) if err != nil || inspectionRow == nil { return reserveac.StatusPending } ckRows, err := in.FileChecklist.ListByInspectionID(ctx, inspectionID) if err != nil { return reserveac.StatusPending } isDoneByHeliFileID := make(map[string]bool, len(ckRows)) for i := range ckRows { isDoneByHeliFileID[string(ckRows[i].HelicopterFileID)] = ckRows[i].IsDone } beforeRows, err := in.HeliFile.ListByHelicopterAndSection(ctx, helicopterID, helicopterfile.SectionBeforeFirstFlightInspection) if err != nil { return reserveac.StatusPending } prepareRows, err := in.HeliFile.ListByHelicopterAndSection(ctx, helicopterID, helicopterfile.SectionFlightPreparationAndWB) if err != nil { return reserveac.StatusPending } beforeData, _ := in.Before.GetByFlightInspectionID(ctx, inspectionID) prepareData, _ := in.Prep.GetByFlightInspection(ctx, inspectionID) beforeDone := countCompletedChecklist(beforeRows, isDoneByHeliFileID) prepareDone := countCompletedChecklist(prepareRows, isDoneByHeliFileID) totalChecklist := len(beforeRows) + len(prepareRows) doneChecklist := beforeDone + prepareDone if totalChecklist > 0 { if doneChecklist == 0 { return reserveac.StatusPending } if doneChecklist < totalChecklist { return reserveac.StatusOnProgress } return reserveac.StatusApproved } if beforeData != nil || prepareData != nil { return reserveac.StatusOnProgress } return reserveac.StatusPending } func countCompletedChecklist(files []helicopterfile.HelicopterFile, doneByID map[string]bool) int { if len(files) == 0 { return 0 } completed := 0 for i := range files { if doneByID[string(files[i].ID)] { completed++ } } return completed } func (s *ReserveAcService) upsertInspectionFileChecklistSection( ctx context.Context, in ReserveAcUpdateInput, inspectionID []byte, helicopterID []byte, section string, payload []requestdto.ReserveAcFileChecklistItem, payloadPointer string, reserveAcID []byte, ) error { templateFiles, err := in.HeliFile.ListByHelicopterAndSection(ctx, helicopterID, section) if err != nil { if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } type requestedChecklist struct { fileID []byte isDone bool } requested := make([]requestedChecklist, 0, len(payload)) for i := range payload { fileID, parseErr := uuidv7.ParseString(strings.TrimSpace(payload[i].HelicopterFileID)) if parseErr != nil { if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "helicopter_file_id is invalid UUID", Pointer: payloadPointer} } requested = append(requested, requestedChecklist{ fileID: fileID, isDone: payload[i].IsDone, }) } requestedByFileID := make(map[string]bool, len(requested)) for i := range requested { requestedByFileID[string(requested[i].fileID)] = requested[i].isDone } existingRows, err := in.FileChecklist.ListByInspectionID(ctx, inspectionID) if err != nil { if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } existingByFileID := make(map[string]*flightinspectionfilechecklist.FlightInspectionFileChecklist, len(existingRows)) for i := range existingRows { fileID := string(existingRows[i].HelicopterFileID) existingByFileID[fileID] = &existingRows[i] } templateSet := make(map[string]struct{}, len(templateFiles)) for i := range templateFiles { fileKey := string(templateFiles[i].ID) templateSet[fileKey] = struct{}{} current := existingByFileID[fileKey] targetDone, hasOverride := requestedByFileID[fileKey] if templateFiles[i].IsMandatory && !targetDone { if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "file_checklist must be fully checked", Pointer: payloadPointer} } if current == nil { if !hasOverride { targetDone = false } newRow := &flightinspectionfilechecklist.FlightInspectionFileChecklist{ FlightInspectionID: inspectionID, HelicopterFileID: templateFiles[i].ID, IsDone: targetDone, CreatedBy: in.ActorUserID, UpdatedBy: in.ActorUserID, } if err := in.FileChecklist.Create(ctx, newRow); err != nil { if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } continue } if !hasOverride { targetDone = current.IsDone } if current.IsDone != targetDone { current.IsDone = targetDone current.UpdatedBy = in.ActorUserID if err := in.FileChecklist.Update(ctx, current); err != nil { if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 400, Title: "Create failed", Detail: err.Error()} } } } for i := range requested { if _, ok := templateSet[string(requested[i].fileID)]; ok { continue } if len(reserveAcID) == 16 { _ = s.Delete(ctx, reserveAcID, in.ActorUserID) } return &ReserveAcUpdateError{Status: 422, Title: "Validation error", Detail: "helicopter_file_id does not belong to selected helicopter/section", Pointer: payloadPointer} } return nil }