init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
package flightdata
import (
"strings"
sharedconst "wucher/internal/shared/const"
)
const (
StatusInProgress = "in_progress"
StatusCompleted = "completed"
)
// IsComplete reports whether the persisted flight-data status is complete.
func IsComplete(row *FlightData) bool {
if row == nil {
return false
}
return strings.EqualFold(strings.TrimSpace(row.Status), StatusCompleted)
}
func DeriveStatus(row *FlightData, details *SPODetails) string {
if !isBaseComplete(row) {
return StatusInProgress
}
switch missionType(row) {
case sharedconst.MissionTypeCAT, sharedconst.MissionTypeNCO, sharedconst.MissionTypeSPO:
return StatusCompleted
case sharedconst.MissionTypeHEMS:
return StatusCompleted
}
return StatusInProgress
}
func hasExactlyOneLocation(a, b []byte) bool {
hasA := len(a) == 16
hasB := len(b) == 16
return hasA != hasB
}
func missionType(row *FlightData) string {
if row == nil || row.Mission == nil {
return ""
}
return strings.ToUpper(strings.TrimSpace(row.Mission.Type))
}
func isBaseComplete(row *FlightData) bool {
if row == nil {
return false
}
requiresRotorBrakeCycle := true
switch missionType(row) {
case sharedconst.MissionTypeCAT, sharedconst.MissionTypeNCO, sharedconst.MissionTypeSPO:
requiresRotorBrakeCycle = false
}
return len(row.MissionID) == 16 &&
hasExactlyOneLocation(row.FromICAOID, row.FromHospitalID) &&
hasExactlyOneLocation(row.ToICAOID, row.ToHospitalID) &&
!row.FlightTakeOff.IsZero() &&
!row.FlightLanding.IsZero() &&
row.FlightDuration > 0 &&
row.LandingCount > 0 &&
(!requiresRotorBrakeCycle || row.RotorBrakeCycle > 0)
}
func isHESLOComplete(details SPOHESLODetails) bool {
if len(details.Flights) == 0 || len(details.Slings) == 0 {
return false
}
for i := range details.Flights {
if details.Flights[i].ROT <= 0 || len(details.Flights[i].FuelTruckID) != 16 || len(details.Flights[i].LoggingSlingID) != 16 {
return false
}
}
for i := range details.Slings {
if len(details.Slings[i].HESLOFlightID) != 16 || len(details.Slings[i].SlingID) != 16 {
return false
}
}
return true
}
func isLoggingComplete(details SPOLoggingDetails) bool {
if len(details.Slings) == 0 {
return false
}
for i := range details.Slings {
if len(details.Slings[i].SlingID) != 16 {
return false
}
}
return true
}
func isHECComplete(details SPOHECDetails) bool {
if len(details.Flights) == 0 || len(details.Slings) == 0 || len(details.Loads) == 0 {
return false
}
for i := range details.Flights {
if details.Flights[i].ROT <= 0 || details.Flights[i].HECCycle <= 0 || len(details.Flights[i].EquipmentID) != 16 {
return false
}
}
for i := range details.Slings {
if len(details.Slings[i].SlingID) != 16 {
return false
}
}
for i := range details.Loads {
if strings.TrimSpace(details.Loads[i].LoadCategory) == "" || details.Loads[i].Quantity <= 0 {
return false
}
}
return true
}

View File

