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 }
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 }
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}