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,9 @@
package easarelease
import "errors"
var (
ErrNotFound = errors.New("easa release not found")
ErrAlreadySigned = errors.New("easa release already signed")
ErrIncompleteForSign = errors.New("easa release required fields are incomplete")
)

View File

@@ -0,0 +1,77 @@
package easarelease
import (
"time"
"gorm.io/gorm"
complaint "wucher/internal/domain/complaint"
flightdomain "wucher/internal/domain/flight"
helicopter "wucher/internal/domain/helicopter"
"wucher/internal/shared/pkg/uuidv7"
)
type EASARelease struct {
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
HelicopterID []byte `gorm:"type:binary(16);not null;index;column:helicopter_id"`
// ComplaintID scopes the release to the specific complaint it closes (optional).
// When set, the release is fetched via get-by-complaint and closes that complaint.
// When NULL the release is standalone (aircraft cleared with no open defect).
ComplaintID []byte `gorm:"type:binary(16);index;column:complaint_id"`
// FlightID — optional flight context. Retained for the standalone (no-complaint)
// sign-off gate, which is still flight-scoped. Not required for complaint releases.
FlightID []byte `gorm:"type:binary(16);index;column:flight_id"`
EASADate *time.Time `gorm:"type:date;column:easa_date"`
EASAACHours *string `gorm:"type:varchar(20);column:easa_ac_hours"`
EASALocation *string `gorm:"type:varchar(255);column:easa_location"`
EASAWONo *string `gorm:"type:varchar(100);column:easa_wo_no"`
EASAMaintManualRevAirframe *string `gorm:"type:varchar(100);column:easa_maint_manual_rev_airframe"`
EASAMaintManualRevEngine *string `gorm:"type:varchar(100);column:easa_maint_manual_rev_engine"`
EASASignerID *string `gorm:"type:varchar(100);column:easa_signer_id"`
EASAPermitNo *string `gorm:"type:varchar(100);column:easa_permit_no"`
SignedAt *time.Time `gorm:"type:datetime(3);column:signed_at"`
SignedBy []byte `gorm:"type:binary(16);column:signed_by"`
Helicopter *helicopter.Helicopter `gorm:"foreignKey:HelicopterID;references:ID"`
Flight *flightdomain.Flight `gorm:"foreignKey:FlightID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
Complaint *complaint.Complaint `gorm:"foreignKey:ComplaintID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
CreatedAt time.Time `gorm:"type:datetime(3);column:created_at"`
CreatedBy []byte `gorm:"type:binary(16);column:created_by"`
UpdatedAt time.Time `gorm:"type:datetime(3);column:updated_at"`
UpdatedBy []byte `gorm:"type:binary(16);column:updated_by"`
DeletedAt *time.Time `gorm:"type:datetime(3);index;column:deleted_at"`
DeletedBy []byte `gorm:"type:binary(16);column:deleted_by"`
}
func (EASARelease) TableName() string { return "easa_releases" }
func (e *EASARelease) BeforeCreate(_ *gorm.DB) error {
if len(e.ID) == 0 {
e.ID = uuidv7.MustBytes()
}
return nil
}
func (e *EASARelease) IsSigned() bool { return e != nil && e.SignedAt != nil }
func (e *EASARelease) IsActive() bool { return e.IsSigned() }
// MissingSignFields reports which mandatory fields are still empty for signing.
// Only the signer is required — it is auto-filled from the duty pilot. The other
// document fields (date, A/C hours, location, WO no, manual revisions) are
// adjustable free text and optional, so a release can be signed without them.
func (e *EASARelease) MissingSignFields() []string {
if e == nil {
return []string{"easa_signer_id"}
}
var missing []string
if e.EASASignerID == nil || *e.EASASignerID == "" {
missing = append(missing, "easa_signer_id")
}
return missing
}

View File

