mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(connector): [Bluesnap] Add support for ApplePay (#1178)
This commit is contained in:
committed by
GitHub
parent
ed22b2af76
commit
919c03e679
@ -494,7 +494,83 @@ impl api::PaymentSession for Bluesnap {}
|
|||||||
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||||
for Bluesnap
|
for Bluesnap
|
||||||
{
|
{
|
||||||
//TODO: implement sessions flow
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsSessionRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
|
||||||
|
self.build_headers(req, connectors)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_content_type(&self) -> &'static str {
|
||||||
|
self.common_get_content_type()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_url(
|
||||||
|
&self,
|
||||||
|
_req: &types::PaymentsSessionRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
Ok(format!(
|
||||||
|
"{}{}",
|
||||||
|
self.base_url(connectors),
|
||||||
|
"services/2/wallets"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsSessionRouterData,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let connector_req = bluesnap::BluesnapCreateWalletToken::try_from(req)?;
|
||||||
|
let bluesnap_req =
|
||||||
|
utils::Encode::<bluesnap::BluesnapCreateWalletToken>::encode_to_string_of_json(
|
||||||
|
&connector_req,
|
||||||
|
)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(bluesnap_req))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsSessionRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
Ok(Some(
|
||||||
|
services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::PaymentsSessionType::get_url(self, req, connectors)?)
|
||||||
|
.attach_default_headers()
|
||||||
|
.headers(types::PaymentsSessionType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.body(types::PaymentsSessionType::get_request_body(self, req)?)
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsSessionRouterData,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::PaymentsSessionRouterData, errors::ConnectorError> {
|
||||||
|
let response: bluesnap::BluesnapWalletTokenResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("BluesnapWalletTokenResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
types::RouterData::try_from(types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
self.build_error_response(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl api::PaymentAuthorize for Bluesnap {}
|
impl api::PaymentAuthorize for Bluesnap {}
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
|
use api_models::enums as api_enums;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use common_utils::{
|
use common_utils::{
|
||||||
ext_traits::{StringExt, ValueExt},
|
ext_traits::{ByteSliceExt, StringExt, ValueExt},
|
||||||
pii::Email,
|
pii::Email,
|
||||||
};
|
};
|
||||||
use error_stack::ResultExt;
|
use error_stack::{IntoReport, ResultExt};
|
||||||
|
use masking::ExposeInterface;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
connector::utils,
|
connector::utils::{self, RouterData},
|
||||||
consts,
|
consts,
|
||||||
core::errors,
|
core::errors,
|
||||||
pii::Secret,
|
pii::Secret,
|
||||||
types::{self, api, storage::enums, transformers::ForeignTryFrom},
|
types::{self, api, storage::enums, transformers::ForeignTryFrom},
|
||||||
utils::Encode,
|
utils::{Encode, OptionExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, PartialEq)]
|
#[derive(Debug, Serialize, PartialEq)]
|
||||||
@ -26,6 +28,15 @@ pub struct BluesnapPaymentsRequest {
|
|||||||
three_d_secure: Option<BluesnapThreeDSecureInfo>,
|
three_d_secure: Option<BluesnapThreeDSecureInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BluesnapCreateWalletToken {
|
||||||
|
wallet_type: String,
|
||||||
|
validation_url: Secret<String>,
|
||||||
|
domain_name: String,
|
||||||
|
display_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, PartialEq)]
|
#[derive(Debug, Serialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BluesnapThreeDSecureInfo {
|
pub struct BluesnapThreeDSecureInfo {
|
||||||
@ -74,6 +85,56 @@ pub enum BluesnapWalletTypes {
|
|||||||
ApplePay,
|
ApplePay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct EncodedPaymentToken {
|
||||||
|
billing_contact: BillingDetails,
|
||||||
|
token: ApplepayPaymentData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BillingDetails {
|
||||||
|
country_code: Option<api_enums::CountryAlpha2>,
|
||||||
|
address_lines: Option<Vec<Secret<String>>>,
|
||||||
|
family_name: Option<Secret<String>>,
|
||||||
|
given_name: Option<Secret<String>>,
|
||||||
|
postal_code: Option<Secret<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ApplepayPaymentData {
|
||||||
|
payment_data: ApplePayEncodedPaymentData,
|
||||||
|
payment_method: ApplepayPaymentMethod,
|
||||||
|
transaction_identifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ApplepayPaymentMethod {
|
||||||
|
display_name: String,
|
||||||
|
network: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pm_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
|
||||||
|
pub struct ApplePayEncodedPaymentData {
|
||||||
|
data: String,
|
||||||
|
header: Option<ApplepayHeader>,
|
||||||
|
signature: String,
|
||||||
|
version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ApplepayHeader {
|
||||||
|
ephemeral_public_key: Secret<String>,
|
||||||
|
public_key_hash: Secret<String>,
|
||||||
|
transaction_id: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
|
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
|
||||||
type Error = error_stack::Report<errors::ConnectorError>;
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||||
@ -104,13 +165,64 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
api_models::payments::WalletData::ApplePay(payment_method_data) => {
|
api_models::payments::WalletData::ApplePay(payment_method_data) => {
|
||||||
let apple_pay_object =
|
let apple_pay_payment_data = consts::BASE64_ENGINE
|
||||||
Encode::<BluesnapApplePayObject>::encode_to_string_of_json(
|
.decode(payment_method_data.payment_data)
|
||||||
&BluesnapApplePayObject {
|
.into_report()
|
||||||
token: payment_method_data,
|
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||||
|
|
||||||
|
let apple_pay_payment_data: ApplePayEncodedPaymentData = apple_pay_payment_data
|
||||||
|
[..]
|
||||||
|
.parse_struct("ApplePayEncodedPaymentData")
|
||||||
|
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||||
|
|
||||||
|
let billing = item
|
||||||
|
.address
|
||||||
|
.billing
|
||||||
|
.to_owned()
|
||||||
|
.get_required_value("billing")
|
||||||
|
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||||
|
field_name: "billing",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let billing_address = billing
|
||||||
|
.address
|
||||||
|
.get_required_value("billing_address")
|
||||||
|
.change_context(errors::ConnectorError::MissingRequiredField {
|
||||||
|
field_name: "billing",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut address = Vec::new();
|
||||||
|
if let Some(add) = billing_address.line1.to_owned() {
|
||||||
|
address.push(add)
|
||||||
|
}
|
||||||
|
if let Some(add) = billing_address.line2.to_owned() {
|
||||||
|
address.push(add)
|
||||||
|
}
|
||||||
|
if let Some(add) = billing_address.line3.to_owned() {
|
||||||
|
address.push(add)
|
||||||
|
}
|
||||||
|
|
||||||
|
let apple_pay_object = Encode::<EncodedPaymentToken>::encode_to_string_of_json(
|
||||||
|
&EncodedPaymentToken {
|
||||||
|
token: ApplepayPaymentData {
|
||||||
|
payment_data: apple_pay_payment_data,
|
||||||
|
payment_method: payment_method_data
|
||||||
|
.payment_method
|
||||||
|
.to_owned()
|
||||||
|
.into(),
|
||||||
|
transaction_identifier: payment_method_data.transaction_identifier,
|
||||||
},
|
},
|
||||||
)
|
billing_contact: BillingDetails {
|
||||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
country_code: billing_address.country,
|
||||||
|
address_lines: Some(address),
|
||||||
|
family_name: billing_address.last_name.to_owned(),
|
||||||
|
given_name: billing_address.first_name.to_owned(),
|
||||||
|
postal_code: billing_address.zip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
|
||||||
Ok(PaymentMethodDetails::Wallet(BluesnapWallet {
|
Ok(PaymentMethodDetails::Wallet(BluesnapWallet {
|
||||||
wallet_type: BluesnapWalletTypes::ApplePay,
|
wallet_type: BluesnapWalletTypes::ApplePay,
|
||||||
encoded_payment_token: consts::BASE64_ENGINE.encode(apple_pay_object),
|
encoded_payment_token: consts::BASE64_ENGINE.encode(apple_pay_object),
|
||||||
@ -134,6 +246,94 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BluesnapPaymentsRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<api_models::payments::ApplepayPaymentMethod> for ApplepayPaymentMethod {
|
||||||
|
fn from(item: api_models::payments::ApplepayPaymentMethod) -> Self {
|
||||||
|
Self {
|
||||||
|
display_name: item.display_name,
|
||||||
|
network: item.network,
|
||||||
|
pm_type: item.pm_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsSessionRouterData> for BluesnapCreateWalletToken {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(item: &types::PaymentsSessionRouterData) -> Result<Self, Self::Error> {
|
||||||
|
let apple_pay_metadata = item.get_connector_meta()?.expose();
|
||||||
|
let applepay_metadata = apple_pay_metadata
|
||||||
|
.parse_value::<api_models::payments::ApplepaySessionTokenData>(
|
||||||
|
"ApplepaySessionTokenData",
|
||||||
|
)
|
||||||
|
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||||
|
Ok(Self {
|
||||||
|
wallet_type: "APPLE_PAY".to_string(),
|
||||||
|
validation_url: consts::APPLEPAY_VALIDATION_URL.to_string().into(),
|
||||||
|
domain_name: applepay_metadata.data.session_token_data.initiative_context,
|
||||||
|
display_name: Some(applepay_metadata.data.session_token_data.display_name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<types::PaymentsSessionResponseRouterData<BluesnapWalletTokenResponse>>
|
||||||
|
for types::PaymentsSessionRouterData
|
||||||
|
{
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(
|
||||||
|
item: types::PaymentsSessionResponseRouterData<BluesnapWalletTokenResponse>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let response = &item.response;
|
||||||
|
|
||||||
|
let wallet_token = consts::BASE64_ENGINE
|
||||||
|
.decode(response.wallet_token.clone().expose())
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||||
|
|
||||||
|
let session_response: api_models::payments::ApplePaySessionResponse = wallet_token[..]
|
||||||
|
.parse_struct("ApplePayResponse")
|
||||||
|
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||||
|
|
||||||
|
let metadata = item.data.get_connector_meta()?.expose();
|
||||||
|
let applepay_metadata = metadata
|
||||||
|
.parse_value::<api_models::payments::ApplepaySessionTokenData>(
|
||||||
|
"ApplepaySessionTokenData",
|
||||||
|
)
|
||||||
|
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
response: Ok(types::PaymentsResponseData::SessionResponse {
|
||||||
|
session_token: types::api::SessionToken::ApplePay(Box::new(
|
||||||
|
api_models::payments::ApplepaySessionTokenResponse {
|
||||||
|
session_token_data: session_response,
|
||||||
|
payment_request_data: api_models::payments::ApplePayPaymentRequest {
|
||||||
|
country_code: item.data.get_billing_country()?,
|
||||||
|
currency_code: item.data.request.currency.to_string(),
|
||||||
|
total: api_models::payments::AmountInfo {
|
||||||
|
label: applepay_metadata.data.payment_request_data.label,
|
||||||
|
total_type: "final".to_string(),
|
||||||
|
amount: item.data.request.amount.to_string(),
|
||||||
|
},
|
||||||
|
merchant_capabilities: applepay_metadata
|
||||||
|
.data
|
||||||
|
.payment_request_data
|
||||||
|
.merchant_capabilities,
|
||||||
|
supported_networks: applepay_metadata
|
||||||
|
.data
|
||||||
|
.payment_request_data
|
||||||
|
.supported_networks,
|
||||||
|
merchant_identifier: applepay_metadata
|
||||||
|
.data
|
||||||
|
.session_token_data
|
||||||
|
.merchant_identifier,
|
||||||
|
},
|
||||||
|
connector: "bluesnap".to_string(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
..item.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRequest {
|
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for BluesnapPaymentsRequest {
|
||||||
type Error = error_stack::Report<errors::ConnectorError>;
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
|
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||||
@ -374,6 +574,13 @@ pub struct BluesnapPaymentsResponse {
|
|||||||
card_transaction_type: BluesnapTxnType,
|
card_transaction_type: BluesnapTxnType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BluesnapWalletTokenResponse {
|
||||||
|
wallet_type: String,
|
||||||
|
wallet_token: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Refund {
|
pub struct Refund {
|
||||||
|
|||||||
@ -27,3 +27,7 @@ pub(crate) const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
|
|||||||
|
|
||||||
pub(crate) const API_KEY_LENGTH: usize = 64;
|
pub(crate) const API_KEY_LENGTH: usize = 64;
|
||||||
pub(crate) const PUB_SUB_CHANNEL: &str = "hyperswitch_invalidate";
|
pub(crate) const PUB_SUB_CHANNEL: &str = "hyperswitch_invalidate";
|
||||||
|
|
||||||
|
// Apple Pay validation url
|
||||||
|
pub(crate) const APPLEPAY_VALIDATION_URL: &str =
|
||||||
|
"https://apple-pay-gateway-cert.apple.com/paymentservices/startSession";
|
||||||
|
|||||||
@ -376,11 +376,13 @@ where
|
|||||||
for (connector, payment_method_type, business_sub_label) in
|
for (connector, payment_method_type, business_sub_label) in
|
||||||
connector_and_supporting_payment_method_type
|
connector_and_supporting_payment_method_type
|
||||||
{
|
{
|
||||||
match api::ConnectorData::get_connector_by_name(
|
let connector_type = get_connector_type_for_session_token(
|
||||||
connectors,
|
payment_method_type,
|
||||||
&connector,
|
request,
|
||||||
api::GetToken::from(payment_method_type),
|
connector.to_owned(),
|
||||||
) {
|
);
|
||||||
|
match api::ConnectorData::get_connector_by_name(connectors, &connector, connector_type)
|
||||||
|
{
|
||||||
Ok(connector_data) => session_connector_data.push(api::SessionConnectorData {
|
Ok(connector_data) => session_connector_data.push(api::SessionConnectorData {
|
||||||
payment_method_type,
|
payment_method_type,
|
||||||
connector: connector_data,
|
connector: connector_data,
|
||||||
@ -407,3 +409,19 @@ impl From<api_models::enums::PaymentMethodType> for api::GetToken {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_connector_type_for_session_token(
|
||||||
|
payment_method_type: api_models::enums::PaymentMethodType,
|
||||||
|
_request: &api::PaymentsSessionRequest,
|
||||||
|
connector: String,
|
||||||
|
) -> api::GetToken {
|
||||||
|
if payment_method_type == api_models::enums::PaymentMethodType::ApplePay {
|
||||||
|
if connector == *"bluesnap" {
|
||||||
|
api::GetToken::Connector
|
||||||
|
} else {
|
||||||
|
api::GetToken::ApplePayMetadata
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
api::GetToken::from(payment_method_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ use std::marker::PhantomData;
|
|||||||
pub use api_models::enums::Connector;
|
pub use api_models::enums::Connector;
|
||||||
use common_utils::{pii, pii::Email};
|
use common_utils::{pii, pii::Email};
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
|
use masking::Secret;
|
||||||
|
|
||||||
use self::{api::payments, storage::enums as storage_enums};
|
use self::{api::payments, storage::enums as storage_enums};
|
||||||
pub use crate::core::payments::PaymentAddress;
|
pub use crate::core::payments::PaymentAddress;
|
||||||
@ -244,7 +245,7 @@ pub struct AuthorizeSessionTokenData {
|
|||||||
pub struct ConnectorCustomerData {
|
pub struct ConnectorCustomerData {
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub email: Option<Email>,
|
pub email: Option<Email>,
|
||||||
pub phone: Option<masking::Secret<String>>,
|
pub phone: Option<Secret<String>>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user