fix: refund fix with connector_metadata (#330)

This commit is contained in:
Nishant Joshi
2023-01-10 19:24:19 +05:30
committed by GitHub
parent ca994f6a3c
commit 9ad56703c4
28 changed files with 163 additions and 91 deletions

View File

@ -354,6 +354,7 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
}
errors::ApiErrorResponse::InvalidCardData { data } => Self::InvalidCardType, // Maybe it is better to de generalize this router error
errors::ApiErrorResponse::CardExpired { data } => Self::ExpiredCard,
errors::ApiErrorResponse::RefundNotPossible { connector } => Self::RefundFailed,
errors::ApiErrorResponse::RefundFailed { data } => Self::RefundFailed, // Nothing at stripe to map
errors::ApiErrorResponse::InternalServerError => Self::InternalServerError, // not a stripe code

View File

@ -218,6 +218,7 @@ impl<F, T>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})

View File

@ -429,6 +429,7 @@ impl TryFrom<types::PaymentsCancelResponseRouterData<AdyenCancelResponse>>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
@ -477,6 +478,7 @@ pub fn get_adyen_response(
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
};
Ok((status, error, payments_response_data))
}
@ -542,6 +544,7 @@ pub fn get_redirection_response(
redirection_data: Some(redirection_data),
redirect: true,
mandate_reference: None,
connector_metadata: None,
};
Ok((status, error, payments_response_data))
}
@ -635,6 +638,7 @@ impl TryFrom<types::PaymentsCaptureResponseRouterData<AdyenCaptureResponse>>
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
amount_captured,
..item.data

View File

