Files
fm_be/internal/service/takeover_service_test.go
2026-07-16 22:16:45 +07:00

742 lines
28 KiB
Go

package service
import (
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"wucher/internal/domain/base"
beforeflightinspection "wucher/internal/domain/before_flight_inspection"
filemanager "wucher/internal/domain/file_manager"
flightinspection "wucher/internal/domain/flight_inspection"
flightprepcheck "wucher/internal/domain/flight_prep_check"
helicopterfile "wucher/internal/domain/helicopter_file"
"wucher/internal/repository/mysql"
"wucher/internal/shared/pkg/uuidv7"
requestdto "wucher/internal/transport/http/dto/request"
)
func openTakeoverServiceTestDB(t *testing.T) *gorm.DB {
t.Helper()
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", filepath.Join(t.TempDir(), "takeover.db"))
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&base.BaseCategory{}, &base.Base{}, &base.BaseOperationalShiftTime{}); err != nil {
t.Fatalf("auto migrate base models: %v", err)
}
if err := db.AutoMigrate(&flightinspection.FlightInspection{}, &beforeflightinspection.BeforeFlightInspection{}, &flightprepcheck.FlightPrepCheck{}); err != nil {
t.Fatalf("auto migrate inspection models: %v", err)
}
if err := db.Exec(`CREATE TABLE helicopter_files (
id BLOB PRIMARY KEY,
section TEXT NOT NULL,
position INTEGER NOT NULL DEFAULT 1,
is_mandatory INTEGER NOT NULL DEFAULT 0,
helicopter_id BLOB NOT NULL,
file_attachment_id BLOB,
source_file_id BLOB,
created_at DATETIME,
created_by BLOB,
updated_at DATETIME,
updated_by BLOB
)`).Error; err != nil {
t.Fatalf("create helicopter_files table: %v", err)
}
if err := db.Exec(`CREATE TABLE flight_inspection_file_checklists (
id BLOB PRIMARY KEY,
is_done INTEGER NOT NULL DEFAULT 0,
helicopter_file_id BLOB NOT NULL,
flight_inspection_id BLOB NOT NULL,
created_at DATETIME,
created_by BLOB,
updated_at DATETIME,
updated_by BLOB,
UNIQUE (flight_inspection_id, helicopter_file_id)
)`).Error; err != nil {
t.Fatalf("create flight_inspection_file_checklists table: %v", err)
}
return db
}
// newChecklistTestService builds a TakeoverService wired with the concrete repository so
// the checklist upsert can load template files (DB access lives in the repository layer).
func newChecklistTestService(db *gorm.DB) *TakeoverService {
return NewTakeoverService(db).WithChecklistTemplateRepository(mysql.NewTakeoverRepository(db))
}
func TestValidateTakeoverInspectionChecklistsAllowsEmptySections(t *testing.T) {
if err := validateTakeoverInspectionChecklists(&requestdto.ReserveAcInspectionCreateAttributes{}); err != nil {
t.Fatalf("expected empty sections to be allowed, got %v", err)
}
}
func TestUpsertTakeoverInspectionTxAllowsMissingFleetStatusFileOnUpdate(t *testing.T) {
db := openTakeoverServiceTestDB(t)
now := time.Now().UTC()
helicopterID := uuidv7.MustBytes()
inspectionID := uuidv7.MustBytes()
inspectionDate := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
if err := db.Create(&flightinspection.FlightInspection{
ID: inspectionID,
InspectionDate: inspectionDate,
Status: flightinspection.StatusDraft,
CreatedAt: now,
UpdatedAt: now,
}).Error; err != nil {
t.Fatalf("create inspection: %v", err)
}
req := &requestdto.ReserveAcInspectionCreateAttributes{
InspectionDate: "2026-06-24",
Before: requestdto.ReserveAcBeforeInspectionAttributes{
OilEngineNR1Checked: boolPtr(true),
OilEngineNR2Checked: boolPtr(true),
HydraulicLHChecked: boolPtr(true),
HydraulicRHChecked: boolPtr(true),
OilTransmissionMGBChecked: boolPtr(true),
OilTransmissionIGBChecked: boolPtr(false),
OilTransmissionTGBChecked: boolPtr(true),
FuelAmount: floatPtr(1),
FuelUnit: stringPtr(beforeflightinspection.FuelUnitLT),
FuelAddedAmount: floatPtr(0),
},
Prepare: requestdto.ReserveAcPrepareInspectionAttributes{
NotamBriefing: boolPtr(true),
WeatherBriefing: boolPtr(true),
OperationalFlightPlan: boolPtr(false),
},
}
gotID, err := newChecklistTestService(db).upsertTakeoverInspectionTx(db, nil, inspectionID, req, helicopterID, uuidv7.MustBytes())
if err != nil {
t.Fatalf("expected update inspection without fleet_status_file to pass, got %v", err)
}
if string(gotID) != string(inspectionID) {
t.Fatalf("unexpected inspection id: %x", gotID)
}
}
func TestUpsertTakeoverInspectionTxAllowsMissingFileChecklistsOnUpdate(t *testing.T) {
db := openTakeoverServiceTestDB(t)
now := time.Now().UTC()
helicopterID := uuidv7.MustBytes()
inspectionID := uuidv7.MustBytes()
inspectionDate := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
if err := db.Create(&flightinspection.FlightInspection{
ID: inspectionID,
InspectionDate: inspectionDate,
Status: flightinspection.StatusDraft,
CreatedAt: now,
UpdatedAt: now,
}).Error; err != nil {
t.Fatalf("create inspection: %v", err)
}
req := &requestdto.ReserveAcInspectionCreateAttributes{
InspectionDate: "2026-06-24",
Before: requestdto.ReserveAcBeforeInspectionAttributes{
OilEngineNR1Checked: boolPtr(true),
OilEngineNR2Checked: boolPtr(true),
HydraulicLHChecked: boolPtr(true),
HydraulicRHChecked: boolPtr(true),
OilTransmissionMGBChecked: boolPtr(true),
OilTransmissionIGBChecked: boolPtr(false),
OilTransmissionTGBChecked: boolPtr(true),
FuelAmount: floatPtr(1),
FuelUnit: stringPtr(beforeflightinspection.FuelUnitLT),
FuelAddedAmount: floatPtr(0),
},
Prepare: requestdto.ReserveAcPrepareInspectionAttributes{
NotamBriefing: boolPtr(true),
WeatherBriefing: boolPtr(true),
OperationalFlightPlan: boolPtr(false),
},
}
gotID, err := newChecklistTestService(db).upsertTakeoverInspectionTx(db, nil, inspectionID, req, helicopterID, uuidv7.MustBytes())
if err != nil {
t.Fatalf("expected update inspection without file_checklists to pass, got %v", err)
}
if string(gotID) != string(inspectionID) {
t.Fatalf("unexpected inspection id: %x", gotID)
}
}
func TestEnsureHelicopterExistsTxDoesNotRequireDeletedAtColumn(t *testing.T) {
db := openTakeoverServiceTestDB(t)
helicopterID := uuidv7.MustBytes()
if err := db.Exec(`CREATE TABLE helicopters (
id BLOB PRIMARY KEY
)`).Error; err != nil {
t.Fatalf("create helicopters table: %v", err)
}
if err := db.Exec(fmt.Sprintf(`INSERT INTO helicopters (id) VALUES (X'%x')`, helicopterID)).Error; err != nil {
t.Fatalf("seed helicopter: %v", err)
}
if err := ensureHelicopterExistsTx(db, helicopterID); err != nil {
t.Fatalf("expected helicopter existence check to pass without deleted_at column, got %v", err)
}
if err := ensureHelicopterExistsTx(db, uuidv7.MustBytes()); err == nil {
t.Fatalf("expected missing helicopter to fail")
}
}
func TestUpsertTakeoverInspectionTxAllowsEmptyFileChecklistsOnUpdate(t *testing.T) {
db := openTakeoverServiceTestDB(t)
now := time.Now().UTC()
helicopterID := uuidv7.MustBytes()
inspectionID := uuidv7.MustBytes()
inspectionDate := time.Date(2026, 6, 24, 0, 0, 0, 0, time.UTC)
if err := db.Create(&flightinspection.FlightInspection{
ID: inspectionID,
InspectionDate: inspectionDate,
Status: flightinspection.StatusDraft,
CreatedAt: now,
UpdatedAt: now,
}).Error; err != nil {
t.Fatalf("create inspection: %v", err)
}
templateFile := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(),
Section: helicopterfile.SectionBeforeFirstFlightInspection,
Position: 1,
IsMandatory: true,
HelicopterID: helicopterID,
FileAttachmentID: uuidv7.MustBytes(),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(&templateFile).Error; err != nil {
t.Fatalf("create helicopter file template: %v", err)
}
req := &requestdto.ReserveAcInspectionCreateAttributes{
InspectionDate: "2026-06-24",
Before: requestdto.ReserveAcBeforeInspectionAttributes{
OilEngineNR1Checked: boolPtr(true),
OilEngineNR2Checked: boolPtr(true),
HydraulicLHChecked: boolPtr(true),
HydraulicRHChecked: boolPtr(true),
OilTransmissionMGBChecked: boolPtr(true),
OilTransmissionIGBChecked: boolPtr(false),
OilTransmissionTGBChecked: boolPtr(true),
FuelAmount: floatPtr(1),
FuelUnit: stringPtr(beforeflightinspection.FuelUnitLT),
FuelAddedAmount: floatPtr(0),
FileChecklist: []requestdto.ReserveAcFileChecklistItem{},
},
Prepare: requestdto.ReserveAcPrepareInspectionAttributes{
NotamBriefing: boolPtr(true),
WeatherBriefing: boolPtr(true),
OperationalFlightPlan: boolPtr(false),
FileChecklist: []requestdto.ReserveAcFileChecklistItem{},
},
}
gotID, err := newChecklistTestService(db).upsertTakeoverInspectionTx(db, nil, inspectionID, req, helicopterID, uuidv7.MustBytes())
if err != nil {
t.Fatalf("expected update inspection with empty file_checklists to pass, got %v", err)
}
if string(gotID) != string(inspectionID) {
t.Fatalf("unexpected inspection id: %x", gotID)
}
var checklistCount int64
if err := db.Table("flight_inspection_file_checklists").Count(&checklistCount).Error; err != nil {
t.Fatalf("count file checklists: %v", err)
}
if checklistCount != 0 {
t.Fatalf("expected no checklist rows to be created on empty update, got %d", checklistCount)
}
}
func boolPtr(v bool) *bool { return &v }
func floatPtr(v float64) *float64 { return &v }
func stringPtr(v string) *string { return &v }
func TestUpsertTakeoverChecklistItemsTxAllowsEmptyTemplateAndEmptyPayload(t *testing.T) {
db := openTakeoverServiceTestDB(t)
inspectionID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", nil, uuidv7.MustBytes())
if err != nil {
t.Fatalf("expected empty template and empty payload to pass, got %v", err)
}
}
func TestUpsertTakeoverChecklistItemsTxRejectsItemsWhenTemplateMissing(t *testing.T) {
db := openTakeoverServiceTestDB(t)
inspectionID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
fileID := uuidv7.MustBytes()
fileIDStr, _ := uuidv7.BytesToString(fileID)
err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{{
HelicopterFileID: fileIDStr,
IsDone: true,
}}, uuidv7.MustBytes())
if err == nil {
t.Fatal("expected error when template is missing and payload has items")
}
if !strings.Contains(err.Error(), "inspection checklist template is empty for section fpwb") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestUpsertTakeoverChecklistItemsTxAllowsOptionalFalseButRequiresMandatoryTrue(t *testing.T) {
db := openTakeoverServiceTestDB(t)
inspectionID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
now := time.Now().UTC()
mandatoryFile := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(),
Section: "fpwb",
Position: 1,
IsMandatory: true,
HelicopterID: helicopterID,
FileAttachmentID: uuidv7.MustBytes(),
CreatedAt: now,
UpdatedAt: now,
}
optionalFile := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(),
Section: "fpwb",
Position: 2,
IsMandatory: false,
HelicopterID: helicopterID,
FileAttachmentID: uuidv7.MustBytes(),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(&mandatoryFile).Error; err != nil {
t.Fatalf("create mandatory file: %v", err)
}
if err := db.Create(&optionalFile).Error; err != nil {
t.Fatalf("create optional file: %v", err)
}
mandatoryIDStr, _ := uuidv7.BytesToString(mandatoryFile.ID)
optionalIDStr, _ := uuidv7.BytesToString(optionalFile.ID)
err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: mandatoryIDStr, IsDone: true},
{HelicopterFileID: optionalIDStr, IsDone: false},
}, uuidv7.MustBytes())
if err != nil {
t.Fatalf("expected optional false to pass, got %v", err)
}
err = newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: mandatoryIDStr, IsDone: false},
{HelicopterFileID: optionalIDStr, IsDone: false},
}, uuidv7.MustBytes())
if err == nil {
t.Fatal("expected mandatory false to fail")
}
if !strings.Contains(err.Error(), "must be fully checked") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestUpsertTakeoverChecklistItemsTxAllowsPartialChecklist(t *testing.T) {
db := openTakeoverServiceTestDB(t)
inspectionID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
now := time.Now().UTC()
mandatoryFile := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 1, IsMandatory: true,
HelicopterID: helicopterID, FileAttachmentID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now,
}
optionalFile := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 2, IsMandatory: false,
HelicopterID: helicopterID, FileAttachmentID: uuidv7.MustBytes(), CreatedAt: now, UpdatedAt: now,
}
if err := db.Create(&mandatoryFile).Error; err != nil {
t.Fatalf("create mandatory file: %v", err)
}
if err := db.Create(&optionalFile).Error; err != nil {
t.Fatalf("create optional file: %v", err)
}
optionalIDStr, _ := uuidv7.BytesToString(optionalFile.ID)
// Submit ONLY the optional file; the unedited mandatory catalog file may be
// omitted from the checklist (no more "must include all helicopter files").
if err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: optionalIDStr, IsDone: true},
}, uuidv7.MustBytes()); err != nil {
t.Fatalf("expected partial checklist omitting non-edited files to pass, got %v", err)
}
// Only the submitted item is recorded.
var count int64
if err := db.Table("flight_inspection_file_checklists").Where("flight_inspection_id = ?", inspectionID).Count(&count).Error; err != nil {
t.Fatalf("count checklist rows: %v", err)
}
if count != 1 {
t.Fatalf("expected exactly 1 checklist row, got %d", count)
}
// A mandatory file that IS submitted but not done is still rejected.
mandatoryIDStr, _ := uuidv7.BytesToString(mandatoryFile.ID)
if err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: mandatoryIDStr, IsDone: false},
}, uuidv7.MustBytes()); err == nil {
t.Fatal("expected submitted-but-undone mandatory file to be rejected")
}
}
func TestUpsertTakeoverChecklistItemsTxExcludesTemplateDerivedFiles(t *testing.T) {
db := openTakeoverServiceTestDB(t)
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
t.Fatalf("migrate files: %v", err)
}
now := time.Now().UTC()
helicopterID := uuidv7.MustBytes()
inspectionID := uuidv7.MustBytes()
templateFileID := uuidv7.MustBytes()
derivedFileID := uuidv7.MustBytes()
folderID := uuidv7.MustBytes()
if err := db.Create(&filemanager.File{ID: templateFileID, FolderID: folderID, Name: "template.docx", ObjectKey: "k1", IsTemplate: true}).Error; err != nil {
t.Fatalf("create template file: %v", err)
}
// A document created from a template (has a non-empty template_uuid).
if err := db.Create(&filemanager.File{ID: derivedFileID, FolderID: folderID, Name: "from-template.docx", ObjectKey: "k2", TemplateUUID: uuidv7.MustBytes()}).Error; err != nil {
t.Fatalf("create derived file: %v", err)
}
templateRow := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 1,
HelicopterID: helicopterID, SourceFileID: templateFileID, CreatedAt: now, UpdatedAt: now,
}
derivedRow := helicopterfile.HelicopterFile{
ID: uuidv7.MustBytes(), Section: "fpwb", Position: 2,
HelicopterID: helicopterID, SourceFileID: derivedFileID, CreatedAt: now, UpdatedAt: now,
}
if err := db.Create(&templateRow).Error; err != nil {
t.Fatalf("create template row: %v", err)
}
if err := db.Create(&derivedRow).Error; err != nil {
t.Fatalf("create derived row: %v", err)
}
templateIDStr, _ := uuidv7.BytesToString(templateRow.ID)
// Payload includes only the real template; the template-derived row must not be
// required by the checklist completeness check.
if err := newChecklistTestService(db).upsertTakeoverChecklistItemsTx(db, inspectionID, helicopterID, "fpwb", []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: templateIDStr, IsDone: true},
}, uuidv7.MustBytes()); err != nil {
t.Fatalf("expected success when only the template is checked, got %v", err)
}
}
func TestValidateTakeoverEditedTemplateFilesTx(t *testing.T) {
db := openTakeoverServiceTestDB(t)
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
t.Fatalf("migrate files: %v", err)
}
svc := newChecklistTestService(db)
takeoverID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
folderID := uuidv7.MustBytes()
now := time.Now().UTC()
// template_uuid on a from-template document == the source HelicopterFile.ID
// that a checklist item's helicopter_file_id references.
editedTemplateA := uuidv7.MustBytes()
editedTemplateB := uuidv7.MustBytes()
if err := db.Create(&helicopterfile.HelicopterFile{
ID: editedTemplateA,
Section: helicopterfile.SectionFlightPreparationAndWB,
HelicopterID: helicopterID,
SourceFileID: uuidv7.MustBytes(),
CreatedAt: now,
UpdatedAt: now,
}).Error; err != nil {
t.Fatalf("create checklist template A: %v", err)
}
if err := db.Create(&helicopterfile.HelicopterFile{
ID: editedTemplateB,
Section: helicopterfile.SectionFlightPreparationAndWB,
HelicopterID: helicopterID,
SourceFileID: uuidv7.MustBytes(),
CreatedAt: now,
UpdatedAt: now,
}).Error; err != nil {
t.Fatalf("create checklist template B: %v", err)
}
// Two documents edited/created from a template for this takeover.
if err := db.Create(&filemanager.File{ID: uuidv7.MustBytes(), FolderID: folderID, Name: "a.docx", ObjectKey: "a", TemplateUUID: editedTemplateA, TakeoverID: takeoverID}).Error; err != nil {
t.Fatalf("create edited file A: %v", err)
}
if err := db.Create(&filemanager.File{ID: uuidv7.MustBytes(), FolderID: folderID, Name: "b.docx", ObjectKey: "b", TemplateUUID: editedTemplateB, TakeoverID: takeoverID}).Error; err != nil {
t.Fatalf("create edited file B: %v", err)
}
// A from-template document for a different takeover must be ignored.
if err := db.Create(&filemanager.File{ID: uuidv7.MustBytes(), FolderID: folderID, Name: "other.docx", ObjectKey: "o", TemplateUUID: uuidv7.MustBytes(), TakeoverID: uuidv7.MustBytes()}).Error; err != nil {
t.Fatalf("create other-takeover file: %v", err)
}
aStr, _ := uuidv7.BytesToString(editedTemplateA)
bStr, _ := uuidv7.BytesToString(editedTemplateB)
unrelatedStr, _ := uuidv7.BytesToString(uuidv7.MustBytes())
// 1) Checklist missing an edited file (B) -> reject.
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: aStr, IsDone: true},
}); err == nil {
t.Fatalf("expected rejection when an edited file is missing from the checklist")
}
// 2) Edited files exist but checklist is empty -> reject.
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, nil); err == nil {
t.Fatalf("expected rejection when edited files exist but checklist is empty")
}
// 3) Checklist includes both edited files plus an unrelated (non-edited)
// item -> success; a checklist item that was not edited is fine.
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: aStr, IsDone: true},
{HelicopterFileID: bStr, IsDone: true},
{HelicopterFileID: unrelatedStr, IsDone: false},
}); err != nil {
t.Fatalf("expected success when all edited files are in the checklist, got %v", err)
}
// 4) No edited files for the takeover -> success regardless of checklist.
if err := svc.validateTakeoverEditedTemplateFilesTx(db, uuidv7.MustBytes(), uuidv7.MustBytes(), nil); err != nil {
t.Fatalf("expected success when there are no edited files, got %v", err)
}
// 5) A template-derived file that has already been trashed should not block
// an inspection update when it is no longer part of the active takeover set.
trashedTemplate := uuidv7.MustBytes()
trashedTakeoverID := uuidv7.MustBytes()
if err := db.Create(&filemanager.File{
ID: uuidv7.MustBytes(),
FolderID: folderID,
Name: "trashed.docx",
ObjectKey: "trashed",
TemplateUUID: trashedTemplate,
TakeoverID: trashedTakeoverID,
Status: filemanager.FileStatusTrashed,
}).Error; err != nil {
t.Fatalf("create trashed edited file: %v", err)
}
if err := svc.validateTakeoverEditedTemplateFilesTx(db, trashedTakeoverID, helicopterID, nil); err != nil {
t.Fatalf("expected trashed edited file to be ignored, got %v", err)
}
}
func TestValidateTakeoverEditedTemplateFilesTxAcceptsLegacySourceFileReferences(t *testing.T) {
db := openTakeoverServiceTestDB(t)
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
t.Fatalf("migrate files: %v", err)
}
svc := newChecklistTestService(db)
takeoverID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
folderID := uuidv7.MustBytes()
now := time.Now().UTC()
sourceFileID := uuidv7.MustBytes()
templateHeliFileID := uuidv7.MustBytes()
if err := db.Create(&filemanager.File{
ID: sourceFileID,
FolderID: folderID,
Name: "template.xlsx",
ObjectKey: "template.xlsx",
}).Error; err != nil {
t.Fatalf("create source template file: %v", err)
}
if err := db.Create(&helicopterfile.HelicopterFile{
ID: templateHeliFileID,
Section: helicopterfile.SectionFlightPreparationAndWB,
HelicopterID: helicopterID,
SourceFileID: sourceFileID,
CreatedAt: now,
UpdatedAt: now,
}).Error; err != nil {
t.Fatalf("create helicopter template row: %v", err)
}
// Legacy data may have template_uuid pointing to the source file ID instead
// of the helicopter_file ID shown in the checklist payload.
if err := db.Create(&filemanager.File{
ID: uuidv7.MustBytes(),
FolderID: folderID,
Name: "draft.xlsx",
ObjectKey: "draft.xlsx",
TemplateUUID: sourceFileID,
TakeoverID: takeoverID,
}).Error; err != nil {
t.Fatalf("create legacy edited file: %v", err)
}
templateChecklistID, _ := uuidv7.BytesToString(templateHeliFileID)
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, []requestdto.ReserveAcFileChecklistItem{
{HelicopterFileID: templateChecklistID, IsDone: true},
}); err != nil {
t.Fatalf("expected legacy source file template_uuid to map back to checklist template, got %v", err)
}
}
func TestValidateTakeoverEditedTemplateFilesTxIgnoresOrphanedTemplateReferences(t *testing.T) {
db := openTakeoverServiceTestDB(t)
if err := db.AutoMigrate(&filemanager.File{}); err != nil {
t.Fatalf("migrate files: %v", err)
}
svc := newChecklistTestService(db)
takeoverID := uuidv7.MustBytes()
helicopterID := uuidv7.MustBytes()
folderID := uuidv7.MustBytes()
if err := db.Create(&filemanager.File{
ID: uuidv7.MustBytes(),
FolderID: folderID,
Name: "orphaned-draft.xlsx",
ObjectKey: "orphaned-draft.xlsx",
TemplateUUID: uuidv7.MustBytes(),
TakeoverID: takeoverID,
}).Error; err != nil {
t.Fatalf("create orphaned edited file: %v", err)
}
if err := svc.validateTakeoverEditedTemplateFilesTx(db, takeoverID, helicopterID, nil); err != nil {
t.Fatalf("expected orphaned historical template reference to be ignored, got %v", err)
}
}
func TestResolveBaseShiftRangeTxPrefersOperationalShiftTime(t *testing.T) {
db := openTakeoverServiceTestDB(t)
now := time.Now().UTC()
category := &base.BaseCategory{ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now}
if err := db.Create(category).Error; err != nil {
t.Fatalf("create category: %v", err)
}
defaultStart := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC)
defaultEnd := time.Date(1900, 1, 1, 18, 0, 0, 0, time.UTC)
baseID := uuidv7.MustBytes()
row := &base.Base{
ID: baseID,
BaseCategoryID: category.ID,
BaseName: "Base A",
DefaultShiftStart: defaultStart.Format("15:04:05"),
DefaultShiftEnd: defaultEnd.Format("15:04:05"),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(row).Error; err != nil {
t.Fatalf("create base: %v", err)
}
opStart := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC)
opEnd := time.Date(1900, 1, 1, 17, 0, 0, 0, time.UTC)
opDate := time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC)
opRow := &base.BaseOperationalShiftTime{
ID: uuidv7.MustBytes(),
BaseID: baseID,
DateStart: &opDate,
DateEnd: &opDate,
ShiftStart: opStart.Format("15:04:05"),
ShiftEnd: opEnd.Format("15:04:05"),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(opRow).Error; err != nil {
t.Fatalf("create operational shift: %v", err)
}
start, end, err := ResolveBaseShiftRangeTx(db, baseID, time.Date(2026, 4, 12, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("resolve shift: %v", err)
}
if start != "09:00" || end != "17:00" {
t.Fatalf("unexpected operational shift: start=%s end=%s", start, end)
}
}
func TestResolveBaseShiftRangeTxFallsBackToDefaultShiftTime(t *testing.T) {
db := openTakeoverServiceTestDB(t)
now := time.Now().UTC()
category := &base.BaseCategory{ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now}
if err := db.Create(category).Error; err != nil {
t.Fatalf("create category: %v", err)
}
defaultStart := time.Date(1900, 1, 1, 9, 0, 0, 0, time.UTC)
defaultEnd := time.Date(1900, 1, 1, 18, 0, 0, 0, time.UTC)
baseID := uuidv7.MustBytes()
row := &base.Base{
ID: baseID,
BaseCategoryID: category.ID,
BaseName: "Base B",
DefaultShiftStart: defaultStart.Format("15:04:05"),
DefaultShiftEnd: defaultEnd.Format("15:04:05"),
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(row).Error; err != nil {
t.Fatalf("create base: %v", err)
}
start, end, err := ResolveBaseShiftRangeTx(db, baseID, time.Date(2026, 4, 13, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("resolve shift: %v", err)
}
if start != "09:00" || end != "18:00" {
t.Fatalf("unexpected default shift: start=%s end=%s", start, end)
}
}
func TestResolveBaseShiftRangeTxResolvesBMCTECETFromCoordinates(t *testing.T) {
db := openTakeoverServiceTestDB(t)
now := time.Now().UTC()
category := &base.BaseCategory{ID: uuidv7.MustBytes(), Key: base.CategoryKeyRegular, Name: "Regular", CreatedAt: now, UpdatedAt: now}
if err := db.Create(category).Error; err != nil {
t.Fatalf("create category: %v", err)
}
baseID := uuidv7.MustBytes()
row := &base.Base{
ID: baseID,
BaseCategoryID: category.ID,
BaseName: "Base C",
Latitude: 47.3769,
Longitude: 8.5417,
DefaultStartTimeType: base.ShiftTimeTypeBMCT,
DefaultEndTimeType: base.ShiftTimeTypeECET,
CreatedAt: now,
UpdatedAt: now,
}
if err := db.Create(row).Error; err != nil {
t.Fatalf("create base: %v", err)
}
start, end, err := ResolveBaseShiftRangeTx(db, baseID, time.Date(2026, 4, 13, 0, 0, 0, 0, time.UTC))
if err != nil {
t.Fatalf("resolve shift: %v", err)
}
if strings.TrimSpace(start) == "" || strings.TrimSpace(end) == "" {
t.Fatalf("expected resolved BMCT/ECET shift, got start=%q end=%q", start, end)
}
if start == "09:00:00" || end == "18:00:00" {
t.Fatalf("expected computed twilight times, got fixed-looking values start=%s end=%s", start, end)
}
}