package service import ( "context" "errors" "strconv" "strings" "time" afterflightinspection "wucher/internal/domain/after_flight_inspection" basedomain "wucher/internal/domain/base" flightdomain "wucher/internal/domain/flight" flightdata "wucher/internal/domain/flight_data" "wucher/internal/domain/mission" sharedconst "wucher/internal/shared/const" "wucher/internal/shared/pkg/util" ) type MissionService struct { repo mission.Repository flightSvc flightdomain.FlightService flightDataSvc flightdata.Service afterFlight afterflightinspection.Service } func missionCodePrefix(missionType string, t time.Time) string { return strings.ToUpper(strings.TrimSpace(missionType)) + "-" + t.Format("06") + "-" } func NewMissionService(repo mission.Repository, deps ...any) *MissionService { var flightSvc flightdomain.FlightService var fdSvc flightdata.Service var afterSvc afterflightinspection.Service for _, dep := range deps { switch v := dep.(type) { case flightdomain.FlightService: flightSvc = v case flightdata.Service: fdSvc = v case afterflightinspection.Service: afterSvc = v } } return &MissionService{repo: repo, flightSvc: flightSvc, flightDataSvc: fdSvc, afterFlight: afterSvc} } func (s *MissionService) Create(ctx context.Context, row *mission.Mission) error { if row == nil { return mission.ErrRequired } if len(row.FlightID) == 0 { return mission.ErrFlightIDRequired } row.Type = strings.ToUpper(strings.TrimSpace(row.Type)) category, err := s.repo.FindCategoryByCode(ctx, row.Type) if err != nil { return err } if category == nil || len(category.ID) == 0 { return mission.ErrTypeInvalid } row.MissionCategoryID = append([]byte(nil), category.ID...) if row.Type == sharedconst.MissionTypeHEMS { if len(row.SubtypeID) == 0 { return mission.ErrSubtypeRequired } ok, err := s.repo.SubCategoryBelongsToCategory(ctx, row.SubtypeID, row.MissionCategoryID) if err != nil { return err } if !ok { return mission.ErrSubtypeInvalid } } else if len(row.SubtypeID) > 0 { return mission.ErrSubtypeForbidden } startTime, endTime, err := s.resolveMissionTimes(ctx, row.FlightID) if err != nil { return err } row.StartTime = startTime row.EndTime = endTime if err := s.ensureNotAfterFlightLocked(ctx, row.FlightID); err != nil { return err } return s.repo.WithTransaction(ctx, func(txCtx context.Context) error { return s.persistMissionCreate(txCtx, row) }) } func (s *MissionService) persistMissionCreate(ctx context.Context, row *mission.Mission) error { if strings.TrimSpace(row.Code) == "" { prefix := missionCodePrefix(row.Type, time.Now()) maxSeq, err := s.repo.MaxCodeSeqByPrefix(ctx, prefix) if err != nil { return err } row.Code = prefix + strconv.Itoa(maxSeq+1) } return s.repo.Create(ctx, row) } func (s *MissionService) CreateType(ctx context.Context, row *mission.MissionCategory) error { if row == nil { return mission.ErrRequired } row.CodeType = strings.ToUpper(strings.TrimSpace(row.CodeType)) row.TypeName = strings.TrimSpace(row.TypeName) if row.CodeType == "" || row.TypeName == "" { return mission.ErrTypeInvalid } return s.repo.CreateCategory(ctx, row) } func (s *MissionService) UpdateByID(ctx context.Context, missionID []byte, missionType string, subtypeID []byte, note string, updatedBy []byte) error { if len(missionID) == 0 { return mission.ErrMissionIDRequired } missionType = strings.ToUpper(strings.TrimSpace(missionType)) category, err := s.repo.FindCategoryByCode(ctx, missionType) if err != nil { return err } if category == nil || len(category.ID) == 0 { return mission.ErrTypeInvalid } if missionType == sharedconst.MissionTypeHEMS { if len(subtypeID) == 0 { return mission.ErrSubtypeRequired } ok, err := s.repo.SubCategoryBelongsToCategory(ctx, subtypeID, category.ID) if err != nil { return err } if !ok { return mission.ErrSubtypeInvalid } } else if len(subtypeID) > 0 { return mission.ErrSubtypeForbidden } return s.repo.UpdateByID(ctx, missionID, missionType, category.ID, subtypeID, note, updatedBy) } func (s *MissionService) FlightIDByMissionID(ctx context.Context, missionID []byte) ([]byte, error) { if len(missionID) != 16 { return nil, nil } row, err := s.repo.GetByID(ctx, missionID) if err != nil { return nil, err } if row == nil || len(row.FlightID) != 16 { return nil, nil } return append([]byte(nil), row.FlightID...), nil } func (s *MissionService) resolveMissionTimes(ctx context.Context, flightID []byte) (string, string, error) { if s.flightSvc == nil { return "", "", mission.ErrScheduleMissing } flightRow, err := s.flightSvc.GetByID(ctx, flightID) if err != nil { return "", "", err } if flightRow == nil || len(flightRow.ID) != 16 || flightRow.Date.IsZero() { return "", "", mission.ErrScheduleMissing } baseRow := missionBaseFromFlight(flightRow) if baseRow == nil { return "", "", mission.ErrBaseMissing } startTime, endTime, err := util.ResolveShiftWindow(baseRow, flightRow.Date) if err != nil { return "", "", mapBaseShiftError(err) } return startTime, endTime, nil } func mapBaseShiftError(err error) error { switch { case errors.Is(err, util.ErrShiftCoordinatesInvalid): return mission.ErrCoordinatesInvalid case errors.Is(err, util.ErrShiftTwilightMissing): return mission.ErrTwilightMissing case errors.Is(err, util.ErrShiftScheduleMissing): return mission.ErrScheduleMissing default: return err } } func (s *MissionService) ensureNotAfterFlightLocked(ctx context.Context, flightID []byte) error { if s.flightSvc == nil || s.afterFlight == nil || len(flightID) != 16 { return nil } flightRow, err := s.flightSvc.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 mission.ErrLockedAfterFlightInspection } return nil } func missionBaseFromFlight(row *flightdomain.Flight) *basedomain.Base { if row == nil || row.Takeover == nil { return nil } return row.Takeover.Base } func (s *MissionService) DeleteByID(ctx context.Context, missionID []byte, deletedBy []byte) error { if len(missionID) == 0 { return mission.ErrMissionIDRequired } return s.repo.DeleteByID(ctx, missionID, deletedBy) } func (s *MissionService) DeleteByFlightID(ctx context.Context, flightID []byte, deletedBy []byte) error { if len(flightID) == 0 { return mission.ErrFlightIDRequired } return s.repo.DeleteByFlightID(ctx, flightID, deletedBy) } func (s *MissionService) AttachFile(ctx context.Context, missionID []byte, fileAttachmentID []byte) error { if len(missionID) != 16 { return mission.ErrMissionIDRequired } if len(fileAttachmentID) != 16 { return mission.ErrFileAttachmentIDRequired } return s.repo.AttachFile(ctx, missionID, fileAttachmentID) } func (s *MissionService) DetachFile(ctx context.Context, missionID []byte, fileAttachmentID []byte) error { if len(missionID) != 16 { return mission.ErrMissionIDRequired } if len(fileAttachmentID) != 16 { return mission.ErrFileAttachmentIDRequired } return s.repo.DetachFile(ctx, missionID, fileAttachmentID) } func (s *MissionService) GetByID(ctx context.Context, missionID []byte) (*mission.Mission, error) { if len(missionID) == 0 { return nil, mission.ErrMissionIDRequired } row, err := s.repo.GetByID(ctx, missionID) if err != nil { return nil, err } if err := s.hydrateFlightDataCompletion(ctx, row); err != nil { return nil, err } return row, nil } func (s *MissionService) ListByFlightID(ctx context.Context, flightID []byte) ([]mission.Mission, error) { if len(flightID) == 0 { return nil, mission.ErrFlightIDRequired } rows, err := s.repo.ListByFlightID(ctx, flightID) if err != nil { return nil, err } return rows, nil } func (s *MissionService) ListByFlightIDs(ctx context.Context, flightIDs [][]byte) ([]mission.Mission, error) { rows, err := s.repo.ListByFlightIDs(ctx, flightIDs) if err != nil { return nil, err } return rows, nil } func (s *MissionService) GetByFlightID(ctx context.Context, flightID []byte) (*mission.Mission, error) { if len(flightID) == 0 { return nil, mission.ErrFlightIDRequired } row, err := s.repo.GetByFlightID(ctx, flightID) if err != nil { return nil, err } return row, nil } func (s *MissionService) List(ctx context.Context, filter, sort string, flightID []byte, flightDataStatus string, limit, offset int) ([]mission.Mission, int64, error) { rows, total, err := s.repo.List(ctx, filter, normalizeMissionSort(sort), flightID, flightDataStatus, limit, offset) if err != nil { return nil, 0, err } return rows, total, nil } func (s *MissionService) ListDatatable(ctx context.Context, filter mission.ListFilter, limit, offset int) ([]mission.Mission, int64, int64, error) { filter.Sort = normalizeMissionSort(filter.Sort) rows, total, filtered, err := s.repo.ListDatatable(ctx, filter, limit, offset) if err != nil { return nil, 0, 0, err } return rows, total, filtered, nil } func (s *MissionService) ListTypes(ctx context.Context) ([]mission.MissionCategory, error) { return s.repo.ListCategoriesWithSubCategories(ctx) } func (s *MissionService) hydrateFlightDataCompletion(ctx context.Context, row *mission.Mission) error { if s.flightDataSvc == nil || row == nil || len(row.ID) == 0 { return nil } fd, err := s.flightDataSvc.GetByMissionID(ctx, row.ID) if err != nil { return err } if fd == nil { return nil } var details *flightdata.SPODetails if strings.ToUpper(strings.TrimSpace(row.Type)) == sharedconst.MissionTypeSPO { details, err = s.flightDataSvc.GetSPODetailsByFlightDataID(ctx, fd.ID) if err != nil { return err } } row.FlightDataStatus = flightdata.DeriveStatus(fd, details) return nil } func normalizeMissionSort(sort string) string { if normalized := normalizeSortByRules(sort, sortField("missions.type", "type"), sortField("flights.date", "flight_date"), sortField("missions.created_at", "created_at"), sortField("missions.updated_at", "updated_at"), ); normalized != "" { return normalized } return "missions.created_at DESC" }