@ -1,3 +1,4 @@
use common_utils::ext_traits::{Encode, ValueExt};
use error_stack::ResultExt;
use serde::{Deserialize, Serialize};
@ -5,6 +6,7 @@ use crate::{
core::errors,
pii::PeekInterface,
types::{self, api, storage::enums},
utils::OptionExt,
};
#[derive(Debug, Serialize, PartialEq, Eq)]
@ -38,21 +40,22 @@ impl TryFrom<&types::ConnectorAuthType> for MerchantAuthentication {
}
}
#[derive(Serialize, PartialEq)]
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
struct CreditCardDetails {
card_number: String,
expiration_date: String,
card_code: String,
card_number: masking::Secret<String, common_utils::pii::CardNumber>,
expiration_date: masking::Secret<String>,
#[serde(skip_serializing_if = "Option::is_none")]
card_code: Option<masking::Secret<String>>,
}
#[derive(Serialize, PartialEq)]
#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
struct BankAccountDetails {
account_number: String,
account_number: masking::Secret<String>,
}
#[derive(Serialize, PartialEq)]
#[derive(Serialize, Deserialize, PartialEq, Debug)]
enum PaymentDetails {
#[serde(rename = "creditCard")]
CreditCard(CreditCardDetails),
@ -63,6 +66,29 @@ enum PaymentDetails {
Paypal,
}
impl From<api_models::payments::PaymentMethod> for PaymentDetails {
fn from(value: api_models::payments::PaymentMethod) -> Self {
match value {
api::PaymentMethod::Card(ref ccard) => {
let expiry_month = ccard.card_exp_month.peek().clone();
let expiry_year = ccard.card_exp_year.peek().clone();
Self::CreditCard(CreditCardDetails {
card_number: ccard.card_number.clone(),
expiration_date: format!("{expiry_year}-{expiry_month}").into(),
card_code: Some(ccard.card_cvc.clone()),
})
}
api::PaymentMethod::BankTransfer => Self::BankAccount(BankAccountDetails {
account_number: "XXXXX".to_string().into(),
}),
api::PaymentMethod::PayLater(_) => Self::Klarna,
api::PaymentMethod::Wallet(_) => Self::Wallet,
api::PaymentMethod::Paypal => Self::Paypal,
}
}
}
#[derive(Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct TransactionRequest {
@ -132,24 +158,7 @@ impl From<enums::CaptureMethod> for AuthorizationType {
impl TryFrom<&types::PaymentsAuthorizeRouterData> for CreateTransactionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let payment_details = match item.request.payment_method_data {
api::PaymentMethod::Card(ref ccard) => {
let expiry_month = ccard.card_exp_month.peek().clone();
let expiry_year = ccard.card_exp_year.peek().clone();
PaymentDetails::CreditCard(CreditCardDetails {
card_number: ccard.card_number.peek().clone(),
expiration_date: format!("{expiry_year}-{expiry_month}"),
card_code: ccard.card_cvc.peek().clone(),
})
}
api::PaymentMethod::BankTransfer => PaymentDetails::BankAccount(BankAccountDetails {
account_number: "XXXXX".to_string(),
}),
api::PaymentMethod::PayLater(_) => PaymentDetails::Klarna,
api::PaymentMethod::Wallet(_) => PaymentDetails::Wallet,
api::PaymentMethod::Paypal => PaymentDetails::Paypal,
};
let payment_details = item.request.payment_method_data.clone().into();
let authorization_indicator_type =
item.request.capture_method.map(|c| AuthorizationIndicator {
authorization_indicator: c.into(),
@ -252,6 +261,7 @@ pub struct TransactionResponse {
auth_code: String,
#[serde(rename = "transId")]
transaction_id: String,
pub(super) account_number: Option<String>,
pub(super) errors: Option<Vec<ErrorMessage>>,
}
@ -294,6 +304,20 @@ impl<F, T>
})
});
let metadata = item
.response
.transaction_response
.account_number
.map(|acc_no| {
Encode::<'_, PaymentDetails>::encode_to_value(&construct_refund_payment_details(
acc_no,
))
})
.transpose()
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "connector_metadata".to_string(),
})?;
Ok(Self {
status,
response: match error {
@ -305,6 +329,7 @@ impl<F, T>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: metadata,
}),
},
..item.data
@ -340,32 +365,26 @@ pub struct CreateRefundRequest {
impl<F> TryFrom<&types::RefundsRouterData<F>> for CreateRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
let (payment_details, merchant_authentication, transaction_request);
payment_details = match item.request.payment_method_data {
api::PaymentMethod::Card(ref ccard) => {
let expiry_month = ccard.card_exp_month.peek().clone();
let expiry_year = ccard.card_exp_year.peek().clone();
let payment_details = item
.request
.connector_metadata
.as_ref()
.get_required_value("connector_metadata")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "connector_metadata".to_string(),
})?
.clone();
PaymentDetails::CreditCard(CreditCardDetails {
card_number: ccard.card_number.peek().clone(),
expiration_date: format!("{expiry_year}-{expiry_month}"),
card_code: ccard.card_cvc.peek().clone(),
})
}
api::PaymentMethod::BankTransfer => PaymentDetails::BankAccount(BankAccountDetails {
account_number: "XXXXX".to_string(),
}),
api::PaymentMethod::PayLater(_) => PaymentDetails::Klarna,
api::PaymentMethod::Wallet(_) => PaymentDetails::Wallet,
api::PaymentMethod::Paypal => PaymentDetails::Paypal,
};
let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?;
merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?;
transaction_request = RefundTransactionRequest {
let transaction_request = RefundTransactionRequest {
transaction_type: TransactionType::Refund,
amount: item.request.refund_amount,
payment: payment_details,
payment: payment_details
.parse_value("PaymentDetails")
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "payment_details".to_string(),
})?,
currency_code: item.request.currency.to_string(),
reference_transaction_id: item.request.connector_transaction_id.clone(),
};
@ -590,6 +609,7 @@ impl<F, Req>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
status: payment_status,
..item.data
@ -610,3 +630,11 @@ pub struct ErrorDetails {
pub struct AuthorizedotnetErrorResponse {
pub error: ErrorDetails,
}
fn construct_refund_payment_details(masked_number: String) -> PaymentDetails {
PaymentDetails::CreditCard(CreditCardDetails {
card_number: masked_number.into(),
expiration_date: "XXXX".to_string().into(),
card_code: None,
})
}

View File

@ -212,6 +212,7 @@ impl<F, T>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})

View File

