use common_utils::errors::IntegrityCheckError; use hyperswitch_domain_models::router_request_types::{ AuthoriseIntegrityObject, CaptureIntegrityObject, PaymentsAuthorizeData, PaymentsCaptureData, PaymentsSyncData, RefundIntegrityObject, RefundsData, SyncIntegrityObject, }; /// Connector Integrity trait to check connector data integrity pub trait FlowIntegrity { /// Output type for the connector type IntegrityObject; /// helps in connector integrity check fn compare( req_integrity_object: Self::IntegrityObject, res_integrity_object: Self::IntegrityObject, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError>; } /// Trait to get connector integrity object based on request and response pub trait GetIntegrityObject { /// function to get response integrity object fn get_response_integrity_object(&self) -> Option; /// function to get request integrity object fn get_request_integrity_object(&self) -> T::IntegrityObject; } /// Trait to check flow type, based on which various integrity checks will be performed pub trait CheckIntegrity { /// Function to check to initiate integrity check fn check_integrity( &self, request: &Request, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError>; } impl CheckIntegrity for RefundsData where T: FlowIntegrity, Request: GetIntegrityObject, { fn check_integrity( &self, request: &Request, connector_refund_id: Option, ) -> Result<(), IntegrityCheckError> { match request.get_response_integrity_object() { Some(res_integrity_object) => { let req_integrity_object = request.get_request_integrity_object(); T::compare( req_integrity_object, res_integrity_object, connector_refund_id, ) } None => Ok(()), } } } impl CheckIntegrity for PaymentsAuthorizeData where T: FlowIntegrity, Request: GetIntegrityObject, { fn check_integrity( &self, request: &Request, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { match request.get_response_integrity_object() { Some(res_integrity_object) => { let req_integrity_object = request.get_request_integrity_object(); T::compare( req_integrity_object, res_integrity_object, connector_transaction_id, ) } None => Ok(()), } } } impl CheckIntegrity for PaymentsCaptureData where T: FlowIntegrity, Request: GetIntegrityObject, { fn check_integrity( &self, request: &Request, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { match request.get_response_integrity_object() { Some(res_integrity_object) => { let req_integrity_object = request.get_request_integrity_object(); T::compare( req_integrity_object, res_integrity_object, connector_transaction_id, ) } None => Ok(()), } } } impl CheckIntegrity for PaymentsSyncData where T: FlowIntegrity, Request: GetIntegrityObject, { fn check_integrity( &self, request: &Request, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { match request.get_response_integrity_object() { Some(res_integrity_object) => { let req_integrity_object = request.get_request_integrity_object(); T::compare( req_integrity_object, res_integrity_object, connector_transaction_id, ) } None => Ok(()), } } } impl FlowIntegrity for RefundIntegrityObject { type IntegrityObject = Self; fn compare( req_integrity_object: Self, res_integrity_object: Self, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { let mut mismatched_fields = Vec::new(); if req_integrity_object.currency != res_integrity_object.currency { mismatched_fields.push(format_mismatch( "currency", &req_integrity_object.currency.to_string(), &res_integrity_object.currency.to_string(), )); } if req_integrity_object.refund_amount != res_integrity_object.refund_amount { mismatched_fields.push(format_mismatch( "refund_amount", &req_integrity_object.refund_amount.to_string(), &res_integrity_object.refund_amount.to_string(), )); } if mismatched_fields.is_empty() { Ok(()) } else { let field_names = mismatched_fields.join(", "); Err(IntegrityCheckError { field_names, connector_transaction_id, }) } } } impl FlowIntegrity for AuthoriseIntegrityObject { type IntegrityObject = Self; fn compare( req_integrity_object: Self, res_integrity_object: Self, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { let mut mismatched_fields = Vec::new(); if req_integrity_object.amount != res_integrity_object.amount { mismatched_fields.push(format_mismatch( "amount", &req_integrity_object.amount.to_string(), &res_integrity_object.amount.to_string(), )); } if req_integrity_object.currency != res_integrity_object.currency { mismatched_fields.push(format_mismatch( "currency", &req_integrity_object.currency.to_string(), &res_integrity_object.currency.to_string(), )); } if mismatched_fields.is_empty() { Ok(()) } else { let field_names = mismatched_fields.join(", "); Err(IntegrityCheckError { field_names, connector_transaction_id, }) } } } impl FlowIntegrity for SyncIntegrityObject { type IntegrityObject = Self; fn compare( req_integrity_object: Self, res_integrity_object: Self, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { let mut mismatched_fields = Vec::new(); res_integrity_object .amount .zip(req_integrity_object.amount) .map(|(res_amount, req_amount)| { if res_amount != req_amount { mismatched_fields.push(format_mismatch( "amount", &req_amount.to_string(), &res_amount.to_string(), )); } }); res_integrity_object .currency .zip(req_integrity_object.currency) .map(|(res_currency, req_currency)| { if res_currency != req_currency { mismatched_fields.push(format_mismatch( "currency", &req_currency.to_string(), &res_currency.to_string(), )); } }); if mismatched_fields.is_empty() { Ok(()) } else { let field_names = mismatched_fields.join(", "); Err(IntegrityCheckError { field_names, connector_transaction_id, }) } } } impl FlowIntegrity for CaptureIntegrityObject { type IntegrityObject = Self; fn compare( req_integrity_object: Self, res_integrity_object: Self, connector_transaction_id: Option, ) -> Result<(), IntegrityCheckError> { let mut mismatched_fields = Vec::new(); res_integrity_object .capture_amount .zip(req_integrity_object.capture_amount) .map(|(res_amount, req_amount)| { if res_amount != req_amount { mismatched_fields.push(format_mismatch( "capture_amount", &req_amount.to_string(), &res_amount.to_string(), )); } }); if req_integrity_object.currency != res_integrity_object.currency { mismatched_fields.push(format_mismatch( "currency", &req_integrity_object.currency.to_string(), &res_integrity_object.currency.to_string(), )); } if mismatched_fields.is_empty() { Ok(()) } else { let field_names = mismatched_fields.join(", "); Err(IntegrityCheckError { field_names, connector_transaction_id, }) } } } impl GetIntegrityObject for PaymentsCaptureData { fn get_response_integrity_object(&self) -> Option { self.integrity_object.clone() } fn get_request_integrity_object(&self) -> CaptureIntegrityObject { CaptureIntegrityObject { capture_amount: Some(self.minor_amount_to_capture), currency: self.currency, } } } impl GetIntegrityObject for RefundsData { fn get_response_integrity_object(&self) -> Option { self.integrity_object.clone() } fn get_request_integrity_object(&self) -> RefundIntegrityObject { RefundIntegrityObject { currency: self.currency, refund_amount: self.minor_refund_amount, } } } impl GetIntegrityObject for PaymentsAuthorizeData { fn get_response_integrity_object(&self) -> Option { self.integrity_object.clone() } fn get_request_integrity_object(&self) -> AuthoriseIntegrityObject { AuthoriseIntegrityObject { amount: self.minor_amount, currency: self.currency, } } } impl GetIntegrityObject for PaymentsSyncData { fn get_response_integrity_object(&self) -> Option { self.integrity_object.clone() } fn get_request_integrity_object(&self) -> SyncIntegrityObject { SyncIntegrityObject { amount: Some(self.amount), currency: Some(self.currency), } } } #[inline] fn format_mismatch(field: &str, expected: &str, found: &str) -> String { format!("{} expected {} but found {}", field, expected, found) }