mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 18:17:13 +08:00 
			
		
		
		
	feat(address): add email field to address (#3682)
This commit is contained in:
		| @ -1996,6 +1996,9 @@ pub struct Address { | ||||
|     pub address: Option<AddressDetails>, | ||||
|  | ||||
|     pub phone: Option<PhoneDetails>, | ||||
|  | ||||
|     #[schema(value_type = Option<String>)] | ||||
|     pub email: Option<Email>, | ||||
| } | ||||
|  | ||||
| // used by customers also, could be moved outside | ||||
|  | ||||
| @ -25,6 +25,7 @@ pub struct AddressNew { | ||||
|     pub created_at: PrimitiveDateTime, | ||||
|     pub modified_at: PrimitiveDateTime, | ||||
|     pub updated_by: String, | ||||
|     pub email: Option<Encryption>, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, Queryable, Identifiable, Serialize, Deserialize)] | ||||
| @ -49,6 +50,7 @@ pub struct Address { | ||||
|     pub merchant_id: String, | ||||
|     pub payment_id: Option<String>, | ||||
|     pub updated_by: String, | ||||
|     pub email: Option<Encryption>, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay, Serialize, Deserialize)] | ||||
| @ -67,6 +69,7 @@ pub struct AddressUpdateInternal { | ||||
|     pub country_code: Option<String>, | ||||
|     pub modified_at: PrimitiveDateTime, | ||||
|     pub updated_by: String, | ||||
|     pub email: Option<Encryption>, | ||||
| } | ||||
|  | ||||
| impl AddressUpdateInternal { | ||||
|  | ||||
| @ -31,6 +31,7 @@ diesel::table! { | ||||
|         payment_id -> Nullable<Varchar>, | ||||
|         #[max_length = 32] | ||||
|         updated_by -> Varchar, | ||||
|         email -> Nullable<Bytea>, | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -39,6 +39,7 @@ impl From<StripeBillingDetails> for payments::Address { | ||||
|                     address.country.as_ref().map(|country| country.to_string()) | ||||
|                 }), | ||||
|             }), | ||||
|             email: details.email, | ||||
|             address: details.address.map(|address| payments::AddressDetails { | ||||
|                 city: address.city, | ||||
|                 country: address.country, | ||||
| @ -184,6 +185,7 @@ impl From<Shipping> for payments::Address { | ||||
|                 number: details.phone, | ||||
|                 country_code: details.address.country.map(|country| country.to_string()), | ||||
|             }), | ||||
|             email: None, | ||||
|             address: Some(payments::AddressDetails { | ||||
|                 city: details.address.city, | ||||
|                 country: details.address.country, | ||||
|  | ||||
| @ -37,6 +37,7 @@ impl From<StripeBillingDetails> for payments::Address { | ||||
|                 number: details.phone, | ||||
|                 country_code: None, | ||||
|             }), | ||||
|             email: details.email, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -143,6 +144,7 @@ impl From<Shipping> for payments::Address { | ||||
|                 number: details.phone, | ||||
|                 country_code: None, | ||||
|             }), | ||||
|             email: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -249,6 +249,12 @@ pub async fn delete_customer( | ||||
|             .await | ||||
|             .switch()?; | ||||
|  | ||||
|     let redacted_encrypted_email: Encryptable< | ||||
|         masking::Secret<_, common_utils::pii::EmailStrategy>, | ||||
|     > = Encryptable::encrypt(REDACTED.to_string().into(), key, GcmAes256) | ||||
|         .await | ||||
|         .switch()?; | ||||
|  | ||||
|     let update_address = storage::AddressUpdate::Update { | ||||
|         city: Some(REDACTED.to_string()), | ||||
|         country: None, | ||||
| @ -262,6 +268,7 @@ pub async fn delete_customer( | ||||
|         phone_number: Some(redacted_encrypted_value.clone()), | ||||
|         country_code: Some(REDACTED.to_string()), | ||||
|         updated_by: merchant_account.storage_scheme.to_string(), | ||||
|         email: Some(redacted_encrypted_email), | ||||
|     }; | ||||
|  | ||||
|     match db | ||||
|  | ||||
| @ -69,7 +69,7 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup | ||||
|             .as_ref() | ||||
|             .and_then(|mandate_data| mandate_data.update_mandate_id.clone()) | ||||
|         { | ||||
|             self.update_mandate_flow( | ||||
|             Box::pin(self.update_mandate_flow( | ||||
|                 state, | ||||
|                 merchant_account, | ||||
|                 mandate_id, | ||||
| @ -79,7 +79,7 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup | ||||
|                 &state.conf.mandates.update_mandate_supported, | ||||
|                 connector_request, | ||||
|                 maybe_customer, | ||||
|             ) | ||||
|             )) | ||||
|             .await | ||||
|         } else { | ||||
|             let connector_integration: services::BoxedConnectorIntegration< | ||||
|  | ||||
| @ -181,6 +181,14 @@ pub async fn create_or_update_address_for_payment_by_request( | ||||
|                             .as_ref() | ||||
|                             .and_then(|value| value.country_code.clone()), | ||||
|                         updated_by: storage_scheme.to_string(), | ||||
|                         email: address | ||||
|                             .email | ||||
|                             .as_ref() | ||||
|                             .cloned() | ||||
|                             .async_lift(|inner| { | ||||
|                                 types::encrypt_optional(inner.map(|inner| inner.expose()), key) | ||||
|                             }) | ||||
|                             .await?, | ||||
|                     }) | ||||
|                 } | ||||
|                 .await | ||||
| @ -370,6 +378,12 @@ pub async fn get_domain_address_for_payments( | ||||
|                 .await?, | ||||
|             payment_id: Some(payment_id.to_owned()), | ||||
|             updated_by: storage_scheme.to_string(), | ||||
|             email: address | ||||
|                 .email | ||||
|                 .as_ref() | ||||
|                 .cloned() | ||||
|                 .async_lift(|inner| types::encrypt_optional(inner.map(|inner| inner.expose()), key)) | ||||
|                 .await?, | ||||
|         }) | ||||
|     } | ||||
|     .await | ||||
|  | ||||
| @ -2,7 +2,7 @@ pub mod helpers; | ||||
| pub mod validator; | ||||
|  | ||||
| use api_models::enums as api_enums; | ||||
| use common_utils::{crypto::Encryptable, ext_traits::ValueExt}; | ||||
| use common_utils::{crypto::Encryptable, ext_traits::ValueExt, pii}; | ||||
| use diesel_models::enums as storage_enums; | ||||
| use error_stack::{report, ResultExt}; | ||||
| use router_env::{instrument, tracing}; | ||||
| @ -1096,6 +1096,7 @@ pub async fn response_handler( | ||||
|         api::payments::Address { | ||||
|             phone: Some(phone_details), | ||||
|             address: Some(address_details), | ||||
|             email: a.email.to_owned().map(pii::Email::from), | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|  | ||||
| @ -108,6 +108,7 @@ pub async fn construct_payout_router_data<'a, F>( | ||||
|             api_models::payments::Address { | ||||
|                 phone: Some(phone_details), | ||||
|                 address: Some(address_details), | ||||
|                 email: a.email.to_owned().map(Email::from), | ||||
|             } | ||||
|         }), | ||||
|     }; | ||||
|  | ||||
| @ -501,6 +501,7 @@ mod storage { | ||||
|                         merchant_id: address_new.merchant_id.clone(), | ||||
|                         payment_id: address_new.payment_id.clone(), | ||||
|                         updated_by: storage_scheme.to_string(), | ||||
|                         email: address_new.email.clone(), | ||||
|                     }; | ||||
|  | ||||
|                     let redis_entry = kv::TypedSql { | ||||
|  | ||||
| @ -39,6 +39,7 @@ pub struct Address { | ||||
|     pub merchant_id: String, | ||||
|     pub payment_id: Option<String>, | ||||
|     pub updated_by: String, | ||||
|     pub email: crypto::OptionalEncryptableEmail, | ||||
| } | ||||
|  | ||||
| #[async_trait] | ||||
| @ -67,6 +68,7 @@ impl behaviour::Conversion for Address { | ||||
|             merchant_id: self.merchant_id, | ||||
|             payment_id: self.payment_id, | ||||
|             updated_by: self.updated_by, | ||||
|             email: self.email.map(Encryption::from), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @ -76,6 +78,7 @@ impl behaviour::Conversion for Address { | ||||
|     ) -> CustomResult<Self, ValidationError> { | ||||
|         async { | ||||
|             let inner_decrypt = |inner| types::decrypt(inner, key.peek()); | ||||
|             let inner_decrypt_email = |inner| types::decrypt(inner, key.peek()); | ||||
|             Ok(Self { | ||||
|                 id: other.id, | ||||
|                 address_id: other.address_id, | ||||
| @ -96,6 +99,7 @@ impl behaviour::Conversion for Address { | ||||
|                 merchant_id: other.merchant_id, | ||||
|                 payment_id: other.payment_id, | ||||
|                 updated_by: other.updated_by, | ||||
|                 email: other.email.async_lift(inner_decrypt_email).await?, | ||||
|             }) | ||||
|         } | ||||
|         .await | ||||
| @ -125,6 +129,7 @@ impl behaviour::Conversion for Address { | ||||
|             created_at: now, | ||||
|             modified_at: now, | ||||
|             updated_by: self.updated_by, | ||||
|             email: self.email.map(Encryption::from), | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| @ -144,6 +149,7 @@ pub enum AddressUpdate { | ||||
|         phone_number: crypto::OptionalEncryptableSecretString, | ||||
|         country_code: Option<String>, | ||||
|         updated_by: String, | ||||
|         email: crypto::OptionalEncryptableEmail, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| @ -163,6 +169,7 @@ impl From<AddressUpdate> for AddressUpdateInternal { | ||||
|                 phone_number, | ||||
|                 country_code, | ||||
|                 updated_by, | ||||
|                 email, | ||||
|             } => Self { | ||||
|                 city, | ||||
|                 country, | ||||
| @ -177,6 +184,7 @@ impl From<AddressUpdate> for AddressUpdateInternal { | ||||
|                 country_code, | ||||
|                 modified_at: date_time::convert_to_pdt(OffsetDateTime::now_utc()), | ||||
|                 updated_by, | ||||
|                 email: email.map(Encryption::from), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -589,6 +589,7 @@ impl<'a> From<&'a domain::Address> for api_types::Address { | ||||
|                 number: address.phone_number.clone().map(Encryptable::into_inner), | ||||
|                 country_code: address.country_code.clone(), | ||||
|             }), | ||||
|             email: address.email.clone().map(pii::Email::from), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -26,6 +26,7 @@ pub use common_utils::{ | ||||
| use data_models::payments::PaymentIntent; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use image::Luma; | ||||
| use masking::ExposeInterface; | ||||
| use nanoid::nanoid; | ||||
| use qrcode; | ||||
| use serde::de::DeserializeOwned; | ||||
| @ -564,6 +565,12 @@ impl CustomerAddress for api_models::customers::CustomerRequest { | ||||
|                     .await?, | ||||
|                 country_code: self.phone_country_code.clone(), | ||||
|                 updated_by: storage_scheme.to_string(), | ||||
|                 email: self | ||||
|                     .email | ||||
|                     .as_ref() | ||||
|                     .cloned() | ||||
|                     .async_lift(|inner| encrypt_optional(inner.map(|inner| inner.expose()), key)) | ||||
|                     .await?, | ||||
|             }) | ||||
|         } | ||||
|         .await | ||||
| @ -623,6 +630,12 @@ impl CustomerAddress for api_models::customers::CustomerRequest { | ||||
|                 created_at: common_utils::date_time::now(), | ||||
|                 modified_at: common_utils::date_time::now(), | ||||
|                 updated_by: storage_scheme.to_string(), | ||||
|                 email: self | ||||
|                     .email | ||||
|                     .as_ref() | ||||
|                     .cloned() | ||||
|                     .async_lift(|inner| encrypt_optional(inner.map(|inner| inner.expose()), key)) | ||||
|                     .await?, | ||||
|             }) | ||||
|         } | ||||
|         .await | ||||
|  | ||||
| @ -62,6 +62,7 @@ impl AdyenTest { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     email: None, | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
| @ -88,6 +89,7 @@ impl AdyenTest { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     email: None, | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
|  | ||||
| @ -55,6 +55,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -54,6 +54,7 @@ fn get_payment_info() -> Option<PaymentInfo> { | ||||
|                     ..Default::default() | ||||
|                 }), | ||||
|                 phone: None, | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -82,6 +82,7 @@ impl CashtocodeTest { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     email: None, | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
|  | ||||
| @ -56,6 +56,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -55,6 +55,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -54,6 +54,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -435,6 +435,7 @@ pub fn get_payment_info() -> PaymentInfo { | ||||
|                     first_name: None, | ||||
|                     last_name: None, | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|         }), | ||||
|         auth_type: None, | ||||
|  | ||||
| @ -67,6 +67,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -64,6 +64,7 @@ impl Globalpay { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     ..Default::default() | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
|  | ||||
| @ -71,6 +71,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -53,6 +53,7 @@ fn get_default_payment_info() -> Option<PaymentInfo> { | ||||
|                 state: Some(Secret::new("Amsterdam".to_string())), | ||||
|             }), | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|     }); | ||||
|     Some(PaymentInfo { | ||||
|  | ||||
| @ -55,6 +55,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     number: Some(Secret::new("1234567890".to_string())), | ||||
|                     country_code: Some("+91".to_string()), | ||||
|                 }), | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -62,6 +62,7 @@ impl PayeezyTest { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     email: None, | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
|  | ||||
| @ -57,6 +57,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     last_name: Some(Secret::new("Doe".to_string())), | ||||
|                 }), | ||||
|                 phone: None, | ||||
|                 email: None, | ||||
|             }), | ||||
|         }), | ||||
|         auth_type: None, | ||||
|  | ||||
| @ -80,6 +80,7 @@ fn get_default_payment_info() -> Option<utils::PaymentInfo> { | ||||
|                     ..Default::default() | ||||
|                 }), | ||||
|                 phone: None, | ||||
|                 email: None, | ||||
|             }), | ||||
|             ..Default::default() | ||||
|         }), | ||||
|  | ||||
| @ -66,6 +66,7 @@ impl WiseTest { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     email: None, | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
|  | ||||
| @ -50,6 +50,7 @@ impl WorldlineTest { | ||||
|                         ..Default::default() | ||||
|                     }), | ||||
|                     phone: None, | ||||
|                     email: None, | ||||
|                 }), | ||||
|                 ..Default::default() | ||||
|             }), | ||||
|  | ||||
| @ -333,10 +333,12 @@ async fn payments_create_core() { | ||||
|         shipping: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         billing: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         statement_descriptor_name: Some("Hyperswtich".to_string()), | ||||
|         statement_descriptor_suffix: Some("Hyperswitch".to_string()), | ||||
| @ -509,10 +511,12 @@ async fn payments_create_core_adyen_no_redirect() { | ||||
|         shipping: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         billing: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         statement_descriptor_name: Some("Juspay".to_string()), | ||||
|         statement_descriptor_suffix: Some("Router".to_string()), | ||||
|  | ||||
| @ -93,10 +93,12 @@ async fn payments_create_core() { | ||||
|         shipping: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         billing: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         statement_descriptor_name: Some("Hyperswitch".to_string()), | ||||
|         statement_descriptor_suffix: Some("Hyperswitch".to_string()), | ||||
| @ -273,9 +275,15 @@ async fn payments_create_core_adyen_no_redirect() { | ||||
|             nick_name: Some(masking::Secret::new("nick_name".into())), | ||||
|         })), | ||||
|         payment_method: Some(api_enums::PaymentMethod::Card), | ||||
|         shipping: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         billing: Some(api::Address { | ||||
|             address: None, | ||||
|             phone: None, | ||||
|             email: None, | ||||
|         }), | ||||
|         statement_descriptor_name: Some("Juspay".to_string()), | ||||
|         statement_descriptor_suffix: Some("Router".to_string()), | ||||
|  | ||||
| @ -0,0 +1,2 @@ | ||||
| -- This file should undo anything in `up.sql` | ||||
| ALTER TABLE address DROP COLUMN IF EXISTS email; | ||||
							
								
								
									
										3
									
								
								migrations/2024-02-15-133957_add_email_to_address/up.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								migrations/2024-02-15-133957_add_email_to_address/up.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| -- Your SQL goes here | ||||
| ALTER TABLE address | ||||
| ADD COLUMN IF NOT EXISTS email BYTEA; | ||||
| @ -4315,6 +4315,10 @@ | ||||
|               } | ||||
|             ], | ||||
|             "nullable": true | ||||
|           }, | ||||
|           "email": { | ||||
|             "type": "string", | ||||
|             "nullable": true | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|  | ||||
| @ -19,7 +19,7 @@ pm.test("[POST]::/payments - Response has JSON Body", function () { | ||||
| let jsonData = {}; | ||||
| try { | ||||
|   jsonData = pm.response.json(); | ||||
| } catch (e) {} | ||||
| } catch (e) { } | ||||
|  | ||||
| // pm.collectionVariables - Set payment_id as variable for jsonData.payment_id | ||||
| if (jsonData?.payment_id) { | ||||
| @ -88,3 +88,12 @@ pm.test( | ||||
|       .true; | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| // Response body should have "billing.email" | ||||
| pm.test( | ||||
|   "[POST]::/payments - Content check if 'billing.email' exists", | ||||
|   function () { | ||||
|     pm.expect(typeof jsonData.connector_transaction_id !== "undefined").to.be | ||||
|       .true; | ||||
|   }, | ||||
| ); | ||||
|  | ||||
| @ -58,7 +58,8 @@ | ||||
|         "phone": { | ||||
|           "number": "8056594427", | ||||
|           "country_code": "+91" | ||||
|         } | ||||
|         }, | ||||
|         "email": "example@example.com" | ||||
|       }, | ||||
|       "shipping": { | ||||
|         "address": { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Narayan Bhat
					Narayan Bhat