use sqlx::{PgConnection, Row};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct BatchHeaderRow {
pub id: Uuid,
pub name: String,
pub state: String,
pub is_wave: bool,
pub user_id: Option<Uuid>,
pub scheduled_date: chrono::DateTime<chrono::Utc>,
pub had_members: bool,
}
#[derive(Debug, Clone)]
pub struct BatchMemberRow {
pub id: Uuid,
pub name: String,
pub state: String,
}
pub struct NewBatchRow<'a> {
pub id: Uuid,
pub name: &'a str,
pub is_wave: bool,
pub user_id: Option<Uuid>,
}
#[derive(Default)]
pub struct PickingBatchProjectionRepository;
impl PickingBatchProjectionRepository {
pub fn new() -> Self {
Self
}
pub async fn insert_batch(
&self,
conn: &mut PgConnection,
b: &NewBatchRow<'_>,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"INSERT INTO inventory.picking_batches
(id, name, is_wave, user_id, scheduled_date, state, had_members)
VALUES ($1, $2, $3, $4, NOW(), 'draft'::picking_batch_state, FALSE)"#,
)
.bind(b.id)
.bind(b.name)
.bind(b.is_wave)
.bind(b.user_id)
.execute(conn)
.await?;
Ok(())
}
pub async fn fetch_batch(
&self,
conn: &mut PgConnection,
batch_id: Uuid,
) -> Result<Option<BatchHeaderRow>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT id, name, state::text AS state, is_wave, user_id,
scheduled_date, had_members
FROM inventory.picking_batches
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(batch_id)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| BatchHeaderRow {
id: r.get("id"),
name: r.get("name"),
state: r.get("state"),
is_wave: r.get("is_wave"),
user_id: r.get("user_id"),
scheduled_date: r.get("scheduled_date"),
had_members: r.get("had_members"),
}))
}
pub async fn fetch_members(
&self,
conn: &mut PgConnection,
batch_id: Uuid,
) -> Result<Vec<BatchMemberRow>, sqlx::Error> {
let rows = sqlx::query(
r#"SELECT id, name, state::text AS state
FROM inventory.transfers
WHERE batch_id = $1 AND (metadata->>'deleted_at') IS NULL
ORDER BY scheduled_date, id"#,
)
.bind(batch_id)
.fetch_all(conn)
.await?;
Ok(rows
.iter()
.map(|r| BatchMemberRow {
id: r.get("id"),
name: r.get("name"),
state: r.get("state"),
})
.collect())
}
pub async fn add_member(
&self,
conn: &mut PgConnection,
batch_id: Uuid,
picking_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE inventory.transfers
SET batch_id = $2
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(picking_id)
.bind(batch_id)
.execute(&mut *conn)
.await?;
sqlx::query(
r#"UPDATE inventory.picking_batches
SET had_members = TRUE
WHERE id = $1"#,
)
.bind(batch_id)
.execute(conn)
.await?;
Ok(())
}
pub async fn remove_member(
&self,
conn: &mut PgConnection,
batch_id: Uuid,
picking_id: Uuid,
) -> Result<bool, sqlx::Error> {
let res = sqlx::query(
r#"UPDATE inventory.transfers
SET batch_id = NULL
WHERE id = $1 AND batch_id = $2 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(picking_id)
.bind(batch_id)
.execute(conn)
.await?;
Ok(res.rows_affected() == 1)
}
pub async fn reproject_batch(
&self,
conn: &mut PgConnection,
batch_id: Uuid,
) -> Result<Option<String>, sqlx::Error> {
let row = sqlx::query(
r#"WITH agg AS (
SELECT
COUNT(*) FILTER (WHERE state <> 'cancel'::transfer_state) AS live_count,
COUNT(*) AS total_count,
MIN(CASE state WHEN 'draft'::transfer_state THEN 0
WHEN 'confirmed'::transfer_state THEN 0
WHEN 'waiting'::transfer_state THEN 1
WHEN 'assigned'::transfer_state THEN 2
WHEN 'done'::transfer_state THEN 3
ELSE 4 END)
FILTER (WHERE state <> 'cancel'::transfer_state) AS rank
FROM inventory.transfers
WHERE batch_id = $1 AND (metadata->>'deleted_at') IS NULL
)
UPDATE inventory.picking_batches b
SET state = CASE
WHEN agg.live_count > 0 THEN CASE agg.rank
WHEN 0 THEN 'draft'::picking_batch_state
WHEN 1 THEN 'waiting'::picking_batch_state
WHEN 2 THEN 'ready'::picking_batch_state
WHEN 3 THEN 'done'::picking_batch_state
ELSE 'draft'::picking_batch_state END
WHEN agg.total_count > 0 THEN 'cancel'::picking_batch_state
WHEN b.had_members THEN 'cancel'::picking_batch_state
ELSE 'draft'::picking_batch_state END
FROM agg
WHERE b.id = $1
RETURNING b.state::text"#,
)
.bind(batch_id)
.fetch_optional(&mut *conn)
.await?;
Ok(row.map(|r| r.get::<String, _>("state")))
}
pub async fn picking_membership(
&self,
conn: &mut PgConnection,
picking_id: Uuid,
) -> Result<Option<Option<Uuid>>, sqlx::Error> {
let row = sqlx::query_scalar::<_, Option<Uuid>>(
r#"SELECT batch_id FROM inventory.transfers
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(picking_id)
.fetch_optional(&mut *conn)
.await?;
Ok(row)
}
pub async fn reproject_batch_of_picking(
&self,
conn: &mut PgConnection,
picking_id: Uuid,
) -> Result<Option<String>, sqlx::Error> {
let batch_id: Option<Uuid> = sqlx::query_scalar(
r#"SELECT batch_id FROM inventory.transfers
WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(picking_id)
.fetch_optional(&mut *conn)
.await?
.flatten();
match batch_id {
Some(b) => self.reproject_batch(conn, b).await,
None => Ok(None),
}
}
}