Files
fm_be/internal/repository/mysql/complaint_repo.go
2026-07-16 22:16:45 +07:00

195 lines
7.9 KiB
Go

package mysql
import (
"context"
"errors"
"gorm.io/gorm"
complaint "wucher/internal/domain/complaint"
)
type ComplaintRepository struct {
db *gorm.DB
}
func NewComplaintRepository(db *gorm.DB) *ComplaintRepository { return &ComplaintRepository{db: db} }
func (r *ComplaintRepository) Create(ctx context.Context, row *complaint.Complaint) error {
return r.db.WithContext(ctx).Create(row).Error
}
func (r *ComplaintRepository) Update(ctx context.Context, row *complaint.Complaint) error {
return r.db.WithContext(ctx).Save(row).Error
}
func (r *ComplaintRepository) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
return r.db.WithContext(ctx).Model(&complaint.Complaint{}).Where("id = ? AND deleted_at IS NULL", id).Updates(map[string]any{"deleted_at": gorm.Expr("NOW(3)"), "deleted_by": deletedBy, "updated_by": deletedBy}).Error
}
func (r *ComplaintRepository) GetByID(ctx context.Context, id []byte) (*complaint.Complaint, error) {
var row complaint.Complaint
err := r.db.WithContext(ctx).
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 *ComplaintRepository) ListByHelicopter(ctx context.Context, helicopterID []byte, limit, offset int) ([]complaint.Complaint, int64, error) {
rows := make([]complaint.Complaint, 0)
var total int64
base := r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("helicopter_id = ? AND deleted_at IS NULL", helicopterID)
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
q := base.Order("reported_at DESC, created_at DESC")
if limit > 0 {
q = q.Limit(limit).Offset(offset)
}
err := q.Find(&rows).Error
return rows, total, err
}
func (r *ComplaintRepository) List(ctx context.Context, filter complaint.ListFilter, sort string, limit, offset int) ([]complaint.Complaint, int64, error) {
rows := make([]complaint.Complaint, 0)
var total int64
base := r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("complaints.deleted_at IS NULL")
if len(filter.HelicopterID) == 16 {
base = base.Where("complaints.helicopter_id = ?", filter.HelicopterID)
}
if len(filter.FlightID) == 16 {
base = base.Where("complaints.flight_id = ?", filter.FlightID)
}
if filter.HoldItemsOnly {
base = base.Where("complaints.fixed_at IS NULL AND complaints.mel_severity > ?", 0)
}
if filter.Search != "" {
like := "%" + filter.Search + "%"
base = base.Where("complaints.description LIKE ? OR CAST(complaints.mel_severity AS CHAR) LIKE ?", like, like)
}
if filter.DateFrom != "" {
base = base.Where("complaints.reported_at >= ?", filter.DateFrom+" 00:00:00")
}
if filter.DateTo != "" {
base = base.Where("complaints.reported_at <= ?", filter.DateTo+" 23:59:59.999")
}
if filter.Person != "" {
like := "%" + filter.Person + "%"
base = base.Where(
"EXISTS (SELECT 1 FROM users u WHERE u.id IN (complaints.reported_by, complaints.mel_classified_by, complaints.nsr_decided_by) "+
"AND TRIM(CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, ''))) LIKE ?)",
like,
)
}
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
q := base
if sort != "" {
q = q.Order(sort)
} else {
q = q.Order("reported_at DESC, created_at DESC")
}
if limit > 0 {
q = q.Limit(limit).Offset(offset)
}
if err := q.Find(&rows).Error; err != nil {
return nil, 0, err
}
return rows, total, nil
}
func (r *ComplaintRepository) HelicopterHasOpenComplaint(ctx context.Context, helicopterID []byte) (bool, error) {
var count int64
err := r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("helicopter_id = ? AND deleted_at IS NULL AND fixed_at IS NULL", helicopterID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// OpenByHelicopterIDs returns the complaints the fleet-status Complaints column shows for
// the given helicopters, in one query (no N+1). That is every OPEN defect (fixed_at IS
// NULL) — including NSR-deferred and MEL-within-grace items, which are still open hold
// items — PLUS the FIXED defects from each helicopter's latest flight (the flight of its
// most recently reported complaint), so a just-resolved defect stays visible as the last
// state until a newer flight reports. Fixed defects from older flights drop off here but
// remain retrievable via the complaint history. A nil/empty helicopterIDs means "all".
func (r *ComplaintRepository) OpenByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) ([]complaint.Complaint, error) {
rows := make([]complaint.Complaint, 0)
q := r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("deleted_at IS NULL").
Where("fixed_at IS NULL OR flight_id = (SELECT c2.flight_id FROM complaints c2 WHERE c2.helicopter_id = complaints.helicopter_id AND c2.deleted_at IS NULL AND c2.flight_id IS NOT NULL ORDER BY c2.reported_at DESC, c2.id DESC LIMIT 1)")
if len(helicopterIDs) > 0 {
q = q.Where("helicopter_id IN ?", helicopterIDs)
}
err := q.Order("reported_at DESC, created_at DESC").Find(&rows).Error
return rows, err
}
func (r *ComplaintRepository) ActiveGroundingByHelicopterIDs(ctx context.Context, helicopterIDs [][]byte) ([]complaint.Complaint, error) {
rows := make([]complaint.Complaint, 0)
// Grounding candidates: open (not fixed), not released (not NSR), and already
// classified (mel_classified_at set). A pending complaint is not yet a grounding
// defect — it neither grounds the aircraft nor blocks the EASA release gate until it
// is classified. action_taken is intentionally NOT excluded — recording corrective
// action no longer lifts the grounding (only an NSR or a signed EASA release does),
// so IsGrounding decides. A nil/empty helicopterIDs means "all helicopters".
q := r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("deleted_at IS NULL AND is_nsr = 0 AND fixed_at IS NULL AND mel_classified_at IS NOT NULL")
if len(helicopterIDs) > 0 {
q = q.Where("helicopter_id IN ?", helicopterIDs)
}
err := q.Find(&rows).Error
return rows, err
}
func (r *ComplaintRepository) HelicopterHasUnactionedComplaint(ctx context.Context, helicopterID []byte) (bool, error) {
var count int64
err := r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("helicopter_id = ? AND deleted_at IS NULL AND fixed_at IS NULL AND is_nsr = 0 AND (action_taken IS NULL OR action_taken = '')", helicopterID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *ComplaintRepository) MarkFixedByHelicopter(ctx context.Context, helicopterID []byte, easaID []byte, actor []byte) error {
return r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("helicopter_id = ? AND deleted_at IS NULL AND fixed_at IS NULL", helicopterID).
Updates(map[string]any{"fixed_at": gorm.Expr("NOW(3)"), "fixed_by_easa_id": easaID, "updated_by": actor}).Error
}
// MarkFixedByComplaint closes a single complaint (by id) — used when a release is
// scoped to one specific defect via complaint_id. No-op if already fixed/deleted.
func (r *ComplaintRepository) MarkFixedByComplaint(ctx context.Context, complaintID []byte, easaID []byte, actor []byte) error {
return r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("id = ? AND deleted_at IS NULL AND fixed_at IS NULL", complaintID).
Updates(map[string]any{"fixed_at": gorm.Expr("NOW(3)"), "fixed_by_easa_id": easaID, "updated_by": actor}).Error
}
// MarkFixedByFlight closes every open complaint reported on a flight — used when a
// release is scoped to a flight via flight_id. No-op for already fixed/deleted rows.
func (r *ComplaintRepository) MarkFixedByFlight(ctx context.Context, flightID []byte, easaID []byte, actor []byte) error {
return r.db.WithContext(ctx).
Model(&complaint.Complaint{}).
Where("flight_id = ? AND deleted_at IS NULL AND fixed_at IS NULL", flightID).
Updates(map[string]any{"fixed_at": gorm.Expr("NOW(3)"), "fixed_by_easa_id": easaID, "updated_by": actor}).Error
}