init push
This commit is contained in:
664
internal/service/fleet_status_service.go
Normal file
664
internal/service/fleet_status_service.go
Normal file
@@ -0,0 +1,664 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user