@ -220,6 +220,7 @@ impl TryFrom<types::PaymentsResponseRouterData<PaymentsResponse>>
redirect: redirection_data.is_some(),
redirection_data,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
@ -259,6 +260,7 @@ impl TryFrom<types::PaymentsSyncResponseRouterData<PaymentsResponse>>
redirect: redirection_data.is_some(),
redirection_data,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})
@ -301,6 +303,7 @@ impl TryFrom<types::PaymentsCancelResponseRouterData<PaymentVoidResponse>>
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
status: response.into(),
..item.data
@ -372,6 +375,7 @@ impl TryFrom<types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>>
redirect: false,
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
}),
status,
amount_captured,

View File

@ -234,6 +234,7 @@ impl TryFrom<types::PaymentsResponseRouterData<CybersourcePaymentsResponse>>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})

View File

@ -131,6 +131,7 @@ impl TryFrom<types::PaymentsResponseRouterData<KlarnaPaymentsResponse>>
redirect: true,
redirection_data: Some(redirection_data),
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})

View File

@ -156,6 +156,7 @@ impl<F, T>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})

View File

@ -396,6 +396,7 @@ impl<F, T>
redirect: redirection_data.is_some(),
redirection_data,
mandate_reference,
connector_metadata: None,
}),
amount_captured: Some(item.response.amount_received),
..item.data
@ -445,6 +446,7 @@ impl<F, T>
redirect: redirection_data.is_some(),
redirection_data,
mandate_reference,
connector_metadata: None,
}),
..item.data
})

View File

@ -161,6 +161,7 @@ impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsR
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..data.clone()
})
@ -249,6 +250,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..data.clone()
})
@ -306,6 +308,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..data.clone()
})

View File

@ -169,6 +169,7 @@ impl TryFrom<types::PaymentsResponseRouterData<WorldpayPaymentsResponse>>
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
}),
..item.data
})

View File

