1648 lines
55 KiB
Go
1648 lines
55 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
basedomain "wucher/internal/domain/base"
|
|
beforeflightinspection "wucher/internal/domain/before_flight_inspection"
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
"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"
|
|
otherpersondomain "wucher/internal/domain/other_person"
|
|
reserveac "wucher/internal/domain/reserve_ac"
|
|
takeoverdomain "wucher/internal/domain/takeover"
|
|
"wucher/internal/shared/pkg/missioncode"
|
|
"wucher/internal/shared/pkg/reserveacutil"
|
|
"wucher/internal/shared/pkg/util"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
requestdto "wucher/internal/transport/http/dto/request"
|
|
)
|
|
|
|
// checklistTemplateFileRepository loads the helicopter files that make up an inspection
|
|
// checklist section. The concrete implementation (mysql.TakeoverRepository) is injected so
|
|
// the service does not depend on the repository package directly. The transaction handle is
|
|
// passed through so the read runs within the caller's ongoing transaction.
|
|
type checklistTemplateFileRepository interface {
|
|
ListChecklistTemplateFiles(tx *gorm.DB, helicopterID []byte, section string) ([]helicopterfile.HelicopterFile, error)
|
|
ListActiveEditedTemplateUUIDs(tx *gorm.DB, takeoverID []byte) ([][]byte, error)
|
|
}
|
|
|
|
type TakeoverService struct {
|
|
db *gorm.DB
|
|
checklistFiles checklistTemplateFileRepository
|
|
}
|
|
|
|
type TakeoverCreateInput struct {
|
|
BaseID []byte
|
|
HasBaseID bool
|
|
DutyDate time.Time
|
|
HasDutyDate bool
|
|
Notes *string
|
|
RosterDetail *requestdto.DutyRosterDetailsInput
|
|
HasRosterDetail bool
|
|
HelicopterID []byte
|
|
HasHelicopterID bool
|
|
Inspection *requestdto.ReserveAcInspectionCreateAttributes
|
|
HasInspection bool
|
|
ActorUserID []byte
|
|
}
|
|
|
|
type TakeoverCreateResult struct {
|
|
TakeoverAcID []byte
|
|
DutyRosterID []byte
|
|
FlightID []byte
|
|
ReserveAcID []byte
|
|
FlightInspectionID []byte
|
|
ShiftStart string
|
|
ShiftEnd string
|
|
}
|
|
|
|
type takeoverExisting struct {
|
|
Flight *flight.Flight
|
|
Takeover *takeoverdomain.TakeoverAc
|
|
Reserve *reserveac.ReserveAc
|
|
Crews []takeoverdomain.TakeoverRosterCrew
|
|
Others []takeoverdomain.TakeoverOtherPerson
|
|
Before *beforeflightinspection.BeforeFlightInspection
|
|
Prep *flightprepcheck.FlightPrepCheck
|
|
}
|
|
|
|
func NewTakeoverService(db *gorm.DB) *TakeoverService {
|
|
return &TakeoverService{db: db}
|
|
}
|
|
|
|
// WithChecklistTemplateRepository injects the repository used to load checklist template
|
|
// files. It must be wired at startup so DB access stays in the repository layer.
|
|
func (s *TakeoverService) WithChecklistTemplateRepository(repo checklistTemplateFileRepository) *TakeoverService {
|
|
if s != nil {
|
|
s.checklistFiles = repo
|
|
}
|
|
return s
|
|
}
|
|
|
|
func logTakeoverServiceStepError(step string, err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
slog.Error("takeover service step failed",
|
|
"step", step,
|
|
"error", err.Error(),
|
|
"error_type", fmt.Sprintf("%T", err),
|
|
)
|
|
}
|
|
|
|
func (s *TakeoverService) Create(ctx context.Context, in TakeoverCreateInput) (*TakeoverCreateResult, error) {
|
|
if s == nil || s.db == nil {
|
|
return nil, errors.New("takeover service is not configured")
|
|
}
|
|
if !in.HasDutyDate {
|
|
return nil, errors.New("duty_date is required")
|
|
}
|
|
if in.HasBaseID && len(in.BaseID) != 16 {
|
|
return nil, errors.New("base_id must be a valid UUID")
|
|
}
|
|
if in.HasHelicopterID && len(in.HelicopterID) != 16 {
|
|
return nil, errors.New("helicopter_id must be a valid UUID")
|
|
}
|
|
if !in.HasBaseID && !in.HasRosterDetail {
|
|
return nil, errors.New("base_id or roster_detail is required")
|
|
}
|
|
|
|
return s.createInTx(ctx, in)
|
|
}
|
|
|
|
func (s *TakeoverService) createInTx(ctx context.Context, in TakeoverCreateInput) (*TakeoverCreateResult, error) {
|
|
result := &TakeoverCreateResult{}
|
|
err := s.withRetryTx(ctx, func(tx *gorm.DB) error {
|
|
if in.HasHelicopterID {
|
|
booked, err := isHelicopterBookedTx(tx, in.HelicopterID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if booked {
|
|
return errors.New("helicopter is already booked")
|
|
}
|
|
}
|
|
|
|
takeoverAc := &takeoverdomain.TakeoverAc{
|
|
Notes: in.Notes,
|
|
CreatedBy: in.ActorUserID,
|
|
UpdatedBy: in.ActorUserID,
|
|
}
|
|
if in.HasBaseID {
|
|
shiftStart, shiftEnd, err := ResolveBaseShiftRangeTx(tx, in.BaseID, in.DutyDate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result.ShiftStart = shiftStart
|
|
result.ShiftEnd = shiftEnd
|
|
if err := ensureBaseExistsTx(tx, in.BaseID); err != nil {
|
|
return err
|
|
}
|
|
takeoverAc.BaseID = append([]byte(nil), in.BaseID...)
|
|
}
|
|
if err := tx.Create(takeoverAc).Error; err != nil {
|
|
return err
|
|
}
|
|
result.TakeoverAcID = append([]byte(nil), takeoverAc.ID...)
|
|
|
|
if in.HasRosterDetail {
|
|
if err := s.upsertTakeoverRosterRowsTx(tx, takeoverAc.ID, in); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
existing := &takeoverExisting{}
|
|
if err := s.upsertTakeoverReserveTx(tx, takeoverAc.ID, in.BaseID, existing, in); err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
prefix := missioncode.BuildPrefix(now)
|
|
latest, err := getLatestMissionCodeByPrefixTx(tx, prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nextCode := missioncode.NextFromLatest(prefix, latest)
|
|
flightRow := &flight.Flight{
|
|
Status: "draft",
|
|
MissionCode: &nextCode,
|
|
Date: dateOnlyUTC(in.DutyDate),
|
|
TakeoverAcID: append([]byte(nil), takeoverAc.ID...),
|
|
CreatedBy: in.ActorUserID,
|
|
UpdatedBy: in.ActorUserID,
|
|
}
|
|
if err := tx.Create(flightRow).Error; err != nil {
|
|
return err
|
|
}
|
|
result.FlightID = append([]byte(nil), flightRow.ID...)
|
|
if existing.Reserve != nil {
|
|
result.ReserveAcID = append([]byte(nil), existing.Reserve.ID...)
|
|
result.FlightInspectionID = append([]byte(nil), existing.Reserve.InspectionID...)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *TakeoverService) Replace(ctx context.Context, existingFlightID []byte, in TakeoverCreateInput) (*TakeoverCreateResult, error) {
|
|
if len(existingFlightID) != 16 {
|
|
return nil, errors.New("id is invalid UUID")
|
|
}
|
|
result := &TakeoverCreateResult{}
|
|
err := s.withRetryTx(ctx, func(tx *gorm.DB) error {
|
|
existing, err := loadTakeoverExistingTx(tx, existingFlightID)
|
|
if err != nil {
|
|
logTakeoverServiceStepError("Replace.loadTakeoverExistingTx", err)
|
|
return err
|
|
}
|
|
if existing == nil || existing.Flight == nil {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
|
|
targetBaseID := []byte(nil)
|
|
if existing.Reserve != nil && len(existing.Reserve.BaseID) > 0 {
|
|
targetBaseID = append([]byte(nil), existing.Reserve.BaseID...)
|
|
} else if existing.Takeover != nil && len(existing.Takeover.BaseID) > 0 {
|
|
targetBaseID = append([]byte(nil), existing.Takeover.BaseID...)
|
|
}
|
|
if in.HasBaseID {
|
|
if len(in.BaseID) != 16 {
|
|
return errors.New("base_id must be a valid UUID")
|
|
}
|
|
if err := ensureBaseExistsTx(tx, in.BaseID); err != nil {
|
|
logTakeoverServiceStepError("Replace.ensureBaseExistsTx", err)
|
|
return err
|
|
}
|
|
targetBaseID = append([]byte(nil), in.BaseID...)
|
|
}
|
|
|
|
effectiveDutyDate := existing.Flight.Date
|
|
if in.HasDutyDate {
|
|
effectiveDutyDate = dateOnlyUTC(in.DutyDate)
|
|
}
|
|
if len(targetBaseID) > 0 {
|
|
shiftStart, shiftEnd, err := ResolveBaseShiftRangeTx(tx, targetBaseID, effectiveDutyDate)
|
|
if err != nil {
|
|
logTakeoverServiceStepError("Replace.ResolveBaseShiftRangeTx", err)
|
|
return err
|
|
}
|
|
result.ShiftStart = shiftStart
|
|
result.ShiftEnd = shiftEnd
|
|
}
|
|
|
|
takeoverID := existing.Flight.TakeoverAcID
|
|
if len(takeoverID) != 16 {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
|
|
currentHelicopterID := []byte(nil)
|
|
if existing.Reserve != nil && len(existing.Reserve.AircraftID) == 16 {
|
|
currentHelicopterID = append([]byte(nil), existing.Reserve.AircraftID...)
|
|
}
|
|
if in.HasHelicopterID && len(in.HelicopterID) == 16 && !bytes.Equal(in.HelicopterID, currentHelicopterID) {
|
|
booked, err := isHelicopterBookedTx(tx, in.HelicopterID, takeoverID)
|
|
if err != nil {
|
|
logTakeoverServiceStepError("Replace.isHelicopterBookedTx", err)
|
|
return err
|
|
}
|
|
if booked {
|
|
return errors.New("helicopter is already booked")
|
|
}
|
|
}
|
|
|
|
takeoverUpdates := map[string]any{"base_id": targetBaseID, "updated_by": in.ActorUserID}
|
|
if in.Notes != nil {
|
|
takeoverUpdates["notes"] = *in.Notes
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverAc{}).
|
|
Where("id = ? AND deleted_at IS NULL", takeoverID).
|
|
Updates(takeoverUpdates).Error; err != nil {
|
|
logTakeoverServiceStepError("Replace.updateTakeover", err)
|
|
return err
|
|
}
|
|
|
|
if err := s.upsertTakeoverReserveTx(tx, takeoverID, targetBaseID, existing, in); err != nil {
|
|
logTakeoverServiceStepError("Replace.upsertTakeoverReserveTx", err)
|
|
return err
|
|
}
|
|
|
|
targetFlightDate := dateOnlyUTC(in.DutyDate)
|
|
if in.HasDutyDate && !existing.Flight.Date.Equal(targetFlightDate) {
|
|
existing.Flight.Date = targetFlightDate
|
|
if err := tx.Save(existing.Flight).Error; err != nil {
|
|
logTakeoverServiceStepError("Replace.saveFlightDate", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if in.HasRosterDetail {
|
|
if err := s.upsertTakeoverRosterRowsTx(tx, takeoverID, in); err != nil {
|
|
logTakeoverServiceStepError("Replace.upsertTakeoverRosterRowsTx", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if existing.Reserve != nil {
|
|
result.ReserveAcID = append([]byte(nil), existing.Reserve.ID...)
|
|
result.FlightInspectionID = append([]byte(nil), existing.Reserve.InspectionID...)
|
|
}
|
|
result.TakeoverAcID = append([]byte(nil), takeoverID...)
|
|
result.FlightID = append([]byte(nil), existing.Flight.ID...)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *TakeoverService) GetByID(ctx context.Context, id []byte) (*takeoverdomain.TakeoverAc, error) {
|
|
if s == nil || s.db == nil {
|
|
return nil, errors.New("takeover service is not configured")
|
|
}
|
|
if len(id) != 16 {
|
|
return nil, errors.New("id is invalid UUID")
|
|
}
|
|
var row takeoverdomain.TakeoverAc
|
|
err := s.db.WithContext(ctx).
|
|
Preload("Base").
|
|
Preload("ReserveAc").
|
|
Preload("ReserveAc.Aircraft").
|
|
Preload("ReserveAc.Inspection").
|
|
Preload("RosterCrews").
|
|
Preload("RosterCrews.User").
|
|
Preload("OtherPeople").
|
|
Preload("Files").
|
|
Preload("Files.FileAttachment").
|
|
Preload("Files.FileAttachment.File").
|
|
Where("id = ? AND deleted_at IS NULL", id).
|
|
First(&row).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return &row, err
|
|
}
|
|
|
|
func (s *TakeoverService) List(ctx context.Context, limit, offset int) ([]takeoverdomain.TakeoverAc, int64, error) {
|
|
if s == nil || s.db == nil {
|
|
return nil, 0, errors.New("takeover service is not configured")
|
|
}
|
|
rows := make([]takeoverdomain.TakeoverAc, 0)
|
|
var total int64
|
|
base := s.db.WithContext(ctx).
|
|
Model(&takeoverdomain.TakeoverAc{}).
|
|
Preload("Base").
|
|
Preload("ReserveAc").
|
|
Preload("ReserveAc.Aircraft").
|
|
Preload("ReserveAc.Inspection").
|
|
Preload("RosterCrews").
|
|
Preload("OtherPeople").
|
|
Preload("Files").
|
|
Preload("Files.FileAttachment").
|
|
Preload("Files.FileAttachment.File").
|
|
Where("deleted_at IS NULL")
|
|
if err := base.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
q := base.Order("created_at DESC")
|
|
if limit > 0 {
|
|
q = q.Limit(limit).Offset(offset)
|
|
}
|
|
if err := q.Find(&rows).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return rows, total, nil
|
|
}
|
|
|
|
func (s *TakeoverService) AttachFile(ctx context.Context, takeoverID []byte, fileAttachmentID []byte) error {
|
|
if s == nil || s.db == nil {
|
|
return errors.New("takeover service is not configured")
|
|
}
|
|
if len(takeoverID) != 16 {
|
|
return errors.New("takeover_id must be a valid UUID")
|
|
}
|
|
if len(fileAttachmentID) != 16 {
|
|
return errors.New("file_attachment_id must be a valid UUID")
|
|
}
|
|
|
|
return s.withRetryTx(ctx, func(tx *gorm.DB) error {
|
|
return attachTakeoverFilesTx(tx, takeoverID, [][]byte{fileAttachmentID})
|
|
})
|
|
}
|
|
|
|
func (s *TakeoverService) DetachFile(ctx context.Context, takeoverID []byte, fileAttachmentID []byte) error {
|
|
if s == nil || s.db == nil {
|
|
return errors.New("takeover service is not configured")
|
|
}
|
|
if len(takeoverID) != 16 {
|
|
return errors.New("takeover_id must be a valid UUID")
|
|
}
|
|
if len(fileAttachmentID) != 16 {
|
|
return errors.New("file_attachment_id must be a valid UUID")
|
|
}
|
|
|
|
return s.withRetryTx(ctx, func(tx *gorm.DB) error {
|
|
var takeoverRow takeoverdomain.TakeoverAc
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", takeoverID).Take(&takeoverRow).Error; err != nil {
|
|
return err
|
|
}
|
|
res := tx.Where("takeover_id = ? AND file_attachment_id = ?", takeoverID, fileAttachmentID).Delete(&takeoverdomain.TakeoverFile{})
|
|
if res.Error != nil {
|
|
return res.Error
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
return gorm.ErrRecordNotFound
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func attachTakeoverFilesTx(tx *gorm.DB, takeoverID []byte, fileAttachmentIDs [][]byte) error {
|
|
if len(takeoverID) != 16 {
|
|
return errors.New("takeover_id must be a valid UUID")
|
|
}
|
|
var takeoverRow takeoverdomain.TakeoverAc
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", takeoverID).Take(&takeoverRow).Error; err != nil {
|
|
return err
|
|
}
|
|
for i := range fileAttachmentIDs {
|
|
fileAttachmentID := fileAttachmentIDs[i]
|
|
if len(fileAttachmentID) != 16 {
|
|
return errors.New("file_attachment_id must be a valid UUID")
|
|
}
|
|
var attachment filemanager.Attachment
|
|
if err := tx.Where("id = ?", fileAttachmentID).Take(&attachment).Error; err != nil {
|
|
return err
|
|
}
|
|
var existing takeoverdomain.TakeoverFile
|
|
err := tx.Where("takeover_id = ? AND file_attachment_id = ?", takeoverID, fileAttachmentID).Take(&existing).Error
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
row := &takeoverdomain.TakeoverFile{
|
|
TakeoverID: append([]byte(nil), takeoverID...),
|
|
FileAttachmentID: append([]byte(nil), fileAttachmentID...),
|
|
}
|
|
if err := tx.Create(row).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *TakeoverService) withRetryTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
|
const maxAttempts = 2
|
|
var lastErr error
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
lastErr = s.db.WithContext(ctx).Transaction(fn)
|
|
if lastErr == nil {
|
|
return nil
|
|
}
|
|
if !isRetryableDBErr(lastErr) || attempt == maxAttempts {
|
|
return lastErr
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func isRetryableDBErr(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg := strings.ToLower(err.Error())
|
|
return strings.Contains(msg, "bad connection") ||
|
|
strings.Contains(msg, "invalid connection") ||
|
|
strings.Contains(msg, "connection refused") ||
|
|
strings.Contains(msg, "broken pipe") ||
|
|
strings.Contains(msg, "driver: bad connection")
|
|
}
|
|
|
|
func loadTakeoverExistingTx(tx *gorm.DB, flightID []byte) (*takeoverExisting, error) {
|
|
var fl flight.Flight
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", flightID).Take(&fl).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
var takeoverRow takeoverdomain.TakeoverAc
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", fl.TakeoverAcID).Take(&takeoverRow).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
crews, err := loadTakeoverRosterCrewsTx(tx, takeoverRow.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
others, err := loadTakeoverOtherPeopleTx(tx, takeoverRow.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var reserve *reserveac.ReserveAc
|
|
if len(takeoverRow.ReserveAcID) == 16 {
|
|
var reserveRow reserveac.ReserveAc
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", takeoverRow.ReserveAcID).Take(&reserveRow).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
} else if err == nil {
|
|
reserve = &reserveRow
|
|
}
|
|
}
|
|
var beforeRow *beforeflightinspection.BeforeFlightInspection
|
|
var prepRow *flightprepcheck.FlightPrepCheck
|
|
if reserve != nil && len(reserve.InspectionID) == 16 {
|
|
var before beforeflightinspection.BeforeFlightInspection
|
|
if err := tx.Where("flight_inspection_id = ?", reserve.InspectionID).Take(&before).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
} else if err == nil {
|
|
beforeRow = &before
|
|
}
|
|
var prep flightprepcheck.FlightPrepCheck
|
|
if err := tx.Where("flight_inspection_id = ?", reserve.InspectionID).Take(&prep).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
} else if err == nil {
|
|
prepRow = &prep
|
|
}
|
|
}
|
|
return &takeoverExisting{Flight: &fl, Takeover: &takeoverRow, Reserve: reserve, Crews: crews, Others: others, Before: beforeRow, Prep: prepRow}, nil
|
|
}
|
|
|
|
func dutyDateTimeString(dutyDate time.Time, clock string) string {
|
|
trimmed := strings.TrimSpace(clock)
|
|
if len(trimmed) == 5 {
|
|
trimmed += ":00"
|
|
}
|
|
return dutyDate.UTC().Format("2006-01-02") + " " + trimmed
|
|
}
|
|
|
|
func ensureBaseExistsTx(tx *gorm.DB, id []byte) error {
|
|
var count int64
|
|
if err := tx.Table("bases").Where("id = ? AND deleted_at IS NULL", id).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return errors.New("base_id does not exist")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ensureHelicopterExistsTx(tx *gorm.DB, id []byte) error {
|
|
var count int64
|
|
if err := tx.Table("helicopters").Where("id = ?", id).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return errors.New("helicopter_id does not exist")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ResolveBaseShiftRangeTx(tx *gorm.DB, baseID []byte, dutyDate time.Time) (string, string, error) {
|
|
if len(baseID) != 16 {
|
|
return "", "", errors.New("base_id does not exist")
|
|
}
|
|
var baseRow basedomain.Base
|
|
err := tx.Preload("OperationalShiftTimes", func(db *gorm.DB) *gorm.DB {
|
|
return db.Where("deleted_at IS NULL").Order("date_start ASC, date_end ASC")
|
|
}).
|
|
Where("id = ? AND deleted_at IS NULL", baseID).
|
|
First(&baseRow).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", "", errors.New("base_id does not exist")
|
|
}
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return util.ResolveShiftWindow(&baseRow, dutyDate)
|
|
}
|
|
|
|
func isHelicopterBookedTx(tx *gorm.DB, helicopterID []byte, excludeTakeoverID []byte) (bool, error) {
|
|
if len(helicopterID) != 16 {
|
|
return false, nil
|
|
}
|
|
q := tx.Table("reserve_acs ra").
|
|
Joins("JOIN takeover_acs ta ON ta.reserve_ac_id = ra.id AND ta.deleted_at IS NULL").
|
|
Joins("JOIN flights f ON f.takeover_ac_id = ta.id").
|
|
Joins("LEFT JOIN after_flight_inspections afi ON afi.flight_inspection_id = ra.inspection_id").
|
|
Where("ra.helicopter_id = ? AND ra.deleted_at IS NULL AND f.deleted_at IS NULL AND afi.id IS NULL", helicopterID)
|
|
if len(excludeTakeoverID) == 16 {
|
|
q = q.Where("ta.id <> ?", excludeTakeoverID)
|
|
}
|
|
var total int64
|
|
if err := q.Count(&total).Error; err != nil {
|
|
return false, err
|
|
}
|
|
return total > 0, nil
|
|
}
|
|
|
|
func ensureUserExistsTx(tx *gorm.DB, id []byte) error {
|
|
var count int64
|
|
if err := tx.Table("users").Where("id = ?", id).Count(&count).Error; err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return errors.New("one or more crew contact ids do not exist")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *TakeoverService) upsertTakeoverChecklistItemsTx(tx *gorm.DB, inspectionID, helicopterID []byte, section string, items []requestdto.ReserveAcFileChecklistItem, actor []byte) error {
|
|
if s == nil || s.checklistFiles == nil {
|
|
return errors.New("checklist template repository is not configured")
|
|
}
|
|
templateFiles, err := s.checklistFiles.ListChecklistTemplateFiles(tx, helicopterID, section)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(templateFiles) == 0 {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("inspection checklist template is empty for section %s", section)
|
|
}
|
|
if len(items) == 0 {
|
|
return fmt.Errorf("%s is required", takeoverChecklistSectionLabel(section))
|
|
}
|
|
|
|
payloadByFileID := make(map[string]bool, len(items))
|
|
for i := range items {
|
|
if len(items[i].HelicopterFileID) == 0 {
|
|
return errors.New("helicopter_file_id is required")
|
|
}
|
|
hfID := strings.TrimSpace(items[i].HelicopterFileID)
|
|
parsedHFID, err := uuidv7.ParseString(hfID)
|
|
if err != nil {
|
|
return errors.New("invalid helicopter_file_id")
|
|
}
|
|
fileKey := string(parsedHFID)
|
|
if _, exists := payloadByFileID[fileKey]; exists {
|
|
return fmt.Errorf("duplicate helicopter_file_id in %s", takeoverChecklistSectionLabel(section))
|
|
}
|
|
payloadByFileID[fileKey] = items[i].IsDone
|
|
}
|
|
|
|
templateSet := make(map[string]struct{}, len(templateFiles))
|
|
for i := range templateFiles {
|
|
templateSet[string(templateFiles[i].ID)] = struct{}{}
|
|
}
|
|
|
|
for fileKey := range payloadByFileID {
|
|
if _, ok := templateSet[fileKey]; ok {
|
|
continue
|
|
}
|
|
return fmt.Errorf("helicopter_file_id does not belong to selected helicopter/section")
|
|
}
|
|
|
|
for i := range templateFiles {
|
|
fileKey := string(templateFiles[i].ID)
|
|
isDone, ok := payloadByFileID[fileKey]
|
|
if !ok {
|
|
// Only the files the user actually submitted are recorded. A catalog
|
|
// file that was not edited need not be included in the checklist
|
|
// (even if it is mandatory) — validateTakeoverEditedTemplateFilesTx
|
|
// separately guarantees every edited file IS included.
|
|
continue
|
|
}
|
|
if templateFiles[i].IsMandatory && !isDone {
|
|
return fmt.Errorf("%s must be fully checked", takeoverChecklistSectionLabel(section))
|
|
}
|
|
row := &flightinspectionfilechecklist.FlightInspectionFileChecklist{
|
|
HelicopterFileID: templateFiles[i].ID,
|
|
FlightInspectionID: inspectionID,
|
|
IsDone: isDone,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if err := tx.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "flight_inspection_id"}, {Name: "helicopter_file_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{"is_done", "updated_by"}),
|
|
}).Create(row).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateTakeoverEditedTemplateFilesTx enforces that every file that was
|
|
// created/edited from an FPWB template for this takeover is included in the
|
|
// submitted flight-preparation file checklist. A file that was not edited does
|
|
// not need to be in the checklist.
|
|
//
|
|
// New data stores template_uuid == HelicopterFile.ID. Legacy data may instead
|
|
// store the underlying source file ID. We accept both by resolving active FPWB
|
|
// checklist templates to a canonical helicopter_file_id before validating.
|
|
// Orphaned historical references that no longer map to an active checklist
|
|
// template are ignored so stale data cannot block takeover updates forever.
|
|
func (s *TakeoverService) validateTakeoverEditedTemplateFilesTx(tx *gorm.DB, takeoverID, helicopterID []byte, items []requestdto.ReserveAcFileChecklistItem) error {
|
|
if len(takeoverID) != 16 {
|
|
return nil
|
|
}
|
|
if s == nil || s.checklistFiles == nil {
|
|
return errors.New("checklist template repository is not configured")
|
|
}
|
|
editedTemplateUUIDs, err := s.checklistFiles.ListActiveEditedTemplateUUIDs(tx, takeoverID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(editedTemplateUUIDs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
templateFiles, err := s.checklistFiles.ListChecklistTemplateFiles(tx, helicopterID, helicopterfile.SectionFlightPreparationAndWB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
templateReferenceToChecklistID := make(map[string]string, len(templateFiles)*3)
|
|
for i := range templateFiles {
|
|
if len(templateFiles[i].ID) == 16 {
|
|
canonicalID := string(templateFiles[i].ID)
|
|
templateReferenceToChecklistID[canonicalID] = canonicalID
|
|
if len(templateFiles[i].SourceFileID) == 16 {
|
|
templateReferenceToChecklistID[string(templateFiles[i].SourceFileID)] = canonicalID
|
|
}
|
|
if templateFiles[i].SourceFile != nil && len(templateFiles[i].SourceFile.ID) == 16 {
|
|
templateReferenceToChecklistID[string(templateFiles[i].SourceFile.ID)] = canonicalID
|
|
}
|
|
if templateFiles[i].FileAttachment != nil && templateFiles[i].FileAttachment.File != nil && len(templateFiles[i].FileAttachment.File.ID) == 16 {
|
|
templateReferenceToChecklistID[string(templateFiles[i].FileAttachment.File.ID)] = canonicalID
|
|
}
|
|
}
|
|
}
|
|
|
|
checklistSet := make(map[string]struct{}, len(items))
|
|
for i := range items {
|
|
hfID := strings.TrimSpace(items[i].HelicopterFileID)
|
|
if hfID == "" {
|
|
continue
|
|
}
|
|
parsedHFID, err := uuidv7.ParseString(hfID)
|
|
if err != nil {
|
|
return errors.New("invalid helicopter_file_id")
|
|
}
|
|
checklistSet[string(parsedHFID)] = struct{}{}
|
|
}
|
|
|
|
for i := range editedTemplateUUIDs {
|
|
if len(editedTemplateUUIDs[i]) != 16 {
|
|
continue
|
|
}
|
|
requiredChecklistID, ok := templateReferenceToChecklistID[string(editedTemplateUUIDs[i])]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, ok := checklistSet[requiredChecklistID]; !ok {
|
|
return fmt.Errorf("edited template file must be included in the flight preparation file checklist")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func takeoverChecklistSectionLabel(section string) string {
|
|
switch section {
|
|
case helicopterfile.SectionBeforeFirstFlightInspection:
|
|
return "inspection.before.file_checklist"
|
|
case helicopterfile.SectionFlightPreparationAndWB:
|
|
return "inspection.prepare.file_checklist"
|
|
default:
|
|
return fmt.Sprintf("inspection.%s.file_checklist", section)
|
|
}
|
|
}
|
|
|
|
// resolveOtherPeopleMasterInputTx fills each id-referenced other-person's name/phone/email
|
|
// from the master (a snapshot), so an id-only reuse still passes the "name required"
|
|
// validation and later upserts to the same master.
|
|
func resolveOtherPeopleMasterInputTx(tx *gorm.DB, detail *requestdto.DutyRosterDetailsInput) error {
|
|
if detail == nil {
|
|
return nil
|
|
}
|
|
for i := range detail.OtherPerson {
|
|
idStr := strings.TrimSpace(detail.OtherPerson[i].OtherPersonID)
|
|
if idStr == "" {
|
|
continue
|
|
}
|
|
id, err := uuidv7.ParseString(idStr)
|
|
if err != nil {
|
|
return errors.New("other_person_id is invalid UUID")
|
|
}
|
|
var master otherpersondomain.OtherPerson
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", id).First(&master).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errors.New("other_person_id not found")
|
|
}
|
|
return err
|
|
}
|
|
detail.OtherPerson[i].Name = master.Name
|
|
detail.OtherPerson[i].MobilePhone = master.MobilePhone
|
|
detail.OtherPerson[i].Email = master.Email
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// upsertOtherPersonTx returns the canonical master person for (name, mobile_phone, email),
|
|
// creating it when absent. On return p.ID is the canonical id.
|
|
func upsertOtherPersonTx(tx *gorm.DB, p *otherpersondomain.OtherPerson) error {
|
|
name := strings.TrimSpace(p.Name)
|
|
phone := strings.TrimSpace(p.MobilePhone)
|
|
email := strings.TrimSpace(p.Email)
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
var existing otherpersondomain.OtherPerson
|
|
err := tx.Where("name = ? AND mobile_phone = ? AND email = ? AND deleted_at IS NULL", name, phone, email).First(&existing).Error
|
|
if err == nil {
|
|
p.ID = existing.ID
|
|
return nil
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
p.Name, p.MobilePhone, p.Email = name, phone, email
|
|
return tx.Create(p).Error
|
|
}
|
|
|
|
func buildTakeoverCrewRows(details *requestdto.DutyRosterDetailsInput, actor []byte) ([]takeoverdomain.TakeoverRosterCrew, []takeoverdomain.TakeoverOtherPerson, *DutyRosterValidationError) {
|
|
if details == nil {
|
|
return []takeoverdomain.TakeoverRosterCrew{}, []takeoverdomain.TakeoverOtherPerson{}, nil
|
|
}
|
|
rows, err := BuildDutyRosterCrewRows(*details, actor)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
crews := make([]takeoverdomain.TakeoverRosterCrew, 0, len(rows))
|
|
others := make([]takeoverdomain.TakeoverOtherPerson, 0)
|
|
for i := range rows {
|
|
if strings.EqualFold(strings.TrimSpace(rows[i].RoleCode), "other_person") {
|
|
others = append(others, takeoverdomain.TakeoverOtherPerson{
|
|
Name: rows[i].NameLabel,
|
|
MobilePhone: rows[i].MobilePhone,
|
|
Email: rows[i].Email,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
})
|
|
continue
|
|
}
|
|
crews = append(crews, takeoverdomain.TakeoverRosterCrew{
|
|
RoleCode: rows[i].RoleCode,
|
|
CrewType: rows[i].CrewType,
|
|
UserID: append([]byte(nil), rows[i].UserID...),
|
|
NameLabel: rows[i].NameLabel,
|
|
MobilePhone: rows[i].MobilePhone,
|
|
Email: rows[i].Email,
|
|
DateStart: rows[i].DateStart,
|
|
DateEnd: rows[i].DateEnd,
|
|
ShiftStart: nullableTimeStringPtr(rows[i].ShiftStart),
|
|
ShiftEnd: nullableTimeStringPtr(rows[i].ShiftEnd),
|
|
FlightInstructor: rows[i].FlightInstructor,
|
|
LineChecker: rows[i].LineChecker,
|
|
Supervisor: rows[i].Supervisor,
|
|
Examiner: rows[i].Examiner,
|
|
CoPilot: rows[i].CoPilot,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
})
|
|
}
|
|
return crews, others, nil
|
|
}
|
|
|
|
func takeoverRosterCrewIdentityKey(row takeoverdomain.TakeoverRosterCrew) string {
|
|
return fmt.Sprintf(
|
|
"%s|%s|%s|%s|%s|%s|%s|%s|%s",
|
|
strings.ToLower(strings.TrimSpace(row.RoleCode)),
|
|
bytesKey(row.UserID),
|
|
strings.ToLower(strings.TrimSpace(row.CrewType)),
|
|
strings.ToLower(strings.TrimSpace(row.NameLabel)),
|
|
strings.ToLower(strings.TrimSpace(row.MobilePhone)),
|
|
strings.ToLower(strings.TrimSpace(row.Email)),
|
|
nullableDateKey(row.DateStart),
|
|
nullableDateKey(row.DateEnd),
|
|
nullableTimeStringKey(row.ShiftStart)+"|"+nullableTimeStringKey(row.ShiftEnd),
|
|
)
|
|
}
|
|
|
|
func takeoverRosterCrewPayloadEqual(a, b takeoverdomain.TakeoverRosterCrew) bool {
|
|
return bytes.Equal(a.UserID, b.UserID) &&
|
|
strings.EqualFold(strings.TrimSpace(a.RoleCode), strings.TrimSpace(b.RoleCode)) &&
|
|
strings.EqualFold(strings.TrimSpace(a.CrewType), strings.TrimSpace(b.CrewType)) &&
|
|
strings.TrimSpace(a.NameLabel) == strings.TrimSpace(b.NameLabel) &&
|
|
strings.TrimSpace(a.MobilePhone) == strings.TrimSpace(b.MobilePhone) &&
|
|
strings.TrimSpace(a.Email) == strings.TrimSpace(b.Email) &&
|
|
nullableDateEqual(a.DateStart, b.DateStart) &&
|
|
nullableDateEqual(a.DateEnd, b.DateEnd) &&
|
|
nullableTimeStringEqual(a.ShiftStart, b.ShiftStart) &&
|
|
nullableTimeStringEqual(a.ShiftEnd, b.ShiftEnd) &&
|
|
a.FlightInstructor == b.FlightInstructor &&
|
|
a.LineChecker == b.LineChecker &&
|
|
a.Supervisor == b.Supervisor &&
|
|
a.Examiner == b.Examiner &&
|
|
a.CoPilot == b.CoPilot
|
|
}
|
|
|
|
func nullableDateEqual(a, b *time.Time) bool {
|
|
switch {
|
|
case a == nil && b == nil:
|
|
return true
|
|
case a == nil || b == nil:
|
|
return false
|
|
default:
|
|
return dateOnlyUTC(*a).Equal(dateOnlyUTC(*b))
|
|
}
|
|
}
|
|
|
|
func nullableDateKey(v *time.Time) string {
|
|
if v == nil {
|
|
return "nil"
|
|
}
|
|
return dateOnlyUTC(*v).Format("2006-01-02")
|
|
}
|
|
|
|
func bytesKey(v []byte) string {
|
|
if len(v) == 0 {
|
|
return "nil"
|
|
}
|
|
return fmt.Sprintf("%x", v)
|
|
}
|
|
|
|
func nullableTimeStringPtr(v string) *string {
|
|
trimmed := strings.TrimSpace(v)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func nullableTimeStringKey(v *string) string {
|
|
if v == nil {
|
|
return "nil"
|
|
}
|
|
return strings.TrimSpace(*v)
|
|
}
|
|
|
|
func nullableTimeStringEqual(a, b *string) bool {
|
|
switch {
|
|
case a == nil && b == nil:
|
|
return true
|
|
case a == nil || b == nil:
|
|
return false
|
|
default:
|
|
return strings.TrimSpace(*a) == strings.TrimSpace(*b)
|
|
}
|
|
}
|
|
|
|
func loadTakeoverRosterCrewsTx(tx *gorm.DB, takeoverID []byte) ([]takeoverdomain.TakeoverRosterCrew, error) {
|
|
rows := make([]takeoverdomain.TakeoverRosterCrew, 0)
|
|
if err := tx.Where("takeover_id = ? AND deleted_at IS NULL", takeoverID).Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func loadTakeoverOtherPeopleTx(tx *gorm.DB, takeoverID []byte) ([]takeoverdomain.TakeoverOtherPerson, error) {
|
|
rows := make([]takeoverdomain.TakeoverOtherPerson, 0)
|
|
if err := tx.Where("takeover_id = ? AND deleted_at IS NULL", takeoverID).Find(&rows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (s *TakeoverService) upsertTakeoverRosterRowsTx(
|
|
tx *gorm.DB,
|
|
takeoverID []byte,
|
|
in TakeoverCreateInput,
|
|
) error {
|
|
// Reuse: fill id-referenced other-people from the master (snapshot) BEFORE rows are
|
|
// built, so an id-only reuse still passes the "name required" validation.
|
|
if err := resolveOtherPeopleMasterInputTx(tx, in.RosterDetail); err != nil {
|
|
return err
|
|
}
|
|
crews, others, crewErr := buildTakeoverCrewRows(in.RosterDetail, in.ActorUserID)
|
|
if crewErr != nil {
|
|
if strings.TrimSpace(crewErr.Detail) != "" {
|
|
return errors.New(crewErr.Detail)
|
|
}
|
|
return errors.New("invalid roster_detail")
|
|
}
|
|
if len(crews) == 0 && len(others) == 0 {
|
|
return nil
|
|
}
|
|
// Register/link each other-person to the reusable master (other_people). The takeover
|
|
// keeps its own snapshot (name/phone/email); other_person_id points at the master.
|
|
for i := range others {
|
|
master := &otherpersondomain.OtherPerson{
|
|
Name: others[i].Name,
|
|
MobilePhone: others[i].MobilePhone,
|
|
Email: others[i].Email,
|
|
CreatedBy: in.ActorUserID,
|
|
UpdatedBy: in.ActorUserID,
|
|
}
|
|
if err := upsertOtherPersonTx(tx, master); err != nil {
|
|
return err
|
|
}
|
|
others[i].OtherPersonID = append([]byte(nil), master.ID...)
|
|
}
|
|
existingCrews, err := loadTakeoverRosterCrewsTx(tx, takeoverID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existingOthers, err := loadTakeoverOtherPeopleTx(tx, takeoverID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existingCrewMap := make(map[string]takeoverdomain.TakeoverRosterCrew, len(existingCrews))
|
|
for i := range existingCrews {
|
|
existingCrewMap[takeoverRosterCrewIdentityKey(existingCrews[i])] = existingCrews[i]
|
|
}
|
|
existingOtherMap := make(map[string]takeoverdomain.TakeoverOtherPerson, len(existingOthers))
|
|
for i := range existingOthers {
|
|
key := strings.ToLower(strings.TrimSpace(existingOthers[i].Name)) + "|" +
|
|
strings.ToLower(strings.TrimSpace(existingOthers[i].MobilePhone)) + "|" +
|
|
strings.ToLower(strings.TrimSpace(existingOthers[i].Email))
|
|
existingOtherMap[key] = existingOthers[i]
|
|
}
|
|
touchedCrewKeys := make(map[string]struct{}, len(crews))
|
|
touchedOtherKeys := make(map[string]struct{}, len(others))
|
|
for i := range crews {
|
|
crews[i].TakeoverID = append([]byte(nil), takeoverID...)
|
|
key := takeoverRosterCrewIdentityKey(crews[i])
|
|
touchedCrewKeys[key] = struct{}{}
|
|
if existingRow, ok := existingCrewMap[key]; ok {
|
|
crews[i].ID = append([]byte(nil), existingRow.ID...)
|
|
if takeoverRosterCrewPayloadEqual(existingRow, crews[i]) {
|
|
continue
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverRosterCrew{}).
|
|
Where("id = ? AND deleted_at IS NULL", crews[i].ID).
|
|
Updates(map[string]any{
|
|
"takeover_id": crews[i].TakeoverID,
|
|
"confirm_at": crews[i].ConfirmAt,
|
|
"role_code": crews[i].RoleCode,
|
|
"crew_type": crews[i].CrewType,
|
|
"user_id": crews[i].UserID,
|
|
"name_label": crews[i].NameLabel,
|
|
"mobile_phone": crews[i].MobilePhone,
|
|
"email": crews[i].Email,
|
|
"date_start": crews[i].DateStart,
|
|
"date_end": crews[i].DateEnd,
|
|
"shift_start": crews[i].ShiftStart,
|
|
"shift_end": crews[i].ShiftEnd,
|
|
"flight_instructor": crews[i].FlightInstructor,
|
|
"line_checker": crews[i].LineChecker,
|
|
"supervisor": crews[i].Supervisor,
|
|
"examiner": crews[i].Examiner,
|
|
"co_pilot": crews[i].CoPilot,
|
|
"updated_by": in.ActorUserID,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := tx.Create(&crews[i]).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, existingRow := range existingCrews {
|
|
key := takeoverRosterCrewIdentityKey(existingRow)
|
|
if _, ok := touchedCrewKeys[key]; ok {
|
|
continue
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverRosterCrew{}).
|
|
Where("id = ? AND deleted_at IS NULL", existingRow.ID).
|
|
Updates(map[string]any{"deleted_at": time.Now().UTC(), "deleted_by": in.ActorUserID, "updated_by": in.ActorUserID}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range others {
|
|
others[i].TakeoverID = append([]byte(nil), takeoverID...)
|
|
key := strings.ToLower(strings.TrimSpace(others[i].Name)) + "|" +
|
|
strings.ToLower(strings.TrimSpace(others[i].MobilePhone)) + "|" +
|
|
strings.ToLower(strings.TrimSpace(others[i].Email))
|
|
touchedOtherKeys[key] = struct{}{}
|
|
if existingRow, ok := existingOtherMap[key]; ok {
|
|
others[i].ID = append([]byte(nil), existingRow.ID...)
|
|
if strings.TrimSpace(existingRow.Name) == strings.TrimSpace(others[i].Name) &&
|
|
strings.TrimSpace(existingRow.MobilePhone) == strings.TrimSpace(others[i].MobilePhone) &&
|
|
strings.TrimSpace(existingRow.Email) == strings.TrimSpace(others[i].Email) {
|
|
continue
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverOtherPerson{}).
|
|
Where("id = ? AND deleted_at IS NULL", others[i].ID).
|
|
Updates(map[string]any{
|
|
"takeover_id": others[i].TakeoverID,
|
|
"other_person_id": others[i].OtherPersonID,
|
|
"name": others[i].Name,
|
|
"mobile_phone": others[i].MobilePhone,
|
|
"email": others[i].Email,
|
|
"updated_by": in.ActorUserID,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := tx.Create(&others[i]).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, existingRow := range existingOthers {
|
|
key := strings.ToLower(strings.TrimSpace(existingRow.Name)) + "|" +
|
|
strings.ToLower(strings.TrimSpace(existingRow.MobilePhone)) + "|" +
|
|
strings.ToLower(strings.TrimSpace(existingRow.Email))
|
|
if _, ok := touchedOtherKeys[key]; ok {
|
|
continue
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverOtherPerson{}).
|
|
Where("id = ? AND deleted_at IS NULL", existingRow.ID).
|
|
Updates(map[string]any{"deleted_at": time.Now().UTC(), "deleted_by": in.ActorUserID, "updated_by": in.ActorUserID}).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *TakeoverService) upsertTakeoverInspectionTx(
|
|
tx *gorm.DB,
|
|
takeoverID []byte,
|
|
inspectionID []byte,
|
|
in *requestdto.ReserveAcInspectionCreateAttributes,
|
|
helicopterID []byte,
|
|
actor []byte,
|
|
) ([]byte, error) {
|
|
if in == nil {
|
|
return inspectionID, nil
|
|
}
|
|
if len(helicopterID) != 16 {
|
|
return nil, errors.New("helicopter_id is required for inspection data")
|
|
}
|
|
// Every file edited/created from an FPWB template for this takeover must be
|
|
// represented in the submitted flight-preparation file checklist. Files that
|
|
// were not edited need not be in the checklist.
|
|
if err := s.validateTakeoverEditedTemplateFilesTx(tx, takeoverID, helicopterID, in.Prepare.FileChecklist); err != nil {
|
|
return nil, err
|
|
}
|
|
inspectionDate, err := time.Parse("2006-01-02", strings.TrimSpace(in.InspectionDate))
|
|
if err != nil {
|
|
return nil, errors.New("inspection.inspection_date must be YYYY-MM-DD")
|
|
}
|
|
if len(inspectionID) == 0 {
|
|
inspection := &flightinspection.FlightInspection{
|
|
InspectionDate: dateOnlyUTC(inspectionDate),
|
|
Status: flightinspection.StatusDraft,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if err := tx.Create(inspection).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
inspectionID = append([]byte(nil), inspection.ID...)
|
|
} else {
|
|
if err := tx.Model(&flightinspection.FlightInspection{}).
|
|
Where("id = ?", inspectionID).
|
|
Updates(map[string]any{
|
|
"inspection_date": inspectionDate,
|
|
"updated_by": actor,
|
|
}).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
beforeRow := &beforeflightinspection.BeforeFlightInspection{
|
|
FlightInspectionID: append([]byte(nil), inspectionID...),
|
|
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,
|
|
FuelAddedAmount: in.Before.FuelAddedAmount,
|
|
Note: in.Before.Note,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if in.Before.FuelUnit != nil {
|
|
unit, ok := beforeflightinspection.NormalizeFuelUnit(*in.Before.FuelUnit)
|
|
if !ok {
|
|
return nil, errors.New("inspection.before.fuel_unit must be one of LT, KG, POUND")
|
|
}
|
|
beforeRow.FuelUnit = unit
|
|
}
|
|
if err := tx.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "flight_inspection_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"oil_engine_nr1_checked",
|
|
"oil_engine_nr2_checked",
|
|
"hydraulic_lh_checked",
|
|
"hydraulic_rh_checked",
|
|
"oil_transmission_mgb_checked",
|
|
"oil_transmission_igb_checked",
|
|
"oil_transmission_tgb_checked",
|
|
"fuel_amount",
|
|
"fuel_unit",
|
|
"fuel_added_amount",
|
|
"note",
|
|
"updated_by",
|
|
}),
|
|
}).Create(beforeRow).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prepRow := &flightprepcheck.FlightPrepCheck{
|
|
FlightInspectionID: append([]byte(nil), inspectionID...),
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if in.Prepare.NotamBriefing != nil {
|
|
prepRow.NOTAMBriefing = *in.Prepare.NotamBriefing
|
|
}
|
|
if in.Prepare.WeatherBriefing != nil {
|
|
prepRow.WeatherBriefing = *in.Prepare.WeatherBriefing
|
|
}
|
|
if in.Prepare.OperationalFlightPlan != nil {
|
|
prepRow.OperationalFlightPlan = *in.Prepare.OperationalFlightPlan
|
|
}
|
|
if err := tx.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "flight_inspection_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"notam_briefing",
|
|
"weather_briefing",
|
|
"operational_flight_plan",
|
|
"updated_by",
|
|
}),
|
|
}).Create(prepRow).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(in.Before.FileChecklist) > 0 {
|
|
if err := s.upsertTakeoverChecklistItemsTx(tx, inspectionID, helicopterID, helicopterfile.SectionBeforeFirstFlightInspection, in.Before.FileChecklist, actor); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if len(in.Prepare.FileChecklist) > 0 {
|
|
if err := s.upsertTakeoverChecklistItemsTx(tx, inspectionID, helicopterID, helicopterfile.SectionFlightPreparationAndWB, in.Prepare.FileChecklist, actor); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return inspectionID, nil
|
|
}
|
|
|
|
func (s *TakeoverService) upsertTakeoverReserveTx(
|
|
tx *gorm.DB,
|
|
takeoverID []byte,
|
|
baseID []byte,
|
|
existing *takeoverExisting,
|
|
in TakeoverCreateInput,
|
|
) error {
|
|
if existing == nil {
|
|
return errors.New("takeover state is not loaded")
|
|
}
|
|
if existing.Reserve == nil {
|
|
if !in.HasHelicopterID && !in.HasInspection {
|
|
return nil
|
|
}
|
|
if !in.HasHelicopterID || !in.HasInspection || in.Inspection == nil {
|
|
return errors.New("helicopter_id and inspection are required to create reserve data")
|
|
}
|
|
if err := ensureHelicopterExistsTx(tx, in.HelicopterID); err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.ensureHelicopterExistsTx", err)
|
|
return err
|
|
}
|
|
inspectionID, err := s.upsertTakeoverInspectionTx(tx, takeoverID, nil, in.Inspection, in.HelicopterID, in.ActorUserID)
|
|
if err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.upsertTakeoverInspectionTx(create)", err)
|
|
return err
|
|
}
|
|
reserveRow := &reserveac.ReserveAc{
|
|
Status: reserveacutil.StatusPending,
|
|
BaseID: append([]byte(nil), baseID...),
|
|
Notes: in.Notes,
|
|
AircraftID: append([]byte(nil), in.HelicopterID...),
|
|
InspectionID: inspectionID,
|
|
CreatedBy: in.ActorUserID,
|
|
UpdatedBy: in.ActorUserID,
|
|
}
|
|
if err := tx.Create(reserveRow).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.createReserve", err)
|
|
return err
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverAc{}).
|
|
Where("id = ? AND deleted_at IS NULL", takeoverID).
|
|
Updates(map[string]any{"reserve_ac_id": reserveRow.ID, "updated_by": in.ActorUserID}).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.attachReserveToTakeover", err)
|
|
return err
|
|
}
|
|
existing.Reserve = reserveRow
|
|
return nil
|
|
}
|
|
|
|
changed := false
|
|
if len(baseID) == 16 && !bytes.Equal(existing.Reserve.BaseID, baseID) {
|
|
existing.Reserve.BaseID = append([]byte(nil), baseID...)
|
|
changed = true
|
|
}
|
|
if in.Notes != nil && (existing.Reserve.Notes == nil || *existing.Reserve.Notes != *in.Notes) {
|
|
existing.Reserve.Notes = in.Notes
|
|
changed = true
|
|
}
|
|
if in.HasHelicopterID {
|
|
if len(in.HelicopterID) != 16 {
|
|
return errors.New("helicopter_id must be a valid UUID")
|
|
}
|
|
if err := ensureHelicopterExistsTx(tx, in.HelicopterID); err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.ensureHelicopterExistsTx(update)", err)
|
|
return err
|
|
}
|
|
if !bytes.Equal(existing.Reserve.AircraftID, in.HelicopterID) {
|
|
existing.Reserve.AircraftID = append([]byte(nil), in.HelicopterID...)
|
|
changed = true
|
|
}
|
|
}
|
|
if in.HasInspection {
|
|
if in.Inspection == nil {
|
|
return errors.New("inspection data is required")
|
|
}
|
|
helicopterID := existing.Reserve.AircraftID
|
|
if in.HasHelicopterID {
|
|
helicopterID = in.HelicopterID
|
|
}
|
|
if len(helicopterID) != 16 {
|
|
return errors.New("helicopter_id is required for inspection data")
|
|
}
|
|
inspectionID, err := s.upsertTakeoverInspectionTx(tx, takeoverID, existing.Reserve.InspectionID, in.Inspection, helicopterID, in.ActorUserID)
|
|
if err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.upsertTakeoverInspectionTx(update)", err)
|
|
return err
|
|
}
|
|
if len(existing.Reserve.InspectionID) != 16 || !bytes.Equal(existing.Reserve.InspectionID, inspectionID) {
|
|
existing.Reserve.InspectionID = append([]byte(nil), inspectionID...)
|
|
changed = true
|
|
}
|
|
}
|
|
if changed {
|
|
existing.Reserve.UpdatedBy = in.ActorUserID
|
|
if err := tx.Save(existing.Reserve).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverReserveTx.saveReserve", err)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
|
|
func (s *TakeoverService) upsertTakeoverReserveTx(
|
|
tx *gorm.DB,
|
|
takeoverID []byte,
|
|
baseID []byte,
|
|
existing *takeoverExisting,
|
|
in TakeoverCreateInput,
|
|
) error {
|
|
if existing == nil {
|
|
return errors.New("takeover state is not available")
|
|
}
|
|
if existing.Reserve == nil {
|
|
if !in.HasHelicopterID && !in.HasInspection {
|
|
return nil
|
|
}
|
|
if !in.HasHelicopterID || len(in.HelicopterID) != 16 {
|
|
return errors.New("helicopter_id is required to create reserve data")
|
|
}
|
|
if !in.HasInspection || in.Inspection == nil {
|
|
return errors.New("inspection is required to create reserve data")
|
|
}
|
|
if err := ensureHelicopterExistsTx(tx, in.HelicopterID); err != nil {
|
|
return err
|
|
}
|
|
inspectionID, err := upsertTakeoverInspectionTx(tx, nil, in.Inspection, in.ActorUserID, in.HelicopterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reserveRow := &reserveac.ReserveAc{
|
|
Status: reserveacutil.StatusPending,
|
|
BaseID: append([]byte(nil), baseID...),
|
|
Notes: in.Notes,
|
|
AircraftID: append([]byte(nil), in.HelicopterID...),
|
|
InspectionID: append([]byte(nil), inspectionID...),
|
|
CreatedBy: in.ActorUserID,
|
|
UpdatedBy: in.ActorUserID,
|
|
}
|
|
if err := tx.Create(reserveRow).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&takeoverdomain.TakeoverAc{}).
|
|
Where("id = ? AND deleted_at IS NULL", takeoverID).
|
|
Updates(map[string]any{"reserve_ac_id": reserveRow.ID, "updated_by": in.ActorUserID}).Error; err != nil {
|
|
return err
|
|
}
|
|
existing.Reserve = reserveRow
|
|
return nil
|
|
}
|
|
|
|
changed := false
|
|
if in.HasBaseID && !bytes.Equal(existing.Reserve.BaseID, baseID) {
|
|
existing.Reserve.BaseID = append([]byte(nil), baseID...)
|
|
changed = true
|
|
}
|
|
if in.Notes != nil && (existing.Reserve.Notes == nil || strings.TrimSpace(*existing.Reserve.Notes) != strings.TrimSpace(*in.Notes)) {
|
|
existing.Reserve.Notes = in.Notes
|
|
changed = true
|
|
}
|
|
if in.HasHelicopterID {
|
|
if len(in.HelicopterID) != 16 {
|
|
return errors.New("helicopter_id must be a valid UUID")
|
|
}
|
|
if err := ensureHelicopterExistsTx(tx, in.HelicopterID); err != nil {
|
|
return err
|
|
}
|
|
if !bytes.Equal(existing.Reserve.AircraftID, in.HelicopterID) {
|
|
existing.Reserve.AircraftID = append([]byte(nil), in.HelicopterID...)
|
|
changed = true
|
|
}
|
|
}
|
|
if in.HasInspection {
|
|
if in.Inspection == nil {
|
|
return errors.New("inspection data is required when inspection is provided")
|
|
}
|
|
heliID := existing.Reserve.AircraftID
|
|
if in.HasHelicopterID {
|
|
heliID = in.HelicopterID
|
|
}
|
|
if len(heliID) != 16 {
|
|
return errors.New("helicopter_id is required to update inspection data")
|
|
}
|
|
inspectionID, err := upsertTakeoverInspectionTx(tx, existing.Reserve.InspectionID, in.Inspection, in.ActorUserID, heliID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(existing.Reserve.InspectionID) != 16 || !bytes.Equal(existing.Reserve.InspectionID, inspectionID) {
|
|
existing.Reserve.InspectionID = append([]byte(nil), inspectionID...)
|
|
changed = true
|
|
}
|
|
}
|
|
if changed {
|
|
existing.Reserve.UpdatedBy = in.ActorUserID
|
|
if err := tx.Save(existing.Reserve).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func upsertTakeoverInspectionTx(tx *gorm.DB, inspectionID []byte, in *requestdto.ReserveAcInspectionCreateAttributes, actor []byte, helicopterID []byte) ([]byte, error) {
|
|
if in == nil {
|
|
return inspectionID, nil
|
|
}
|
|
existingInspection := len(inspectionID) == 16
|
|
if len(helicopterID) != 16 {
|
|
return nil, errors.New("helicopter_id is required to update inspection data")
|
|
}
|
|
if err := validateTakeoverInspectionChecklists(in); err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.validateTakeoverInspectionChecklists", err)
|
|
return nil, err
|
|
}
|
|
inspectionDate, err := time.Parse("2006-01-02", strings.TrimSpace(in.InspectionDate))
|
|
if err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.parseInspectionDate", err)
|
|
return nil, errors.New("inspection.inspection_date must be YYYY-MM-DD")
|
|
}
|
|
if len(inspectionID) != 16 {
|
|
inspection := &flightinspection.FlightInspection{
|
|
InspectionDate: dateOnlyUTC(inspectionDate),
|
|
Status: flightinspection.StatusDraft,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if err := tx.Create(inspection).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.createFlightInspection", err)
|
|
return nil, err
|
|
}
|
|
inspectionID = append([]byte(nil), inspection.ID...)
|
|
} else if err := tx.Model(&flightinspection.FlightInspection{}).
|
|
Where("id = ?", inspectionID).
|
|
Updates(map[string]any{"inspection_date": dateOnlyUTC(inspectionDate), "updated_by": actor}).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.updateFlightInspection", err)
|
|
return nil, err
|
|
}
|
|
|
|
beforeRow := &beforeflightinspection.BeforeFlightInspection{
|
|
FlightInspectionID: append([]byte(nil), inspectionID...),
|
|
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,
|
|
FuelAddedAmount: in.Before.FuelAddedAmount,
|
|
Note: in.Before.Note,
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if in.Before.FuelUnit != nil {
|
|
unit, ok := beforeflightinspection.NormalizeFuelUnit(*in.Before.FuelUnit)
|
|
if !ok {
|
|
return nil, errors.New("inspection.before.fuel_unit must be one of LT, KG, POUND")
|
|
}
|
|
beforeRow.FuelUnit = unit
|
|
}
|
|
if err := tx.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "flight_inspection_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{"oil_engine_nr1_checked", "oil_engine_nr2_checked", "hydraulic_lh_checked", "hydraulic_rh_checked", "oil_transmission_mgb_checked", "oil_transmission_igb_checked", "oil_transmission_tgb_checked", "fuel_amount", "fuel_unit", "fuel_added_amount", "note", "updated_by"}),
|
|
}).Create(beforeRow).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.upsertBeforeFlightInspection", err)
|
|
return nil, err
|
|
}
|
|
|
|
prepRow := &flightprepcheck.FlightPrepCheck{
|
|
FlightInspectionID: append([]byte(nil), inspectionID...),
|
|
CreatedBy: actor,
|
|
UpdatedBy: actor,
|
|
}
|
|
if in.Prepare.NotamBriefing != nil {
|
|
prepRow.NOTAMBriefing = *in.Prepare.NotamBriefing
|
|
}
|
|
if in.Prepare.WeatherBriefing != nil {
|
|
prepRow.WeatherBriefing = *in.Prepare.WeatherBriefing
|
|
}
|
|
if in.Prepare.OperationalFlightPlan != nil {
|
|
prepRow.OperationalFlightPlan = *in.Prepare.OperationalFlightPlan
|
|
}
|
|
if err := tx.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "flight_inspection_id"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{"notam_briefing", "weather_briefing", "operational_flight_plan", "updated_by"}),
|
|
}).Create(prepRow).Error; err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.upsertFlightPrepCheck", err)
|
|
return nil, err
|
|
}
|
|
|
|
if !existingInspection || len(in.Before.FileChecklist) > 0 {
|
|
if err := s.upsertTakeoverChecklistItemsTx(tx, inspectionID, helicopterID, helicopterfile.SectionBeforeFirstFlightInspection, in.Before.FileChecklist, actor); err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.beforeChecklist", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
if !existingInspection || len(in.Prepare.FileChecklist) > 0 {
|
|
if err := s.upsertTakeoverChecklistItemsTx(tx, inspectionID, helicopterID, helicopterfile.SectionFlightPreparationAndWB, in.Prepare.FileChecklist, actor); err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.prepareChecklist", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
// On updates, an omitted fleet_status_file block means "keep the existing
|
|
// checklist as-is". Only enforce the mandatory-file check when the caller
|
|
// actually sends the section, or when we're creating a brand new inspection.
|
|
if !existingInspection || len(in.FleetStatusFile.FileChecklist) > 0 {
|
|
if err := validateMandatoryFleetStatusFilesTx(tx, helicopterID, in.FleetStatusFile.FileChecklist); err != nil {
|
|
logTakeoverServiceStepError("upsertTakeoverInspectionTx.validateMandatoryFleetStatusFilesTx", err)
|
|
return nil, err
|
|
}
|
|
}
|
|
return inspectionID, nil
|
|
}
|
|
*/
|
|
|
|
func validateTakeoverInspectionChecklists(in *requestdto.ReserveAcInspectionCreateAttributes) error {
|
|
if in == nil {
|
|
return nil
|
|
}
|
|
if err := validateTakeoverChecklistSection(in.Before.FileChecklist, "inspection.before.file_checklist"); err != nil {
|
|
return err
|
|
}
|
|
if err := validateTakeoverChecklistSection(in.Prepare.FileChecklist, "inspection.prepare.file_checklist"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateTakeoverChecklistSection(items []requestdto.ReserveAcFileChecklistItem, label string) error {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
for i := range items {
|
|
if !items[i].IsDone {
|
|
return fmt.Errorf("%s must be fully checked", label)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateMandatoryFleetStatusFilesTx(tx *gorm.DB, helicopterID []byte, items []requestdto.ReserveAcFileChecklistItem) error {
|
|
if len(helicopterID) != 16 {
|
|
return nil
|
|
}
|
|
type fsfRow struct {
|
|
ID []byte `gorm:"column:id"`
|
|
}
|
|
latest := tx.Table("fleet_statuses").
|
|
Select("id").
|
|
Where("helicopter_id = ? AND deleted_at IS NULL", helicopterID).
|
|
Order("created_at DESC").
|
|
Limit(1)
|
|
mandatory := make([]fsfRow, 0)
|
|
if err := tx.Table("fleet_status_files").
|
|
Select("id").
|
|
Where("is_mandatory = ? AND fleet_status_id = (?)", true, latest).
|
|
Scan(&mandatory).Error; err != nil {
|
|
return err
|
|
}
|
|
if len(mandatory) == 0 {
|
|
return nil
|
|
}
|
|
done := make(map[string]bool, len(items))
|
|
for i := range items {
|
|
id, err := uuidv7.ParseString(strings.TrimSpace(items[i].HelicopterFileID))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
done[string(id)] = items[i].IsDone
|
|
}
|
|
for _, m := range mandatory {
|
|
if !done[string(m.ID)] {
|
|
return fmt.Errorf("inspection.fleet_status_file: a mandatory file must be checked (is_done) before takeover")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadTakeoverReserveTx(tx *gorm.DB, id []byte) (*reserveac.ReserveAc, error) {
|
|
if len(id) != 16 {
|
|
return nil, nil
|
|
}
|
|
var row reserveac.ReserveAc
|
|
if err := tx.Where("id = ? AND deleted_at IS NULL", id).Take(&row).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &row, nil
|
|
}
|
|
|
|
func getLatestMissionCodeByPrefixTx(tx *gorm.DB, prefix string) (string, error) {
|
|
var out struct {
|
|
MissionCode string `gorm:"column:mission_code"`
|
|
}
|
|
err := tx.Table("flights").
|
|
Select("mission_code").
|
|
Where("mission_code LIKE ? AND deleted_at IS NULL", prefix+"%").
|
|
Order("mission_code DESC").
|
|
Limit(1).
|
|
Scan(&out).Error
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return out.MissionCode, nil
|
|
}
|