package mysql import ( "context" "errors" "log" "time" "gorm.io/gorm" flightdata "wucher/internal/domain/flight_data" "wucher/internal/shared/pkg/txctx" "wucher/internal/shared/pkg/uuidv7" ) type FlightDataRepository struct { db *gorm.DB hasHesloFlightsTable bool hasHesloSlingsTable bool hasLoggingSlingsTable bool hasHecFlightsTable bool hasHecSlingsTable bool hasHecLoadsTable bool } func NewFlightDataRepository(db *gorm.DB) *FlightDataRepository { schema := newSchemaCache(db) return &FlightDataRepository{ db: db, hasHesloFlightsTable: schema.HasTable("heslo_flights"), hasHesloSlingsTable: schema.HasTable("heslo_slings"), hasLoggingSlingsTable: schema.HasTable("logging_slings"), hasHecFlightsTable: schema.HasTable("hec_flights"), hasHecSlingsTable: schema.HasTable("hec_slings"), hasHecLoadsTable: schema.HasTable("hec_loads"), } } func (r *FlightDataRepository) Create(ctx context.Context, row *flightdata.FlightData) error { if row == nil { return nil } now := time.Now().UTC() if len(row.ID) != 16 { row.ID = uuidv7.MustBytes() } if row.CreatedAt.IsZero() { row.CreatedAt = now } if row.UpdatedAt.IsZero() { row.UpdatedAt = row.CreatedAt } return txctx.DB(ctx, r.db).Table("flight_data").Create(flightDataPersistenceValues(row)).Error } func (r *FlightDataRepository) Update(ctx context.Context, row *flightdata.FlightData) error { if row == nil { return nil } row.UpdatedAt = time.Now().UTC() updates := flightDataPersistenceValues(row) delete(updates, "id") delete(updates, "created_at") delete(updates, "created_by") return r.db.WithContext(ctx). Model(&flightdata.FlightData{}). Where("id = ?", row.ID). Updates(updates).Error } func (r *FlightDataRepository) Delete(ctx context.Context, id []byte, deletedBy []byte) error { now := time.Now().UTC() updates := map[string]any{ "deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy, } return r.db.WithContext(ctx). Model(&flightdata.FlightData{}). Where("id = ? AND deleted_at IS NULL", id). Updates(updates).Error } func (r *FlightDataRepository) GetByID(ctx context.Context, id []byte) (*flightdata.FlightData, error) { var row flightdata.FlightData err := r.db.WithContext(ctx). Preload("Mission"). Preload("Mission.Flight"). Preload("Mission.Flight.Takeover"). Preload("Mission.Flight.Takeover.RosterCrews"). Preload("Mission.Flight.Takeover.OtherPeople"). Preload("CoPilot"). Preload("FromICAO"). Preload("FromHospital"). Preload("ToICAO"). Preload("ToHospital"). Where("id = ? AND deleted_at IS NULL", id). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightDataRepository) GetByMissionID(ctx context.Context, missionID []byte) (*flightdata.FlightData, error) { var row flightdata.FlightData err := r.db.WithContext(ctx). Preload("Mission"). Preload("Mission.Flight"). Preload("Mission.Flight.Takeover"). Preload("Mission.Flight.Takeover.RosterCrews"). Preload("Mission.Flight.Takeover.OtherPeople"). Preload("CoPilot"). Preload("FromICAO"). Preload("FromHospital"). Preload("ToICAO"). Preload("ToHospital"). Where("mission_id = ? AND deleted_at IS NULL", missionID). Order("created_at DESC"). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightDataRepository) ListByMissionID(ctx context.Context, missionID []byte) ([]flightdata.FlightData, error) { rows := make([]flightdata.FlightData, 0) err := r.db.WithContext(ctx). Preload("Mission"). Preload("Mission.Flight"). Preload("Mission.Flight.Takeover"). Preload("Mission.Flight.Takeover.RosterCrews"). Preload("Mission.Flight.Takeover.OtherPeople"). Preload("CoPilot"). Preload("FromICAO"). Preload("FromHospital"). Preload("ToICAO"). Preload("ToHospital"). Where("mission_id = ? AND deleted_at IS NULL", missionID). Order("created_at ASC, id ASC"). Find(&rows).Error return rows, err } func (r *FlightDataRepository) GetByFlightID(ctx context.Context, flightID []byte) (*flightdata.FlightData, error) { var row flightdata.FlightData err := r.db.WithContext(ctx). Preload("Mission"). Preload("CoPilot"). Preload("FromICAO"). Preload("FromHospital"). Preload("ToICAO"). Preload("ToHospital"). Where("mission_id IN (SELECT id FROM missions WHERE flight_id = ? AND deleted_at IS NULL) AND deleted_at IS NULL", flightID). Order("created_at DESC"). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return &row, err } func (r *FlightDataRepository) List(ctx context.Context, date string, missionID, flightDataID []byte, limit, offset int) ([]flightdata.FlightData, int64, error) { rows := make([]flightdata.FlightData, 0) var total int64 base := r.db.WithContext(ctx). Model(&flightdata.FlightData{}). Preload("Mission"). Preload("Mission.Flight"). Preload("Mission.Flight.Takeover"). Preload("Mission.Flight.Takeover.RosterCrews"). Preload("Mission.Flight.Takeover.OtherPeople"). Preload("CoPilot"). Preload("FromICAO"). Preload("FromHospital"). Preload("ToICAO"). Preload("ToHospital"). Where("deleted_at IS NULL") if date != "" { base = base.Where("DATE(take_off) = ?", date) } if len(missionID) == 16 { base = base.Where("mission_id = ?", missionID) } if len(flightDataID) == 16 { base = base.Where("id = ?", flightDataID) } if err := base.Count(&total).Error; err != nil { return nil, 0, err } query := base.Order("take_off DESC, created_at DESC") if limit > 0 { query = query.Limit(limit).Offset(offset) } if err := query.Find(&rows).Error; err != nil { return nil, 0, err } return rows, total, nil } func (r *FlightDataRepository) GetSPODetailsByFlightDataID(ctx context.Context, flightDataID []byte) (*flightdata.SPODetails, error) { details := &flightdata.SPODetails{ HESLO: flightdata.SPOHESLODetails{ Flights: make([]flightdata.SPOHESLOFlight, 0), Slings: make([]flightdata.SPOHESLOSling, 0), }, Logging: flightdata.SPOLoggingDetails{ Slings: make([]flightdata.SPOLoggingSling, 0), }, HEC: flightdata.SPOHECDetails{ Flights: make([]flightdata.SPOHECFlight, 0), Slings: make([]flightdata.SPOHECSling, 0), Loads: make([]flightdata.SPOHECLoad, 0), }, } if len(flightDataID) != 16 { return details, nil } db := r.db.WithContext(ctx) if r.hasHesloFlightsTable { type hesloFlightRow struct { ID []byte `gorm:"column:id"` ROT int `gorm:"column:rot"` FuelTruckID []byte `gorm:"column:fuel_truck_id"` FuelTruckName string `gorm:"column:fuel_truck_name"` LoggingSlingID []byte `gorm:"column:logging_sling_id"` LoggingSlingName string `gorm:"column:logging_sling_name"` } rows := make([]hesloFlightRow, 0) if err := db.Table("heslo_flights hf"). Select("hf.id, hf.rot, hf.fuel_truck_id, fuel.name AS fuel_truck_name, hf.logging_sling_id, log.name AS logging_sling_name"). Joins("LEFT JOIN facilities fuel ON fuel.id = hf.fuel_truck_id"). Joins("LEFT JOIN facilities log ON log.id = hf.logging_sling_id"). Where("hf.flight_data_id = ?", flightDataID). Order("hf.created_at ASC"). Scan(&rows).Error; err != nil { return nil, err } for i := range rows { details.HESLO.Flights = append(details.HESLO.Flights, flightdata.SPOHESLOFlight{ ID: rows[i].ID, ROT: rows[i].ROT, FuelTruckID: rows[i].FuelTruckID, FuelTruckName: rows[i].FuelTruckName, LoggingSlingID: rows[i].LoggingSlingID, LoggingSlingName: rows[i].LoggingSlingName, }) } } if r.hasHesloSlingsTable && r.hasHesloFlightsTable { type hesloSlingRow struct { ID []byte `gorm:"column:id"` HESLOFlightID []byte `gorm:"column:heslo_flight_id"` SlingID []byte `gorm:"column:facility_sling_id"` SlingName string `gorm:"column:sling_name"` } rows := make([]hesloSlingRow, 0) if err := db.Table("heslo_slings hs"). Select("hs.id, hs.heslo_flight_id, hs.facility_sling_id, sling.name AS sling_name"). Joins("JOIN heslo_flights hf ON hf.id = hs.heslo_flight_id"). Joins("LEFT JOIN facilities sling ON sling.id = hs.facility_sling_id"). Where("hf.flight_data_id = ?", flightDataID). Order("hs.created_at ASC"). Scan(&rows).Error; err != nil { return nil, err } for i := range rows { details.HESLO.Slings = append(details.HESLO.Slings, flightdata.SPOHESLOSling{ ID: rows[i].ID, HESLOFlightID: rows[i].HESLOFlightID, SlingID: rows[i].SlingID, SlingName: rows[i].SlingName, }) } } if r.hasLoggingSlingsTable { type loggingSlingRow struct { ID []byte `gorm:"column:id"` SlingID []byte `gorm:"column:facility_sling_id"` SlingName string `gorm:"column:sling_name"` } rows := make([]loggingSlingRow, 0) if err := db.Table("logging_slings ls"). Select("ls.id, ls.facility_sling_id, sling.name AS sling_name"). Joins("LEFT JOIN facilities sling ON sling.id = ls.facility_sling_id"). Where("ls.flight_data_id = ?", flightDataID). Order("ls.created_at ASC"). Scan(&rows).Error; err != nil { return nil, err } for i := range rows { details.Logging.Slings = append(details.Logging.Slings, flightdata.SPOLoggingSling{ ID: rows[i].ID, SlingID: rows[i].SlingID, SlingName: rows[i].SlingName, }) } } if r.hasHecFlightsTable { type hecFlightRow struct { ID []byte `gorm:"column:id"` ROT int `gorm:"column:rot"` HECCycle int `gorm:"column:hec_cycle"` EquipmentID []byte `gorm:"column:equipment_id"` EquipmentName string `gorm:"column:equipment_name"` } rows := make([]hecFlightRow, 0) if err := db.Table("hec_flights hf"). Select("hf.id, hf.rot, hf.hec_cycle, hf.equipment_id, eq.name AS equipment_name"). Joins("LEFT JOIN facilities eq ON eq.id = hf.equipment_id"). Where("hf.flight_data_id = ?", flightDataID). Order("hf.created_at ASC"). Scan(&rows).Error; err != nil { return nil, err } for i := range rows { details.HEC.Flights = append(details.HEC.Flights, flightdata.SPOHECFlight{ ID: rows[i].ID, ROT: rows[i].ROT, HECCycle: rows[i].HECCycle, EquipmentID: rows[i].EquipmentID, EquipmentName: rows[i].EquipmentName, }) } } if r.hasHecSlingsTable { type hecSlingRow struct { ID []byte `gorm:"column:id"` SlingID []byte `gorm:"column:hec_sling_id"` SlingName string `gorm:"column:sling_name"` } rows := make([]hecSlingRow, 0) if err := db.Table("hec_slings hs"). Select("hs.id, hs.hec_sling_id, sling.name AS sling_name"). Joins("LEFT JOIN facilities sling ON sling.id = hs.hec_sling_id"). Where("hs.flight_data_id = ?", flightDataID). Order("hs.created_at ASC"). Scan(&rows).Error; err != nil { return nil, err } for i := range rows { details.HEC.Slings = append(details.HEC.Slings, flightdata.SPOHECSling{ ID: rows[i].ID, SlingID: rows[i].SlingID, SlingName: rows[i].SlingName, }) } } if r.hasHecLoadsTable { type hecLoadRow struct { ID []byte `gorm:"column:id"` LoadCategory string `gorm:"column:load_category"` Quantity int `gorm:"column:quantity"` } rows := make([]hecLoadRow, 0) if err := db.Table("hec_loads"). Select("id, load_category, quantity"). Where("flight_data_id = ?", flightDataID). Order("created_at ASC"). Scan(&rows).Error; err != nil { return nil, err } for i := range rows { details.HEC.Loads = append(details.HEC.Loads, flightdata.SPOHECLoad{ ID: rows[i].ID, LoadCategory: rows[i].LoadCategory, Quantity: rows[i].Quantity, }) } } return details, nil } func (r *FlightDataRepository) UpsertSPODetails(ctx context.Context, flightDataID []byte, data *flightdata.SPOUpsertData) error { if len(flightDataID) != 16 || data == nil { return nil } log.Printf("[UpsertSPODetails] start: heslo=%v logging=%v hec=%v hasHesloFlights=%v hasLogging=%v hasHecFlights=%v", data.HESLO != nil, data.Logging != nil, data.HEC != nil, r.hasHesloFlightsTable, r.hasLoggingSlingsTable, r.hasHecFlightsTable) txErr := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now().UTC() if data.HESLO != nil && r.hasHesloFlightsTable { if r.hasHesloSlingsTable { if err := tx.Exec( "DELETE hs FROM heslo_slings hs JOIN heslo_flights hf ON hf.id = hs.heslo_flight_id WHERE hf.flight_data_id = ?", flightDataID, ).Error; err != nil { return err } } if err := tx.Exec("DELETE FROM heslo_flights WHERE flight_data_id = ?", flightDataID).Error; err != nil { return err } for _, f := range data.HESLO.Flights { hf := &flightdata.HESLOFlight{ ID: uuidv7.MustBytes(), FlightDataID: flightDataID, ROT: f.ROT, FacilityFuelTruckID: f.FuelTruckID, LoggingSlingID: f.LoggingSlingID, CreatedAt: now, } if err := tx.Omit("FlightData", "FacilityFuelTruck", "LoggingSling").Create(hf).Error; err != nil { return err } if r.hasHesloSlingsTable { for _, s := range f.Slings { if len(s.SlingID) != 16 { continue } hs := &flightdata.HESLOSling{ ID: uuidv7.MustBytes(), HESLOFlightID: hf.ID, FacilitySlingID: s.SlingID, CreatedAt: now, } if err := tx.Omit("HESLOFlight", "FacilitySling").Create(hs).Error; err != nil { return err } } } } } if data.Logging != nil && r.hasLoggingSlingsTable { if err := tx.Exec("DELETE FROM logging_slings WHERE flight_data_id = ?", flightDataID).Error; err != nil { return err } for _, s := range data.Logging.Slings { if len(s.SlingID) != 16 { continue } ls := &flightdata.LoggingSling{ ID: uuidv7.MustBytes(), FlightDataID: flightDataID, FacilitySlingID: s.SlingID, CreatedAt: now, } if err := tx.Omit("FlightData", "FacilitySling").Create(ls).Error; err != nil { return err } } } if data.HEC != nil { if r.hasHecSlingsTable { if err := tx.Exec("DELETE FROM hec_slings WHERE flight_data_id = ?", flightDataID).Error; err != nil { return err } } if r.hasHecLoadsTable { if err := tx.Exec("DELETE FROM hec_loads WHERE flight_data_id = ?", flightDataID).Error; err != nil { return err } } if r.hasHecFlightsTable { if err := tx.Exec("DELETE FROM hec_flights WHERE flight_data_id = ?", flightDataID).Error; err != nil { return err } for _, f := range data.HEC.Flights { hf := &flightdata.HECFlight{ ID: uuidv7.MustBytes(), FlightDataID: flightDataID, ROT: f.ROT, HECCycle: f.HECCycle, EquipmentID: f.EquipmentID, CreatedAt: now, } if err := tx.Omit("FlightData", "Equipment").Create(hf).Error; err != nil { return err } } } if r.hasHecSlingsTable { for _, s := range data.HEC.Slings { if len(s.SlingID) != 16 { continue } hs := &flightdata.HECSling{ ID: uuidv7.MustBytes(), FlightDataID: flightDataID, HECSlingID: s.SlingID, CreatedAt: now, } if err := tx.Omit("FlightData", "HECSling").Create(hs).Error; err != nil { return err } } } if r.hasHecLoadsTable { for _, l := range data.HEC.Loads { hl := &flightdata.HECLoads{ ID: uuidv7.MustBytes(), FlightDataID: flightDataID, LoadCategory: l.LoadCategory, Quantity: l.Quantity, CreatedAt: now, } if err := tx.Omit("FlightData").Create(hl).Error; err != nil { return err } } } } return nil }) log.Printf("[UpsertSPODetails] done: err=%v", txErr) return txErr } func flightDataPersistenceValues(row *flightdata.FlightData) map[string]any { if row == nil { return map[string]any{} } return map[string]any{ "id": nullableBytesValue(row.ID), "mission_id": nullableBytesValue(row.MissionID), "co_pilot_id": nullableBytesValue(row.CoPilotID), "from_icao_id": nullableBytesValue(row.FromICAOID), "from_hospital_id": nullableBytesValue(row.FromHospitalID), "to_icao_id": nullableBytesValue(row.ToICAOID), "to_hospital_id": nullableBytesValue(row.ToHospitalID), "take_off": nullableTimeValue(row.FlightTakeOff), "landing": nullableTimeValue(row.FlightLanding), "duration": row.FlightDuration, "flight_type": row.FlightType, "red": row.FlightRED, "max_n1": row.MaxN1, "max_n2": row.MaxN2, "pax_count": row.PaxCount, "ticket_no": row.TicketNo, "engine": row.Engine, "landing_count": row.LandingCount, "rotor_brake_cycle": row.RotorBrakeCycle, "hook_releases": row.HookReleases, "delivery_note_number": row.DeliveryNoteNumber, "customer_name": row.CustomerName, "flight_plan_distance": row.FlightPlanDistance, "flight_plan_time": row.FlightPlanTime, "flight_plan_true_course": row.FlightPlanTrueCourse, "fuel_before_flight": row.FuelBeforeFlight, "fuel_upload": row.FuelUpload, "fuel_after_flight": row.FuelAfterFlight, "fuel_planning": row.FuelPlanning, "flight_position": row.FlightPositioning, "other_information": row.OtherInformation, "status": row.Status, "created_at": row.CreatedAt, "created_by": nullableBytesValue(row.CreatedBy), "updated_at": row.UpdatedAt, "updated_by": nullableBytesValue(row.UpdatedBy), "deleted_at": nullableTimePtrValue(row.DeletedAt), "deleted_by": nullableBytesValue(row.DeletedBy), } } func nullableBytesValue(v []byte) any { if len(v) == 0 { return nil } return v } func nullableTimeValue(v time.Time) any { if v.IsZero() { return nil } return v } func nullableTimePtrValue(v *time.Time) any { if v == nil { return nil } return *v }