@@ -0,0 +1,83 @@
package easarelease
import (
"reflect"
"testing"
"time"
)
func str(s string) *string { return &s }
func TestEASAReleaseIsSigned(t *testing.T) {
now := time.Now()
if (&EASARelease{}).IsSigned() {
t.Fatal("unsigned release reported as signed")
}
if !(&EASARelease{SignedAt: &now}).IsSigned() {
t.Fatal("signed release reported as unsigned")
}
var nilRel *EASARelease
if nilRel.IsSigned() {
t.Fatal("nil release reported as signed")
}
}
func TestEASAReleaseMissingSignFields(t *testing.T) {
// Only the auto-filled signer is required to sign; the adjustable document
// fields (date, A/C hours, location, WO no, manual revisions) are optional.
onlySigner := []string{"easa_signer_id"}
now := time.Now()
cases := []struct {
name string
row *EASARelease
want []string
}{
{name: "nil requires signer", row: nil, want: onlySigner},
{name: "empty requires signer", row: &EASARelease{}, want: onlySigner},
{name: "signer present lists none", row: &EASARelease{EASASignerID: str("019d61db-53c6-7280-87ec-e65f205c6d3b")}, want: nil},
{name: "other fields empty but signer set lists none", row: &EASARelease{EASASignerID: str("019d61db-53c6-7280-87ec-e65f205c6d3b"), EASADate: &now}, want: nil},
{name: "blank signer counts as missing", row: &EASARelease{EASASignerID: str("")}, want: onlySigner},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := tc.row.MissingSignFields()
if len(got) == 0 && len(tc.want) == 0 {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("MissingSignFields() = %v, want %v", got, tc.want)
}
})
}
}
func TestEASAReleaseIsActiveAndTableName(t *testing.T) {
if (EASARelease{}).TableName() != "easa_releases" {
t.Fatalf("unexpected table name")
}
now := time.Now()
if (&EASARelease{}).IsActive() {
t.Fatal("unsigned release must not be active")
}
if !(&EASARelease{SignedAt: &now}).IsActive() {
t.Fatal("signed release must be active")
}
}
func TestEASAReleaseBeforeCreate(t *testing.T) {
e := &EASARelease{}
if err := e.BeforeCreate(nil); err != nil {
t.Fatalf("BeforeCreate err: %v", err)
}
if len(e.ID) == 0 {
t.Fatal("BeforeCreate should set ID")
}
prev := string(e.ID)
if err := e.BeforeCreate(nil); err != nil {
t.Fatalf("BeforeCreate err: %v", err)
}
if string(e.ID) != prev {
t.Fatal("BeforeCreate must not overwrite an existing ID")
}
}

View File

@@ -0,0 +1,16 @@
package easarelease
import "context"
type Repository interface {
Create(ctx context.Context, row *EASARelease) error
Update(ctx context.Context, row *EASARelease) error
Delete(ctx context.Context, id []byte, deletedBy []byte) error
GetByID(ctx context.Context, id []byte) (*EASARelease, error)
GetByFlightID(ctx context.Context, flightID []byte) (*EASARelease, error)
GetByComplaintID(ctx context.Context, complaintID []byte) (*EASARelease, error)
ListByHelicopter(ctx context.Context, helicopterID []byte) ([]EASARelease, error)
GetLatestByHelicopter(ctx context.Context, helicopterID []byte) (*EASARelease, error)
GetActiveByHelicopter(ctx context.Context, helicopterID []byte) (*EASARelease, error)
VoidByID(ctx context.Context, id []byte, reason string, actor []byte) error
}

View File

@@ -0,0 +1,16 @@
package easarelease
import "context"
type Service interface {
Create(ctx context.Context, row *EASARelease) error
Update(ctx context.Context, row *EASARelease) error
Delete(ctx context.Context, id []byte, deletedBy []byte) error
GetByID(ctx context.Context, id []byte) (*EASARelease, error)
GetByFlightID(ctx context.Context, flightID []byte) (*EASARelease, error)
GetByComplaintID(ctx context.Context, complaintID []byte) (*EASARelease, error)
ListByHelicopter(ctx context.Context, helicopterID []byte) ([]EASARelease, error)
GetActiveByHelicopter(ctx context.Context, helicopterID []byte) (*EASARelease, error)
Sign(ctx context.Context, id []byte, signerID []byte) error
Amend(ctx context.Context, currentID []byte, reason string, actor []byte) (newID []byte, err error)
}