@@ -0,0 +1,123 @@
package flightdata
import (
"testing"
"time"
missiondomain "wucher/internal/domain/mission"
)
func TestIsComplete(t *testing.T) {
if IsComplete(nil) {
t.Fatal("expected nil row to be incomplete")
}
if IsComplete(&FlightData{Status: StatusInProgress}) {
t.Fatal("expected on_progress status to be incomplete")
}
if !IsComplete(&FlightData{Status: StatusCompleted}) {
t.Fatal("expected complete status to be complete")
}
}
func TestDeriveStatus(t *testing.T) {
now := time.Now().UTC()
base := &FlightData{
MissionID: []byte("1234567890123456"),
FromICAOID: []byte("3456789012345678"),
ToHospitalID: []byte("4567890123456789"),
FlightTakeOff: now,
FlightLanding: now,
FlightDuration: time.Hour,
LandingCount: 1,
DeliveryNoteNumber: "DN-1",
CustomerName: "Customer",
}
t.Run("cat complete", func(t *testing.T) {
base.Mission = &missiondomain.Mission{Type: "CAT"}
if got := DeriveStatus(base, nil); got != StatusCompleted {
t.Fatalf("expected complete, got %q", got)
}
})
t.Run("nco complete without rotor brake cycle", func(t *testing.T) {
base.Mission = &missiondomain.Mission{Type: "NCO"}
if got := DeriveStatus(base, nil); got != StatusCompleted {
t.Fatalf("expected complete, got %q", got)
}
})
t.Run("hems requires rotor brake cycle", func(t *testing.T) {
base.Mission = &missiondomain.Mission{Type: "HEMS"}
if got := DeriveStatus(base, nil); got != StatusInProgress {
t.Fatalf("expected on_progress, got %q", got)
}
base.RotorBrakeCycle = 1
if got := DeriveStatus(base, nil); got != StatusCompleted {
t.Fatalf("expected complete after rotor brake cycle is set, got %q", got)
}
base.RotorBrakeCycle = 0
})
t.Run("spo complete without details", func(t *testing.T) {
base.Mission = &missiondomain.Mission{Type: "SPO"}
if got := DeriveStatus(base, nil); got != StatusCompleted {
t.Fatalf("expected complete, got %q", got)
}
})
t.Run("spo complete with details", func(t *testing.T) {
base.Mission = &missiondomain.Mission{Type: "SPO"}
details := &SPODetails{
HESLO: SPOHESLODetails{
Flights: []SPOHESLOFlight{{
ROT: 1,
FuelTruckID: []byte("5678901234567890"),
LoggingSlingID: []byte("6789012345678901"),
}},
Slings: []SPOHESLOSling{{
HESLOFlightID: []byte("5678901234567890"),
SlingID: []byte("7890123456789012"),
}},
},
Logging: SPOLoggingDetails{
Slings: []SPOLoggingSling{{
SlingID: []byte("8901234567890123"),
}},
},
HEC: SPOHECDetails{
Flights: []SPOHECFlight{{
ROT: 1,
HECCycle: 1,
EquipmentID: []byte("9012345678901234"),
}},
Slings: []SPOHECSling{{
SlingID: []byte("0123456789012345"),
}},
Loads: []SPOHECLoad{{
LoadCategory: "LT200",
Quantity: 1,
}},
},
}
if got := DeriveStatus(base, details); got != StatusCompleted {
t.Fatalf("expected complete, got %q", got)
}
})
t.Run("spo missing section still complete", func(t *testing.T) {
base.Mission = &missiondomain.Mission{Type: "SPO"}
details := &SPODetails{
HESLO: SPOHESLODetails{
Flights: []SPOHESLOFlight{{
ROT: 1,
FuelTruckID: []byte("5678901234567890"),
LoggingSlingID: []byte("6789012345678901"),
}},
},
}
if got := DeriveStatus(base, details); got != StatusCompleted {
t.Fatalf("expected complete, got %q", got)
}
})
}

View File

@@ -0,0 +1,32 @@
package flightdata
import "errors"
var (
ErrRequired = errors.New("flight_data required")
ErrMissionReference = errors.New("flight_data mission reference invalid")
ErrCoPilotReference = errors.New("flight_data co_pilot reference invalid")
ErrOriginLocationReference = errors.New("flight_data origin location reference invalid")
ErrDestinationLocationReference = errors.New("flight_data destination location reference invalid")
ErrFacilityReference = errors.New("flight_data facility reference invalid")
ErrCreateBlocked = errors.New("flight_data create blocked")
ErrLocked = errors.New("flight_data locked")
ErrLockedAfterFlightInspection = errors.New("flight_data locked by after_flight_inspection")
)
const (
ErrorMessageRequired = "flight data is required"
ErrorMessageMissionReference = "mission must reference an existing mission"
ErrorMessageCoPilotReference = "co_pilot must reference an existing user"
ErrorMessageOriginLocationReference = "origin must reference exactly one location type"
ErrorMessageDestinationLocationReference = "destination must reference exactly one location type"
ErrorMessageFacilityReference = "facility reference is invalid"
ErrorMessageFieldRequired = "field is required"
ErrorMessageInvalidUUID = "invalid UUID"
ErrorMessageInvalidRFC3339 = "invalid RFC3339 datetime"
ErrorMessageInvalidPathUUID = "uuid is invalid UUID"
ErrorMessageNotFound = "flight data not found"
ErrorMessageCreateBlocked = "flight data cannot be created because fm report already exists for this flight"
ErrorMessageLocked = "flight data is locked because fm report is completed"
ErrorMessageLockedAfterFlightInspection = "flight data is locked because after flight inspection already exists"
)

