flightrules

package
v1.3.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: AGPL-3.0 Imports: 6 Imported by: 0

Documentation

Overview

Package flightrules is the single source of truth for "who/what counts as PIC, Dual, FI, Night, IFR, MP/SP …" across the codebase.

flightcalc/ owns the *write* path (mutates Flight on save). flightrules owns the *read* path (pure helpers consumed by handlers, exporters, PDF renderers, stats and tests). The two share the role + name primitives below.

IMPORTANT: do NOT inline any of these rules in handlers/, repository/ or exporter code. A grep-guard in scripts/run-all-tests.sh enforces this.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CoPilotColumnTime added in v1.3.7

func CoPilotColumnTime(f *models.Flight) int

CoPilotColumnTime returns the minutes for the co-pilot (SIC) column of the paper layouts: SIC plus cruise relief co-pilot time, with the breakdown annotated in remarks by CombinedRemarks.

func CombinedRemarks

func CombinedRemarks(f *models.Flight, flags ...RemarkFlag) string

CombinedRemarks returns the user-facing combined Remarks + Endorsements string, suffixed with the declared-function-time annotations the paper layouts require ([PICUS h:mm] / [SPIC h:mm] / [Examiner h:mm] / [Relief h:mm] — the PIC and co-pilot columns fold these times in, per PICColumnTime and CoPilotColumnTime), and optionally with FAA-style inline flags ([IPC] / [FR] / [PC]). Empty endorsements/remarks are skipped; the separator between a non-empty remark and a non-empty endorsement is " | ".

Flag suffixes are only added when the corresponding boolean on `flight` is true AND the flag was requested by the caller. Most callers pass all three flags and let the model state decide.

func CountsAsFlightTime added in v1.3.6

func CountsAsFlightTime(f *models.Flight) bool

CountsAsFlightTime reports whether a row contributes to flight totals. An FSTD session never does: AMC1 FCL.050 records session time separately and it may not be summed with flight time. Neither does a flight the user was carried on as a passenger, which is not flight time at all.

This is the single source of truth for the distinction. Aggregates in internal/repository/postgres carry the equivalent `NOT is_simulator AND NOT is_passenger` predicate in SQL.

func DisplayPICName

func DisplayPICName(f *models.Flight, userName string) string

DisplayPICName returns the value to render in the "PIC Name" column of a logbook row, applying the canonical fallback chain:

  1. flight.PICName, if set (a stale "Self" with a known instructor is treated as unset)
  2. flight.InstructorName, if set (legacy column)
  3. first non-self Instructor in flight.CrewMembers
  4. first non-self Examiner in flight.CrewMembers
  5. original PICName if present
  6. "SELF"

An FSTD session has no pilot-in-command and returns "".

userName is the authenticated user's display name; pass "" if unknown. All exporters (CSV, PDF, future formats) MUST use this helper instead of re-implementing the fallback locally.

func EffectiveIFRTime

func EffectiveIFRTime(f *models.Flight) int

EffectiveIFRTime returns the IFR/Instrument time minutes that should be reported for a flight. Explicit and derived values alike are capped at TotalTime; an unset IFRTime is derived from ActualInstrumentTime + SimulatedInstrumentTime.

IFR is an operational condition of a flight (AMC1 FCL.050 Col 14), so an FSTD session always reports zero — its instrument work stays in SimulatedInstrumentTime and the FSTD columns.

This is the single source of truth for IFR derivation: imports, the auto-calc pipeline and exporters all flow through it.

func ExaminerNameFromCrew added in v1.3.0

func ExaminerNameFromCrew(f *models.Flight, userName string) string

ExaminerNameFromCrew is the Examiner-role counterpart of InstructorNameFromCrew: the first third-party Examiner in the crew, or "".

func FAADualColumnTime added in v1.3.7

func FAADualColumnTime(f *models.Flight) int

FAADualColumnTime returns the minutes for the dual-received column of the FAA layouts: dual received plus SPIC (supervised student flying with an instructor on board), annotated in remarks by CombinedRemarks.

func FAASICColumnTime added in v1.3.7

func FAASICColumnTime(f *models.Flight) int

FAASICColumnTime returns the minutes for the SIC column of the FAA layouts: SIC plus PICUS plus cruise relief — time in a required co-pilot seat is SIC time under 14 CFR §61.51(f), whatever Part-FCL calls it. The breakdown is annotated in remarks by CombinedRemarks.

func FSTDFields

