Skip to content
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

Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Copy link
Contributor

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)
*/

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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

  • Use destructure + .map for requisition line generation
  • Fix comments
  • Use destructure for requisition update (saw it in one of @roxy-dao PRs, cleaner syntax then mut 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

Copy link
Contributor

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.

// 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);

Expand All @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:nit
I'm generally ok with calling a variable result but that suggested to me that the operation would return a Result<T,E> enum. I think something like response_requisition or just requisition would be better here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen Shot 2022-09-13 at 7 18 30 PM

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 Ok(result). I am happy to reconsider and use more descriptive naming if it proves to be a problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a major at all, I use it a LOT too!
Sorry I think I've slipped into using these PR reviews to discuss/reflect on stylistic things.
In this case when reading the I found myself asking why is this a result when it's just creating a struct?
It was actually how I noticed that the number creation could fail during struct creation, so the variable naming kind of helped me in a way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I think I've slipped into using these PR reviews to discuss/reflect on stylistic things.

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,
Expand All @@ -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 ?
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?)

	[requisition]comment:=Get indexed string(17001; 175)+$t_serial+Choose([requisition]comment=""; ""; " ("+[requisition]comment+")")  // "From request requisition N (comment - if any)"
	[requisition]requester_reference:=Get indexed string(17001; 175)+$t_serial+Choose([requisition]requester_reference=""; ""; " ("+[requisition]requester_reference+")")  // "From request requisition N (requester_reference - if any)"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 Questionsin this issue

comment: _,
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or like my other comment on requisition.comment, you should have the related lines to look up comments from the requester. But to do that effectively you'd need to add requisition_line.linked_requisition_line_id as said here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the responder can then edit the comment and the original message from the requester is lost

Yeah, this is the main reason why I feel so iffy about it, can edit the comments that are transferred.

But to do that effectively you'd need to add requisition_line.linked_requisition_line_id as said #579 (comment)

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:

https://github.com/openmsupply/open-msupply/blob/f720b50cc79a57e685fb394f4fc7be20d02e169e/server/graphql/types/src/types/requisition_line.rs#L102-L125

https://github.com/openmsupply/open-msupply/blob/f720b50cc79a57e685fb394f4fc7be20d02e169e/server/graphql/core/src/loader/invoice_line.rs#L51-L86

}| RequisitionLineRow {
id: uuid(),
requisition_id: response_requisition_id.to_string(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mSupply also has a linked_requisition_line_ID that links back to the copied line in the request requisition. Admittedly not much used at all though, was the conclusion to get rid of it?

image

(This omits any possible use in dashboards I'm not aware of)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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();

Copy link
Contributor

@Chris-Petty Chris-Petty Sep 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with the logging in omSupply, but legacy mSupply logs the creation of the response:

image

Perhaps a new issue to follow on?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
Creating description statically is a different pattern to create_response_requisition.
Might be nice to be consistent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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,
Expand All @@ -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()
Expand All @@ -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))
Expand Down
17 changes: 10 additions & 7 deletions server/service/src/processors/transfer/requisition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
Expand All @@ -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),
];

Expand All @@ -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()));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -182,6 +184,7 @@ trait RequisitionTransferProcessor {
Ok(result)
}

/// Caller MUST guarantee that record.requisition.name_id is a store active on this site
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment

fn try_process_record(
&self,
connection: &StorageConnection,
Expand Down
1 change: 1 addition & 0 deletions server/service/src/processors/transfer/requisition/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ impl RequisitionTransferTester {
}
}

/// Line uniqueness is checked in caller method where requisition line count is checked
fn check_line(
connection: &StorageConnection,
response_requisition_id: &str,
Expand Down
Loading