View File

@@ -0,0 +1,281 @@
package flightdata
import (
"time"
"gorm.io/gorm"
facilitydomain "wucher/internal/domain/facility"
filemanager "wucher/internal/domain/file_manager"
hospitaldomain "wucher/internal/domain/hospital"
icaodomain "wucher/internal/domain/icao"
missiondomain "wucher/internal/domain/mission"
userdomain "wucher/internal/domain/user"
"wucher/internal/shared/pkg/uuidv7"
)
type FlightData struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
MissionID []byte `gorm:"type:binary(16);index;not null;column:mission_id"`
CoPilotID []byte `gorm:"type:binary(16);index;column:co_pilot_id"`
FromICAOID []byte `gorm:"type:binary(16);index;column:from_icao_id"`
FromHospitalID []byte `gorm:"type:binary(16);index;column:from_hospital_id"`
ToICAOID []byte `gorm:"type:binary(16);index;column:to_icao_id"`
ToHospitalID []byte `gorm:"type:binary(16);index;column:to_hospital_id"`
FlightTakeOff time.Time `gorm:"column:take_off"`
FlightLanding time.Time `gorm:"column:landing"`
FlightDuration time.Duration `gorm:"column:duration"`
FlightType string `gorm:"type:varchar(255);column:flight_type"`
FlightRED time.Duration `gorm:"column:red"`
MaxN1 float64 `gorm:"column:max_n1"`
MaxN2 float64 `gorm:"column:max_n2"`
PaxCount int `gorm:"column:pax_count"`
TicketNo string `gorm:"type:varchar(255);column:ticket_no"`
Engine string `gorm:"type:varchar(255);column:engine"`
LandingCount int `gorm:"column:landing_count"`
RotorBrakeCycle int `gorm:"column:rotor_brake_cycle"`
HookReleases int `gorm:"column:hook_releases"`
DeliveryNoteNumber string `gorm:"type:varchar(255);column:delivery_note_number"`
CustomerName string `gorm:"type:varchar(255);column:customer_name"`
FlightPlanDistance float64 `gorm:"column:flight_plan_distance"`
FlightPlanTime time.Duration `gorm:"column:flight_plan_time"`
FlightPlanTrueCourse float64 `gorm:"column:flight_plan_true_course"`
FuelBeforeFlight float64 `gorm:"column:fuel_before_flight"`
FuelUpload float64 `gorm:"column:fuel_upload"`
FuelAfterFlight float64 `gorm:"column:fuel_after_flight"`
FuelPlanning float64 `gorm:"column:fuel_planning"`
FlightPositioning bool `gorm:"type:boolean;not null;default:false;column:flight_position"`
OtherInformation string `gorm:"type:text;column:other_information"`
Status string `gorm:"type:varchar(20);not null;default:on_progress;column:status"`
Mission *missiondomain.Mission `gorm:"foreignKey:MissionID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
TypeOfFlight *TypeOfFlight `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
CoPilot *userdomain.User `gorm:"foreignKey:CoPilotID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
FromICAO *icaodomain.ICAO `gorm:"foreignKey:FromICAOID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
FromHospital *hospitaldomain.Hospital `gorm:"foreignKey:FromHospitalID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ToICAO *icaodomain.ICAO `gorm:"foreignKey:ToICAOID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ToHospital *hospitaldomain.Hospital `gorm:"foreignKey:ToHospitalID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
Attachments []filemanager.Attachment `gorm:"-"`
CreatedAt time.Time `gorm:"column:created_at"`
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
UpdatedAt time.Time `gorm:"column:updated_at"`
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
DeletedAt *time.Time `gorm:"index;column:deleted_at"`
DeletedBy []byte `gorm:"type:binary(16);column:deleted_by"`
}
func (FlightData) TableName() string { return "flight_data" }
func (f *FlightData) BeforeCreate(_ *gorm.DB) error {
if len(f.ID) == 0 {
f.ID = uuidv7.MustBytes()
}
return nil
}
type TypeOfFlight struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FlightDataID []byte `gorm:"type:binary(16);index;not null;column:flight_data_id"`
Category string `gorm:"type:enum('ZNO', 'ANO', 'ZTZ', 'ZSP');not null;column:category"`
Quantity int `gorm:"not null;column:quantity"`
DurationTime time.Duration `gorm:"not null;column:duration"`
FlightData *FlightData `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
type HESLOFlight struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FlightDataID []byte `gorm:"type:binary(16);index;not null;column:flight_data_id"`
ROT int `gorm:"not null;column:rot"`
FacilityFuelTruckID []byte `gorm:"type:binary(16);column:fuel_truck_id"`
LoggingSlingID []byte `gorm:"type:binary(16);column:logging_sling_id"`
CreatedAt time.Time `gorm:"column:created_at"`
FlightData *FlightData `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
FacilityFuelTruck *facilitydomain.Facility `gorm:"foreignKey:FacilityFuelTruckID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
LoggingSling *facilitydomain.Facility `gorm:"foreignKey:LoggingSlingID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
}
func (HESLOFlight) TableName() string { return "heslo_flights" }
type HESLOSling struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FacilitySlingID []byte `gorm:"type:binary(16);column:facility_sling_id"`
HESLOFlightID []byte `gorm:"type:binary(16);index;not null;column:heslo_flight_id"`
CreatedAt time.Time `gorm:"column:created_at"`
HESLOFlight *HESLOFlight `gorm:"foreignKey:HESLOFlightID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
FacilitySling *facilitydomain.Facility `gorm:"foreignKey:FacilitySlingID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
}
func (HESLOSling) TableName() string { return "heslo_slings" }
type LoggingSling struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FlightDataID []byte `gorm:"type:binary(16);index;not null;column:flight_data_id"`
FacilitySlingID []byte `gorm:"type:binary(16);index;column:facility_sling_id"`
CreatedAt time.Time `gorm:"column:created_at"`
FacilitySling *facilitydomain.Facility `gorm:"foreignKey:FacilitySlingID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
FlightData *FlightData `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
func (LoggingSling) TableName() string { return "logging_slings" }
type HECFlight struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FlightDataID []byte `gorm:"type:binary(16);index;not null;column:flight_data_id"`
ROT int `gorm:"not null;column:rot"`
HECCycle int `gorm:"not null;column:hec_cycle"`
EquipmentID []byte `gorm:"type:binary(16);column:equipment_id"`
CreatedAt time.Time `gorm:"column:created_at"`
FlightData *FlightData `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Equipment *facilitydomain.Facility `gorm:"foreignKey:EquipmentID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
}
func (HECFlight) TableName() string { return "hec_flights" }
type HECSling struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FlightDataID []byte `gorm:"type:binary(16);index;not null;column:flight_data_id"`
HECSlingID []byte `gorm:"type:binary(16);column:hec_sling_id"`
CreatedAt time.Time `gorm:"column:created_at"`
FlightData *FlightData `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
HECSling *facilitydomain.Facility `gorm:"foreignKey:HECSlingID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
}
func (HECSling) TableName() string { return "hec_slings" }
type HECLoads struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
FlightDataID []byte `gorm:"type:binary(16);index;not null;column:flight_data_id"`
LoadCategory string `gorm:"type:varchar(20);not null;column:load_category"`
Quantity int `gorm:"not null;column:quantity"`
CreatedAt time.Time `gorm:"column:created_at"`
FlightData *FlightData `gorm:"foreignKey:FlightDataID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
func (HECLoads) TableName() string { return "hec_loads" }
type SPODetails struct {
HESLO SPOHESLODetails
Logging SPOLoggingDetails
HEC SPOHECDetails
}
type SPOHESLODetails struct {
Flights []SPOHESLOFlight
Slings []SPOHESLOSling
}
type SPOHESLOFlight struct {
ID []byte
ROT int
FuelTruckID []byte
FuelTruckName string
LoggingSlingID []byte
LoggingSlingName string
}
type SPOHESLOSling struct {
ID []byte
HESLOFlightID []byte
SlingID []byte
SlingName string
}
type SPOLoggingDetails struct {
Slings []SPOLoggingSling
}
type SPOLoggingSling struct {
ID []byte
SlingID []byte
SlingName string
}
type SPOHECDetails struct {
Flights []SPOHECFlight
Slings []SPOHECSling
Loads []SPOHECLoad
}
type SPOHECFlight struct {
ID []byte
ROT int
HECCycle int
EquipmentID []byte
EquipmentName string
}
type SPOHECSling struct {
ID []byte
SlingID []byte
SlingName string
}
type SPOHECLoad struct {
ID []byte
LoadCategory string
Quantity int
}
// SPO upsert input types (write path)
type SPOUpsertData struct {
HESLO *SPOHESLOUpsert
Logging *SPOLoggingUpsert
HEC *SPOHECUpsert
}
type SPOHESLOUpsert struct {
Flights []SPOHESLOFlightUpsert
}
type SPOHESLOFlightUpsert struct {
ROT int
FuelTruckID []byte
LoggingSlingID []byte
Slings []SPOHESLOSlingUpsert
}
type SPOHESLOSlingUpsert struct {
SlingID []byte
}
type SPOLoggingUpsert struct {
Slings []SPOLoggingSlingUpsert
}
type SPOLoggingSlingUpsert struct {
SlingID []byte
}
type SPOHECUpsert struct {
Flights []SPOHECFlightUpsert
Slings []SPOHECSlingUpsert
Loads []SPOHECLoadUpsert
}
type SPOHECFlightUpsert struct {
ROT int
HECCycle int
EquipmentID []byte
}
type SPOHECSlingUpsert struct {
SlingID []byte
}
type SPOHECLoadUpsert struct {
LoadCategory string
Quantity int
}

View File

@@ -0,0 +1,18 @@
package flightdata
import (
"context"
)
type Repository interface {
Create(ctx context.Context, row *FlightData) error
Update(ctx context.Context, row *FlightData) error
Delete(ctx context.Context, id []byte, deletedBy []byte) error
GetByID(ctx context.Context, id []byte) (*FlightData, error)
GetByFlightID(ctx context.Context, flightID []byte) (*FlightData, error)
GetByMissionID(ctx context.Context, missionID []byte) (*FlightData, error)
ListByMissionID(ctx context.Context, missionID []byte) ([]FlightData, error)
List(ctx context.Context, date string, missionID, flightDataID []byte, limit, offset int) ([]FlightData, int64, error)
GetSPODetailsByFlightDataID(ctx context.Context, flightDataID []byte) (*SPODetails, error)
UpsertSPODetails(ctx context.Context, flightDataID []byte, data *SPOUpsertData) error
}

View File

@@ -0,0 +1,19 @@
package flightdata
import (
"context"
)
type Service interface {
Create(ctx context.Context, row *FlightData) error
CreatePlaceholder(ctx context.Context, row *FlightData) error
Update(ctx context.Context, row *FlightData) error
Delete(ctx context.Context, id []byte, deletedBy []byte) error
GetByID(ctx context.Context, id []byte) (*FlightData, error)
GetByFlightID(ctx context.Context, flightID []byte) (*FlightData, error)
GetByMissionID(ctx context.Context, missionID []byte) (*FlightData, error)
ListByMissionID(ctx context.Context, missionID []byte) ([]FlightData, error)
List(ctx context.Context, date string, missionID, flightDataID []byte, limit, offset int) ([]FlightData, int64, error)
GetSPODetailsByFlightDataID(ctx context.Context, flightDataID []byte) (*SPODetails, error)
UpsertSPODetails(ctx context.Context, flightDataID []byte, data *SPOUpsertData) error
}