use super::map_db_error;
use chrono::{DateTime, Utc};
use sqlx::FromRow;
use sqlx::postgres::PgPool;
use stateset_core::{
CommerceError, CreateEdiDocument, EdiAggregateSummary, EdiCount, EdiDirection, EdiDocument,
EdiDocumentFilter, EdiDocumentId, EdiDocumentRepository, EdiStatus, Result,
};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct PgEdiDocumentRepository {
pool: PgPool,
}
#[derive(FromRow)]
struct EdiDocumentRow {
id: Uuid,
document_type: String,
direction: String,
status: String,
partner: Option<String>,
reference: Option<String>,
payload: Option<String>,
error_message: Option<String>,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
impl PgEdiDocumentRepository {
pub const fn new(pool: PgPool) -> Self {
Self { pool }
}
fn row_to_doc(row: EdiDocumentRow) -> Result<EdiDocument> {
let direction: EdiDirection = row.direction.parse().map_err(|e| {
CommerceError::DatabaseError(format!(
"Invalid edi_document.direction '{}': {}",
row.direction, e
))
})?;
let status: EdiStatus = row.status.parse().map_err(|e| {
CommerceError::DatabaseError(format!(
"Invalid edi_document.status '{}': {}",
row.status, e
))
})?;
Ok(EdiDocument {
id: row.id.into(),
document_type: row.document_type,
direction,
status,
partner: row.partner,
reference: row.reference,
payload: row.payload,
error_message: row.error_message,
created_at: row.created_at,
updated_at: row.updated_at,
})
}
async fn fetch_async(&self, id: Uuid) -> Result<Option<EdiDocument>> {
let row = sqlx::query_as::<_, EdiDocumentRow>("SELECT * FROM edi_documents WHERE id = $1")
.bind(id)
.fetch_optional(&self.pool)
.await
.map_err(map_db_error)?;
row.map(Self::row_to_doc).transpose()
}
pub async fn create_async(&self, input: CreateEdiDocument) -> Result<EdiDocument> {
let id = EdiDocumentId::new();
let now = Utc::now();
sqlx::query(
"INSERT INTO edi_documents (id, document_type, direction, status, partner, reference, payload, created_at, updated_at)
VALUES ($1, $2, $3, 'pending', $4, $5, $6, $7, $7)",
)
.bind(Uuid::from(id))
.bind(&input.document_type)
.bind(input.direction.to_string())
.bind(&input.partner)
.bind(&input.reference)
.bind(&input.payload)
.bind(now)
.execute(&self.pool)
.await
.map_err(map_db_error)?;
self.fetch_async(id.into()).await?.ok_or(CommerceError::NotFound)
}
pub async fn get_async(&self, id: EdiDocumentId) -> Result<Option<EdiDocument>> {
self.fetch_async(id.into()).await
}
pub async fn list_async(&self, filter: EdiDocumentFilter) -> Result<Vec<EdiDocument>> {
let limit = super::effective_limit(filter.limit);
let offset = i64::from(filter.offset.unwrap_or(0));
let mut query = String::from("SELECT * FROM edi_documents WHERE 1=1");
let mut param_idx = 1;
if filter.document_type.is_some() {
query.push_str(&format!(" AND document_type = ${param_idx}"));
param_idx += 1;
}
if filter.direction.is_some() {
query.push_str(&format!(" AND direction = ${param_idx}"));
param_idx += 1;
}
if filter.status.is_some() {
query.push_str(&format!(" AND status = ${param_idx}"));
param_idx += 1;
}
if filter.partner.is_some() {
query.push_str(&format!(" AND partner = ${param_idx}"));
param_idx += 1;
}
query.push_str(&format!(
" ORDER BY created_at DESC LIMIT ${} OFFSET ${}",
param_idx,
param_idx + 1
));
let mut q = sqlx::query_as::<_, EdiDocumentRow>(&query);
if let Some(ref document_type) = filter.document_type {
q = q.bind(document_type);
}
if let Some(direction) = filter.direction {
q = q.bind(direction.to_string());
}
if let Some(status) = filter.status {
q = q.bind(status.to_string());
}
if let Some(ref partner) = filter.partner {
q = q.bind(partner);
}
let rows = q.bind(limit).bind(offset).fetch_all(&self.pool).await.map_err(map_db_error)?;
rows.into_iter().map(Self::row_to_doc).collect()
}
pub async fn set_status_async(
&self,
id: EdiDocumentId,
status: EdiStatus,
error_message: Option<String>,
) -> Result<EdiDocument> {
let now = Utc::now();
sqlx::query(
"UPDATE edi_documents SET status = $1, error_message = $2, updated_at = $3 WHERE id = $4",
)
.bind(status.to_string())
.bind(&error_message)
.bind(now)
.bind(Uuid::from(id))
.execute(&self.pool)
.await
.map_err(map_db_error)?;
self.fetch_async(id.into()).await?.ok_or(CommerceError::NotFound)
}
pub async fn summary_async(&self) -> Result<EdiAggregateSummary> {
let total: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM edi_documents")
.fetch_one(&self.pool)
.await
.map_err(map_db_error)?;
let by_status: Vec<(String, i64)> = sqlx::query_as(
"SELECT status, COUNT(*) FROM edi_documents GROUP BY status ORDER BY status",
)
.fetch_all(&self.pool)
.await
.map_err(map_db_error)?;
let by_type: Vec<(String, i64)> = sqlx::query_as(
"SELECT document_type, COUNT(*) FROM edi_documents GROUP BY document_type ORDER BY document_type",
)
.fetch_all(&self.pool)
.await
.map_err(map_db_error)?;
let to_counts = |rows: Vec<(String, i64)>| -> Vec<EdiCount> {
rows.into_iter()
.map(|(key, count)| EdiCount { key, count: count.unsigned_abs() })
.collect()
};
Ok(EdiAggregateSummary {
total: total.unsigned_abs(),
by_status: to_counts(by_status),
by_type: to_counts(by_type),
})
}
}
impl EdiDocumentRepository for PgEdiDocumentRepository {
fn create(&self, input: CreateEdiDocument) -> Result<EdiDocument> {
super::block_on(self.create_async(input))
}
fn get(&self, id: EdiDocumentId) -> Result<Option<EdiDocument>> {
super::block_on(self.get_async(id))
}
fn list(&self, filter: EdiDocumentFilter) -> Result<Vec<EdiDocument>> {
super::block_on(self.list_async(filter))
}
fn set_status(
&self,
id: EdiDocumentId,
status: EdiStatus,
error_message: Option<String>,
) -> Result<EdiDocument> {
super::block_on(self.set_status_async(id, status, error_message))
}
fn summary(&self) -> Result<EdiAggregateSummary> {
super::block_on(self.summary_async())
}
}