665 lines
20 KiB
Go
665 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
complaint "wucher/internal/domain/complaint"
|
|
filemanager "wucher/internal/domain/file_manager"
|
|
fleethistory "wucher/internal/domain/fleet_history"
|
|
fleetstatus "wucher/internal/domain/fleet_status"
|
|
"wucher/internal/domain/helicopter"
|
|
helicopterusage "wucher/internal/domain/helicopter_usage"
|
|
"wucher/internal/shared/pkg/nextdue"
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
shareddto "wucher/internal/transport/http/dto/shared"
|
|
)
|
|
|
|
type fleetStatusAttachments interface {
|
|
CreateAttachment(ctx context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error)
|
|
DeleteAttachment(ctx context.Context, id []byte) error
|
|
}
|
|
|
|
type fleetStatusHelicopters interface {
|
|
GetByID(ctx context.Context, id []byte) (*helicopter.Helicopter, error)
|
|
Update(ctx context.Context, h *helicopter.Helicopter) error
|
|
}
|
|
|
|
type fleetStatusComplaints interface {
|
|
ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error)
|
|
ResolveUserNames(ctx context.Context, complaints []complaint.Complaint) (map[string]string, error)
|
|
}
|
|
|
|
type fleetStatusUsage interface {
|
|
GetByHelicopterID(ctx context.Context, helicopterID []byte) (*helicopterusage.HelicopterUsage, error)
|
|
ReviseManualTotals(ctx context.Context, helicopterID []byte, in helicopterusage.ManualSummaryInput, actor []byte) error
|
|
}
|
|
|
|
const (
|
|
FleetStatusFileRefType = "fleet_status"
|
|
|
|
autoAOGReasonPrefixNextDue = "[auto_next_due]"
|
|
autoAOGReasonPrefixComplaint = "[auto_complaint]"
|
|
)
|
|
|
|
type NextDueEvaluation struct {
|
|
InspectionType string
|
|
AOG bool
|
|
Reason string
|
|
}
|
|
|
|
type ComponentHours struct {
|
|
Airframe float64
|
|
Engine1 float64
|
|
Engine2 float64
|
|
}
|
|
|
|
type FleetStatusService struct {
|
|
repo fleetstatus.Repository
|
|
attachments fleetStatusAttachments
|
|
helicopters fleetStatusHelicopters
|
|
complaints fleetStatusComplaints
|
|
history *FleetHistoryService
|
|
usage fleetStatusUsage
|
|
}
|
|
|
|
func (s *FleetStatusService) Create(ctx context.Context, row *fleetstatus.FleetStatus) error {
|
|
if s.attachments == nil {
|
|
if err := s.repo.Create(ctx, row); err != nil {
|
|
return err
|
|
}
|
|
return s.syncAutomaticAOG(ctx, row)
|
|
}
|
|
if len(row.ID) == 0 {
|
|
row.ID = uuidv7.MustBytes()
|
|
}
|
|
files, createdIDs, err := s.resolveFileLinks(ctx, row.ID, row.Files, nil, row.UpdatedBy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row.Files = files
|
|
if err := s.repo.Create(ctx, row); err != nil {
|
|
s.rollbackAttachments(ctx, createdIDs)
|
|
return err
|
|
}
|
|
if err := s.syncAutomaticAOG(ctx, row); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FleetStatusService) Update(ctx context.Context, row *fleetstatus.FleetStatus) error {
|
|
if s.attachments == nil {
|
|
if err := s.repo.Update(ctx, row); err != nil {
|
|
return err
|
|
}
|
|
return s.syncAutomaticAOG(ctx, row)
|
|
}
|
|
|
|
newKeys := make(map[string]struct{})
|
|
for i := range row.Files {
|
|
if len(row.Files[i].FileID) == 16 {
|
|
key, _ := uuidv7.BytesToString(row.Files[i].FileID)
|
|
newKeys[key] = struct{}{}
|
|
}
|
|
}
|
|
if len(newKeys) == 0 {
|
|
if err := s.repo.Update(ctx, row); err != nil {
|
|
return err
|
|
}
|
|
return s.syncAutomaticAOG(ctx, row)
|
|
}
|
|
|
|
existing, err := s.repo.GetByID(ctx, row.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reuse := make(map[string][]byte)
|
|
type oldRef struct {
|
|
key string
|
|
attID []byte
|
|
}
|
|
var olds []oldRef
|
|
if existing != nil {
|
|
for i := range existing.Files {
|
|
ef := existing.Files[i]
|
|
if ef.Attachment == nil || len(ef.Attachment.FileID) != 16 {
|
|
continue
|
|
}
|
|
key, _ := uuidv7.BytesToString(ef.Attachment.FileID)
|
|
reuse[key] = ef.FileAttachmentID
|
|
olds = append(olds, oldRef{key: key, attID: ef.FileAttachmentID})
|
|
}
|
|
}
|
|
|
|
files, createdIDs, err := s.resolveFileLinks(ctx, row.ID, row.Files, reuse, row.UpdatedBy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row.Files = files
|
|
|
|
if err := s.repo.Update(ctx, row); err != nil {
|
|
s.rollbackAttachments(ctx, createdIDs)
|
|
return err
|
|
}
|
|
|
|
if err := s.syncAutomaticAOG(ctx, row); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, o := range olds {
|
|
if _, keep := newKeys[o.key]; keep {
|
|
continue
|
|
}
|
|
_ = s.attachments.DeleteAttachment(ctx, o.attID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *FleetStatusService) WithUsage(u fleetStatusUsage) *FleetStatusService {
|
|
s.usage = u
|
|
return s
|
|
}
|
|
|
|
func (s *FleetStatusService) ReviseSummary(ctx context.Context, helicopterID []byte, in helicopterusage.ManualSummaryInput, actor []byte) error {
|
|
if s.usage == nil || len(helicopterID) != 16 {
|
|
return nil
|
|
}
|
|
return s.usage.ReviseManualTotals(ctx, helicopterID, in, actor)
|
|
}
|
|
|
|
func (s *FleetStatusService) WithHistory(h *FleetHistoryService) *FleetStatusService {
|
|
s.history = h
|
|
return s
|
|
}
|
|
|
|
func (s *FleetStatusService) recordAutoAOG(ctx context.Context, heli *helicopter.Helicopter, set bool, reason string) {
|
|
if s.history == nil || heli == nil {
|
|
return
|
|
}
|
|
action := fleethistory.ActionAOGCleared
|
|
msg := "AOG cleared (auto: maintenance no longer due)"
|
|
if set {
|
|
action = fleethistory.ActionAOGSet
|
|
msg = "AOG set (auto: maintenance next due)"
|
|
if r := strings.TrimSpace(reason); r != "" {
|
|
msg = "AOG set (auto) — " + r
|
|
}
|
|
}
|
|
if err := s.history.Record(ctx, heli.ID, fleethistory.EntityHelicopter, heli.ID, action, msg, nil); err != nil {
|
|
slog.WarnContext(ctx, "fleet status: record auto-AOG history failed", slog.Any("error", err))
|
|
}
|
|
}
|
|
|
|
func NewFleetStatusService(repo fleetstatus.Repository, attachments fleetStatusAttachments, helicopters fleetStatusHelicopters, complaints fleetStatusComplaints) *FleetStatusService {
|
|
return &FleetStatusService{repo: repo, attachments: attachments, helicopters: helicopters, complaints: complaints}
|
|
}
|
|
|
|
func (s *FleetStatusService) syncAutomaticAOG(ctx context.Context, row *fleetstatus.FleetStatus) error {
|
|
if s == nil || s.helicopters == nil || row == nil || len(row.HelicopterID) != 16 {
|
|
return nil
|
|
}
|
|
heli, err := s.helicopters.GetByID(ctx, row.HelicopterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if heli == nil {
|
|
idStr, _ := uuidv7.BytesToString(row.HelicopterID)
|
|
slog.WarnContext(ctx, "fleet status: helicopter not found, skipping AOG sync", slog.String("helicopter_id", idStr))
|
|
return nil
|
|
}
|
|
now := time.Now().UTC()
|
|
|
|
var evals []NextDueEvaluation
|
|
if hasEligibleSchedule(row.MaintenanceSchedules) {
|
|
summary, sErr := s.GetSummaryByHelicopterID(ctx, row.HelicopterID)
|
|
if sErr != nil {
|
|
return sErr
|
|
}
|
|
hours := ComponentHours{
|
|
Airframe: derefFloat(summary.Airframe.Hours),
|
|
Engine1: derefFloat(summary.Engine1.Hours),
|
|
Engine2: derefFloat(summary.Engine2.Hours),
|
|
}
|
|
evals, err = EvaluateNextDueSchedules(row.MaintenanceSchedules, hours, summary.Airframe.Hours != nil, heli.NR1, heli.NR2, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
loadComplaints := func() ([]complaint.Complaint, error) {
|
|
if s.complaints == nil {
|
|
return nil, nil
|
|
}
|
|
rows, _, lErr := s.complaints.ListByHelicopter(ctx, row.HelicopterID, 100, 0)
|
|
return rows, lErr
|
|
}
|
|
grounded, reason, source, err := decideAutoAOG(evals, now, loadComplaints, func(c complaint.Complaint) string {
|
|
return s.complaintAOGReason(ctx, c)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.applyAutoAOG(ctx, heli, grounded, reason, source)
|
|
}
|
|
|
|
func decideAutoAOG(evals []NextDueEvaluation, now time.Time, loadComplaints func() ([]complaint.Complaint, error), complaintReason func(complaint.Complaint) string) (grounded bool, reason string, source string, err error) {
|
|
for i := range evals {
|
|
if !evals[i].AOG {
|
|
continue
|
|
}
|
|
cleanReason := strings.TrimSpace(evals[i].Reason)
|
|
if cleanReason == "" {
|
|
cleanReason = fmt.Sprintf("%s next due reached", evals[i].InspectionType)
|
|
}
|
|
return true, autoAOGReasonPrefixNextDue + " " + cleanReason, helicopter.AOGSourceAuto, nil
|
|
}
|
|
|
|
complaints, err := loadComplaints()
|
|
if err != nil {
|
|
return false, "", "", err
|
|
}
|
|
for i := range complaints {
|
|
if complaints[i].IsGrounding(now) {
|
|
return true, complaintReason(complaints[i]), helicopter.AOGSourceComplaint, nil
|
|
}
|
|
}
|
|
return false, "", "", nil
|
|
}
|
|
|
|
func (s *FleetStatusService) applyAutoAOG(ctx context.Context, heli *helicopter.Helicopter, grounded bool, reason, source string) error {
|
|
if grounded {
|
|
wasAOG := heli.AirOnGround
|
|
if !heli.AirOnGround || strings.TrimSpace(heli.AOGReason) != reason || strings.TrimSpace(heli.AOGSource) != source {
|
|
heli.AirOnGround = true
|
|
heli.AOGReason = reason
|
|
heli.AOGSource = source
|
|
if err := s.helicopters.Update(ctx, heli); err != nil {
|
|
return err
|
|
}
|
|
if !wasAOG {
|
|
s.recordAutoAOG(ctx, heli, true, reason)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if isAutoAOG(heli) {
|
|
heli.AirOnGround = false
|
|
heli.AOGReason = ""
|
|
heli.AOGSource = helicopter.AOGSourceUnknown
|
|
if err := s.helicopters.Update(ctx, heli); err != nil {
|
|
return err
|
|
}
|
|
s.recordAutoAOG(ctx, heli, false, "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasEligibleSchedule(schedules []fleetstatus.MaintenanceSchedule) bool {
|
|
for i := range schedules {
|
|
if strings.TrimSpace(schedules[i].Due) != "" || strings.TrimSpace(schedules[i].Ext) != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *FleetStatusService) OverdueInspections(ctx context.Context, helicopterID []byte) ([]fleetstatus.OverdueInspection, error) {
|
|
if s == nil || s.helicopters == nil || len(helicopterID) != 16 {
|
|
return nil, nil
|
|
}
|
|
latest, err := s.repo.LatestByHelicopterIDs(ctx, [][]byte{helicopterID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
row := latest[string(helicopterID)]
|
|
if row == nil || !hasEligibleSchedule(row.MaintenanceSchedules) {
|
|
return nil, nil
|
|
}
|
|
heli, err := s.helicopters.GetByID(ctx, helicopterID)
|
|
if err != nil || heli == nil {
|
|
return nil, err
|
|
}
|
|
summary, err := s.GetSummaryByHelicopterID(ctx, helicopterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hours := ComponentHours{
|
|
Airframe: derefFloat(summary.Airframe.Hours),
|
|
Engine1: derefFloat(summary.Engine1.Hours),
|
|
Engine2: derefFloat(summary.Engine2.Hours),
|
|
}
|
|
evals, err := EvaluateNextDueSchedules(row.MaintenanceSchedules, hours, summary.Airframe.Hours != nil, heli.NR1, heli.NR2, time.Now().UTC())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]fleetstatus.OverdueInspection, 0, len(evals))
|
|
for i := range evals {
|
|
if !evals[i].AOG {
|
|
continue
|
|
}
|
|
detail := strings.TrimSpace(evals[i].Reason)
|
|
if detail == "" {
|
|
detail = evals[i].InspectionType + " next due reached"
|
|
}
|
|
out = append(out, fleetstatus.OverdueInspection{InspectionType: evals[i].InspectionType, Detail: detail})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func isAutoAOG(heli *helicopter.Helicopter) bool {
|
|
if heli == nil || !heli.AirOnGround {
|
|
return false
|
|
}
|
|
src := strings.TrimSpace(heli.AOGSource)
|
|
if src == helicopter.AOGSourceAuto || src == helicopter.AOGSourceComplaint {
|
|
return true
|
|
}
|
|
reason := strings.TrimSpace(heli.AOGReason)
|
|
return strings.HasPrefix(reason, autoAOGReasonPrefixNextDue) || strings.HasPrefix(reason, autoAOGReasonPrefixComplaint)
|
|
}
|
|
|
|
func (s *FleetStatusService) complaintAOGReason(ctx context.Context, c complaint.Complaint) string {
|
|
if c.MELSeverity > complaint.MELSeverityNonMEL {
|
|
return autoAOGReasonPrefixComplaint + " MEL " + shareddto.MELLabel(c.MELSeverity) + " grace period expired"
|
|
}
|
|
if s.complaints != nil {
|
|
if names, err := s.complaints.ResolveUserNames(ctx, []complaint.Complaint{c}); err == nil {
|
|
if name := names[string(c.ReportedBy)]; name != "" {
|
|
return autoAOGReasonPrefixComplaint + " NON-MEL complaint reported by " + name
|
|
}
|
|
}
|
|
}
|
|
return autoAOGReasonPrefixComplaint + " NON-MEL complaint reported"
|
|
}
|
|
|
|
func derefFloat(v *float64) float64 {
|
|
if v == nil {
|
|
return 0
|
|
}
|
|
return *v
|
|
}
|
|
|
|
func (s *FleetStatusService) resolveFileLinks(
|
|
ctx context.Context,
|
|
fleetStatusID []byte,
|
|
files []fleetstatus.FleetStatusFile,
|
|
reuse map[string][]byte,
|
|
actor []byte,
|
|
) ([]fleetstatus.FleetStatusFile, [][]byte, error) {
|
|
refID, _ := uuidv7.BytesToString(fleetStatusID)
|
|
out := make([]fleetstatus.FleetStatusFile, 0, len(files))
|
|
createdIDs := make([][]byte, 0, len(files))
|
|
for i := range files {
|
|
f := files[i]
|
|
f.FleetStatusID = fleetStatusID
|
|
if len(f.FileID) != 16 {
|
|
if len(f.FileAttachmentID) == 16 {
|
|
out = append(out, f)
|
|
}
|
|
continue
|
|
}
|
|
key, _ := uuidv7.BytesToString(f.FileID)
|
|
if attID, ok := reuse[key]; ok {
|
|
f.FileAttachmentID = attID
|
|
f.FileID = nil
|
|
out = append(out, f)
|
|
continue
|
|
}
|
|
cat := f.Category
|
|
att, err := s.attachments.CreateAttachment(ctx, filemanager.CreateAttachmentParams{
|
|
FileID: f.FileID,
|
|
RefType: FleetStatusFileRefType,
|
|
RefID: refID,
|
|
Category: &cat,
|
|
ActorID: actor,
|
|
})
|
|
if err != nil {
|
|
s.rollbackAttachments(ctx, createdIDs)
|
|
return nil, nil, err
|
|
}
|
|
createdIDs = append(createdIDs, att.ID)
|
|
f.FileAttachmentID = att.ID
|
|
f.FileID = nil
|
|
out = append(out, f)
|
|
}
|
|
return out, createdIDs, nil
|
|
}
|
|
|
|
func (s *FleetStatusService) rollbackAttachments(ctx context.Context, ids [][]byte) {
|
|
for _, id := range ids {
|
|
_ = s.attachments.DeleteAttachment(ctx, id)
|
|
}
|
|
}
|
|
|
|
func (s *FleetStatusService) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
|
|
return s.repo.Delete(ctx, id, deletedBy)
|
|
}
|
|
|
|
func (s *FleetStatusService) GetByID(ctx context.Context, id []byte) (*fleetstatus.FleetStatus, error) {
|
|
return s.repo.GetByID(ctx, id)
|
|
}
|
|
|
|
func (s *FleetStatusService) List(ctx context.Context, filter, sort string, limit, offset int) ([]fleetstatus.FleetStatus, int64, error) {
|
|
return s.repo.List(ctx, filter, normalizeSortByRules(sort,
|
|
sortField("created_at"),
|
|
sortField("updated_at"),
|
|
), limit, offset)
|
|
}
|
|
|
|
func (s *FleetStatusService) LatestByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) (map[string]*fleetstatus.FleetStatus, error) {
|
|
return s.repo.LatestByHelicopterIDs(ctx, helicopterIDs)
|
|
}
|
|
|
|
func (s *FleetStatusService) RecalculateAOGByHelicopter(ctx context.Context, helicopterID []byte) error {
|
|
if s == nil || len(helicopterID) != 16 {
|
|
return nil
|
|
}
|
|
latest, err := s.repo.LatestByHelicopterIDs(ctx, [][]byte{helicopterID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row := latest[string(helicopterID)]
|
|
if row == nil {
|
|
row = &fleetstatus.FleetStatus{HelicopterID: append([]byte(nil), helicopterID...)}
|
|
}
|
|
return s.syncAutomaticAOG(ctx, row)
|
|
}
|
|
|
|
func (s *FleetStatusService) RecalculateAllAOG(ctx context.Context) (int, error) {
|
|
if s == nil {
|
|
return 0, nil
|
|
}
|
|
ids, err := s.repo.HelicopterIDsWithFleetStatus(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
n := 0
|
|
failed := 0
|
|
for _, id := range ids {
|
|
if err := s.RecalculateAOGByHelicopter(ctx, id); err != nil {
|
|
failed++
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
slog.WarnContext(ctx, "fleet status AOG recompute failed for helicopter", slog.String("helicopter_id", idStr), slog.Any("error", err))
|
|
continue
|
|
}
|
|
n++
|
|
}
|
|
if failed > 0 {
|
|
slog.WarnContext(ctx, "fleet status AOG recompute completed with failures", slog.Int("succeeded", n), slog.Int("failed", failed))
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (s *FleetStatusService) RecalculateAOGByMission(ctx context.Context, missionID []byte) error {
|
|
if s == nil || len(missionID) == 0 {
|
|
return nil
|
|
}
|
|
helicopterID, err := s.repo.HelicopterIDByMissionID(ctx, missionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.RecalculateAOGByHelicopter(ctx, helicopterID)
|
|
}
|
|
|
|
func (s *FleetStatusService) MarkServiced(ctx context.Context, id []byte, servicedAt time.Time, actorID []byte) error {
|
|
if servicedAt.IsZero() {
|
|
servicedAt = time.Now().UTC()
|
|
}
|
|
if err := s.repo.MarkServiced(ctx, id, servicedAt.UTC(), actorID); err != nil {
|
|
return err
|
|
}
|
|
row, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if row == nil {
|
|
idStr, _ := uuidv7.BytesToString(id)
|
|
slog.WarnContext(ctx, "fleet status: not found after mark-serviced, skipping AOG sync", slog.String("id", idStr))
|
|
return nil
|
|
}
|
|
return s.syncAutomaticAOG(ctx, row)
|
|
}
|
|
|
|
func (s *FleetStatusService) ListServiceHistoryByHelicopterID(ctx context.Context, helicopterID []byte, limit, offset int) ([]fleetstatus.FleetStatusServiceLog, int64, error) {
|
|
return s.repo.ListServiceHistoryByHelicopterID(ctx, helicopterID, limit, offset)
|
|
}
|
|
|
|
func (s *FleetStatusService) GetSummaryByHelicopterID(ctx context.Context, helicopterID []byte) (*shareddto.FleetStatusSummary, error) {
|
|
if s.usage == nil || len(helicopterID) != 16 {
|
|
return &shareddto.FleetStatusSummary{}, nil
|
|
}
|
|
usage, err := s.usage.GetByHelicopterID(ctx, helicopterID)
|
|
if err != nil {
|
|
return &shareddto.FleetStatusSummary{}, err
|
|
}
|
|
if usage == nil {
|
|
return &shareddto.FleetStatusSummary{}, nil
|
|
}
|
|
hasE1, hasE2 := true, true
|
|
if s.helicopters != nil {
|
|
if heli, herr := s.helicopters.GetByID(ctx, helicopterID); herr == nil && heli != nil {
|
|
hasE1, hasE2 = heli.NR1, heli.NR2
|
|
}
|
|
}
|
|
summary := buildFleetStatusSummary(usage, hasE1, hasE2)
|
|
return &summary, nil
|
|
}
|
|
|
|
func EvaluateNextDueSchedules(schedules []fleetstatus.MaintenanceSchedule, hours ComponentHours, hasUsageData, hasEngine1, hasEngine2 bool, now time.Time) ([]NextDueEvaluation, error) {
|
|
parser := nextdue.NewParser()
|
|
out := make([]NextDueEvaluation, 0, len(schedules))
|
|
for i := range schedules {
|
|
it := strings.TrimSpace(schedules[i].InspectionType)
|
|
if (it == fleetstatus.InspectionTypeENG1 && !hasEngine1) || (it == fleetstatus.InspectionTypeENG2 && !hasEngine2) {
|
|
out = append(out, NextDueEvaluation{InspectionType: it})
|
|
continue
|
|
}
|
|
current := hours.Airframe
|
|
switch it {
|
|
case fleetstatus.InspectionTypeENG1:
|
|
current = hours.Engine1
|
|
case fleetstatus.InspectionTypeENG2:
|
|
current = hours.Engine2
|
|
}
|
|
ev, err := evaluateInspectionDue(parser, it, schedules[i].Due, schedules[i].Ext, current, hasUsageData, now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, ev)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func evaluateInspectionDue(parser nextdue.Parser, inspectionType, dueRaw, extRaw string, currentHours float64, hasUsageData bool, now time.Time) (NextDueEvaluation, error) {
|
|
due, err := parser.Parse("due", dueRaw)
|
|
if err != nil {
|
|
return NextDueEvaluation{}, err
|
|
}
|
|
ext, err := parser.Parse("ext", extRaw)
|
|
if err != nil {
|
|
return NextDueEvaluation{}, err
|
|
}
|
|
ev := NextDueEvaluation{InspectionType: strings.TrimSpace(inspectionType)}
|
|
|
|
limit := due
|
|
limitIsExt := false
|
|
if !nextdue.IsUnset(ext) {
|
|
limit, limitIsExt = ext, true
|
|
}
|
|
if nextdue.IsUnset(limit) {
|
|
return ev, nil
|
|
}
|
|
|
|
switch limit.Kind {
|
|
case nextdue.ValueKindHours:
|
|
if !hasUsageData {
|
|
return ev, nil
|
|
}
|
|
if limit.Hours != nil && currentHours > *limit.Hours {
|
|
ev.AOG = true
|
|
ev.Reason = nextDueOverdueReason(inspectionType, limitIsExt, true)
|
|
}
|
|
case nextdue.ValueKindDate:
|
|
if limit.Date != nil && nextdue.NormalizeDate(now).After(nextdue.NormalizeDate(*limit.Date)) {
|
|
ev.AOG = true
|
|
ev.Reason = nextDueOverdueReason(inspectionType, limitIsExt, false)
|
|
}
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func nextDueOverdueReason(inspectionType string, isExt, isHours bool) string {
|
|
it := strings.TrimSpace(inspectionType)
|
|
switch {
|
|
case isExt && isHours:
|
|
return it + " extended hours reached"
|
|
case isExt:
|
|
return it + " extended due date reached"
|
|
case isHours:
|
|
return it + " due hours reached"
|
|
default:
|
|
return it + " due date reached"
|
|
}
|
|
}
|
|
|
|
func buildFleetStatusSummary(u *helicopterusage.HelicopterUsage, hasE1, hasE2 bool) shareddto.FleetStatusSummary {
|
|
airframeHours := u.TotalAirframeHours
|
|
airframeCycles := float64(u.TotalAirframeCycles)
|
|
landings := float64(u.TotalLanding)
|
|
flightReports := float64(u.TotalFlightReport)
|
|
rotorBrakeCycles := float64(u.TotalRotorBrakeCycle)
|
|
hookReleases := float64(u.TotalHookRelease)
|
|
summary := shareddto.FleetStatusSummary{
|
|
Airframe: shareddto.FleetStatusSummaryAirframe{
|
|
Hours: &airframeHours,
|
|
Cycles: &airframeCycles,
|
|
},
|
|
Landings: &landings,
|
|
FlightReports: &flightReports,
|
|
RotorBrakeCycles: &rotorBrakeCycles,
|
|
HookReleases: &hookReleases,
|
|
}
|
|
if hasE1 {
|
|
summary.Engine1 = buildFleetStatusUsageEngine(u.TotalEngine1Hours, u.TotalEngine1GpcNgN1, u.TotalEngine1PtcNfN2, u.TotalEngine1Ccc)
|
|
}
|
|
if hasE2 {
|
|
summary.Engine2 = buildFleetStatusUsageEngine(u.TotalEngine2Hours, u.TotalEngine2GpcNgN1, u.TotalEngine2PtcNfN2, u.TotalEngine2Ccc)
|
|
}
|
|
return summary
|
|
}
|
|
|
|
func buildFleetStatusUsageEngine(hours, gpcNgN1, ptcNfN2, ccc float64) shareddto.FleetStatusSummaryEngine {
|
|
cccVal := ccc
|
|
return shareddto.FleetStatusSummaryEngine{
|
|
Hours: &hours,
|
|
GpcNgN1: &gpcNgN1,
|
|
PtcNfN2: &ptcNfN2,
|
|
Ccc: &cccVal,
|
|
}
|
|
}
|