use crate::models::{CreateQuoteRequest, Quote, QuoteState, UpdateQuoteRequest};
use chrono::Utc;
use sqlx::{Pool, Postgres, Row};
use tmf_apis_core::{TmfError, TmfResult};
use uuid::Uuid;
fn map_sqlx_error(err: sqlx::Error) -> TmfError {
TmfError::Database(err.to_string())
}
fn parse_quote_state(s: &str) -> QuoteState {
match s.to_uppercase().as_str() {
"IN_PROGRESS" => QuoteState::InProgress,
"READY" => QuoteState::Ready,
"CANCELLED" => QuoteState::Cancelled,
"ACCEPTED" => QuoteState::Accepted,
"REJECTED" => QuoteState::Rejected,
"EXPIRED" => QuoteState::Expired,
_ => QuoteState::InProgress,
}
}
fn quote_state_to_string(state: &QuoteState) -> String {
match state {
QuoteState::InProgress => "IN_PROGRESS".to_string(),
QuoteState::Ready => "READY".to_string(),
QuoteState::Cancelled => "CANCELLED".to_string(),
QuoteState::Accepted => "ACCEPTED".to_string(),
QuoteState::Rejected => "REJECTED".to_string(),
QuoteState::Expired => "EXPIRED".to_string(),
}
}
fn row_to_quote(row: &sqlx::postgres::PgRow) -> Quote {
use tmf_apis_core::BaseEntity;
use tmf_apis_core::LifecycleStatus;
Quote {
base: BaseEntity {
id: row.get("id"),
href: row.get("href"),
name: row.get("name"),
description: row.get("description"),
version: row.get("version"),
lifecycle_status: LifecycleStatus::Active,
last_update: row.get("last_update"),
valid_for: None,
},
state: parse_quote_state(row.get("state")),
quote_item: None, related_party: None, quote_date: row.get("quote_date"),
valid_until: row.get("valid_until"),
total_price: row
.try_get::<Option<serde_json::Value>, _>("total_price")
.ok()
.flatten()
.and_then(|v| serde_json::from_value(v).ok()),
expected_order_date: row.get("expected_order_date"),
}
}
pub async fn get_quotes(pool: &Pool<Postgres>) -> TmfResult<Vec<Quote>> {
let rows = sqlx::query(
"SELECT id, href, name, description, version, state, quote_date,
valid_until, total_price, expected_order_date, last_update
FROM quotes ORDER BY quote_date DESC",
)
.fetch_all(pool)
.await
.map_err(map_sqlx_error)?;
Ok(rows.iter().map(row_to_quote).collect())
}
pub async fn get_quote_by_id(pool: &Pool<Postgres>, id: Uuid) -> TmfResult<Option<Quote>> {
let row = sqlx::query(
"SELECT id, href, name, description, version, state, quote_date,
valid_until, total_price, expected_order_date, last_update
FROM quotes WHERE id = $1",
)
.bind(id)
.fetch_optional(pool)
.await
.map_err(map_sqlx_error)?;
Ok(row.map(|r| row_to_quote(&r)))
}
pub async fn create_quote(pool: &Pool<Postgres>, request: CreateQuoteRequest) -> TmfResult<Quote> {
let id = Uuid::new_v4();
let now = Utc::now();
let href = format!("/tmf-api/quoteManagement/v4/quote/{}", id);
let state = quote_state_to_string(&QuoteState::InProgress);
let total_price_json = None::<serde_json::Value>;
sqlx::query(
"INSERT INTO quotes (
id, href, name, description, version, state, quote_date,
valid_until, total_price, expected_order_date, created_at, last_update
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
)
.bind(id)
.bind(&href)
.bind(&request.name)
.bind(&request.description)
.bind(request.version.as_deref().unwrap_or("1.0.0"))
.bind(&state)
.bind(now)
.bind(request.valid_until)
.bind(total_price_json)
.bind(request.expected_order_date)
.bind(now)
.bind(now)
.execute(pool)
.await
.map_err(map_sqlx_error)?;
if let Some(items) = request.quote_item {
for item in items {
let item_id = Uuid::new_v4();
let unit_price_json = item
.unit_price
.as_ref()
.and_then(|m| serde_json::to_value(m).ok());
let item_total_price = item.unit_price.as_ref().and_then(|up| {
item.quantity.map(|q| crate::models::Money {
value: up.value * q as f64,
unit: up.unit.clone(),
})
});
let item_total_price_json = item_total_price
.as_ref()
.and_then(|m| serde_json::to_value(m).ok());
sqlx::query(
"INSERT INTO quote_items (
id, quote_id, product_offering_id, product_specification_id,
quantity, unit_price, total_price
) VALUES ($1, $2, $3, $4, $5, $6, $7)",
)
.bind(item_id)
.bind(id)
.bind(item.product_offering_id)
.bind(item.product_specification_id)
.bind(item.quantity)
.bind(unit_price_json)
.bind(item_total_price_json)
.execute(pool)
.await
.map_err(map_sqlx_error)?;
}
}
if let Some(parties) = request.related_party {
for party in parties {
let party_id = Uuid::new_v4();
sqlx::query(
"INSERT INTO quote_related_parties (id, quote_id, name, role)
VALUES ($1, $2, $3, $4)",
)
.bind(party_id)
.bind(id)
.bind(&party.name)
.bind(&party.role)
.execute(pool)
.await
.map_err(map_sqlx_error)?;
}
}
get_quote_by_id(pool, id)
.await?
.ok_or_else(|| TmfError::NotFound("Quote not found after creation".to_string()))
}
pub async fn update_quote(
pool: &Pool<Postgres>,
id: Uuid,
request: UpdateQuoteRequest,
) -> TmfResult<Quote> {
let state_str = request.state.as_ref().map(quote_state_to_string);
sqlx::query(
"UPDATE quotes SET
state = COALESCE($1, state),
description = COALESCE($2, description),
valid_until = COALESCE($3, valid_until),
last_update = CURRENT_TIMESTAMP
WHERE id = $4",
)
.bind(state_str)
.bind(&request.description)
.bind(request.valid_until)
.bind(id)
.execute(pool)
.await
.map_err(map_sqlx_error)?;
get_quote_by_id(pool, id)
.await?
.ok_or_else(|| TmfError::NotFound("Quote not found".to_string()))
}
pub async fn delete_quote(pool: &Pool<Postgres>, id: Uuid) -> TmfResult<()> {
let result = sqlx::query("DELETE FROM quotes WHERE id = $1")
.bind(id)
.execute(pool)
.await
.map_err(map_sqlx_error)?;
if result.rows_affected() == 0 {
return Err(TmfError::NotFound("Quote not found".to_string()));
}
Ok(())
}