Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions e2e/lib/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@ export type components = {
last_updated: string;
pilots: components["schemas"]["CompatPilotDto"][];
};
/** @description Seconds grouped by position suffix, independently of the controller's rating. */
/** @description Eligible seconds grouped by the controller rating required for the position. */
ControllerOnlineTimeByPosition: {
/**
* Format: int64
Expand All @@ -1398,8 +1398,7 @@ export type components = {
/** Format: date-time */
as_of: string;
/**
* @description Current-quarter seconds for S1 (GND/DEL/RMP), S2 (TWR), S3 (APP), C1+ (CTR).
* DEP and FSS count toward total_seconds only.
* @description Current-quarter eligible seconds for S1 (GND/DEL/RMP), S2 (TWR), S3 (APP), C1+ (CTR).
*/
by_position: components["schemas"]["ControllerOnlineTimeByPosition"];
lifetime: components["schemas"]["ControllerOnlineTimeSummaryDto"];
Expand All @@ -1410,11 +1409,11 @@ export type components = {
total_seconds: number;
};
ControllerOnlineTimeSummaryDto: {
/** @description Position groups in seconds. DEP and FSS count toward total_seconds only. */
/** @description Eligible position groups in seconds. */
by_position: components["schemas"]["ControllerOnlineTimeByPosition"];
/**
* Format: int64
* @description All recorded VATPRC controlling seconds, including the current session.
* @description All eligible VATPRC controlling seconds, including the current session.
*/
total_seconds: number;
};
Expand Down
6 changes: 4 additions & 2 deletions e2e/src/atc/online-time.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { expect, test } from "vitest";
import { getClient } from "../../lib/backend.js";

test("GET /api/users/me/atc/online-time returns quarter and lifetime totals by position", async () => {
const controller = await getClient(["controller"], { cid: "1573922" });
const controller = await getClient(["controller"], {
cid: process.env.E2E_CONTROLLER_CID ?? "1573922",
});

const { data, error, response } = await controller.GET(
"/api/users/me/atc/online-time",
Expand Down Expand Up @@ -55,6 +57,6 @@ test("GET /api/users/me/atc/online-time returns quarter and lifetime totals by p
for (const summary of [data, data.lifetime]) {
expect(
Object.values(summary.by_position).reduce((a, b) => a + b, 0),
).toBeLessThanOrEqual(summary.total_seconds);
).toBe(summary.total_seconds);
}
});
70 changes: 70 additions & 0 deletions src/adapter/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ impl CompatClient {
Ok(serde_json::from_str(&self.cached_get(&url).await?)?)
}

#[instrument(skip(self), fields(cid = %cid))]
pub async fn get_member(&self, cid: &str) -> Result<VatsimMember, CompatClientError> {
let url = format!("{VATSIM_CORE_BASE_URL}/members/{cid}");
Ok(serde_json::from_str(&self.cached_get(&url).await?)?)
}

#[instrument(skip(self))]
pub async fn get_track_audio_version(&self) -> Result<String, CompatClientError> {
self.cached_get(TRACK_AUDIO_VERSION_URL).await
Expand Down Expand Up @@ -231,3 +237,67 @@ pub struct AtcConnection {
pub start: Option<chrono::DateTime<Utc>>,
pub end: Option<chrono::DateTime<Utc>>,
}

#[derive(Debug, Deserialize)]
pub struct VatsimMember {
pub rating: VatsimRating,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(try_from = "i32")]
pub enum VatsimRating {
Inactive,
Suspended,
Observer,
S1,
S2,
S3,
C1,
C2,
C3,
I1,
I2,
I3,
Supervisor,
Administrator,
}

impl TryFrom<i32> for VatsimRating {
type Error = String;

fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
-1 => Ok(Self::Inactive),
0 => Ok(Self::Suspended),
1 => Ok(Self::Observer),
2 => Ok(Self::S1),
3 => Ok(Self::S2),
4 => Ok(Self::S3),
5 => Ok(Self::C1),
6 => Ok(Self::C2),
7 => Ok(Self::C3),
8 => Ok(Self::I1),
9 => Ok(Self::I2),
10 => Ok(Self::I3),
11 => Ok(Self::Supervisor),
12 => Ok(Self::Administrator),
_ => Err(format!("invalid VATSIM controller rating {value}")),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn deserializes_vatsim_member_rating_to_enum() {
let member = serde_json::from_str::<VatsimMember>(r#"{"rating":11}"#).unwrap();
assert_eq!(member.rating, VatsimRating::Supervisor);
}

#[test]
fn rejects_unknown_vatsim_member_rating() {
assert!(serde_json::from_str::<VatsimMember>(r#"{"rating":13}"#).is_err());
}
}
14 changes: 14 additions & 0 deletions src/modules/atc_position/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const SELECT: &str = r#"

pub(crate) trait AtcPositionRepository<'executor> {
async fn list_atc_positions(self) -> Result<Vec<AtcPosition>, sqlx::Error>;
async fn list_tier_2_atc_position_callsigns(self) -> Result<Vec<String>, sqlx::Error>;
async fn find_atc_position(self, callsign: &str) -> Result<Option<AtcPosition>, sqlx::Error>;
async fn find_atc_position_for_update(
self,
Expand Down Expand Up @@ -51,6 +52,19 @@ where
.await
}

async fn list_tier_2_atc_position_callsigns(self) -> Result<Vec<String>, sqlx::Error> {
sqlx::query_scalar(
r#"
SELECT callsign
FROM public.atc_position
WHERE is_tier_2
ORDER BY callsign
"#,
)
.fetch_all(self)
.await
}

async fn find_atc_position(self, callsign: &str) -> Result<Option<AtcPosition>, sqlx::Error> {
sqlx::query_as::<_, AtcPosition>(&format!("{SELECT} WHERE callsign = $1"))
.bind(callsign)
Expand Down
7 changes: 3 additions & 4 deletions src/modules/controller/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,16 @@ pub struct ControllerOnlineTimeDto {
pub period_start: DateTime<Utc>,
pub as_of: DateTime<Utc>,
pub total_seconds: u64,
/// Current-quarter seconds for S1 (GND/DEL/RMP), S2 (TWR), S3 (APP), C1+ (CTR).
/// DEP and FSS count toward total_seconds only.
/// Current-quarter eligible seconds for S1 (GND/DEL/RMP), S2 (TWR), S3 (APP), C1+ (CTR).
pub by_position: ControllerOnlineTimeByPosition,
pub lifetime: ControllerOnlineTimeSummaryDto,
}

#[derive(Serialize, utoipa::ToSchema)]
pub struct ControllerOnlineTimeSummaryDto {
/// All recorded VATPRC controlling seconds, including the current session.
/// All eligible VATPRC controlling seconds, including the current session.
pub total_seconds: u64,
/// Position groups in seconds. DEP and FSS count toward total_seconds only.
/// Eligible position groups in seconds.
pub by_position: ControllerOnlineTimeByPosition,
}

Expand Down
2 changes: 1 addition & 1 deletion src/modules/controller/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct ControllerOnlineTimeSummary {
pub by_position: ControllerOnlineTimeByPosition,
}

/// Seconds grouped by position suffix, independently of the controller's rating.
/// Eligible seconds grouped by the controller rating required for the position.
#[derive(Default, Debug, PartialEq, Eq, Serialize, utoipa::ToSchema)]
pub struct ControllerOnlineTimeByPosition {
/// GND, DEL and RMP seconds.
Expand Down
Loading
Loading