mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(connector): [Airwallex] add multiple redirect support for 3DS (#811)
Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in> Co-authored-by: Jagan Elavarasan <jaganelavarasan@gmail.com>
This commit is contained in:
@ -40,6 +40,7 @@ pub enum AttemptStatus {
|
|||||||
Failure,
|
Failure,
|
||||||
PaymentMethodAwaited,
|
PaymentMethodAwaited,
|
||||||
ConfirmationAwaited,
|
ConfirmationAwaited,
|
||||||
|
DeviceDataCollectionPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
@ -767,9 +768,10 @@ impl From<AttemptStatus> for IntentStatus {
|
|||||||
AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
|
AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
|
||||||
|
|
||||||
AttemptStatus::Authorized => Self::RequiresCapture,
|
AttemptStatus::Authorized => Self::RequiresCapture,
|
||||||
AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction,
|
AttemptStatus::AuthenticationPending | AttemptStatus::DeviceDataCollectionPending => {
|
||||||
|
Self::RequiresCustomerAction
|
||||||
|
}
|
||||||
AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||||
|
|
||||||
AttemptStatus::PartialCharged
|
AttemptStatus::PartialCharged
|
||||||
| AttemptStatus::Started
|
| AttemptStatus::Started
|
||||||
| AttemptStatus::AuthenticationSuccessful
|
| AttemptStatus::AuthenticationSuccessful
|
||||||
|
|||||||
@ -1222,6 +1222,8 @@ pub struct Metadata {
|
|||||||
#[schema(value_type = Object, example = r#"{ "city": "NY", "unit": "245" }"#)]
|
#[schema(value_type = Object, example = r#"{ "city": "NY", "unit": "245" }"#)]
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub data: pii::SecretSerdeValue,
|
pub data: pii::SecretSerdeValue,
|
||||||
|
/// Payload coming in request as a metadata field
|
||||||
|
pub payload: Option<pii::SecretSerdeValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
|
||||||
|
|||||||
@ -88,7 +88,7 @@ impl ConnectorCommon for Airwallex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl api::Payment for Airwallex {}
|
impl api::Payment for Airwallex {}
|
||||||
|
impl api::PaymentsCompleteAuthorize for Airwallex {}
|
||||||
impl api::PreVerify for Airwallex {}
|
impl api::PreVerify for Airwallex {}
|
||||||
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::PaymentsResponseData>
|
||||||
for Airwallex
|
for Airwallex
|
||||||
@ -461,6 +461,95 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl
|
||||||
|
ConnectorIntegration<
|
||||||
|
api::CompleteAuthorize,
|
||||||
|
types::CompleteAuthorizeData,
|
||||||
|
types::PaymentsResponseData,
|
||||||
|
> for Airwallex
|
||||||
|
{
|
||||||
|
fn get_headers(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
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::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<String, errors::ConnectorError> {
|
||||||
|
let connector_payment_id = req
|
||||||
|
.request
|
||||||
|
.connector_transaction_id
|
||||||
|
.clone()
|
||||||
|
.ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
|
||||||
|
Ok(format!(
|
||||||
|
"{}api/v1/pa/payment_intents/{}/confirm_continue",
|
||||||
|
self.base_url(connectors),
|
||||||
|
connector_payment_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn get_request_body(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
) -> CustomResult<Option<String>, errors::ConnectorError> {
|
||||||
|
let req_obj = airwallex::AirwallexCompleteRequest::try_from(req)?;
|
||||||
|
let req = utils::Encode::<airwallex::AirwallexCompleteRequest>::encode_to_string_of_json(
|
||||||
|
&req_obj,
|
||||||
|
)
|
||||||
|
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||||
|
Ok(Some(req))
|
||||||
|
}
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
connectors: &settings::Connectors,
|
||||||
|
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||||
|
Ok(Some(
|
||||||
|
services::RequestBuilder::new()
|
||||||
|
.method(services::Method::Post)
|
||||||
|
.url(&types::PaymentsComeplteAuthorizeType::get_url(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.headers(types::PaymentsComeplteAuthorizeType::get_headers(
|
||||||
|
self, req, connectors,
|
||||||
|
)?)
|
||||||
|
.body(types::PaymentsComeplteAuthorizeType::get_request_body(
|
||||||
|
self, req,
|
||||||
|
)?)
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn handle_response(
|
||||||
|
&self,
|
||||||
|
data: &types::PaymentsCompleteAuthorizeRouterData,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<types::PaymentsCompleteAuthorizeRouterData, errors::ConnectorError> {
|
||||||
|
let response: airwallex::AirwallexPaymentsResponse = res
|
||||||
|
.response
|
||||||
|
.parse_struct("AirwallexPaymentsResponse")
|
||||||
|
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||||
|
types::RouterData::try_from(types::ResponseRouterData {
|
||||||
|
response,
|
||||||
|
data: data.clone(),
|
||||||
|
http_code: res.status_code,
|
||||||
|
})
|
||||||
|
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_error_response(
|
||||||
|
&self,
|
||||||
|
res: Response,
|
||||||
|
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
|
||||||
|
self.build_error_response(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl api::PaymentCapture for Airwallex {}
|
impl api::PaymentCapture for Airwallex {}
|
||||||
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::PaymentsResponseData>
|
||||||
for Airwallex
|
for Airwallex
|
||||||
@ -906,3 +995,18 @@ impl api::IncomingWebhook for Airwallex {
|
|||||||
Ok(details.data.object)
|
Ok(details.data.object)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl services::ConnectorRedirectResponse for Airwallex {
|
||||||
|
fn get_flow_type(
|
||||||
|
&self,
|
||||||
|
_query_params: &str,
|
||||||
|
_json_payload: Option<serde_json::Value>,
|
||||||
|
action: services::PaymentAction,
|
||||||
|
) -> CustomResult<payments::CallConnectorAction, errors::ConnectorError> {
|
||||||
|
match action {
|
||||||
|
services::PaymentAction::PSync | services::PaymentAction::CompleteAuthorize => {
|
||||||
|
Ok(payments::CallConnectorAction::Trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -110,7 +110,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AirwallexPaymentsRequest {
|
|||||||
request_id: Uuid::new_v4().to_string(),
|
request_id: Uuid::new_v4().to_string(),
|
||||||
payment_method,
|
payment_method,
|
||||||
payment_method_options,
|
payment_method_options,
|
||||||
return_url: item.request.router_return_url.clone(),
|
return_url: item.request.complete_authorize_url.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,6 +140,39 @@ impl<F, T> TryFrom<types::ResponseRouterData<F, AirwallexAuthUpdateResponse, T,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
pub struct AirwallexCompleteRequest {
|
||||||
|
request_id: String,
|
||||||
|
three_ds: AirwallexThreeDsData,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
three_ds_type: AirwallexThreeDsType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
pub struct AirwallexThreeDsData {
|
||||||
|
acs_response: Option<common_utils::pii::SecretSerdeValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
|
pub enum AirwallexThreeDsType {
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "3ds_continue")]
|
||||||
|
ThreeDSContinue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for AirwallexCompleteRequest {
|
||||||
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
|
fn try_from(item: &types::PaymentsCompleteAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
request_id: Uuid::new_v4().to_string(),
|
||||||
|
three_ds: AirwallexThreeDsData {
|
||||||
|
acs_response: item.request.payload.clone().map(Secret::new),
|
||||||
|
},
|
||||||
|
three_ds_type: AirwallexThreeDsType::ThreeDSContinue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||||
pub struct AirwallexPaymentsCaptureRequest {
|
pub struct AirwallexPaymentsCaptureRequest {
|
||||||
// Unique ID to be sent for each transaction/operation request to the connector
|
// Unique ID to be sent for each transaction/operation request to the connector
|
||||||
@ -191,27 +224,43 @@ pub enum AirwallexPaymentStatus {
|
|||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AirwallexPaymentStatus> for enums::AttemptStatus {
|
fn get_payment_status(response: &AirwallexPaymentsResponse) -> enums::AttemptStatus {
|
||||||
fn from(item: AirwallexPaymentStatus) -> Self {
|
match response.status.clone() {
|
||||||
match item {
|
AirwallexPaymentStatus::Succeeded => enums::AttemptStatus::Charged,
|
||||||
AirwallexPaymentStatus::Succeeded => Self::Charged,
|
AirwallexPaymentStatus::Failed => enums::AttemptStatus::Failure,
|
||||||
AirwallexPaymentStatus::Failed => Self::Failure,
|
AirwallexPaymentStatus::Pending => enums::AttemptStatus::Pending,
|
||||||
AirwallexPaymentStatus::Pending => Self::Pending,
|
AirwallexPaymentStatus::RequiresPaymentMethod => enums::AttemptStatus::PaymentMethodAwaited,
|
||||||
AirwallexPaymentStatus::RequiresPaymentMethod => Self::PaymentMethodAwaited,
|
AirwallexPaymentStatus::RequiresCustomerAction => response.next_action.as_ref().map_or(
|
||||||
AirwallexPaymentStatus::RequiresCustomerAction => Self::AuthenticationPending,
|
enums::AttemptStatus::AuthenticationPending,
|
||||||
AirwallexPaymentStatus::RequiresCapture => Self::Authorized,
|
|next_action| match next_action.stage {
|
||||||
AirwallexPaymentStatus::Cancelled => Self::Voided,
|
AirwallexNextActionStage::WaitingDeviceDataCollection => {
|
||||||
}
|
enums::AttemptStatus::DeviceDataCollectionPending
|
||||||
|
}
|
||||||
|
AirwallexNextActionStage::WaitingUserInfoInput => {
|
||||||
|
enums::AttemptStatus::AuthenticationPending
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AirwallexPaymentStatus::RequiresCapture => enums::AttemptStatus::Authorized,
|
||||||
|
AirwallexPaymentStatus::Cancelled => enums::AttemptStatus::Voided,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
pub enum AirwallexNextActionStage {
|
||||||
|
WaitingDeviceDataCollection,
|
||||||
|
WaitingUserInfoInput,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct AirwallexRedirectFormData {
|
pub struct AirwallexRedirectFormData {
|
||||||
#[serde(rename = "JWT")]
|
#[serde(rename = "JWT")]
|
||||||
jwt: String,
|
jwt: Option<String>,
|
||||||
#[serde(rename = "threeDSMethodData")]
|
#[serde(rename = "threeDSMethodData")]
|
||||||
three_ds_method_data: String,
|
three_ds_method_data: Option<String>,
|
||||||
token: String,
|
token: Option<String>,
|
||||||
|
provider: Option<String>,
|
||||||
|
version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
@ -219,6 +268,7 @@ pub struct AirwallexPaymentsNextAction {
|
|||||||
url: Url,
|
url: Url,
|
||||||
method: services::Method,
|
method: services::Method,
|
||||||
data: AirwallexRedirectFormData,
|
data: AirwallexRedirectFormData,
|
||||||
|
stage: AirwallexNextActionStage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
@ -232,11 +282,46 @@ pub struct AirwallexPaymentsResponse {
|
|||||||
next_action: Option<AirwallexPaymentsNextAction>,
|
next_action: Option<AirwallexPaymentsNextAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_redirection_form(
|
||||||
|
response_url_data: AirwallexPaymentsNextAction,
|
||||||
|
) -> Option<services::RedirectForm> {
|
||||||
|
Some(services::RedirectForm {
|
||||||
|
endpoint: response_url_data.url.to_string(),
|
||||||
|
method: response_url_data.method,
|
||||||
|
form_fields: std::collections::HashMap::from([
|
||||||
|
//Some form fields might be empty based on the authentication type by the connector
|
||||||
|
(
|
||||||
|
"JWT".to_string(),
|
||||||
|
response_url_data.data.jwt.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"threeDSMethodData".to_string(),
|
||||||
|
response_url_data
|
||||||
|
.data
|
||||||
|
.three_ds_method_data
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"token".to_string(),
|
||||||
|
response_url_data.data.token.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"provider".to_string(),
|
||||||
|
response_url_data.data.provider.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"version".to_string(),
|
||||||
|
response_url_data.data.version.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl<F, T>
|
impl<F, T>
|
||||||
TryFrom<types::ResponseRouterData<F, AirwallexPaymentsResponse, T, types::PaymentsResponseData>>
|
TryFrom<types::ResponseRouterData<F, AirwallexPaymentsResponse, T, types::PaymentsResponseData>>
|
||||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||||
{
|
{
|
||||||
type Error = error_stack::Report<errors::ParsingError>;
|
type Error = error_stack::Report<errors::ConnectorError>;
|
||||||
fn try_from(
|
fn try_from(
|
||||||
item: types::ResponseRouterData<
|
item: types::ResponseRouterData<
|
||||||
F,
|
F,
|
||||||
@ -245,23 +330,48 @@ impl<F, T>
|
|||||||
types::PaymentsResponseData,
|
types::PaymentsResponseData,
|
||||||
>,
|
>,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
let redirection_data =
|
let (status, redirection_data) = item.response.next_action.clone().map_or(
|
||||||
item.response
|
// If no next action is there, map the status and set redirection form as None
|
||||||
.next_action
|
(get_payment_status(&item.response), None),
|
||||||
.map(|response_url_data| services::RedirectForm {
|
|response_url_data| {
|
||||||
endpoint: response_url_data.url.to_string(),
|
// If the connector sends a customer action response that is already under
|
||||||
method: response_url_data.method,
|
// process from our end it can cause an infinite loop to break this this check
|
||||||
form_fields: std::collections::HashMap::from([
|
// is added and fail the payment
|
||||||
("JWT".to_string(), response_url_data.data.jwt),
|
if matches!(
|
||||||
(
|
(
|
||||||
"threeDSMethodData".to_string(),
|
response_url_data.stage.clone(),
|
||||||
response_url_data.data.three_ds_method_data,
|
item.data.status,
|
||||||
),
|
item.response.status.clone(),
|
||||||
("token".to_string(), response_url_data.data.token),
|
),
|
||||||
]),
|
// If the connector sends waiting for DDC and our status is already DDC Pending
|
||||||
});
|
// that means we initiated the call to collect the data and now we expect a different response
|
||||||
|
(
|
||||||
|
AirwallexNextActionStage::WaitingDeviceDataCollection,
|
||||||
|
enums::AttemptStatus::DeviceDataCollectionPending,
|
||||||
|
_
|
||||||
|
)
|
||||||
|
// If the connector sends waiting for Customer Action and our status is already Authenticaition Pending
|
||||||
|
// that means we initiated the call to authenticate and now we do not expect a requires_customer action
|
||||||
|
// it will start a loop
|
||||||
|
| (
|
||||||
|
_,
|
||||||
|
enums::AttemptStatus::AuthenticationPending,
|
||||||
|
AirwallexPaymentStatus::RequiresCustomerAction,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Fail the payment for above conditions
|
||||||
|
(enums::AttemptStatus::AuthenticationFailed, None)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
//Build the redirect form and update the payment status
|
||||||
|
get_payment_status(&item.response),
|
||||||
|
get_redirection_form(response_url_data),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
status: enums::AttemptStatus::from(item.response.status),
|
status,
|
||||||
reference_id: Some(item.response.id.clone()),
|
reference_id: Some(item.response.id.clone()),
|
||||||
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
response: Ok(types::PaymentsResponseData::TransactionResponse {
|
||||||
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id),
|
||||||
|
|||||||
@ -6,6 +6,7 @@ pub mod transformers;
|
|||||||
|
|
||||||
use std::{fmt::Debug, marker::PhantomData, time::Instant};
|
use std::{fmt::Debug, marker::PhantomData, time::Instant};
|
||||||
|
|
||||||
|
use api_models::payments::Metadata;
|
||||||
use error_stack::{IntoReport, ResultExt};
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use router_env::tracing;
|
use router_env::tracing;
|
||||||
@ -247,6 +248,14 @@ pub trait PaymentRedirectFlow: Sync {
|
|||||||
|
|
||||||
fn get_payment_action(&self) -> services::PaymentAction;
|
fn get_payment_action(&self) -> services::PaymentAction;
|
||||||
|
|
||||||
|
fn generate_response(
|
||||||
|
&self,
|
||||||
|
payments_response: api_models::payments::PaymentsResponse,
|
||||||
|
merchant_account: storage_models::merchant_account::MerchantAccount,
|
||||||
|
payment_id: String,
|
||||||
|
connector: String,
|
||||||
|
) -> RouterResult<api::RedirectionResponse>;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn handle_payments_redirect_response(
|
async fn handle_payments_redirect_response(
|
||||||
&self,
|
&self,
|
||||||
@ -290,13 +299,8 @@ pub trait PaymentRedirectFlow: Sync {
|
|||||||
.attach_printable("Failed to get the response in json"),
|
.attach_printable("Failed to get the response in json"),
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let result = helpers::get_handle_response_url(
|
let result =
|
||||||
resource_id,
|
self.generate_response(payments_response, merchant_account, resource_id, connector)?;
|
||||||
&merchant_account,
|
|
||||||
payments_response,
|
|
||||||
connector,
|
|
||||||
)
|
|
||||||
.attach_printable("No redirection response")?;
|
|
||||||
|
|
||||||
Ok(services::ApplicationResponse::JsonForRedirection(result))
|
Ok(services::ApplicationResponse::JsonForRedirection(result))
|
||||||
}
|
}
|
||||||
@ -317,6 +321,11 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
|||||||
let payment_confirm_req = api::PaymentsRequest {
|
let payment_confirm_req = api::PaymentsRequest {
|
||||||
payment_id: Some(req.resource_id.clone()),
|
payment_id: Some(req.resource_id.clone()),
|
||||||
merchant_id: req.merchant_id.clone(),
|
merchant_id: req.merchant_id.clone(),
|
||||||
|
metadata: Some(Metadata {
|
||||||
|
order_details: None,
|
||||||
|
data: masking::Secret::new("{}".into()),
|
||||||
|
payload: Some(req.json_payload.unwrap_or("{}".into()).into()),
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
payments_core::<api::CompleteAuthorize, api::PaymentsResponse, _, _, _>(
|
payments_core::<api::CompleteAuthorize, api::PaymentsResponse, _, _, _>(
|
||||||
@ -333,6 +342,47 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
|||||||
fn get_payment_action(&self) -> services::PaymentAction {
|
fn get_payment_action(&self) -> services::PaymentAction {
|
||||||
services::PaymentAction::CompleteAuthorize
|
services::PaymentAction::CompleteAuthorize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_response(
|
||||||
|
&self,
|
||||||
|
payments_response: api_models::payments::PaymentsResponse,
|
||||||
|
merchant_account: storage_models::merchant_account::MerchantAccount,
|
||||||
|
payment_id: String,
|
||||||
|
connector: String,
|
||||||
|
) -> RouterResult<api::RedirectionResponse> {
|
||||||
|
// There might be multiple redirections needed for some flows
|
||||||
|
// If the status is requires customer action, then send the startpay url again
|
||||||
|
// The redirection data must have been provided and updated by the connector
|
||||||
|
match payments_response.status {
|
||||||
|
api_models::enums::IntentStatus::RequiresCustomerAction => {
|
||||||
|
let startpay_url = payments_response
|
||||||
|
.next_action
|
||||||
|
.and_then(|next_action| next_action.redirect_to_url)
|
||||||
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.into_report()
|
||||||
|
.attach_printable(
|
||||||
|
"did not receive redirect to url when status is requires customer action",
|
||||||
|
)?;
|
||||||
|
Ok(api::RedirectionResponse {
|
||||||
|
return_url: String::new(),
|
||||||
|
params: vec![],
|
||||||
|
return_url_with_query_params: startpay_url,
|
||||||
|
http_method: "GET".to_string(),
|
||||||
|
headers: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// If the status is terminal status, then redirect to merchant return url to provide status
|
||||||
|
api_models::enums::IntentStatus::Succeeded
|
||||||
|
| api_models::enums::IntentStatus::Failed
|
||||||
|
| api_models::enums::IntentStatus::Cancelled | api_models::enums::IntentStatus::RequiresCapture=> helpers::get_handle_response_url(
|
||||||
|
payment_id,
|
||||||
|
&merchant_account,
|
||||||
|
payments_response,
|
||||||
|
connector,
|
||||||
|
),
|
||||||
|
_ => Err(errors::ApiErrorResponse::InternalServerError).into_report().attach_printable_lazy(|| format!("Could not proceed with payment as payment status {} cannot be handled during redirection",payments_response.status))?
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -371,6 +421,21 @@ impl PaymentRedirectFlow for PaymentRedirectSync {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_response(
|
||||||
|
&self,
|
||||||
|
payments_response: api_models::payments::PaymentsResponse,
|
||||||
|
merchant_account: storage_models::merchant_account::MerchantAccount,
|
||||||
|
payment_id: String,
|
||||||
|
connector: String,
|
||||||
|
) -> RouterResult<api::RedirectionResponse> {
|
||||||
|
helpers::get_handle_response_url(
|
||||||
|
payment_id,
|
||||||
|
&merchant_account,
|
||||||
|
payments_response,
|
||||||
|
connector,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_payment_action(&self) -> services::PaymentAction {
|
fn get_payment_action(&self) -> services::PaymentAction {
|
||||||
services::PaymentAction::PSync
|
services::PaymentAction::PSync
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,7 +88,6 @@ macro_rules! default_imp_for_complete_authorize{
|
|||||||
default_imp_for_complete_authorize!(
|
default_imp_for_complete_authorize!(
|
||||||
connector::Aci,
|
connector::Aci,
|
||||||
connector::Adyen,
|
connector::Adyen,
|
||||||
connector::Airwallex,
|
|
||||||
connector::Applepay,
|
connector::Applepay,
|
||||||
connector::Authorizedotnet,
|
connector::Authorizedotnet,
|
||||||
connector::Bambora,
|
connector::Bambora,
|
||||||
@ -131,7 +130,6 @@ macro_rules! default_imp_for_connector_redirect_response{
|
|||||||
default_imp_for_connector_redirect_response!(
|
default_imp_for_connector_redirect_response!(
|
||||||
connector::Aci,
|
connector::Aci,
|
||||||
connector::Adyen,
|
connector::Adyen,
|
||||||
connector::Airwallex,
|
|
||||||
connector::Applepay,
|
connector::Applepay,
|
||||||
connector::Authorizedotnet,
|
connector::Authorizedotnet,
|
||||||
connector::Bambora,
|
connector::Bambora,
|
||||||
|
|||||||
@ -339,7 +339,7 @@ pub fn create_startpay_url(
|
|||||||
payment_intent: &storage::PaymentIntent,
|
payment_intent: &storage::PaymentIntent,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}/payments/start/{}/{}/{}",
|
"{}/payments/redirect/{}/{}/{}",
|
||||||
server.base_url,
|
server.base_url,
|
||||||
payment_intent.payment_id,
|
payment_intent.payment_id,
|
||||||
payment_intent.merchant_id,
|
payment_intent.merchant_id,
|
||||||
@ -355,7 +355,7 @@ pub fn create_redirect_url(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let creds_identifier_path = creds_identifier.map_or_else(String::new, |cd| format!("/{}", cd));
|
let creds_identifier_path = creds_identifier.map_or_else(String::new, |cd| format!("/{}", cd));
|
||||||
format!(
|
format!(
|
||||||
"{}/payments/{}/{}/response/{}",
|
"{}/payments/{}/{}/redirect/response/{}",
|
||||||
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name,
|
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name,
|
||||||
) + &creds_identifier_path
|
) + &creds_identifier_path
|
||||||
}
|
}
|
||||||
@ -376,7 +376,7 @@ pub fn create_complete_authorize_url(
|
|||||||
connector_name: &String,
|
connector_name: &String,
|
||||||
) -> String {
|
) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}/payments/{}/{}/complete/{}",
|
"{}/payments/{}/{}/redirect/complete/{}",
|
||||||
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name
|
router_base_url, payment_attempt.payment_id, payment_attempt.merchant_id, connector_name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -933,6 +933,7 @@ pub fn get_handle_response_url(
|
|||||||
connector: String,
|
connector: String,
|
||||||
) -> RouterResult<api::RedirectionResponse> {
|
) -> RouterResult<api::RedirectionResponse> {
|
||||||
let payments_return_url = response.return_url.as_ref();
|
let payments_return_url = response.return_url.as_ref();
|
||||||
|
|
||||||
let redirection_response = make_pg_redirect_response(payment_id, &response, connector);
|
let redirection_response = make_pg_redirect_response(payment_id, &response, connector);
|
||||||
|
|
||||||
let return_url = make_merchant_url_with_response(
|
let return_url = make_merchant_url_with_response(
|
||||||
|
|||||||
@ -2,6 +2,7 @@ use std::marker::PhantomData;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
|
use masking::ExposeOptionInterface;
|
||||||
use router_derive::PaymentOperation;
|
use router_derive::PaymentOperation;
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
|||||||
let db = &*state.store;
|
let db = &*state.store;
|
||||||
let merchant_id = &merchant_account.merchant_id;
|
let merchant_id = &merchant_account.merchant_id;
|
||||||
let storage_scheme = merchant_account.storage_scheme;
|
let storage_scheme = merchant_account.storage_scheme;
|
||||||
let (mut payment_intent, mut payment_attempt, currency, amount, connector_response);
|
let (mut payment_intent, mut payment_attempt, currency, amount);
|
||||||
|
|
||||||
let payment_id = payment_id
|
let payment_id = payment_id
|
||||||
.get_payment_intent_id()
|
.get_payment_intent_id()
|
||||||
@ -154,7 +155,7 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
connector_response = db
|
let mut connector_response = db
|
||||||
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
.find_connector_response_by_payment_id_merchant_id_attempt_id(
|
||||||
&payment_attempt.payment_id,
|
&payment_attempt.payment_id,
|
||||||
&payment_attempt.merchant_id,
|
&payment_attempt.merchant_id,
|
||||||
@ -166,6 +167,13 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
|
|||||||
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
error.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
connector_response.encoded_data = request.metadata.clone().and_then(|secret_metadata| {
|
||||||
|
secret_metadata
|
||||||
|
.payload
|
||||||
|
.expose_option()
|
||||||
|
.map(|exposed_payload| exposed_payload.to_string())
|
||||||
|
});
|
||||||
|
|
||||||
payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id);
|
payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id);
|
||||||
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
|
payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id);
|
||||||
payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());
|
payment_intent.return_url = request.return_url.as_ref().map(|a| a.to_string());
|
||||||
|
|||||||
@ -318,17 +318,10 @@ impl<F: Clone> UpdateTracker<F, PaymentData<F>, api::PaymentsRequest> for Paymen
|
|||||||
let payment_method = payment_data.payment_attempt.payment_method;
|
let payment_method = payment_data.payment_attempt.payment_method;
|
||||||
let browser_info = payment_data.payment_attempt.browser_info.clone();
|
let browser_info = payment_data.payment_attempt.browser_info.clone();
|
||||||
|
|
||||||
let (intent_status, attempt_status) = match payment_data.payment_attempt.authentication_type
|
let (intent_status, attempt_status) = (
|
||||||
{
|
storage_enums::IntentStatus::Processing,
|
||||||
Some(storage_enums::AuthenticationType::NoThreeDs) => (
|
storage_enums::AttemptStatus::Pending,
|
||||||
storage_enums::IntentStatus::Processing,
|
);
|
||||||
storage_enums::AttemptStatus::Pending,
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
storage_enums::IntentStatus::RequiresCustomerAction,
|
|
||||||
storage_enums::AttemptStatus::AuthenticationPending,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let connector = payment_data.payment_attempt.connector.clone();
|
let connector = payment_data.payment_attempt.connector.clone();
|
||||||
let straight_through_algorithm = payment_data
|
let straight_through_algorithm = payment_data
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::{fmt::Debug, marker::PhantomData};
|
use std::{fmt::Debug, marker::PhantomData};
|
||||||
|
|
||||||
use error_stack::ResultExt;
|
use error_stack::{IntoReport, ResultExt};
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
use super::{flows::Feature, PaymentAddress, PaymentData};
|
use super::{flows::Feature, PaymentAddress, PaymentData};
|
||||||
@ -681,6 +681,14 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
|
|||||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||||
field_name: "browser_info",
|
field_name: "browser_info",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let json_payload = payment_data
|
||||||
|
.connector_response
|
||||||
|
.encoded_data
|
||||||
|
.map(serde_json::to_value)
|
||||||
|
.transpose()
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
setup_future_usage: payment_data.payment_intent.setup_future_usage,
|
setup_future_usage: payment_data.payment_intent.setup_future_usage,
|
||||||
mandate_id: payment_data.mandate_id.clone(),
|
mandate_id: payment_data.mandate_id.clone(),
|
||||||
@ -695,6 +703,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::CompleteAuthoriz
|
|||||||
email: payment_data.email,
|
email: payment_data.email,
|
||||||
payment_method_data: payment_data.payment_method_data,
|
payment_method_data: payment_data.payment_method_data,
|
||||||
connector_transaction_id: payment_data.connector_response.connector_transaction_id,
|
connector_transaction_id: payment_data.connector_response.connector_transaction_id,
|
||||||
|
payload: json_payload,
|
||||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,22 +109,21 @@ impl Payments {
|
|||||||
web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)),
|
web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/start/{payment_id}/{merchant_id}/{attempt_id}")
|
web::resource("/redirect/{payment_id}/{merchant_id}/{attempt_id}")
|
||||||
.route(web::get().to(payments_start)),
|
.route(web::get().to(payments_start)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource(
|
web::resource(
|
||||||
"/{payment_id}/{merchant_id}/response/{connector}/{creds_identifier}",
|
"/{payment_id}/{merchant_id}/redirect/response/{connector}/{creds_identifier}",
|
||||||
)
|
)
|
||||||
.route(web::get().to(payments_redirect_response_with_creds_identifier)),
|
.route(web::get().to(payments_redirect_response_with_creds_identifier)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{payment_id}/{merchant_id}/response/{connector}")
|
web::resource("/{payment_id}/{merchant_id}/redirect/response/{connector}")
|
||||||
.route(web::get().to(payments_redirect_response)),
|
.route(web::get().to(payments_redirect_response)),
|
||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{payment_id}/{merchant_id}/complete/{connector}")
|
web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}")
|
||||||
.route(web::get().to(payments_complete_authorize))
|
|
||||||
.route(web::post().to(payments_complete_authorize)),
|
.route(web::post().to(payments_complete_authorize)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,12 +60,12 @@ pub async fn payments_create(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Payments - Start
|
// /// Payments - Redirect
|
||||||
// ///
|
// ///
|
||||||
// /// The entry point for a payment which involves the redirection flow. This redirects the user to the authentication page
|
// /// For a payment which involves the redirection flow. This redirects the user to the authentication page
|
||||||
// #[utoipa::path(
|
// #[utoipa::path(
|
||||||
// get,
|
// get,
|
||||||
// path = "/payments/start/{payment_id}/{merchant_id}/{attempt_id}",
|
// path = "/payments/redirect/{payment_id}/{merchant_id}/{attempt_id}",
|
||||||
// params(
|
// params(
|
||||||
// ("payment_id" = String, Path, description = "The identifier for payment"),
|
// ("payment_id" = String, Path, description = "The identifier for payment"),
|
||||||
// ("merchant_id" = String, Path, description = "The identifier for merchant"),
|
// ("merchant_id" = String, Path, description = "The identifier for merchant"),
|
||||||
|
|||||||
@ -27,7 +27,7 @@ use crate::{
|
|||||||
logger,
|
logger,
|
||||||
routes::{app::AppStateInfo, metrics, AppState},
|
routes::{app::AppStateInfo, metrics, AppState},
|
||||||
services::authentication as auth,
|
services::authentication as auth,
|
||||||
types::{self, api, storage, ErrorResponse},
|
types::{self, api, ErrorResponse},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
|
pub type BoxedConnectorIntegration<'a, T, Req, Resp> =
|
||||||
@ -447,19 +447,6 @@ pub struct ApplicationRedirectResponse {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&storage::PaymentAttempt> for ApplicationRedirectResponse {
|
|
||||||
fn from(payment_attempt: &storage::PaymentAttempt) -> Self {
|
|
||||||
Self {
|
|
||||||
url: format!(
|
|
||||||
"/payments/start/{}/{}/{}",
|
|
||||||
&payment_attempt.payment_id,
|
|
||||||
&payment_attempt.merchant_id,
|
|
||||||
&payment_attempt.attempt_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct RedirectForm {
|
pub struct RedirectForm {
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
|
|||||||
@ -199,6 +199,7 @@ pub struct CompleteAuthorizeData {
|
|||||||
pub mandate_id: Option<api_models::payments::MandateIds>,
|
pub mandate_id: Option<api_models::payments::MandateIds>,
|
||||||
pub off_session: Option<bool>,
|
pub off_session: Option<bool>,
|
||||||
pub setup_mandate_details: Option<payments::MandateData>,
|
pub setup_mandate_details: Option<payments::MandateData>,
|
||||||
|
pub payload: Option<serde_json::Value>,
|
||||||
pub browser_info: Option<BrowserInformation>,
|
pub browser_info: Option<BrowserInformation>,
|
||||||
pub connector_transaction_id: Option<String>,
|
pub connector_transaction_id: Option<String>,
|
||||||
pub connector_meta: Option<serde_json::Value>,
|
pub connector_meta: Option<serde_json::Value>,
|
||||||
|
|||||||
@ -128,7 +128,10 @@ impl ForeignFrom<storage_enums::AttemptStatus> for storage_enums::IntentStatus {
|
|||||||
storage_enums::AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
|
storage_enums::AttemptStatus::PaymentMethodAwaited => Self::RequiresPaymentMethod,
|
||||||
|
|
||||||
storage_enums::AttemptStatus::Authorized => Self::RequiresCapture,
|
storage_enums::AttemptStatus::Authorized => Self::RequiresCapture,
|
||||||
storage_enums::AttemptStatus::AuthenticationPending => Self::RequiresCustomerAction,
|
storage_enums::AttemptStatus::AuthenticationPending
|
||||||
|
| storage_enums::AttemptStatus::DeviceDataCollectionPending => {
|
||||||
|
Self::RequiresCustomerAction
|
||||||
|
}
|
||||||
storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
storage_enums::AttemptStatus::Unresolved => Self::RequiresMerchantAction,
|
||||||
|
|
||||||
storage_enums::AttemptStatus::PartialCharged
|
storage_enums::AttemptStatus::PartialCharged
|
||||||
|
|||||||
@ -63,6 +63,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
|
|||||||
}),
|
}),
|
||||||
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
|
||||||
router_return_url: Some("https://google.com".to_string()),
|
router_return_url: Some("https://google.com".to_string()),
|
||||||
|
complete_authorize_url: Some("https://google.com".to_string()),
|
||||||
..utils::PaymentAuthorizeType::default().0
|
..utils::PaymentAuthorizeType::default().0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ pub enum AttemptStatus {
|
|||||||
Failure,
|
Failure,
|
||||||
PaymentMethodAwaited,
|
PaymentMethodAwaited,
|
||||||
ConfirmationAwaited,
|
ConfirmationAwaited,
|
||||||
|
DeviceDataCollectionPending,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
DELETE FROM pg_enum
|
||||||
|
WHERE enumlabel = 'device_data_collection_pending'
|
||||||
|
AND enumtypid = (
|
||||||
|
SELECT oid FROM pg_type WHERE typname = 'AttemptStatus'
|
||||||
|
)
|
||||||
@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "AttemptStatus" ADD VALUE IF NOT EXISTS 'device_data_collection_pending';
|
||||||
Reference in New Issue
Block a user