func FSTDFields(f *models.Flight, dateLayout string, timeFmt func(int) string) (date, kind, dur string)

FSTDFields returns the (date, type, time) triple to fill into the FSTD columns. dateLayout is the Go time layout used to format the date (e.g. "02.01.2006" for full EASA CSV, "02.01" for the compact PDF column). timeFmt formats the minutes value the way the caller wants (HH:MM, decimal hours, etc.).

Returns ("", "", "") when IsFSTDRow is false.

func HasDeclaredFunctionTime added in v1.3.7

func HasDeclaredFunctionTime(f *models.Flight) bool

HasDeclaredFunctionTime reports whether the pilot declared any PICUS, SPIC or cruise relief minutes on the flight. Each names a pilot-flying (or relief) seat the operation provides, so it declares the seat the same way an entered co-pilot time does.

func InstructorNameFromCrew

func InstructorNameFromCrew(f *models.Flight, userName string) string

InstructorNameFromCrew returns the name of the first CrewMember with the Instructor role whose name does NOT match userName (i.e. a third-party instructor). Returns "" if no such member exists or if `f` is nil.

userName is the authenticated user's display name and may be empty; in that case ALL Instructor crew members are treated as third parties (matches the conservative behaviour of DetermineRole).

func IsFSTDRow

func IsFSTDRow(f *models.Flight) bool

IsFSTDRow reports whether a flight row should populate the FSTD-block columns (EASA AMC1 FCL.050 Cols 20-22, FAA §61.51(b)(1)(iv)).

Both: a non-empty FSTDType identifier AND non-zero simulated flight time.

func IsMultiPilotOperation added in v1.3.0

func IsMultiPilotOperation(flight *models.Flight, role Role, aircraft *AircraftFacts) bool

IsMultiPilotOperation reports whether the flight was flown with a two-pilot crew, filling the EASA AMC1 FCL.050 multi-pilot column (Col 10).

It requires a multi-pilot aircraft: the column records time flown in aeroplanes certificated for a minimum crew of two (FCL.010). A safety pilot in a single-pilot aeroplane is a required crew member under 14 CFR §91.109(b) and logs co-pilot time, but the aeroplane remains single-pilot and the time is not multi-pilot time.

func IsNightAt

func IsNightAt(t time.Time, lat, lon float64) bool

IsNightAt is the single point of truth for "is this instant night?" at a given latitude/longitude; it delegates to pkg/solar and every caller goes through here.

func MatchesUser

func MatchesUser(candidate, userName string) bool

MatchesUser reports whether `candidate` refers to the same person as `userName`. Comparison is case- and whitespace-insensitive, and also recognises the "Last, First" contact-picker form against a stored "First Last" profile (and vice versa).

Examples (all match "Amelia Earhart"):

  • "Amelia Earhart", "amelia earhart", "AMELIA EARHART"
  • " Amelia Earhart ", "Amelia\u00a0Earhart"
  • "Earhart, Amelia"

Examples (do NOT match): "Amelia M. Earhart", "A. Earhart".

func MayLogCoPilotTime added in v1.3.6

func MayLogCoPilotTime(flight *models.Flight, userName string, aircraft *AircraftFacts) bool

MayLogCoPilotTime reports whether the operation permits the user to log co-pilot time.

Co-pilot time requires a seat that the operation actually calls for:

  • a multi-pilot aircraft — certificated for a minimum crew of two (EASA FCL.010; 14 CFR §61.51(f)(1) "aircraft type certificated for more than one pilot");
  • a required safety pilot during simulated instrument flight (14 CFR §91.109(b), loggable under §61.51(f)(2)) — the user is listed with the SafetyPilot role;
  • the user declaring the co-pilot seat themselves, by listing their own SIC crew entry or by entering a co-pilot time directly (SICTimeOverride). Two-pilot operations mandated by an operations manual rather than by the type certificate (EASA FCL.010; §61.51(f)(2), §135.99(c)) are recorded this way.

A single-pilot aircraft with a third-party PIC and no such declaration carries no co-pilot seat, whoever else is on board.

A derived SICTime is not a declaration: only SICTimeOverride distinguishes a co-pilot time the pilot entered from one a previous derivation wrote, so re-deriving can still correct a row it filled in itself.

func NormalizeLegacyRole added in v1.2.3

func NormalizeLegacyRole(s string) string

NormalizeLegacyRole maps a free-form role string (case-insensitive) to one of our canonical CrewRole values. Unknown / empty strings return "" to signal "fall back to positional inference".

