-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
578 requisition transfer processor #579
Changes from 5 commits
cdf1c83
b136e62
d4eccc7
61227a0
bb82aa4
e657047
e92057a
d45bb42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,47 +9,58 @@ use repository::{ | |
}; | ||
use util::uuid::uuid; | ||
|
||
const DESCRIPTION: &'static str = "Create response requisition from request requisition"; | ||
|
||
pub struct CreateResponseRequisitionProcessor; | ||
impl RequisitionTransferProcessor for CreateResponseRequisitionProcessor { | ||
fn get_description(&self) -> String { | ||
"Create response requisition from request requisition".to_string() | ||
DESCRIPTION.to_string() | ||
} | ||
|
||
/// Response requisition is created from source requisition when all below conditions are met: | ||
/// | ||
/// 1. Source requisition name_id is for a store that is active on current site (transfer processor driver guarantees this) | ||
/// 2. Source requisition is Request requisition | ||
/// 3. Source requisition is Status is Sent | ||
/// 4. Response requisition does not exists (no link is found for source requisition) | ||
/// | ||
/// Only runs once: | ||
/// 5. Because new response requisition is linked to source requisition when it's created and `4.` will never be true again | ||
fn try_process_record( | ||
&self, | ||
connection: &StorageConnection, | ||
record_for_processing: &RequisitionTransferProcessorRecord, | ||
) -> Result<Option<String>, RepositoryError> { | ||
// Check can execute | ||
let RequisitionTransferProcessorRecord { | ||
linked_requisition, | ||
requisition: source_requisition, | ||
linked_requisition: response_requisition, | ||
requisition: request_requisition, | ||
.. | ||
} = &record_for_processing; | ||
|
||
if source_requisition.requisition_row.r#type != RequisitionRowType::Request { | ||
// 2. | ||
if request_requisition.requisition_row.r#type != RequisitionRowType::Request { | ||
return Ok(None); | ||
} | ||
|
||
if source_requisition.requisition_row.status != RequisitionRowStatus::Sent { | ||
// 3. | ||
if request_requisition.requisition_row.status != RequisitionRowStatus::Sent { | ||
return Ok(None); | ||
} | ||
|
||
if linked_requisition.is_some() { | ||
// 4. | ||
if response_requisition.is_some() { | ||
return Ok(None); | ||
} | ||
|
||
// Execute | ||
let new_requisition = | ||
generate_linked_requisition(connection, &source_requisition, record_for_processing)?; | ||
let new_response_requisition = | ||
generate_response_requisition(connection, &request_requisition, record_for_processing)?; | ||
|
||
let new_requisition_lines = generate_linked_requisition_lines( | ||
let new_requisition_lines = generate_response_requisition_lines( | ||
connection, | ||
&new_requisition, | ||
&source_requisition.requisition_row, | ||
&new_response_requisition.id, | ||
&request_requisition.requisition_row, | ||
)?; | ||
|
||
RequisitionRowRepository::new(connection).upsert_one(&new_requisition)?; | ||
RequisitionRowRepository::new(connection).upsert_one(&new_response_requisition)?; | ||
|
||
let requisition_line_row_repository = RequisitionLineRowRepository::new(connection); | ||
|
||
|
@@ -59,42 +70,42 @@ impl RequisitionTransferProcessor for CreateResponseRequisitionProcessor { | |
|
||
let result = format!( | ||
"requisition ({}) lines ({:?}) source requisition ({})", | ||
new_requisition.id, | ||
new_response_requisition.id, | ||
new_requisition_lines.into_iter().map(|r| r.id), | ||
source_requisition.requisition_row.id | ||
request_requisition.requisition_row.id | ||
); | ||
|
||
Ok(Some(result)) | ||
} | ||
} | ||
|
||
fn generate_linked_requisition( | ||
fn generate_response_requisition( | ||
connection: &StorageConnection, | ||
source_requisition: &Requisition, | ||
request_requisition: &Requisition, | ||
record_for_processing: &RequisitionTransferProcessorRecord, | ||
) -> Result<RequisitionRow, RepositoryError> { | ||
let store_id = record_for_processing.other_party_store_id.clone(); | ||
let name_id = source_requisition.store_row.name_id.clone(); | ||
let name_id = request_requisition.store_row.name_id.clone(); | ||
|
||
let source_requistition_row = &source_requisition.requisition_row; | ||
let request_requisition_row = &request_requisition.requisition_row; | ||
|
||
let requisition_number = | ||
next_number(connection, &NumberRowType::ResponseRequisition, &store_id)?; | ||
|
||
let result = RequisitionRow { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :nit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use it quite a bit. I tend to agree that we should be a bit more descriptive. I've been using this syntax quite a lot, to avoid extra nesting with Ok(), just makes it look slightly cleaner. I hope it's not a biggy since it's quite convenient to not think too much about naming if the variable is returned straight away with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a major at all, I use it a LOT too! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Can have a chat in person sometime, and we can summaries in omSupply chat |
||
id: uuid(), | ||
requisition_number: next_number( | ||
connection, | ||
&NumberRowType::ResponseRequisition, | ||
&store_id, | ||
)?, | ||
requisition_number, | ||
name_id, | ||
store_id, | ||
r#type: RequisitionRowType::Response, | ||
status: RequisitionRowStatus::New, | ||
created_datetime: Utc::now().naive_utc(), | ||
their_reference: source_requistition_row.their_reference.clone(), | ||
max_months_of_stock: source_requistition_row.max_months_of_stock.clone(), | ||
min_months_of_stock: source_requistition_row.min_months_of_stock.clone(), | ||
linked_requisition_id: Some(source_requistition_row.id.clone()), | ||
expected_delivery_date: source_requistition_row.expected_delivery_date, | ||
their_reference: request_requisition_row.their_reference.clone(), | ||
max_months_of_stock: request_requisition_row.max_months_of_stock.clone(), | ||
min_months_of_stock: request_requisition_row.min_months_of_stock.clone(), | ||
// 5. | ||
linked_requisition_id: Some(request_requisition_row.id.clone()), | ||
expected_delivery_date: request_requisition_row.expected_delivery_date, | ||
// Default | ||
user_id: None, | ||
sent_datetime: None, | ||
|
@@ -106,32 +117,44 @@ fn generate_linked_requisition( | |
Ok(result) | ||
} | ||
|
||
fn generate_linked_requisition_lines( | ||
fn generate_response_requisition_lines( | ||
connection: &StorageConnection, | ||
linked_requisition: &RequisitionRow, | ||
source_requisition: &RequisitionRow, | ||
response_requisition_id: &str, | ||
request_requisition: &RequisitionRow, | ||
) -> Result<Vec<RequisitionLineRow>, RepositoryError> { | ||
let source_lines = get_lines_for_requisition(connection, &source_requisition.id)?; | ||
|
||
let mut new_lines = Vec::new(); | ||
|
||
for source_line in source_lines.into_iter() { | ||
new_lines.push(RequisitionLineRow { | ||
id: uuid(), | ||
requisition_id: linked_requisition.id.clone(), | ||
item_id: source_line.requisition_line_row.item_id, | ||
requested_quantity: source_line.requisition_line_row.requested_quantity, | ||
suggested_quantity: source_line.requisition_line_row.suggested_quantity, | ||
available_stock_on_hand: source_line.requisition_line_row.available_stock_on_hand, | ||
average_monthly_consumption: source_line | ||
.requisition_line_row | ||
.average_monthly_consumption, | ||
snapshot_datetime: source_line.requisition_line_row.snapshot_datetime, | ||
// Default | ||
supply_quantity: 0, | ||
comment: None, | ||
}); | ||
} | ||
|
||
Ok(new_lines) | ||
let request_lines = get_lines_for_requisition(connection, &request_requisition.id)?; | ||
|
||
let response_lines = request_lines | ||
.into_iter() | ||
.map(|l| l.requisition_line_row) | ||
.map( | ||
|RequisitionLineRow { | ||
id: _, | ||
requisition_id: _, | ||
item_id, | ||
requested_quantity, | ||
suggested_quantity, | ||
supply_quantity: _, | ||
available_stock_on_hand, | ||
average_monthly_consumption, | ||
snapshot_datetime, | ||
// TODO should comment transfer ? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wondered the same - and as usual, I ask WWMD? (what would mSupply do?)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙏 But I feel sooo ify about this, I've made a action plan under |
||
comment: _, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In mSupply it does. Allows for the requester to comment per line why they want it, ordering so much etc., and for it to show up for the user that is handling responding to the requisitions. I think this user story is a bit half baked though - the responder can then edit the comment and the original message from the requester is lost 🤷♂️. Anyway I think we should probably just copy it over here for now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, this is the main reason why I feel so iffy about it, can edit the comments that are transferred.
I actually think it wouldn't be that costly and it's quite easy to do for requisitions, we do something similar with some transfer lines already for outbound shipment lines on request/response requisition: |
||
}| RequisitionLineRow { | ||
id: uuid(), | ||
requisition_id: response_requisition_id.to_string(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've made another question in #608, with link to this comment |
||
item_id, | ||
requested_quantity, | ||
suggested_quantity, | ||
available_stock_on_hand, | ||
average_monthly_consumption, | ||
snapshot_datetime, | ||
// Default | ||
supply_quantity: 0, | ||
comment: None, | ||
}, | ||
) | ||
.collect(); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added to logging section: #608 |
||
Ok(response_lines) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,27 @@ | ||
use super::{RequisitionTransferProcessor, RequisitionTransferProcessorRecord}; | ||
use repository::{ | ||
RepositoryError, RequisitionRowRepository, RequisitionRowType, StorageConnection, | ||
RepositoryError, RequisitionRow, RequisitionRowRepository, RequisitionRowType, | ||
StorageConnection, | ||
}; | ||
|
||
const DESCRIPTION: &'static str = "Link request requisition to response requisition"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, thanks: cdf1c83 |
||
|
||
pub struct LinkeRequestRequisitionProcessor; | ||
pub struct LinkRequestRequisitionProcessor; | ||
|
||
impl RequisitionTransferProcessor for LinkeRequestRequisitionProcessor { | ||
impl RequisitionTransferProcessor for LinkRequestRequisitionProcessor { | ||
fn get_description(&self) -> String { | ||
DESCRIPTION.to_string() | ||
} | ||
|
||
/// Request requisition is linked to source requisition when all below conditions are met: | ||
/// | ||
/// 1. Source requisition name_id is for a store that is active on current site (transfer processor driver guarantees this) | ||
/// 2. Source requisition is Response requisition | ||
/// 3. Linked requisition exists (the request requisition) | ||
/// 4. There is no link between request requisition and source response requisition | ||
/// | ||
/// Only runs once: | ||
/// 5. Because link is created between linked request requisition and source response requisition and `4.` will never be true again | ||
fn try_process_record( | ||
&self, | ||
connection: &StorageConnection, | ||
|
@@ -20,20 +30,20 @@ impl RequisitionTransferProcessor for LinkeRequestRequisitionProcessor { | |
// Check can execute | ||
let RequisitionTransferProcessorRecord { | ||
linked_requisition, | ||
requisition: source_requisition, | ||
requisition: response_requisition, | ||
.. | ||
} = &record_for_processing; | ||
|
||
if source_requisition.requisition_row.r#type != RequisitionRowType::Response { | ||
// 2. | ||
if response_requisition.requisition_row.r#type != RequisitionRowType::Response { | ||
return Ok(None); | ||
} | ||
|
||
let linked_requisition = match &linked_requisition { | ||
// 3. | ||
let request_requisition = match &linked_requisition { | ||
Some(linked_requisition) => linked_requisition, | ||
None => return Ok(None), | ||
}; | ||
|
||
if linked_requisition | ||
// 4. | ||
if request_requisition | ||
.requisition_row | ||
.linked_requisition_id | ||
.is_some() | ||
|
@@ -42,14 +52,17 @@ impl RequisitionTransferProcessor for LinkeRequestRequisitionProcessor { | |
} | ||
|
||
// Execute | ||
let mut update_linked_requisition = linked_requisition.requisition_row.clone(); | ||
update_linked_requisition.linked_requisition_id = | ||
Some(source_requisition.requisition_row.id.clone()); | ||
RequisitionRowRepository::new(connection).upsert_one(&update_linked_requisition)?; | ||
let linked_request_requisition = RequisitionRow { | ||
// 5. | ||
linked_requisition_id: Some(response_requisition.requisition_row.id.clone()), | ||
..request_requisition.requisition_row.clone() | ||
}; | ||
|
||
RequisitionRowRepository::new(connection).upsert_one(&linked_request_requisition)?; | ||
|
||
let result = format!( | ||
"requisition ({}) source requisition ({})", | ||
update_linked_requisition.id, source_requisition.requisition_row.id | ||
linked_request_requisition.id, response_requisition.requisition_row.id | ||
); | ||
|
||
Ok(Some(result)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ use thiserror::Error; | |
use crate::{ | ||
processors::transfer::requisition::{ | ||
create_response_requisition::CreateResponseRequisitionProcessor, | ||
link_request_requisition::LinkeRequestRequisitionProcessor, | ||
link_request_requisition::LinkRequestRequisitionProcessor, | ||
update_request_requisition_status::UpdateRequestRequstionStatusProcessor, | ||
}, | ||
service_provider::ServiceProvider, | ||
|
@@ -27,6 +27,8 @@ const CHANGELOG_BATCH_SIZE: u32 = 20; | |
#[derive(Clone, Debug)] | ||
pub(crate) struct RequisitionTransferProcessorRecord { | ||
requisition: Requisition, | ||
/// Linked requisition, both relations are checked | ||
/// (requisition.id = linked_requistion.linked_requisition_id OR requisition.linked_requistion_id = requisition.id) | ||
linked_requisition: Option<Requisition>, | ||
other_party_store_id: String, | ||
} | ||
|
@@ -44,18 +46,16 @@ pub(crate) enum ProcessRequisitionTransfersError { | |
#[error("Name id is missing from requisition changelog {0:?}")] | ||
NameIdIsMissingFromChangelog(ChangelogRow), | ||
#[error("Name is not an active store {0:?}")] | ||
NameIsNotAnAciveStore(ChangelogRow), | ||
NameIsNotAnActiveStore(ChangelogRow), | ||
} | ||
|
||
// TODO transaction | ||
|
||
pub(crate) fn process_requisition_transfers( | ||
service_provider: &ServiceProvider, | ||
) -> Result<(), ProcessRequisitionTransfersError> { | ||
use ProcessRequisitionTransfersError as Error; | ||
let processors: Vec<Box<dyn RequisitionTransferProcessor>> = vec![ | ||
Box::new(CreateResponseRequisitionProcessor), | ||
Box::new(LinkeRequestRequisitionProcessor), | ||
Box::new(LinkRequestRequisitionProcessor), | ||
Box::new(UpdateRequestRequstionStatusProcessor), | ||
]; | ||
|
||
|
@@ -66,6 +66,8 @@ pub(crate) fn process_requisition_transfers( | |
|
||
let changelog_repo = ChangelogRepository::new(&ctx.connection); | ||
let key_value_store_repo = KeyValueStoreRepository::new(&ctx.connection); | ||
// For transfers, changelog MUST be filtered by records where name_id is active store on this site | ||
// this is the contract obligation for try_process_record in ProcessorTrait | ||
let filter = ChangelogFilter::new() | ||
.table_name(ChangelogTableName::Requisition.equal_to()) | ||
.name_id(EqualFilter::equal_any(active_stores.name_ids())); | ||
|
@@ -103,7 +105,7 @@ pub(crate) fn process_requisition_transfers( | |
linked_requisition, | ||
other_party_store_id: active_stores | ||
.get_store_id_for_name_id(name_id) | ||
.ok_or_else(|| Error::NameIsNotAnAciveStore(log.clone()))?, | ||
.ok_or_else(|| Error::NameIsNotAnActiveStore(log.clone()))?, | ||
}; | ||
|
||
// Try record against all of the processors | ||
|
@@ -144,7 +146,7 @@ fn get_upsert_record( | |
.query_one(RequisitionFilter::new_match_id(&changelog_row.record_id)) | ||
.map_err(|e| DatabaseError(changelog_row.record_id.clone(), e))? | ||
.ok_or_else(|| RequisitionNotFound(changelog_row.clone()))?; | ||
|
||
let linked_requisition = match &requisition.requisition_row.linked_requisition_id { | ||
Some(id) => repo | ||
.query_one(RequisitionFilter::new_match_id(id)) | ||
|
@@ -182,6 +184,7 @@ trait RequisitionTransferProcessor { | |
Ok(result) | ||
} | ||
|
||
/// Caller MUST guarantee that record.requisition.name_id is a store active on this site | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great comment |
||
fn try_process_record( | ||
&self, | ||
connection: &StorageConnection, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a major as I think people can read the code but might be nice to have a quick summary of the logic here.
Maybe need a second opinion on if this is might be overkill but I've found comments similar to this really helpful in the past.
e.g.
/*
At this point we have found a request requisition for our store that doesn't have a response requisition created for it yet.
We need to create a new response requisition record for this along with the associated requisition_lines.
We don't update the original record to add the linked requisition id, as this should be done by the remote store which owns the record. (See
link_requisition_request
for this logic)*/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea, my plan originally was to outsource this to diagrams, but have to have comments in code to at the very least justify why this would only run once: cdf1c83
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix comment: b136e62
fix typo: d4eccc7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also did the same for shipments, and noticed that it is possible to have a double update on shipment (although it wouldn't cause infinite loop but still not ideal, fixed with new condition in
UpdateInboundShipmentProcessor
: 59567f1, basically Outbound Shipment is linked, and updated, which causes Inbound shipment to be updated, condition was also restricted to update InboundShipment only if OutboundShipment status was changed).I would also like to remove a little bit confusion by calling shipment/requisitions with better names within processors (don't use source and linked, just go with request and response, inbound and outbound), I'll try tomorrow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh man, i ended up updating requisition and shipments with more specific variable naming, and did a couple of extras
.map
for requisition line generationmut
and slightly safer with immutability i think)Anyways the commit is here: 61227a0, this makes the total diff from the last time you looked at this pr this: 5743790...61227a0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks awesome @andreievg! Those inline logic comments are super helpful.