airlab_lib/model/
lot.rs

1use crate::ctx::Ctx;
2use crate::model::ModelManager;
3use crate::model::Result;
4use crate::model::base::{self, DbBmc};
5use chrono::prelude::*;
6use modql::field::Fields;
7use modql::filter::{FilterNodes, ListOptions, OpValsInt64, OpValsString};
8use serde::{Deserialize, Serialize};
9use sqlx::FromRow;
10use tracing::warn;
11
12impl LotBmc {
13    #[must_use]
14    pub fn get_create_sql(drop_table: bool) -> String {
15        let table = Self::TABLE;
16        format!(
17            r##"{}
18create table if not exists "{table}" (
19  id serial primary key,
20  group_id integer NOT NULL,
21  created_by integer NOT NULL,
22  clone_id integer NOT NULL,
23  provider_id integer,
24  name character varying NOT NULL,
25  reference character varying,
26  requested_by integer,
27  approved_by integer,
28  ordered_by integer,
29  received_by integer,
30  finished_by integer,
31  number character varying,
32  status smallint DEFAULT 0 NOT NULL,
33  purpose character varying,
34  url character varying,
35  price character varying,
36  note character varying,
37  requested_at timestamp with time zone,
38  approved_at timestamp with time zone,
39  ordered_at timestamp with time zone,
40  received_at timestamp with time zone,
41  finished_at timestamp with time zone,
42  is_archived boolean DEFAULT false NOT NULL,
43  meta jsonb,
44  created_at timestamp with time zone DEFAULT now() NOT NULL,
45  updated_at timestamp with time zone DEFAULT now() NOT NULL
46);
47CREATE INDEX "IDX_lot_clone_id" ON lot USING btree (clone_id);
48CREATE INDEX "IDX_lot_created_by" ON lot USING btree (created_by);
49CREATE INDEX "IDX_lot_group_id" ON lot USING btree (group_id);
50CREATE INDEX "IDX_lot_provider_id" ON lot USING btree (provider_id);
51CREATE INDEX "IDX_lot_status" ON lot USING btree (status);
52        "##,
53            if drop_table {
54                format!("drop table if exists {table};")
55            } else {
56                String::new()
57            }
58        )
59    }
60}
61
62#[derive(Debug, Clone, Fields, FromRow, Serialize, Default, Deserialize)]
63pub struct Lot {
64    pub id: i32,
65    #[serde(rename = "groupId")]
66    pub group_id: i32,
67    #[serde(rename = "createdBy")]
68    pub created_by: Option<i32>,
69
70    #[serde(rename = "cloneId")]
71    pub clone_id: i32,
72    #[serde(rename = "providerId")]
73    pub provider_id: Option<i32>,
74    pub name: String,
75    pub reference: Option<String>,
76    #[serde(rename = "requestedBy")]
77    pub requested_by: Option<i32>,
78    #[serde(rename = "approvedBy")]
79    pub approved_by: Option<i32>,
80    #[serde(rename = "orderedBy")]
81    pub ordered_by: Option<i32>,
82    #[serde(rename = "receivedBy")]
83    pub received_by: Option<i32>,
84    #[serde(rename = "finishedBy")]
85    pub finished_by: Option<i32>,
86    pub number: Option<String>,
87    pub status: Option<i16>,
88    pub purpose: Option<String>,
89    pub url: Option<String>,
90    pub price: Option<String>,
91    pub note: Option<String>,
92    #[serde(rename = "requestedAt")]
93    pub requested_at: Option<chrono::DateTime<chrono::Utc>>,
94    #[serde(rename = "orderedAt")]
95    pub ordered_at: Option<chrono::DateTime<chrono::Utc>>,
96    #[serde(rename = "receivedAt")]
97    pub received_at: Option<chrono::DateTime<chrono::Utc>>,
98    #[serde(rename = "finishedAt")]
99    pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
100    #[serde(rename = "isArchived")]
101    pub is_archived: bool,
102    pub meta: Option<serde_json::Value>,
103    #[serde(rename = "createdAt")]
104    pub created_at: chrono::DateTime<chrono::Utc>,
105    #[serde(rename = "updatedAt")]
106    pub updated_at: chrono::DateTime<chrono::Utc>,
107}
108
109#[derive(Fields, Deserialize, Clone, Debug)]
110pub struct LotForCreate {
111    pub name: String,
112    #[serde(rename = "groupId")]
113    pub group_id: i32,
114    #[serde(rename = "cloneId")]
115    pub clone_id: i32,
116    #[serde(rename = "createdBy")]
117    pub created_by: Option<i32>,
118    #[serde(rename = "providerId")]
119    pub provider_id: Option<i32>,
120    pub reference: Option<String>,
121    #[serde(rename = "requestedBy")]
122    pub requested_by: Option<i32>,
123    #[serde(rename = "approvedBy")]
124    pub approved_by: Option<i32>,
125    #[serde(rename = "orderedBy")]
126    pub ordered_by: Option<i32>,
127    #[serde(rename = "receivedBy")]
128    pub received_by: Option<i32>,
129    #[serde(rename = "finishedBy")]
130    pub finished_by: Option<i32>,
131    pub status: Option<i16>,
132    pub purpose: Option<String>,
133    pub url: Option<String>,
134    pub price: Option<String>,
135    pub note: Option<String>,
136    #[serde(rename = "requestedAt")]
137    pub requested_at: Option<chrono::DateTime<chrono::Utc>>,
138    #[serde(rename = "orderedAt")]
139    pub ordered_at: Option<chrono::DateTime<chrono::Utc>>,
140    #[serde(rename = "receivedAt")]
141    pub received_at: Option<chrono::DateTime<chrono::Utc>>,
142    #[serde(rename = "finishedAt")]
143    pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
144    #[serde(rename = "isArchived")]
145    pub is_archived: Option<bool>,
146    //pub meta: Option<String>,
147}
148
149#[derive(Fields, Default, Deserialize, Debug)]
150pub struct LotForUpdate {
151    pub name: Option<String>,
152    pub reference: Option<String>,
153    #[serde(rename = "createdBy")]
154    pub created_by: Option<i32>,
155    #[serde(rename = "requestedBy")]
156    pub requested_by: Option<i32>,
157    #[serde(rename = "approvedBy")]
158    pub approved_by: Option<i32>,
159    #[serde(rename = "orderedBy")]
160    pub ordered_by: Option<i32>,
161    #[serde(rename = "receivedBy")]
162    pub received_by: Option<i32>,
163    #[serde(rename = "finishedBy")]
164    pub finished_by: Option<i32>,
165    pub status: Option<i16>,
166    pub number: Option<String>,
167    pub purpose: Option<String>,
168    pub url: Option<String>,
169    pub price: Option<String>,
170    pub note: Option<String>,
171    #[serde(rename = "approvedAt")]
172    pub approved_at: Option<chrono::DateTime<chrono::Utc>>,
173    #[serde(rename = "requestedAt")]
174    pub requested_at: Option<chrono::DateTime<chrono::Utc>>,
175    #[serde(rename = "orderedAt")]
176    pub ordered_at: Option<chrono::DateTime<chrono::Utc>>,
177    #[serde(rename = "receivedAt")]
178    pub received_at: Option<chrono::DateTime<chrono::Utc>>,
179    #[serde(rename = "finishedAt")]
180    pub finished_at: Option<chrono::DateTime<chrono::Utc>>,
181    #[serde(rename = "updatedAt")]
182    pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
183    #[serde(rename = "isArchived")]
184    pub is_archived: Option<bool>,
185    //pub meta: Option<String>,
186}
187
188#[derive(FilterNodes, Deserialize, Default, Debug)]
189pub struct LotFilter {
190    id: Option<OpValsInt64>,
191    group_id: Option<OpValsInt64>,
192    clone_id: Option<OpValsInt64>,
193    provider_id: Option<OpValsInt64>,
194    status: Option<OpValsInt64>,
195    name: Option<OpValsString>,
196}
197
198pub struct LotBmc;
199
200impl DbBmc for LotBmc {
201    const TABLE: &'static str = "lot";
202}
203
204impl LotBmc {
205    pub async fn create(ctx: &Ctx, mm: &ModelManager, lot_c: LotForCreate) -> Result<i32> {
206        base::create::<Self, _>(ctx, mm, lot_c).await
207    }
208    pub async fn create_full(ctx: &Ctx, mm: &ModelManager, lot_c: Lot) -> Result<i32> {
209        base::create::<Self, _>(ctx, mm, lot_c).await
210    }
211
212    pub async fn get(ctx: &Ctx, mm: &ModelManager, id: i32) -> Result<Lot> {
213        base::get::<Self, _>(ctx, mm, id).await
214    }
215
216    pub async fn list(
217        ctx: &Ctx,
218        mm: &ModelManager,
219        filters: Option<Vec<LotFilter>>,
220        list_options: Option<ListOptions>,
221    ) -> Result<Vec<Lot>> {
222        base::list::<Self, _, _>(ctx, mm, filters, list_options).await
223    }
224
225    pub async fn update(
226        ctx: &Ctx,
227        mm: &ModelManager,
228        id: i32,
229        member_id: i32,
230        mut lot_u: LotForUpdate,
231    ) -> Result<()> {
232        warn!("{:?}", lot_u.status);
233        if let Some(status) = lot_u.status {
234            if status == 1 {
235                lot_u.approved_by = Some(member_id);
236                lot_u.approved_at = Some(Utc::now());
237            } else if status == 3 {
238                lot_u.ordered_by = Some(member_id);
239                lot_u.ordered_at = Some(Utc::now());
240            } else if status == 4 {
241                lot_u.received_by = Some(member_id);
242                lot_u.received_at = Some(Utc::now());
243            } else if status == 6 {
244                lot_u.finished_by = Some(member_id);
245                lot_u.finished_at = Some(Utc::now());
246            }
247        }
248        lot_u.updated_at = Some(Utc::now());
249        base::update::<Self, _>(ctx, mm, id, lot_u).await
250    }
251
252    pub async fn delete(ctx: &Ctx, mm: &ModelManager, id: i32) -> Result<()> {
253        base::delete::<Self>(ctx, mm, id).await
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::_dev_utils;
261    use crate::model::Error;
262    use anyhow::Result;
263    use serde_json::json;
264
265    #[ignore]
266    #[tokio::test]
267    async fn test_lot_create_ok() -> Result<()> {
268        let mm = ModelManager::new().await?;
269        let ctx = Ctx::root_ctx();
270        let fx_name = "test_create_ok name";
271
272        let lot_c = LotForCreate {
273            name: fx_name.to_string(),
274            created_by: Some(261),
275            group_id: 1,
276            clone_id: 3123,
277            provider_id: Some(103),
278            reference: None,
279            approved_by: None,
280            finished_by: None,
281            finished_at: None,
282            requested_by: None,
283            is_archived: Some(false),
284            ordered_at: None,
285            note: None,
286            ordered_by: None,
287            received_by: None,
288            price: None,
289            purpose: None,
290            status: None,
291            received_at: None,
292            requested_at: None,
293            url: None,
294        };
295        let id = LotBmc::create(&ctx, &mm, lot_c).await?;
296
297        let lot = LotBmc::get(&ctx, &mm, id).await?;
298        assert_eq!(lot.name, fx_name);
299
300        LotBmc::delete(&ctx, &mm, id).await?;
301
302        Ok(())
303    }
304
305    #[ignore]
306    #[tokio::test]
307    async fn test_lot_get_err_not_found() -> Result<()> {
308        let mm = ModelManager::new().await?;
309        let ctx = Ctx::root_ctx();
310        let fx_id = 100;
311
312        let res = LotBmc::get(&ctx, &mm, fx_id).await;
313
314        assert!(
315            matches!(
316                res,
317                Err(Error::EntityNotFound {
318                    entity: "lot",
319                    id: 100
320                })
321            ),
322            "EntityNotFound not matching"
323        );
324
325        Ok(())
326    }
327
328    #[ignore]
329    #[tokio::test]
330    async fn test_lot_list_all_ok() -> Result<()> {
331        let mm = _dev_utils::init_test().await;
332        let ctx = Ctx::root_ctx();
333        let tname = "test_lot_update_ok";
334        let seeds = _dev_utils::get_lot_seed(tname);
335        _dev_utils::seed_lots(&ctx, &mm, &seeds).await?;
336
337        let lots = LotBmc::list(&ctx, &mm, None, None).await?;
338
339        let lots: Vec<Lot> = lots
340            .into_iter()
341            .filter(|t| t.name.starts_with("test_list_all_ok-lot"))
342            .collect();
343        assert_eq!(lots.len(), 4, "number of seeded lots.");
344
345        if false {
346            for lot in lots.iter() {
347                LotBmc::delete(&ctx, &mm, lot.id).await?;
348            }
349        }
350
351        Ok(())
352    }
353
354    #[ignore]
355    #[tokio::test]
356    async fn test_lot_list_by_filter_ok() -> Result<()> {
357        let mm = _dev_utils::init_test().await;
358        let ctx = Ctx::root_ctx();
359        let tname = "test_lot_list_by_filter_ok";
360        let seeds = _dev_utils::get_lot_seed(tname);
361        _dev_utils::seed_lots(&ctx, &mm, &seeds).await?;
362
363        let filters: Vec<LotFilter> = serde_json::from_value(json!([
364            {
365                "name": {
366                    "$endsWith": ".a",
367                    "$containsAny": ["01", "02"]
368                }
369            },
370            {
371                "name": {"$contains": "03"}
372            }
373        ]))?;
374        let list_options = serde_json::from_value(json!({
375            "order_bys": "!id"
376        }))?;
377        let lots = LotBmc::list(&ctx, &mm, Some(filters), Some(list_options)).await?;
378
379        assert_eq!(lots.len(), 3);
380        assert!(lots[0].name.ends_with("03"));
381        assert!(lots[1].name.ends_with("02.a"));
382        assert!(lots[2].name.ends_with("01.a"));
383
384        if false {
385            let lots = LotBmc::list(
386                &ctx,
387                &mm,
388                Some(serde_json::from_value(json!([{
389                    "name": {"$startsWith": "test_list_by_filter_ok"}
390                }]))?),
391                None,
392            )
393            .await?;
394            assert_eq!(lots.len(), 5);
395            for lot in lots.iter() {
396                LotBmc::delete(&ctx, &mm, lot.id).await?;
397            }
398        }
399
400        Ok(())
401    }
402
403    #[ignore]
404    #[tokio::test]
405    async fn test_lot_update_ok() -> Result<()> {
406        let mm = _dev_utils::init_test().await;
407        let ctx = Ctx::root_ctx();
408        let tname = "test_lot_update_ok";
409        let seeds = _dev_utils::get_lot_seed(tname);
410        let fx_lot = _dev_utils::seed_lots(&ctx, &mm, &seeds).await?.remove(0);
411        let member_id = 45;
412
413        LotBmc::update(
414            &ctx,
415            &mm,
416            fx_lot.id,
417            member_id,
418            LotForUpdate {
419                name: Some(tname.to_string()),
420                ..Default::default()
421            },
422        )
423        .await?;
424
425        let lot = LotBmc::get(&ctx, &mm, fx_lot.id).await?;
426        assert_eq!(lot.name, tname);
427
428        Ok(())
429    }
430
431    #[ignore]
432    #[tokio::test]
433    async fn test_lot_delete_err_not_found() -> Result<()> {
434        let mm = _dev_utils::init_test().await;
435        let ctx = Ctx::root_ctx();
436        let fx_id = 100;
437
438        let res = LotBmc::delete(&ctx, &mm, fx_id).await;
439
440        assert!(
441            matches!(
442                res,
443                Err(Error::EntityNotFound {
444                    entity: "lot",
445                    id: 100
446                })
447            ),
448            "EntityNotFound not matching"
449        );
450
451        Ok(())
452    }
453}