use fathom_function::{chrono::NaiveDate, forms::deserialize_date, tracing}; use pipeline_application::{ application::{ Application, FieldAssessment, PurposeOfMeasurement, SurfaceLocation, features::{FeatureIdentification, FeatureType}, orientation::Orientation, }, serialization::{serialize_meter, serialize_millimeter}, }; use serde::{Deserialize, Serialize}; use serde_this_or_that::as_opt_u64; use uom::si::f64::Length; use uuid::Uuid; #[fathom_function::function] async fn upload_field_assessment(input: Input) -> Result { let app = Application::new_from_compile_env(input.org_id, input.project_id).unwrap(); for pipeline_id in input.pipeline_id { app.upload_field_assessment(pipeline_id, &input.report) .await .map_err(|err| { tracing::error!(%pipeline_id, ?err, "Error uploading field assessment"); format!("{err:?}") })?; } Ok(Output { status: "Success".to_owned(), }) } #[derive(Debug, Serialize)] struct Output { status: String, } #[derive(Debug, Deserialize)] struct Input { org_id: Uuid, project_id: String, pipeline_id: Vec, #[serde(flatten)] report: Report, } #[derive(Debug, Deserialize)] struct Report { /// The date of this field inspection. #[serde(deserialize_with = "deserialize_date")] date: NaiveDate, /// The log distance: the absolute distance measured in meters from the launcher. #[serde(with = "serialize_meter")] log_distance: Length, /// The Girthweld number. These follow a sequential order of 10 (e.g., 10, 20, 30, ...) to /// enumerate each joint from the initial to the final one. This ordering system facilitates /// future repairs using the "Pipeline Sectional Replacement" method. #[serde(deserialize_with = "as_opt_u64")] girth_weld: Option, /// The circumferential position of the starting point of an anomaly, specifically the top-left /// position on the pipe. /// /// This orientation is measured in O'clock notation, ranging from 00:00 to 12:00, indicating /// the position around the circumference of the pipe. This is the number of hours #[serde(deserialize_with = "as_opt_u64")] anomaly_orientation_hours: Option, /// The circumferential position of the starting point of an anomaly, specifically the top-left /// position on the pipe. /// /// This orientation is measured in O'clock notation, ranging from 00:00 to 12:00, indicating /// the position around the circumference of the pipe. This is the number of minutes #[serde(deserialize_with = "as_opt_u64")] anomaly_orientation_minutes: Option, /// The measured wall thickness of the pipeline taken from an un-corroded location. Corresponds /// to the un-corroded wall thickness. #[serde(with = "serialize_millimeter::opt")] measured_wall_thickness: Option, /// The measured wall thickness of the pipeline taken from the river bottom of the corrosion. #[serde(with = "serialize_millimeter::opt")] measured_remaining_wall_thickness: Option, /// The surface location of the anomaly. /// /// Each identified anomaly, denoted as ANOM, falls into one of four categories: Internal /// (INT), External (EXT), Midwall (MID), or Unknown (UNK) for any reason. These /// classifications are assigned based on the specific characteristics and location of the /// anomaly on the pipeline. The categorization helps provide a comprehensive understanding of /// the nature of the anomalies present. surface_location: Option, /// The measured length (in mm) of an anomaly, (the length of the corrosion box) in relation to /// the reporting threshold. #[serde(with = "serialize_millimeter")] measured_length: Length, /// The circumferential width of an anomaly, this denotes the measured measurement of the /// anomaly's width in circumferential direction, comparable to the width of the corrosion box, /// concerning the reporting threshold and the unit is mm. #[serde(with = "serialize_millimeter::opt")] measured_width: Option, /// The measured depth of the anomaly. /// /// This measurement is essential for evaluating the structural integrity of the pipeline. A /// higher percentage of metal loss indicates a more significant impact on the pipeline's /// strength and durability. #[serde(with = "serialize_millimeter")] measured_max_depth: Length, /// The reason for this field assessment purpose_of_measurement: PurposeOfMeasurement, /// The accuracy of the tool used #[serde(with = "serialize_millimeter::opt")] tool_accuracy: Option, /// The accuracy of the pit gauge used #[serde(with = "serialize_millimeter::opt")] pit_gauge_accuracy: Option, /// The inspector name or identifier inspector: String, /// And additional remarks about the inspection remarks: Option, } impl Report { fn orientation(&self) -> Option { Some(Orientation::new( self.anomaly_orientation_hours? as _, self.anomaly_orientation_minutes? as _, )) } } impl From<&Report> for FieldAssessment { fn from(value: &Report) -> Self { Self { date: value.date.and_time(Default::default()).and_utc(), log_distance: value.log_distance, feature_type: Some(FeatureType::ANOM), feature_identification: Some(FeatureIdentification::CORR), girth_weld: value.girth_weld.map(|v| v as _), anomaly_orientation: value.orientation(), measured_wall_thickness: value.measured_wall_thickness, measured_remaining_wall_thickness: value.measured_remaining_wall_thickness, surface_location: value.surface_location, measured_length: value.measured_length, measured_width: value.measured_width, measured_max_depth: value.measured_max_depth, purpose_of_measurement: value.purpose_of_measurement.to_owned(), tool_accuracy: value.tool_accuracy, pit_gauge_accuracy: value.pit_gauge_accuracy, inspector: value.inspector.to_owned(), remarks: value.remarks.to_owned(), } } }