package service import ( "context" "strings" afterflightinspection "wucher/internal/domain/after_flight_inspection" flightdomain "wucher/internal/domain/flight" flightdata "wucher/internal/domain/flight_data" fmreport "wucher/internal/domain/fm_report" sharedconst "wucher/internal/shared/const" "wucher/internal/shared/pkg/uuidv7" ) type aogRecalculator interface { RecalculateAOGByMission(ctx context.Context, missionID []byte) error } type missionFlightResolver interface { FlightIDByMissionID(ctx context.Context, missionID []byte) ([]byte, error) } type fmReportLookup interface { GetByFlightID(ctx context.Context, flightID []byte) (*fmreport.Report, error) } type flightLookup interface { GetByID(context.Context, []byte) (*flightdomain.Flight, error) } type afterFlightInspectionLookup interface { GetByFlightInspectionID(context.Context, []byte) (*afterflightinspection.AfterFlightInspection, error) } type FlightDataService struct { repo flightdata.Repository aog aogRecalculator mission missionFlightResolver flight flightLookup fmReports fmReportLookup afterFlight afterFlightInspectionLookup } func NewFlightDataService(repo flightdata.Repository) *FlightDataService { return &FlightDataService{repo: repo} } func (s *FlightDataService) WithAOGRecalculator(a aogRecalculator) *FlightDataService { s.aog = a return s } func (s *FlightDataService) WithMissionResolver(resolver missionFlightResolver) *FlightDataService { s.mission = resolver return s } func (s *FlightDataService) WithFlightLookup(lookup flightLookup) *FlightDataService { s.flight = lookup return s } func (s *FlightDataService) WithFMReportLookup(lookup fmReportLookup) *FlightDataService { s.fmReports = lookup return s } func (s *FlightDataService) WithAfterFlightInspectionLookup(lookup afterFlightInspectionLookup) *FlightDataService { s.afterFlight = lookup return s } func (s *FlightDataService) recalcAOG(ctx context.Context, missionID []byte) { if s.aog == nil || len(missionID) == 0 { return } _ = s.aog.RecalculateAOGByMission(ctx, missionID) } func (s *FlightDataService) Create(ctx context.Context, row *flightdata.FlightData) error { if row == nil { return flightdata.ErrRequired } if err := validateFlightDataLocationPair(row.FromICAOID, row.FromHospitalID, true); err != nil { return err } if err := validateFlightDataLocationPair(row.ToICAOID, row.ToHospitalID, false); err != nil { return err } if err := s.canCreateForMission(ctx, row.MissionID); err != nil { return err } row.Status = flightdata.StatusInProgress if err := s.repo.Create(ctx, row); err != nil { return err } s.recalcAOG(ctx, row.MissionID) if err := s.syncStatus(ctx, row.ID); err != nil { return err } return nil } func (s *FlightDataService) CreatePlaceholder(ctx context.Context, row *flightdata.FlightData) error { if row == nil { return flightdata.ErrRequired } if err := s.canCreateForMission(ctx, row.MissionID); err != nil { return err } row.Status = flightdata.StatusInProgress if err := s.repo.Create(ctx, row); err != nil { return err } return nil } func (s *FlightDataService) Update(ctx context.Context, row *flightdata.FlightData) error { if row == nil { return flightdata.ErrRequired } if err := validateFlightDataLocationPair(row.FromICAOID, row.FromHospitalID, true); err != nil { return err } if err := validateFlightDataLocationPair(row.ToICAOID, row.ToHospitalID, false); err != nil { return err } if err := s.ensureEditable(ctx, row.MissionID); err != nil { return err } if err := s.repo.Update(ctx, row); err != nil { return err } s.recalcAOG(ctx, row.MissionID) if err := s.syncStatus(ctx, row.ID); err != nil { return err } return nil } func (s *FlightDataService) Delete(ctx context.Context, id []byte, deletedBy []byte) error { var missionID []byte if row, err := s.repo.GetByID(ctx, id); err == nil && row != nil { missionID = row.MissionID if err := s.ensureAfterFlightInspectionNotLocked(ctx, missionID); err != nil { return err } if err := s.ensureEditable(ctx, missionID); err != nil { return err } } if err := s.repo.Delete(ctx, id, deletedBy); err != nil { return err } s.recalcAOG(ctx, missionID) return nil } func (s *FlightDataService) GetByID(ctx context.Context, id []byte) (*flightdata.FlightData, error) { return s.repo.GetByID(ctx, id) } func (s *FlightDataService) GetByFlightID(ctx context.Context, flightID []byte) (*flightdata.FlightData, error) { return s.repo.GetByFlightID(ctx, flightID) } func (s *FlightDataService) GetByMissionID(ctx context.Context, missionID []byte) (*flightdata.FlightData, error) { return s.repo.GetByMissionID(ctx, missionID) } func (s *FlightDataService) ListByMissionID(ctx context.Context, missionID []byte) ([]flightdata.FlightData, error) { return s.repo.ListByMissionID(ctx, missionID) } func (s *FlightDataService) List(ctx context.Context, date string, missionID, flightDataID []byte, limit, offset int) ([]flightdata.FlightData, int64, error) { return s.repo.List(ctx, date, missionID, flightDataID, limit, offset) } func (s *FlightDataService) GetSPODetailsByFlightDataID(ctx context.Context, flightDataID []byte) (*flightdata.SPODetails, error) { return s.repo.GetSPODetailsByFlightDataID(ctx, flightDataID) } func (s *FlightDataService) UpsertSPODetails(ctx context.Context, flightDataID []byte, data *flightdata.SPOUpsertData) error { return s.repo.UpsertSPODetails(ctx, flightDataID, data) } func (s *FlightDataService) CanCreateForMission(ctx context.Context, missionID []byte) error { return s.canCreateForMission(ctx, missionID) } func (s *FlightDataService) canCreateForMission(ctx context.Context, missionID []byte) error { if err := s.ensureAfterFlightInspectionNotLocked(ctx, missionID); err != nil { return err } flightID, err := s.flightIDByMissionID(ctx, missionID) if err != nil { return err } if len(flightID) != 16 || s.fmReports == nil { return nil } report, err := s.fmReports.GetByFlightID(ctx, flightID) if err != nil { return err } if report != nil { return flightdata.ErrCreateBlocked } return nil } func (s *FlightDataService) ensureEditable(ctx context.Context, missionID []byte) error { flightID, err := s.flightIDByMissionID(ctx, missionID) if err != nil { return err } if len(flightID) != 16 || s.fmReports == nil { return nil } report, err := s.fmReports.GetByFlightID(ctx, flightID) if err != nil { return err } if report != nil && report.CompletedAt != nil { return flightdata.ErrLocked } return nil } func (s *FlightDataService) ensureAfterFlightInspectionNotLocked(ctx context.Context, missionID []byte) error { flightID, err := s.flightIDByMissionID(ctx, missionID) if err != nil { return err } if len(flightID) != 16 || s.flight == nil || s.afterFlight == nil { return nil } flightRow, err := s.flight.GetByID(ctx, flightID) if err != nil { return err } if flightRow == nil || flightRow.Takeover == nil || flightRow.Takeover.ReserveAc == nil || len(flightRow.Takeover.ReserveAc.InspectionID) != 16 { return nil } afterRow, err := s.afterFlight.GetByFlightInspectionID(ctx, flightRow.Takeover.ReserveAc.InspectionID) if err != nil { return err } if afterRow != nil { return flightdata.ErrLockedAfterFlightInspection } return nil } func (s *FlightDataService) flightIDByMissionID(ctx context.Context, missionID []byte) ([]byte, error) { if len(missionID) != 16 || s.mission == nil { return nil, nil } flightID, err := s.mission.FlightIDByMissionID(ctx, missionID) if err != nil { return nil, err } return flightID, nil } func (s *FlightDataService) AttachmentReferenceType() string { return sharedconst.FlightDataResourceType } func (s *FlightDataService) AttachmentReferenceID(row *flightdata.FlightData) string { if row == nil { return "" } id, err := uuidv7.BytesToString(row.ID) if err != nil { return "" } return id } func (s *FlightDataService) syncStatus(ctx context.Context, id []byte) error { if len(id) != 16 { return nil } row, err := s.repo.GetByID(ctx, id) if err != nil || row == nil { return err } if row.Mission == nil && len(row.MissionID) == 16 { if byMissionID, lookupErr := s.repo.GetByMissionID(ctx, row.MissionID); lookupErr == nil && byMissionID != nil { row = byMissionID } } details, err := s.loadSPODetails(ctx, row) if err != nil { return err } desired := flightdata.DeriveStatus(row, details) if strings.EqualFold(strings.TrimSpace(row.Status), desired) { return nil } row.Status = desired return s.repo.Update(ctx, row) } func (s *FlightDataService) loadSPODetails(ctx context.Context, row *flightdata.FlightData) (*flightdata.SPODetails, error) { if row == nil || row.Mission == nil { return nil, nil } if !strings.EqualFold(strings.TrimSpace(row.Mission.Type), sharedconst.MissionTypeSPO) { return nil, nil } details, err := s.repo.GetSPODetailsByFlightDataID(ctx, row.ID) if err != nil { return nil, err } return details, nil } func validateFlightDataLocationPair(a, b []byte, isOrigin bool) error { hasA := len(a) > 0 hasB := len(b) > 0 if hasA == hasB { if isOrigin { return flightdata.ErrOriginLocationReference } return flightdata.ErrDestinationLocationReference } return nil }