ForeFlight's role vocabulary in the structured Person cell includes PIC, SIC, Student, Instructor, FlightAttendant, Observer, Engineer and Other; the latter four are flattened to Passenger.

func NormalizeName

func NormalizeName(s string) string

NormalizeName returns a comparable form of a person name: lower-case, trimmed of leading/trailing whitespace, with all runs of internal whitespace collapsed to a single ASCII space, and Unicode whitespace (e.g. U+00A0 non-breaking space, tabs, newlines) folded to ASCII space.

This is the only place name normalization should happen. Empty input yields empty output.

func PICColumnTime added in v1.3.7

func PICColumnTime(f *models.Flight) int

PICColumnTime returns the minutes for the PIC column of the paper layouts: PIC plus PICUS plus SPIC (EASA AMC1 FCL.050 — supervised PIC time is entered in the PIC column, with the breakdown annotated in remarks by CombinedRemarks).

func PICNameFromCrew added in v1.3.0

func PICNameFromCrew(f *models.Flight, userName string) string

PICNameFromCrew is the PIC-role counterpart of InstructorNameFromCrew: the first third-party PIC in the crew, or "". Used when the user flies as co-pilot (SIC) — the other pilot is PIC of record.

func RendersLogbookRow added in v1.3.7

func RendersLogbookRow(f *models.Flight) bool

RendersLogbookRow reports whether a row has anything to print in an exported logbook. A flight the user was carried on as a passenger has no time in any column and prints nothing; an FSTD session has no flight time but fills the FSTD columns.

func ResolvePICNameForSave

func ResolvePICNameForSave(f *models.Flight, userName string) *string

ResolvePICNameForSave returns the value that should be persisted into `flight.PICName` when a flight is saved. The write-path equivalent of DisplayPICName:

  • existing PICName wins (user explicitly set it),
  • if the user is PIC and no instructor is involved → "Self",
  • if the user is Dual and an InstructorName is set → that instructor,
  • if the user is Dual and a non-self Instructor exists in CrewMembers → that instructor's name,
  • if the user is Dual and a non-self Examiner exists in CrewMembers → that examiner's name,
  • otherwise nil (column stays empty; DisplayPICName renders "SELF").

The CRUD handler must call this once at save time.

func RowTimes

func RowTimes(f *models.Flight, acClass string) (spSE, spME, mp int)

RowTimes returns the minutes that should be filled into the SP-SE / SP-ME / MP columns for `flight` given its piloting category. Exactly one of the three returned values is non-zero (the others are 0).

Types

type AircraftFacts added in v1.3.6

type AircraftFacts struct {
	Registration string
	IsMultiPilot bool
}

AircraftFacts carries the fleet facts the rules need about the aircraft a flight was flown in. A nil *AircraftFacts means the registration has no fleet entry and nothing is known about it.

func (*AircraftFacts) IsMultiPilotAircraft added in v1.3.6

func (a *AircraftFacts) IsMultiPilotAircraft() bool

IsMultiPilotAircraft reports whether the aircraft is certificated for a minimum crew of two pilots. An unknown aircraft is not.

type LegacyCrewInput

type LegacyCrewInput struct {
	Person1, Person2, Person3, Person4, Person5, Person6                         string
	Person1Role, Person2Role, Person3Role, Person4Role, Person5Role, Person6Role string
	InstructorName                                                               string
	HasDualReceived                                                              bool // any positive DualReceived value
	HasDualGiven                                                                 bool // any positive DualGiven value
}

LegacyCrewInput describes the flat set of legacy/spreadsheet crew columns: up to six "personN" names, an InstructorName, and the user-declared DualReceived / DualGiven totals.

ForeFlight optionally encodes a role tag inside each Person cell ("Name;Role;email"). When that explicit role is available the caller SHOULD pass it via the matching PersonNRole field; it wins over positional inference.

type LegacyCrewMember

type LegacyCrewMember struct {
	Name string
	Role string
}

LegacyCrewMember is a name+role pair returned by InferLegacyCrew. Callers map it to their own crew type (generated.FlightCrewMemberInput, models.FlightCrewMember, …). Role values match the strings in models.CrewRole ("PIC", "SIC", "Instructor", "Student", "Passenger").

func InferLegacyCrew

func InferLegacyCrew(in LegacyCrewInput) []LegacyCrewMember