@ -107,6 +107,8 @@ pub enum ApiErrorResponse {
MandateNotFound,
#[error(error_type = ErrorType::ValidationError, code = "RE_03", message = "Return URL is not configured and not passed in payments request.")]
ReturnUrlUnavailable,
#[error(error_type = ErrorType::ValidationError, code = "RE_03", message = "Refunds not possible through hyperswitch. Please raise Refunds through {connector} dashboard")]
RefundNotPossible { connector: String },
#[error(error_type = ErrorType::DuplicateRequest, code = "RE_04", message = "The merchant account with the specified details already exists in our records.")]
DuplicateMerchantAccount,
#[error(error_type = ErrorType::DuplicateRequest, code = "RE_04", message = "The merchant connector account with the specified details already exists in our records.")]
@ -163,6 +165,7 @@ impl actix_web::ResponseError for ApiErrorResponse {
| Self::InvalidCardData { .. }
| Self::CardExpired { .. }
| Self::RefundFailed { .. }
| Self::RefundNotPossible { .. }
| Self::VerificationFailed { .. }
| Self::PaymentUnexpectedState { .. }
| Self::MandateValidationFailed { .. } => StatusCode::BAD_REQUEST, // 400

View File

@ -178,6 +178,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
resource_id,
redirection_data,
redirect,
connector_metadata,
..
} => {
let connector_transaction_id = match resource_id {
@ -206,6 +207,7 @@ async fn payment_response_update_tracker<F: Clone, T>(
.mandate_id
.clone()
.map(|mandate| mandate.mandate_id),
connector_metadata,
};
let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate {

View File

@ -66,6 +66,7 @@ where
redirection_data: None,
redirect: false,
mandate_reference: None,
connector_metadata: None,
});
let router_return_url = Some(helpers::create_redirect_url(

View File

@ -113,12 +113,13 @@ pub async fn trigger_refund_to_gateway(
.attach_printable("Transaction in invalid")
})?;
validator::validate_for_valid_refunds(payment_attempt)?;
let router_data = core_utils::construct_refund_router_data(
state,
&connector_id,
merchant_account,
(payment_attempt.amount, currency),
None,
payment_intent,
payment_attempt,
refund,
@ -261,7 +262,6 @@ pub async fn sync_refund_with_gateway(
&connector_id,
merchant_account,
(payment_attempt.amount, currency),
None,
payment_intent,
payment_attempt,
refund,

View File

@ -1,4 +1,5 @@
use error_stack::report;
use common_utils::ext_traits::StringExt;
use error_stack::{report, ResultExt};
use router_env::{instrument, tracing};
use time::PrimitiveDateTime;
@ -7,7 +8,7 @@ use crate::{
db::StorageInterface,
logger,
types::storage::{self, enums},
utils,
utils::{self, OptionExt},
};
#[derive(Debug, thiserror::Error)]
@ -135,3 +136,35 @@ pub fn validate_refund_list(limit: Option<i64>) -> CustomResult<i64, errors::Api
None => Ok(10),
}
}
pub fn validate_for_valid_refunds(
payment_attempt: &storage_models::payment_attempt::PaymentAttempt,
) -> RouterResult<()> {
let connector: api_models::enums::Connector = payment_attempt
.connector
.clone()
.get_required_value("connector")?
.parse_enum("connector")
.change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven)?;
let payment_method = payment_attempt
.payment_method
.get_required_value("payment_method")?;
utils::when(
matches!(
(connector, payment_method),
(
api_models::enums::Connector::Braintree,
storage_models::enums::PaymentMethodType::Paypal
) | (
api_models::enums::Connector::Klarna,
storage_models::enums::PaymentMethodType::Klarna
)
),
|| {
Err(errors::ApiErrorResponse::RefundNotPossible {
connector: connector.to_string(),
})
},
)?;
Ok(())
}

View File

@ -3,13 +3,13 @@ use std::marker::PhantomData;
use error_stack::ResultExt;
use router_env::{instrument, tracing};
use super::payments::{helpers, PaymentAddress};
use super::payments::PaymentAddress;
use crate::{
consts,
core::errors::{self, RouterResult},
routes::AppState,
types::{
self, api,
self,
storage::{self, enums},
},
utils::{generate_id, OptionExt, ValueExt},
@ -22,7 +22,6 @@ pub async fn construct_refund_router_data<'a, F>(
connector_id: &str,
merchant_account: &storage::MerchantAccount,
money: (i64, enums::Currency),
payment_method_data: Option<&'a api::PaymentMethod>,
payment_intent: &'a storage::PaymentIntent,
payment_attempt: &storage::PaymentAttempt,
refund: &'a storage::Refund,
@ -48,17 +47,6 @@ pub async fn construct_refund_router_data<'a, F>(
let payment_method_type = payment_attempt
.payment_method
.get_required_value("payment_method_type")?;
let payment_method_data = match payment_method_data.cloned() {
Some(v) => v,
None => {
let (pm, _) = helpers::Vault::get_payment_method_data_from_locker(
state,
&payment_attempt.attempt_id,
)
.await?;
pm.get_required_value("payment_method_data")?
}
};
let router_data = types::RouterData {
flow: PhantomData,
@ -80,11 +68,11 @@ pub async fn construct_refund_router_data<'a, F>(
amount_captured: payment_intent.amount_captured,
request: types::RefundsData {
refund_id: refund.refund_id.clone(),
payment_method_data,
connector_transaction_id: refund.connector_transaction_id.clone(),
refund_amount: refund.refund_amount,
currency,
amount,
connector_metadata: payment_attempt.connector_metadata.clone(),
reason: refund.refund_reason.clone(),
},

View File

@ -243,6 +243,7 @@ impl PaymentAttemptInterface for MockDb {
browser_info: None,
payment_token: None,
error_code: payment_attempt.error_code,
connector_metadata: None,
};
payment_attempts.push(payment_attempt.clone());
Ok(payment_attempt)
@ -381,6 +382,7 @@ mod storage {
browser_info: payment_attempt.browser_info.clone(),
payment_token: payment_attempt.payment_token.clone(),
error_code: payment_attempt.error_code.clone(),
connector_metadata: payment_attempt.connector_metadata.clone(),
};
let field = format!("pa_{}", created_attempt.attempt_id);

View File

@ -163,6 +163,7 @@ pub enum PaymentsResponseData {
redirection_data: Option<services::RedirectForm>,
redirect: bool,
mandate_reference: Option<String>,
connector_metadata: Option<serde_json::Value>,
},
SessionResponse {
session_token: api::SessionToken,
@ -195,7 +196,6 @@ impl ResponseId {
#[derive(Debug, Clone)]
pub struct RefundsData {
pub refund_id: String,
pub payment_method_data: payments::PaymentMethod,
pub connector_transaction_id: String,
pub currency: storage_enums::Currency,
/// Amount for the payment against which this refund is issued
@ -203,6 +203,8 @@ pub struct RefundsData {
pub reason: Option<String>,
/// Amount to be refunded
pub refund_amount: i64,
/// Arbitrary metadata required for refund
pub connector_metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]

View File

@ -82,15 +82,9 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(types::api::CCard {
card_number: Secret::new("4200000000000000".to_string()),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_holder_name: Secret::new("John Doe".to_string()),
card_cvc: Secret::new("999".to_string()),
}),
connector_transaction_id: String::new(),
refund_amount: 100,
connector_metadata: None,
reason: None,
},
payment_method_id: None,

View File

@ -82,15 +82,9 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(types::api::CCard {
card_number: Secret::new("5424000000000015".to_string()),
card_exp_month: Secret::new("10".to_string()),
card_exp_year: Secret::new("2025".to_string()),
card_holder_name: Secret::new("John Doe".to_string()),
card_cvc: Secret::new("999".to_string()),
}),
connector_transaction_id: String::new(),
refund_amount: 1,
connector_metadata: None,
reason: None,
},
response: Err(types::ErrorResponse::default()),

View File

@ -79,15 +79,9 @@ fn construct_refund_router_data<F>() -> types::RefundsRouterData<F> {
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_number: "4242424242424242".to_string().into(),
card_exp_month: "10".to_string().into(),
card_exp_year: "35".to_string().into(),
card_holder_name: "John Doe".to_string().into(),
card_cvc: "123".to_string().into(),
}),
connector_transaction_id: String::new(),
refund_amount: 10,
connector_metadata: None,
reason: None,
},
response: Err(types::ErrorResponse::default()),

View File

@ -114,9 +114,9 @@ pub trait ConnectorActions: Connector {
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
connector_transaction_id: transaction_id,
refund_amount: 100,
connector_metadata: None,
reason: None,
}),
);
@ -137,9 +137,9 @@ pub trait ConnectorActions: Connector {
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
connector_transaction_id: transaction_id,
refund_amount: 100,
connector_metadata: None,
reason: None,
}),
);
@ -248,9 +248,9 @@ impl Default for PaymentRefundType {
amount: 1000,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
payment_method_data: types::api::PaymentMethod::Card(CCardType::default().0),
connector_transaction_id: String::new(),
refund_amount: 100,
connector_metadata: None,
reason: None,
};
Self(data)

