diff --git a/config/development.toml b/config/development.toml index d69719bd41..59d73a4b8b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -71,7 +71,6 @@ mock_locker = true basilisk_host = "" locker_enabled = true - [forex_api] call_delay = 21600 local_fetch_retry_count = 5 diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index d8c864746a..eeb10e6228 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -73,6 +73,9 @@ pub struct CustomerResponse { /// object. #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, + /// The identifier for the default payment method. + #[schema(max_length = 64, example = "pm_djh2837dwduh890123")] + pub default_payment_method_id: Option, } #[derive(Default, Clone, Debug, Deserialize, Serialize)] diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 32d3dc30bd..92f6993390 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -2,7 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::{ payment_methods::{ - CustomerPaymentMethodsListResponse, PaymentMethodDeleteResponse, PaymentMethodListRequest, + CustomerDefaultPaymentMethodResponse, CustomerPaymentMethodsListResponse, + DefaultPaymentMethod, PaymentMethodDeleteResponse, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, }, payments::{ @@ -95,6 +96,16 @@ impl ApiEventMetric for PaymentMethodResponse { impl ApiEventMetric for PaymentMethodUpdate {} +impl ApiEventMetric for DefaultPaymentMethod { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.payment_method_id.clone(), + payment_method: None, + payment_method_type: None, + }) + } +} + impl ApiEventMetric for PaymentMethodDeleteResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentMethod { @@ -121,6 +132,16 @@ impl ApiEventMetric for PaymentMethodListRequest { impl ApiEventMetric for PaymentMethodListResponse {} +impl ApiEventMetric for CustomerDefaultPaymentMethodResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.default_payment_method_id.clone().unwrap_or_default(), + payment_method: Some(self.payment_method), + payment_method_type: self.payment_method_type, + }) + } +} + impl ApiEventMetric for PaymentListFilterConstraints { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 83e1a2c488..35193b958f 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -185,6 +185,10 @@ pub struct PaymentMethodResponse { #[cfg(feature = "payouts")] #[schema(value_type = Option)] pub bank_transfer: Option, + + #[schema(value_type = Option, example = "2024-02-24T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_used_at: Option, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -550,6 +554,10 @@ pub struct PaymentMethodListRequest { /// Indicates whether the payment method is eligible for card netwotks #[schema(value_type = Option>, example = json!(["visa", "mastercard"]))] pub card_networks: Option>, + + /// Indicates the limit of last used payment methods + #[schema(example = 1)] + pub limit: Option, } impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { @@ -618,6 +626,9 @@ impl<'de> serde::Deserialize<'de> for PaymentMethodListRequest { Some(inner) => inner.push(map.next_value()?), None => output.card_networks = Some(vec![map.next_value()?]), }, + "limit" => { + set_or_reject_duplicate(&mut output.limit, "limit", map.next_value()?)?; + } _ => {} } } @@ -731,12 +742,30 @@ pub struct PaymentMethodDeleteResponse { #[schema(example = true)] pub deleted: bool, } +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct CustomerDefaultPaymentMethodResponse { + /// The unique identifier of the Payment method + #[schema(example = "card_rGK4Vi5iSW70MY7J2mIy")] + pub default_payment_method_id: Option, + /// The unique identifier of the customer. + #[schema(example = "cus_meowerunwiuwiwqw")] + pub customer_id: String, + /// The type of payment method use for the payment. + #[schema(value_type = PaymentMethod,example = "card")] + pub payment_method: api_enums::PaymentMethod, + /// This is a sub-category of payment method. + #[schema(value_type = Option,example = "credit")] + pub payment_method_type: Option, +} #[derive(Debug, Clone, serde::Serialize, ToSchema)] pub struct CustomerPaymentMethod { /// Token for payment method in temporary card locker which gets refreshed often #[schema(example = "7ebf443f-a050-4067-84e5-e6f6d4800aef")] pub payment_token: String, + /// The unique identifier of the customer. + #[schema(example = "pm_iouuy468iyuowqs")] + pub payment_method_id: String, /// The unique identifier of the customer. #[schema(example = "cus_meowerunwiuwiwqw")] @@ -798,6 +827,14 @@ pub struct CustomerPaymentMethod { /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, + + /// A timestamp (ISO 8601 code) that determines when the payment method was last used + #[schema(value_type = Option,example = "2024-02-24T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub last_used_at: Option, + /// Indicates if the payment method has been set to default or not + #[schema(example = true)] + pub default_payment_method_set: bool, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] @@ -810,6 +847,11 @@ pub struct PaymentMethodId { pub payment_method_id: String, } +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct DefaultPaymentMethod { + pub customer_id: String, + pub payment_method_id: String, +} //------------------------------------------------TokenizeService------------------------------------------------ #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct TokenizePayloadEncrypted { diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index c0cebba775..cefb0c240e 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -37,6 +37,7 @@ pub struct Customer { pub connector_customer: Option, pub modified_at: PrimitiveDateTime, pub address_id: Option, + pub default_payment_method_id: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -51,4 +52,5 @@ pub struct CustomerUpdateInternal { pub modified_at: Option, pub connector_customer: Option, pub address_id: Option, + pub default_payment_method_id: Option, } diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 09be739581..6191a768ef 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -34,12 +34,13 @@ pub struct PaymentMethod { pub metadata: Option, pub payment_method_data: Option, pub locker_id: Option, + pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, } -#[derive(Clone, Debug, Eq, PartialEq, Insertable, Queryable, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, Eq, PartialEq, Insertable, router_derive::DebugAsDisplay)] #[diesel(table_name = payment_methods)] pub struct PaymentMethodNew { pub customer_id: String, @@ -64,6 +65,7 @@ pub struct PaymentMethodNew { pub metadata: Option, pub payment_method_data: Option, pub locker_id: Option, + pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, @@ -96,6 +98,7 @@ impl Default for PaymentMethodNew { last_modified: now, metadata: Option::default(), payment_method_data: Option::default(), + last_used_at: now, connector_mandate_details: Option::default(), customer_acceptance: Option::default(), status: storage_enums::PaymentMethodStatus::Active, @@ -117,6 +120,9 @@ pub enum PaymentMethodUpdate { PaymentMethodDataUpdate { payment_method_data: Option, }, + LastUsedUpdate { + last_used_at: PrimitiveDateTime, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -124,6 +130,7 @@ pub enum PaymentMethodUpdate { pub struct PaymentMethodUpdateInternal { metadata: Option, payment_method_data: Option, + last_used_at: Option, } impl PaymentMethodUpdateInternal { @@ -140,12 +147,19 @@ impl From for PaymentMethodUpdateInternal { PaymentMethodUpdate::MetadataUpdate { metadata } => Self { metadata, payment_method_data: None, + last_used_at: None, }, PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data, } => Self { metadata: None, payment_method_data, + last_used_at: None, + }, + PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { + metadata: None, + payment_method_data: None, + last_used_at: Some(last_used_at), }, } } diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs index aa22796276..e3f2e51137 100644 --- a/crates/diesel_models/src/query/payment_method.rs +++ b/crates/diesel_models/src/query/payment_method.rs @@ -90,20 +90,16 @@ impl PaymentMethod { conn: &PgPooledConn, customer_id: &str, merchant_id: &str, + limit: Option, ) -> StorageResult> { - generics::generic_filter::< - ::Table, - _, - <::Table as Table>::PrimaryKey, - _, - >( + generics::generic_filter::<::Table, _, _, _>( conn, dsl::customer_id .eq(customer_id.to_owned()) .and(dsl::merchant_id.eq(merchant_id.to_owned())), + limit, None, - None, - None, + Some(dsl::last_used_at.desc()), ) .await } @@ -114,21 +110,17 @@ impl PaymentMethod { customer_id: &str, merchant_id: &str, status: storage_enums::PaymentMethodStatus, + limit: Option, ) -> StorageResult> { - generics::generic_filter::< - ::Table, - _, - <::Table as Table>::PrimaryKey, - _, - >( + generics::generic_filter::<::Table, _, _, _>( conn, dsl::customer_id .eq(customer_id.to_owned()) .and(dsl::merchant_id.eq(merchant_id.to_owned())) .and(dsl::status.eq(status)), + limit, None, - None, - None, + Some(dsl::last_used_at.desc()), ) .await } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index f61e530031..9e72d8dc21 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -227,6 +227,8 @@ diesel::table! { modified_at -> Timestamp, #[max_length = 64] address_id -> Nullable, + #[max_length = 64] + default_payment_method_id -> Nullable, } } @@ -831,6 +833,7 @@ diesel::table! { payment_method_data -> Nullable, #[max_length = 64] locker_id -> Nullable, + last_used_at -> Timestamp, connector_mandate_details -> Nullable, customer_acceptance -> Nullable, #[max_length = 64] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index ef573093be..e21797d2ee 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -115,6 +115,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::customers::customers_update, routes::customers::customers_delete, routes::customers::customers_mandates_list, + routes::customers::default_payment_method_set_api, //Routes for payment methods routes::payment_method::create_payment_method_api, @@ -188,6 +189,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::CustomerPaymentMethodsListResponse, api_models::payment_methods::PaymentMethodDeleteResponse, api_models::payment_methods::PaymentMethodUpdate, + api_models::payment_methods::CustomerDefaultPaymentMethodResponse, api_models::payment_methods::CardDetailFromLocker, api_models::payment_methods::CardDetail, api_models::payment_methods::RequestPaymentMethodTypes, @@ -374,6 +376,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::BrowserInformation, api_models::payments::PaymentCreatePaymentLinkConfig, api_models::payment_methods::RequiredFieldInfo, + api_models::payment_methods::DefaultPaymentMethod, api_models::payment_methods::MaskedBankDetails, api_models::payment_methods::SurchargeDetailsResponse, api_models::payment_methods::SurchargeResponse, diff --git a/crates/openapi/src/routes/customers.rs b/crates/openapi/src/routes/customers.rs index 19901cbbeb..6011621fc1 100644 --- a/crates/openapi/src/routes/customers.rs +++ b/crates/openapi/src/routes/customers.rs @@ -116,3 +116,23 @@ pub async fn customers_list() {} security(("api_key" = [])) )] pub async fn customers_mandates_list() {} + +/// Customers - Set Default Payment Method +/// +/// Set the Payment Method as Default for the Customer. +#[utoipa::path( + get, + path = "/{customer_id}/payment_methods/{payment_method_id}/default", + params ( + ("method_id" = String, Path, description = "Set the Payment Method as Default for the Customer"), + ), + responses( + (status = 200, description = "Payment Method has been set as default", body =CustomerDefaultPaymentMethodResponse ), + (status = 400, description = "Payment Method has already been set as default for that customer"), + (status = 404, description = "Payment Method not found for the customer") + ), + tag = "Customer Set Default Payment Method", + operation_id = "Set the Payment Method as Default", + security(("ephemeral_key" = [])) +)] +pub async fn default_payment_method_set_api() {} diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 521e3a2216..02127efe6d 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -108,6 +108,7 @@ pub async fn create_customer( address_id: address.clone().map(|addr| addr.address_id), created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), + default_payment_method_id: None, }) } .await @@ -208,6 +209,7 @@ pub async fn delete_customer( .find_payment_method_by_customer_id_merchant_id_list( &req.customer_id, &merchant_account.merchant_id, + None, ) .await { diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index 1347bb8ffd..c5ddb1ed6b 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -43,7 +43,11 @@ pub async fn rust_locker_migration( for customer in domain_customers { let result = db - .find_payment_method_by_customer_id_merchant_id_list(&customer.customer_id, merchant_id) + .find_payment_method_by_customer_id_merchant_id_list( + &customer.customer_id, + merchant_id, + None, + ) .change_context(errors::ApiErrorResponse::InternalServerError) .and_then(|pm| { call_to_locker( diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index bdbd9b02d9..df1fb1e358 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -156,6 +156,10 @@ impl PaymentMethodRetrieve for Oss { helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), + card_token + .payment_method_id + .as_ref() + .unwrap_or(&card_token.token), payment_intent, card_token_data, ) @@ -167,6 +171,10 @@ impl PaymentMethodRetrieve for Oss { helpers::retrieve_card_with_permanent_token( state, card_token.locker_id.as_ref().unwrap_or(&card_token.token), + card_token + .payment_method_id + .as_ref() + .unwrap_or(&card_token.token), payment_intent, card_token_data, ) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index a92215936a..8c00938cad 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -7,8 +7,9 @@ use api_models::{ admin::{self, PaymentMethodsEnabled}, enums::{self as api_enums}, payment_methods::{ - BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, MaskedBankDetails, - PaymentExperienceTypes, PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, + BankAccountConnectorDetails, CardDetailsPaymentMethod, CardNetworkTypes, + CustomerDefaultPaymentMethodResponse, MaskedBankDetails, PaymentExperienceTypes, + PaymentMethodsData, RequestPaymentMethodTypes, RequiredFieldInfo, ResponsePaymentMethodIntermediate, ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled, }, @@ -25,6 +26,7 @@ use diesel_models::{ business_profile::BusinessProfile, encryption::Encryption, enums as storage_enums, payment_method, }; +use domain::CustomerUpdate; use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; @@ -128,11 +130,12 @@ pub fn store_default_payment_method( created: Some(common_utils::date_time::now()), recurring_enabled: false, //[#219] installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(common_utils::date_time::now()), }; + (payment_method_response, None) } - #[instrument(skip_all)] pub async fn get_or_insert_payment_method( db: &dyn db::StorageInterface, @@ -2584,6 +2587,8 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( customer_id: Option<&str>, ) -> errors::RouterResponse { let db = state.store.as_ref(); + let limit = req.clone().and_then(|pml_req| pml_req.limit); + if let Some(customer_id) = customer_id { Box::pin(list_customer_payment_method( &state, @@ -2591,6 +2596,7 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( key_store, None, customer_id, + limit, )) .await } else { @@ -2614,6 +2620,7 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( key_store, payment_intent, &customer_id, + limit, )) .await } @@ -2634,6 +2641,7 @@ pub async fn list_customer_payment_method( key_store: domain::MerchantKeyStore, payment_intent: Option, customer_id: &str, + limit: Option, ) -> errors::RouterResponse { let db = &*state.store; @@ -2645,13 +2653,14 @@ pub async fn list_customer_payment_method( } }; - db.find_customer_by_customer_id_merchant_id( - customer_id, - &merchant_account.merchant_id, - &key_store, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + let customer = db + .find_customer_by_customer_id_merchant_id( + customer_id, + &merchant_account.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; let key = key_store.key.get_inner().peek(); @@ -2671,6 +2680,7 @@ pub async fn list_customer_payment_method( customer_id, &merchant_account.merchant_id, common_enums::PaymentMethodStatus::Active, + limit, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; @@ -2758,6 +2768,7 @@ pub async fn list_customer_payment_method( let pma = api::CustomerPaymentMethod { payment_token: parent_payment_method_token.to_owned(), + payment_method_id: pm.payment_method_id.clone(), customer_id: pm.customer_id, payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, @@ -2773,6 +2784,9 @@ pub async fn list_customer_payment_method( bank: bank_details, surcharge_details: None, requires_cvv, + last_used_at: Some(pm.last_used_at), + default_payment_method_set: customer.default_payment_method_id.is_some() + && customer.default_payment_method_id == Some(pm.payment_method_id), }; customer_pms.push(pma.to_owned()); @@ -3059,7 +3073,96 @@ async fn get_bank_account_connector_details( None => Ok(None), } } +pub async fn set_default_payment_method( + state: routes::AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + customer_id: &str, + payment_method_id: String, +) -> errors::RouterResponse { + let db = &*state.store; + //check for the customer + let customer = db + .find_customer_by_customer_id_merchant_id( + customer_id, + &merchant_account.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + // check for the presence of payment_method + let payment_method = db + .find_payment_method(&payment_method_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + utils::when( + payment_method.customer_id != customer_id + && payment_method.merchant_id != merchant_account.merchant_id, + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "The payment_method_id is not valid".to_string(), + }) + .into_report() + }, + )?; + + utils::when( + Some(payment_method_id.clone()) == customer.default_payment_method_id, + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Payment Method is already set as default".to_string(), + }) + .into_report() + }, + )?; + + let customer_update = CustomerUpdate::UpdateDefaultPaymentMethod { + default_payment_method_id: Some(payment_method_id.clone()), + }; + + // update the db with the default payment method id + let updated_customer_details = db + .update_customer_by_customer_id_merchant_id( + customer_id.to_owned(), + merchant_account.merchant_id, + customer_update, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update the default payment method id for the customer")?; + let resp = CustomerDefaultPaymentMethodResponse { + default_payment_method_id: updated_customer_details.default_payment_method_id, + customer_id: customer.customer_id, + payment_method_type: payment_method.payment_method_type, + payment_method: payment_method.payment_method, + }; + + Ok(services::ApplicationResponse::Json(resp)) +} + +pub async fn update_last_used_at( + pm_id: &str, + state: &routes::AppState, +) -> errors::RouterResult<()> { + let update_last_used = storage::PaymentMethodUpdate::LastUsedUpdate { + last_used_at: common_utils::date_time::now(), + }; + let payment_method = state + .store + .find_payment_method(pm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + state + .store + .update_payment_method(payment_method, update_last_used) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update the last_used_at in db")?; + + Ok(()) +} #[cfg(feature = "payouts")] pub async fn get_bank_from_hs_locker( state: &routes::AppState, @@ -3243,9 +3346,10 @@ pub async fn retrieve_payment_method( card, metadata: pm.metadata, created: Some(pm.created_at), - recurring_enabled: false, //[#219] - installment_payment_enabled: false, //[#219] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219], + recurring_enabled: false, + installment_payment_enabled: false, + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(pm.last_used_at), }, )) } diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 19ecf733df..35491d747e 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -327,7 +327,8 @@ pub fn mk_add_bank_response_hs( created: Some(common_utils::date_time::now()), recurring_enabled: false, // [#256] installment_payment_enabled: false, // #[#256] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(common_utils::date_time::now()), } } @@ -370,7 +371,8 @@ pub fn mk_add_card_response_hs( created: Some(common_utils::date_time::now()), recurring_enabled: false, // [#256] installment_payment_enabled: false, // #[#256] - payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), // [#256] + payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + last_used_at: Some(common_utils::date_time::now()), // [#256] } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index b94b8627ef..782d1814cc 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1070,7 +1070,6 @@ where customer, ) .await?; - *payment_data = pd; // Validating the blocklist guard and generate the fingerprint @@ -1141,7 +1140,6 @@ where let pm_token = router_data .add_payment_method_token(state, &connector, &tokenization_action) .await?; - if let Some(payment_method_token) = pm_token.clone() { router_data.payment_method_token = Some(router_types::PaymentMethodToken::Token( payment_method_token, @@ -1846,7 +1844,7 @@ async fn decide_payment_method_tokenize_action( } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum TokenizationAction { TokenizeInRouter, TokenizeInConnector, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0baba7035c..e0b8a5f623 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1131,6 +1131,7 @@ pub(crate) async fn get_payment_method_create_request( customer_id: Some(customer.customer_id.to_owned()), card_network: None, }; + Ok(payment_method_request) } }, @@ -1402,6 +1403,7 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R, Ctx>( modified_at: common_utils::date_time::now(), connector_customer: None, address_id: None, + default_payment_method_id: None, }) } .await @@ -1545,7 +1547,8 @@ pub async fn retrieve_payment_method_with_temporary_token( pub async fn retrieve_card_with_permanent_token( state: &AppState, - token: &str, + locker_id: &str, + payment_method_id: &str, payment_intent: &PaymentIntent, card_token_data: Option<&CardToken>, ) -> RouterResult { @@ -1556,11 +1559,11 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::UnprocessableEntity { message: "no customer id provided for the payment".to_string(), })?; - - let card = cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, token) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("failed to fetch card information from the permanent locker")?; + let card = + cards::get_card_from_locker(state, customer_id, &payment_intent.merchant_id, locker_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch card information from the permanent locker")?; // The card_holder_name from locker retrieved card is considered if it is a non-empty string or else card_holder_name is picked // from payment_method_data.card_token object @@ -1593,7 +1596,7 @@ pub async fn retrieve_card_with_permanent_token( card_issuing_country: None, bank_code: None, }; - + cards::update_last_used_at(payment_method_id, state).await?; Ok(api::PaymentMethodData::Card(api_card)) } diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index d08f020850..21fd4cf85e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -426,6 +426,7 @@ async fn skip_saving_card_in_locker( metadata: None, created: Some(common_utils::date_time::now()), bank_transfer: None, + last_used_at: Some(common_utils::date_time::now()), }; Ok((pm_resp, None)) @@ -445,6 +446,7 @@ async fn skip_saving_card_in_locker( installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), bank_transfer: None, + last_used_at: Some(common_utils::date_time::now()), }; Ok((payment_method_response, None)) } @@ -491,6 +493,7 @@ pub async fn save_in_locker( recurring_enabled: false, //[#219] installment_payment_enabled: false, //[#219] payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), //[#219] + last_used_at: Some(common_utils::date_time::now()), }; Ok((payment_method_response, None)) } diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index a9ed90e793..fcd2569ea9 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -433,6 +433,7 @@ pub async fn get_or_create_customer_details( created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), address_id: None, + default_payment_method_id: None, }; Ok(Some( diff --git a/crates/router/src/core/pm_auth.rs b/crates/router/src/core/pm_auth.rs index 982ed9cae9..2987370f28 100644 --- a/crates/router/src/core/pm_auth.rs +++ b/crates/router/src/core/pm_auth.rs @@ -297,6 +297,7 @@ async fn store_bank_details_in_payment_methods( .find_payment_method_by_customer_id_merchant_id_list( &customer_id, &merchant_account.merchant_id, + None, ) .await .change_context(ApiErrorResponse::InternalServerError)?; diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 6aea1a2cbc..35c93f334a 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1267,9 +1267,10 @@ impl PaymentMethodInterface for KafkaStore { &self, customer_id: &str, merchant_id: &str, + limit: Option, ) -> CustomResult, errors::StorageError> { self.diesel_store - .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id) + .find_payment_method_by_customer_id_merchant_id_list(customer_id, merchant_id, limit) .await } @@ -1278,9 +1279,15 @@ impl PaymentMethodInterface for KafkaStore { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + limit: Option, ) -> CustomResult, errors::StorageError> { self.diesel_store - .find_payment_method_by_customer_id_merchant_id_status(customer_id, merchant_id, status) + .find_payment_method_by_customer_id_merchant_id_status( + customer_id, + merchant_id, + status, + limit, + ) .await } diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index f15ceecd1c..ddd2857cc2 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -24,6 +24,7 @@ pub trait PaymentMethodInterface { &self, customer_id: &str, merchant_id: &str, + limit: Option, ) -> CustomResult, errors::StorageError>; async fn find_payment_method_by_customer_id_merchant_id_status( @@ -31,6 +32,7 @@ pub trait PaymentMethodInterface { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + limit: Option, ) -> CustomResult, errors::StorageError>; async fn insert_payment_method( @@ -104,12 +106,18 @@ impl PaymentMethodInterface for Store { &self, customer_id: &str, merchant_id: &str, + limit: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; - storage::PaymentMethod::find_by_customer_id_merchant_id(&conn, customer_id, merchant_id) - .await - .map_err(Into::into) - .into_report() + storage::PaymentMethod::find_by_customer_id_merchant_id( + &conn, + customer_id, + merchant_id, + limit, + ) + .await + .map_err(Into::into) + .into_report() } async fn find_payment_method_by_customer_id_merchant_id_status( @@ -117,6 +125,7 @@ impl PaymentMethodInterface for Store { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + limit: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; storage::PaymentMethod::find_by_customer_id_merchant_id_status( @@ -124,6 +133,7 @@ impl PaymentMethodInterface for Store { customer_id, merchant_id, status, + limit, ) .await .map_err(Into::into) @@ -221,6 +231,7 @@ impl PaymentMethodInterface for MockDb { payment_method_issuer_code: payment_method_new.payment_method_issuer_code, metadata: payment_method_new.metadata, payment_method_data: payment_method_new.payment_method_data, + last_used_at: payment_method_new.last_used_at, connector_mandate_details: payment_method_new.connector_mandate_details, customer_acceptance: payment_method_new.customer_acceptance, status: payment_method_new.status, @@ -233,6 +244,7 @@ impl PaymentMethodInterface for MockDb { &self, customer_id: &str, merchant_id: &str, + _limit: Option, ) -> CustomResult, errors::StorageError> { let payment_methods = self.payment_methods.lock().await; let payment_methods_found: Vec = payment_methods @@ -256,6 +268,7 @@ impl PaymentMethodInterface for MockDb { customer_id: &str, merchant_id: &str, status: common_enums::PaymentMethodStatus, + _limit: Option, ) -> CustomResult, errors::StorageError> { let payment_methods = self.payment_methods.lock().await; let payment_methods_found: Vec = payment_methods diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 73558c78d7..1d82bc7539 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -632,6 +632,10 @@ impl Customers { web::resource("/{customer_id}/payment_methods") .route(web::get().to(list_customer_payment_method_api)), ) + .service( + web::resource("/{customer_id}/payment_methods/{payment_method_id}/default") + .route(web::post().to(default_payment_method_set_api)), + ) .service( web::resource("/{customer_id}") .route(web::get().to(customers_retrieve)) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 9471289a0c..edbdee7bf6 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -96,7 +96,8 @@ impl From for ApiIdentifier { | Flow::PaymentMethodsRetrieve | Flow::PaymentMethodsUpdate | Flow::PaymentMethodsDelete - | Flow::ValidatePaymentMethod => Self::PaymentMethods, + | Flow::ValidatePaymentMethod + | Flow::DefaultPaymentMethodsSet => Self::PaymentMethods, Flow::PmAuthLinkTokenCreate | Flow::PmAuthExchangeToken => Self::PaymentMethodAuth, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 89ca36c8c1..5469f98165 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -102,6 +102,12 @@ pub async fn list_customer_payment_method_api( let flow = Flow::CustomerPaymentMethodsList; let payload = query_payload.into_inner(); let customer_id = customer_id.into_inner().0; + + let ephemeral_auth = + match auth::is_ephemeral_auth(req.headers(), &*state.store, &customer_id).await { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(err), + }; Box::pin(api::server_wrap( flow, state, @@ -116,7 +122,7 @@ pub async fn list_customer_payment_method_api( Some(&customer_id), ) }, - &auth::ApiKeyAuth, + &*ephemeral_auth, api_locking::LockAction::NotApplicable, )) .await @@ -157,6 +163,7 @@ pub async fn list_customer_payment_method_api_client( Ok((auth, _auth_flow)) => (auth, _auth_flow), Err(e) => return api::log_and_return_error_response(e), }; + Box::pin(api::server_wrap( flow, state, @@ -252,6 +259,42 @@ pub async fn payment_method_delete_api( )) .await } + +#[instrument(skip_all, fields(flow = ?Flow::DefaultPaymentMethodsSet))] +pub async fn default_payment_method_set_api( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::DefaultPaymentMethodsSet; + let payload = path.into_inner(); + let customer_id = payload.clone().customer_id; + + let ephemeral_auth = + match auth::is_ephemeral_auth(req.headers(), &*state.store, &customer_id).await { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(err), + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, default_payment_method| { + cards::set_default_payment_method( + state, + auth.merchant_account, + auth.key_store, + &customer_id, + default_payment_method.payment_method_id, + ) + }, + &*ephemeral_auth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 894b3d830d..009033d534 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -204,7 +204,7 @@ type BoxedConnector = Box<&'static (dyn Connector + Sync)>; // Normal flow will call the connector and follow the flow specific operations (capture, authorize) // SessionTokenFromMetadata will avoid calling the connector instead create the session token ( for sdk ) -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq, Debug)] pub enum GetToken { GpayMetadata, ApplePayMetadata, @@ -214,7 +214,7 @@ pub enum GetToken { /// Routing algorithm will output merchant connector identifier instead of connector name /// In order to support backwards compatibility for older routing algorithms and merchant accounts /// the support for connector name is retained -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ConnectorData { pub connector: BoxedConnector, pub connector_name: types::Connector, diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index 32430c0918..6f08a7fd7e 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -32,6 +32,7 @@ impl From<(domain::Customer, Option)> for CustomerResp created_at: cust.created_at, metadata: cust.metadata, address, + default_payment_method_id: cust.default_payment_method_id, } .into() } diff --git a/crates/router/src/types/api/payment_methods.rs b/crates/router/src/types/api/payment_methods.rs index ca852f832e..642e94ad69 100644 --- a/crates/router/src/types/api/payment_methods.rs +++ b/crates/router/src/types/api/payment_methods.rs @@ -1,10 +1,11 @@ pub use api_models::payment_methods::{ CardDetail, CardDetailFromLocker, CardDetailsPaymentMethod, CustomerPaymentMethod, - CustomerPaymentMethodsListResponse, DeleteTokenizeByTokenRequest, GetTokenizePayloadRequest, - GetTokenizePayloadResponse, PaymentMethodCreate, PaymentMethodDeleteResponse, PaymentMethodId, - PaymentMethodList, PaymentMethodListRequest, PaymentMethodListResponse, PaymentMethodResponse, - PaymentMethodUpdate, PaymentMethodsData, TokenizePayloadEncrypted, TokenizePayloadRequest, - TokenizedCardValue1, TokenizedCardValue2, TokenizedWalletValue1, TokenizedWalletValue2, + CustomerPaymentMethodsListResponse, DefaultPaymentMethod, DeleteTokenizeByTokenRequest, + GetTokenizePayloadRequest, GetTokenizePayloadResponse, PaymentMethodCreate, + PaymentMethodDeleteResponse, PaymentMethodId, PaymentMethodList, PaymentMethodListRequest, + PaymentMethodListResponse, PaymentMethodResponse, PaymentMethodUpdate, PaymentMethodsData, + TokenizePayloadEncrypted, TokenizePayloadRequest, TokenizedCardValue1, TokenizedCardValue2, + TokenizedWalletValue1, TokenizedWalletValue2, }; use error_stack::report; diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs index fe575851dc..5437d06a2e 100644 --- a/crates/router/src/types/domain/customer.rs +++ b/crates/router/src/types/domain/customer.rs @@ -22,6 +22,7 @@ pub struct Customer { pub modified_at: PrimitiveDateTime, pub connector_customer: Option, pub address_id: Option, + pub default_payment_method_id: Option, } #[async_trait::async_trait] @@ -45,6 +46,7 @@ impl super::behaviour::Conversion for Customer { modified_at: self.modified_at, connector_customer: self.connector_customer, address_id: self.address_id, + default_payment_method_id: self.default_payment_method_id, }) } @@ -72,6 +74,7 @@ impl super::behaviour::Conversion for Customer { modified_at: item.modified_at, connector_customer: item.connector_customer, address_id: item.address_id, + default_payment_method_id: item.default_payment_method_id, }) } .await @@ -114,6 +117,9 @@ pub enum CustomerUpdate { ConnectorCustomer { connector_customer: Option, }, + UpdateDefaultPaymentMethod { + default_payment_method_id: Option, + }, } impl From for CustomerUpdateInternal { @@ -138,12 +144,20 @@ impl From for CustomerUpdateInternal { connector_customer, modified_at: Some(date_time::now()), address_id, + ..Default::default() }, CustomerUpdate::ConnectorCustomer { connector_customer } => Self { connector_customer, modified_at: Some(common_utils::date_time::now()), ..Default::default() }, + CustomerUpdate::UpdateDefaultPaymentMethod { + default_payment_method_id, + } => Self { + default_payment_method_id, + modified_at: Some(common_utils::date_time::now()), + ..Default::default() + }, } } } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 62c09c4245..4790e60acb 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -123,6 +123,8 @@ pub enum Flow { PaymentMethodsUpdate, /// Payment methods delete flow. PaymentMethodsDelete, + /// Default Payment method flow. + DefaultPaymentMethodsSet, /// Payments create flow. PaymentsCreate, /// Payments Retrieve flow. diff --git a/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/down.sql b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/down.sql new file mode 100644 index 0000000000..fc9fc6350e --- /dev/null +++ b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_methods DROP COLUMN IF EXISTS last_used_at; \ No newline at end of file diff --git a/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/up.sql b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/up.sql new file mode 100644 index 0000000000..f1c0aab4de --- /dev/null +++ b/migrations/2024-02-21-120100_add_last_used_at_in_payment_methods/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_methods ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP; \ No newline at end of file diff --git a/migrations/2024-02-21-143530_add_default_payment_method_in_customers/down.sql b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/down.sql new file mode 100644 index 0000000000..948123ce7e --- /dev/null +++ b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE customers DROP COLUMN IF EXISTS default_payment_method; diff --git a/migrations/2024-02-21-143530_add_default_payment_method_in_customers/up.sql b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/up.sql new file mode 100644 index 0000000000..abaeb1df71 --- /dev/null +++ b/migrations/2024-02-21-143530_add_default_payment_method_in_customers/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE customers +ADD COLUMN IF NOT EXISTS default_payment_method_id VARCHAR(64); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 30e75c028d..3fcf6bca79 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4080,6 +4080,50 @@ } ] } + }, + "/{customer_id}/payment_methods/{payment_method_id}/default": { + "get": { + "tags": [ + "Customer Set Default Payment Method" + ], + "summary": "Customers - Set Default Payment Method", + "description": "Customers - Set Default Payment Method\n\nSet the Payment Method as Default for the Customer.", + "operationId": "Set the Payment Method as Default", + "parameters": [ + { + "name": "method_id", + "in": "path", + "description": "Set the Payment Method as Default for the Customer", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Payment Method has been set as default", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerDefaultPaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Payment Method has already been set as default for that customer" + }, + "404": { + "description": "Payment Method not found for the customer" + } + }, + "security": [ + { + "ephemeral_key": [] + } + ] + } } }, "components": { @@ -7308,6 +7352,37 @@ } } }, + "CustomerDefaultPaymentMethodResponse": { + "type": "object", + "required": [ + "customer_id", + "payment_method" + ], + "properties": { + "default_payment_method_id": { + "type": "string", + "description": "The unique identifier of the Payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIy", + "nullable": true + }, + "customer_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "cus_meowerunwiuwiwqw" + }, + "payment_method": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_type": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodType" + } + ], + "nullable": true + } + } + }, "CustomerDeleteResponse": { "type": "object", "required": [ @@ -7384,11 +7459,13 @@ "type": "object", "required": [ "payment_token", + "payment_method_id", "customer_id", "payment_method", "recurring_enabled", "installment_payment_enabled", - "requires_cvv" + "requires_cvv", + "default_payment_method_set" ], "properties": { "payment_token": { @@ -7396,6 +7473,11 @@ "description": "Token for payment method in temporary card locker which gets refreshed often", "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef" }, + "payment_method_id": { + "type": "string", + "description": "The unique identifier of the customer.", + "example": "pm_iouuy468iyuowqs" + }, "customer_id": { "type": "string", "description": "The unique identifier of the customer.", @@ -7495,6 +7577,18 @@ "type": "boolean", "description": "Whether this payment method requires CVV to be collected", "example": true + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was last used", + "example": "2024-02-24T11:04:09.922Z", + "nullable": true + }, + "default_payment_method_set": { + "type": "boolean", + "description": "Indicates if the payment method has been set to default or not", + "example": true } } }, @@ -7644,6 +7738,28 @@ "type": "object", "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", "nullable": true + }, + "default_payment_method_id": { + "type": "string", + "description": "The identifier for the default payment method.", + "example": "pm_djh2837dwduh890123", + "nullable": true, + "maxLength": 64 + } + } + }, + "DefaultPaymentMethod": { + "type": "object", + "required": [ + "customer_id", + "payment_method_id" + ], + "properties": { + "customer_id": { + "type": "string" + }, + "payment_method_id": { + "type": "string" } } }, @@ -11888,6 +12004,12 @@ } ], "nullable": true + }, + "last_used_at": { + "type": "string", + "format": "date-time", + "example": "2024-02-24T11:04:09.922Z", + "nullable": true } } },