InferLegacyCrew is the single source of truth that converts the legacy flat crew columns (used by ForeFlight / Garmin / older spreadsheet CSVs) into a CrewMembers array. Both the CSV importer and any future legacy re-importer MUST use this — do not re-derive crew roles by hand.

Rules:

  • Training flight ≡ InstructorName set OR HasDualReceived.
  • Instructor-giving ≡ HasDualGiven (and not a training flight).
  • Person1 is treated as Instructor on a training flight, Student when the user is giving instruction, else PIC. If InstructorName is set and differs from Person1, both are emitted (Person1 keeps a PIC role, InstructorName is added as Instructor).
  • Person2 is Student on a training flight, Passenger otherwise.
  • Persons 3-6 are always Passenger.
  • InstructorName with no Person1 is emitted as Instructor on its own.

When the caller supplies an explicit role for a person via the matching PersonNRole field (see NormalizeLegacyRole for the accepted vocabulary), that role wins over the positional rules for that person; other persons still follow the positional rules.

type PilotingCategory

type PilotingCategory int

PilotingCategory is the per-row bucket used for the EASA AMC1 FCL.050 columns "SP-SE" / "SP-ME" / "Multi-Pilot".

const (
	// CategorySPSE: single-pilot single-engine.
	CategorySPSE PilotingCategory = iota
	// CategorySPME: single-pilot multi-engine.
	CategorySPME
	// CategoryMP: multi-pilot.
	CategoryMP
)

func PilotingCategoryFor

func PilotingCategoryFor(f *models.Flight, acClass string) PilotingCategory

PilotingCategoryFor returns the bucket for `flight`. acClass is the user's stored AircraftClass for this registration (e.g. "SEP", "MEP", "SET", "MET", "SES"), passed in by callers that have aircraft data. When acClass is empty (no fleet entry available) the rule defaults to SP-SE.

Rule:

  • MultiPilotTime > 0 ⇒ MP (user-declared)
  • acClass starts with "ME" or "MET" or "SET" ⇒ SP-ME
  • otherwise ⇒ SP-SE

type RemarkFlag

type RemarkFlag int

RemarkFlag is an optional inline marker appended to a flight's remarks when present (FAA logbook convention). Order matches FAA convention: [IPC], then [FR] (Flight Review), then [PC] (Proficiency Check).

const (
	FlagIPC RemarkFlag = iota
	FlagFlightReview
	FlagProficiencyCheck
)

type Role

type Role int

Role classifies the user's pilot role on a flight, derived from the crew composition relative to the authenticated user's display name.

const (
	// RolePIC: user is sole/lead pilot, no instruction context.
	RolePIC Role = iota
	// RoleDualReceiving: a third-party Instructor on board is giving the
	// user instruction, or a third-party Examiner is conducting a check
	// ride (Dual received).
	RoleDualReceiving
	// RoleDualGiving: the user is acting as instructor — either a Student
	// is on board, or the user themselves is listed with the Instructor
	// role (Dual given / FI).
	RoleDualGiving
	// RoleSIC: the user is co-pilot on a multi-pilot operation — another
	// person is listed with the PIC role, or the user themselves is listed
	// with the SIC role. Per AMC1 FCL.050 only the designated PIC logs PIC
	// time; the co-pilot logs co-pilot (SIC) time, even if both pilots are
	// qualified PICs (FOCA GM/INFO "Logging of Flight Time" §2.3.3).
	RoleSIC
	// RolePassenger: another person is PIC and the operation does not carry
	// a second pilot, so the user is carried rather than crewed and logs no
	// pilot function time.
	RolePassenger
)

func DetermineRole

func DetermineRole(flight *models.Flight, userName string, aircraft *AircraftFacts) Role

DetermineRole inspects the crew list to classify the user's pilot role. aircraft carries the fleet facts for the registration flown, or nil when the registration has no fleet entry.

Precedence: a third-party Instructor or Examiner (name ≠ user) makes the user a Dual receiver, regardless of any Student also being present (NfL 2021-2-602 §4.2.2 no. 4; EASA AMC1 FCL.050). A Student or self-listed Instructor makes the user a Dual giver. A third-party PIC or a self-listed SIC makes the user the co-pilot (SIC) — but only where the operation permits co-pilot time (MayLogCoPilotTime); otherwise the user is carried as a passenger. A self-listed Examiner leaves the user as PIC; a self-listed PIC crew entry keeps the user as PIC and wins over a simultaneous third-party PIC entry. With no crew list at all, a declared SICTime makes the user the co-pilot. Otherwise the user is PIC.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL