init push
This commit is contained in:
373
internal/repository/mysql/helicopter_repo.go
Normal file
373
internal/repository/mysql/helicopter_repo.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"wucher/internal/domain/helicopter"
|
||||
"wucher/internal/shared/pkg/sortkey"
|
||||
)
|
||||
|
||||
type HelicopterRepository struct {
|
||||
db *gorm.DB
|
||||
hasUsersTable bool
|
||||
}
|
||||
|
||||
func NewHelicopterRepository(db *gorm.DB) *HelicopterRepository {
|
||||
schema := newSchemaCache(db)
|
||||
return &HelicopterRepository{
|
||||
db: db,
|
||||
hasUsersTable: schema.HasTable("users"),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) Create(ctx context.Context, h *helicopter.Helicopter) error {
|
||||
requestedIsActive := h.IsActive
|
||||
db := r.db.WithContext(ctx)
|
||||
if err := db.Create(h).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Model(&helicopter.Helicopter{}).Where("id = ?", h.ID).UpdateColumn("is_active", requestedIsActive).Error
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) CreateWithDB(ctx context.Context, db *gorm.DB, h *helicopter.Helicopter) error {
|
||||
requestedIsActive := h.IsActive
|
||||
if err := db.WithContext(ctx).Create(h).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return db.WithContext(ctx).Model(&helicopter.Helicopter{}).Where("id = ?", h.ID).UpdateColumn("is_active", requestedIsActive).Error
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) Transaction(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return r.db.WithContext(ctx).Transaction(fn)
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) Update(ctx context.Context, h *helicopter.Helicopter) error {
|
||||
// Persist only helicopter columns; avoid association auto-save side effects
|
||||
// when handler payload carries preloaded FotoAttachment relation.
|
||||
return r.db.WithContext(ctx).Omit(clause.Associations).Save(h).Error
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) ExistsByIdentifier(ctx context.Context, identifier string, excludeID []byte) (bool, error) {
|
||||
db := r.db.WithContext(ctx).Model(&helicopter.Helicopter{}).
|
||||
Where("UPPER(TRIM(identifier)) = ?", strings.ToUpper(strings.TrimSpace(identifier)))
|
||||
if len(excludeID) == 16 {
|
||||
db = db.Where("id <> ?", excludeID)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return total > 0, nil
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) Delete(ctx context.Context, id []byte) error {
|
||||
if err := ensureNoReferenceBeforeDelete(ctx, r.db, "helicopters", id); err != nil {
|
||||
return err
|
||||
}
|
||||
return mapDeleteConstraintError(r.db.WithContext(ctx).Delete(&helicopter.Helicopter{}, "id = ?", id).Error)
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) GetByID(ctx context.Context, id []byte) (*helicopter.Helicopter, error) {
|
||||
var h helicopter.Helicopter
|
||||
err := r.withAuditUsers(r.db.WithContext(ctx)).
|
||||
Preload("FotoAttachment").
|
||||
Preload("FotoAttachment.File").
|
||||
Where("helicopters.id = ?", id).
|
||||
First(&h).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return &h, err
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) List(ctx context.Context, filter string, statuses []string, sort string, limit, offset int, groundedIDs [][]byte) ([]helicopter.Helicopter, int64, error) {
|
||||
var helicopters []helicopter.Helicopter
|
||||
var total int64
|
||||
|
||||
base := r.withAuditUsers(r.db.WithContext(ctx))
|
||||
if filter != "" {
|
||||
like := "%" + filter + "%"
|
||||
base = base.Where("helicopters.designation LIKE ? OR helicopters.identifier LIKE ? OR helicopters.type LIKE ?", like, like, like)
|
||||
}
|
||||
base = r.applyStatusFilter(base, statuses, groundedIDs)
|
||||
|
||||
query := base
|
||||
if sort != "" {
|
||||
query = query.Order(sort)
|
||||
} else {
|
||||
for _, clause := range sortkey.ActivePositiveSortClauses("helicopters", "is_active", "sortkey", "designation", false) {
|
||||
query = query.Order(clause)
|
||||
}
|
||||
}
|
||||
if err := base.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit).Offset(offset)
|
||||
}
|
||||
query = query.Preload("FotoAttachment").Preload("FotoAttachment.File")
|
||||
if err := query.Find(&helicopters).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return helicopters, total, nil
|
||||
}
|
||||
|
||||
// applyStatusFilter restricts query to helicopters whose derived operational
|
||||
// status is in statuses. The conditions mirror helicopter.DeriveStatus: AOG
|
||||
// (air_on_ground) outranks an active flight assignment ("booked"), and
|
||||
// "available" means neither; "mcf" has no data source yet, so it matches
|
||||
// nothing. Multiple statuses are OR-ed together. Returns query unchanged when
|
||||
// no status filter applies.
|
||||
func (r *HelicopterRepository) applyStatusFilter(query *gorm.DB, statuses []string, groundedIDs [][]byte) *gorm.DB {
|
||||
if len(statuses) == 0 {
|
||||
return query
|
||||
}
|
||||
|
||||
// inUse mirrors IsInUse: the helicopter has a non-deleted flight assignment
|
||||
// reachable through takeover_acs.reserve_ac_id. A fresh subquery is built
|
||||
// per use to avoid sharing builder state.
|
||||
inUse := func() *gorm.DB {
|
||||
return r.db.
|
||||
Table("reserve_acs ra").
|
||||
Select("1").
|
||||
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 = helicopters.id").
|
||||
Where("ra.deleted_at IS NULL AND f.deleted_at IS NULL AND afi.id IS NULL")
|
||||
}
|
||||
|
||||
// A helicopter is AOG when its persisted air_on_ground flag is set OR it is
|
||||
// grounded by an open complaint (computed in Go from IsGrounding and passed in via
|
||||
// groundedIDs, since the read-time grounding is not persisted in air_on_ground).
|
||||
hasGrounded := len(groundedIDs) > 0
|
||||
|
||||
group := r.db.Session(&gorm.Session{NewDB: true})
|
||||
seen := make(map[string]struct{}, len(statuses))
|
||||
for _, status := range statuses {
|
||||
if _, ok := seen[status]; ok {
|
||||
continue
|
||||
}
|
||||
seen[status] = struct{}{}
|
||||
switch status {
|
||||
case helicopter.StatusAOG:
|
||||
aog := r.db.Where("helicopters.air_on_ground = ?", true)
|
||||
if hasGrounded {
|
||||
aog = aog.Or("helicopters.id IN ?", groundedIDs)
|
||||
}
|
||||
group = group.Or(aog)
|
||||
case helicopter.StatusBooked:
|
||||
booked := r.db.Where("helicopters.air_on_ground = ?", false).Where("EXISTS (?)", inUse())
|
||||
if hasGrounded {
|
||||
booked = booked.Where("helicopters.id NOT IN ?", groundedIDs)
|
||||
}
|
||||
group = group.Or(booked)
|
||||
case helicopter.StatusAvailable:
|
||||
avail := r.db.Where("helicopters.air_on_ground = ?", false).Where("NOT EXISTS (?)", inUse())
|
||||
if hasGrounded {
|
||||
avail = avail.Where("helicopters.id NOT IN ?", groundedIDs)
|
||||
}
|
||||
group = group.Or(avail)
|
||||
case helicopter.StatusMCF:
|
||||
group = group.Or("1 = 0") // no data source yet
|
||||
}
|
||||
}
|
||||
return query.Where(group)
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) NextReportNumber(ctx context.Context, id []byte) (string, int, error) {
|
||||
var reportNumber string
|
||||
var sequence int
|
||||
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var h helicopter.Helicopter
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", id).First(&h).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
identifier := strings.TrimSpace(h.Identifier)
|
||||
if identifier == "" {
|
||||
return errors.New("helicopter identifier is empty")
|
||||
}
|
||||
if base, _, ok := splitIdentifierBase(identifier); ok {
|
||||
identifier = base
|
||||
}
|
||||
|
||||
next := h.ReportSequence + 1
|
||||
if err := tx.Model(&helicopter.Helicopter{}).Where("id = ?", h.ID).Update("report_sequence", next).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sequence = next
|
||||
reportNumber = fmt.Sprintf("%s-%04d", identifier, next)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return reportNumber, sequence, nil
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) IsInUse(ctx context.Context, id []byte) (bool, error) {
|
||||
if len(id) != 16 {
|
||||
return false, nil
|
||||
}
|
||||
var total int64
|
||||
err := r.db.WithContext(ctx).
|
||||
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", id).
|
||||
Count(&total).Error
|
||||
return total > 0, err
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) InUseByAircraftIDs(ctx context.Context, ids [][]byte) (map[string]bool, error) {
|
||||
out := make(map[string]bool)
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
filtered := make([][]byte, 0, len(ids))
|
||||
seen := make(map[string]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if len(id) != 16 {
|
||||
continue
|
||||
}
|
||||
key := string(id)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
filtered = append(filtered, append([]byte(nil), id...))
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
HelicopterID []byte `gorm:"column:helicopter_id"`
|
||||
}
|
||||
rows := make([]row, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("reserve_acs ra").
|
||||
Select("DISTINCT ra.helicopter_id").
|
||||
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.deleted_at IS NULL AND f.deleted_at IS NULL AND afi.id IS NULL").
|
||||
Where("ra.helicopter_id IN ?", filtered).
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
out[string(rows[i].HelicopterID)] = true
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ActiveFlightIDByAircraftIDs returns, per helicopter, the id of its current active flight
|
||||
// (a takeover flight with no after-flight inspection yet). Mirrors the InUseByAircraftIDs
|
||||
// join so "in use" and "which flight" stay consistent. Only in-use helicopters appear in
|
||||
// the map; the newest flight wins if several are open. Used by the fleet status page to
|
||||
// create an EASA release scoped to the flight.
|
||||
func (r *HelicopterRepository) ActiveFlightIDByAircraftIDs(ctx context.Context, ids [][]byte) (map[string][]byte, error) {
|
||||
out := make(map[string][]byte)
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
filtered := make([][]byte, 0, len(ids))
|
||||
seen := make(map[string]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
if len(id) != 16 {
|
||||
continue
|
||||
}
|
||||
key := string(id)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
filtered = append(filtered, append([]byte(nil), id...))
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
HelicopterID []byte `gorm:"column:helicopter_id"`
|
||||
FlightID []byte `gorm:"column:flight_id"`
|
||||
}
|
||||
rows := make([]row, 0)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Table("reserve_acs ra").
|
||||
Select("ra.helicopter_id AS helicopter_id, f.id AS flight_id").
|
||||
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.deleted_at IS NULL AND f.deleted_at IS NULL AND afi.id IS NULL").
|
||||
Where("ra.helicopter_id IN ?", filtered).
|
||||
Order("f.created_at DESC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
k := string(rows[i].HelicopterID)
|
||||
if _, ok := out[k]; !ok && len(rows[i].FlightID) == 16 {
|
||||
out[k] = rows[i].FlightID
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func splitIdentifierBase(raw string) (string, int, bool) {
|
||||
s := strings.TrimSpace(raw)
|
||||
idx := strings.LastIndex(s, "-")
|
||||
if idx <= 0 || idx+5 != len(s) {
|
||||
return "", 0, false
|
||||
}
|
||||
base := strings.TrimSpace(s[:idx])
|
||||
if base == "" {
|
||||
return "", 0, false
|
||||
}
|
||||
seq, err := strconv.Atoi(s[idx+1:])
|
||||
if err != nil || seq <= 0 {
|
||||
return "", 0, false
|
||||
}
|
||||
return base, seq, true
|
||||
}
|
||||
|
||||
func (r *HelicopterRepository) withAuditUsers(db *gorm.DB) *gorm.DB {
|
||||
base := db.Model(&helicopter.Helicopter{})
|
||||
if !r.hasUsersTable {
|
||||
return base.Select("helicopters.*, '' AS created_by_name, '' AS updated_by_name")
|
||||
}
|
||||
return base.
|
||||
Select(`
|
||||
helicopters.*,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(CONCAT(COALESCE(cu.first_name, ''), ' ', COALESCE(cu.last_name, ''))), ''),
|
||||
NULLIF(TRIM(cu.username), ''),
|
||||
NULLIF(TRIM(cu.email), ''),
|
||||
''
|
||||
) AS created_by_name,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(CONCAT(COALESCE(uu.first_name, ''), ' ', COALESCE(uu.last_name, ''))), ''),
|
||||
NULLIF(TRIM(uu.username), ''),
|
||||
NULLIF(TRIM(uu.email), ''),
|
||||
''
|
||||
) AS updated_by_name
|
||||
`).
|
||||
Joins("LEFT JOIN users cu ON cu.id = helicopters.created_by").
|
||||
Joins("LEFT JOIN users uu ON uu.id = helicopters.updated_by")
|
||||
}
|
||||
Reference in New Issue
Block a user