View File

@ -38,6 +38,7 @@ pub struct PaymentAttempt {
pub browser_info: Option<serde_json::Value>,
pub error_code: Option<String>,
pub payment_token: Option<String>,
pub connector_metadata: Option<serde_json::Value>,
}
#[derive(
@ -76,6 +77,7 @@ pub struct PaymentAttemptNew {
pub browser_info: Option<serde_json::Value>,
pub payment_token: Option<String>,
pub error_code: Option<String>,
pub connector_metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -116,6 +118,7 @@ pub enum PaymentAttemptUpdate {
payment_method_id: Option<Option<String>>,
redirect: Option<bool>,
mandate_id: Option<String>,
connector_metadata: Option<serde_json::Value>,
},
StatusUpdate {
status: storage_enums::AttemptStatus,
@ -147,6 +150,7 @@ pub struct PaymentAttemptUpdateInternal {
browser_info: Option<serde_json::Value>,
payment_token: Option<String>,
error_code: Option<String>,
connector_metadata: Option<serde_json::Value>,
}
impl PaymentAttemptUpdate {
@ -238,6 +242,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
payment_method_id,
redirect,
mandate_id,
connector_metadata,
} => Self {
status: Some(status),
connector,
@ -247,6 +252,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
modified_at: Some(common_utils::date_time::now()),
redirect,
mandate_id,
connector_metadata,
..Default::default()
},
PaymentAttemptUpdate::ErrorUpdate {

View File

@ -217,6 +217,7 @@ diesel::table! {
browser_info -> Nullable<Jsonb>,
error_code -> Nullable<Varchar>,
payment_token -> Nullable<Varchar>,
connector_metadata -> Nullable<Jsonb>,
}
}

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE payment_attempt DROP COLUMN connector_metadata;

View File

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE payment_attempt ADD COLUMN connector_metadata JSONB DEFAULT NULL;