Files
fm_be/internal/service/mission_service.go
2026-07-16 22:16:45 +07:00

357 lines
10 KiB
Go

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"
}