Skip to content

Commit 63aeb4b

Browse files
committed
feat: fix error handling and add createCustomer impl
1 parent 077a3ff commit 63aeb4b

File tree

4 files changed

+370
-54
lines changed

4 files changed

+370
-54
lines changed

crates/hyperswitch_connectors/src/connectors/facilitapay.rs

Lines changed: 225 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@ use hyperswitch_domain_models::{
1414
router_data::{AccessToken, ErrorResponse, RouterData},
1515
router_flow_types::{
1616
access_token_auth::AccessTokenAuth,
17-
payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void},
17+
payments::{
18+
Authorize, Capture, CreateConnectorCustomer, PSync, PaymentMethodToken, Session,
19+
SetupMandate, Void,
20+
},
1821
refunds::{Execute, RSync},
1922
},
2023
router_request_types::{
21-
AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData,
22-
PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData,
23-
RefundsData, SetupMandateRequestData,
24+
AccessTokenRequestData, ConnectorCustomerData, PaymentMethodTokenizationData,
25+
PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData,
26+
PaymentsSyncData, RefundsData, SetupMandateRequestData,
2427
},
2528
router_response_types::{PaymentsResponseData, RefundsResponseData},
2629
types::{
27-
PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData,
28-
RefundSyncRouterData, RefundsRouterData,
30+
ConnectorCustomerRouterData, PaymentsAuthorizeRouterData, PaymentsCaptureRouterData,
31+
PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData,
2932
},
3033
};
3134
use hyperswitch_interfaces::{
@@ -41,15 +44,14 @@ use hyperswitch_interfaces::{
4144
};
4245
use masking::{Mask, PeekInterface};
4346
use requests::{
44-
FacilitapayAuthRequest, FacilitapayPaymentsRequest, FacilitapayRefundRequest,
45-
FacilitapayRouterData,
47+
FacilitapayAuthRequest, FacilitapayCustomerRequest, FacilitapayPaymentsRequest,
48+
FacilitapayRefundRequest, FacilitapayRouterData,
4649
};
4750
use responses::{
48-
FacilitapayAuthResponse, FacilitapayErrorResponse, FacilitapayPaymentsResponse,
49-
FacilitapayRefundResponse,
51+
FacilitapayAuthResponse, FacilitapayCustomerResponse, FacilitapayErrorResponse,
52+
FacilitapayPaymentsResponse, FacilitapayRefundResponse,
5053
};
5154

52-
// use transformers as facilitapay;
5355
use crate::{
5456
constants::headers,
5557
types::{RefreshTokenRouterData, ResponseRouterData},
@@ -69,6 +71,7 @@ impl Facilitapay {
6971
}
7072
}
7173

74+
impl api::ConnectorCustomer for Facilitapay {}
7275
impl api::Payment for Facilitapay {}
7376
impl api::PaymentSession for Facilitapay {}
7477
impl api::ConnectorAccessToken for Facilitapay {}
@@ -139,30 +142,220 @@ impl ConnectorCommon for Facilitapay {
139142
res: Response,
140143
event_builder: Option<&mut ConnectorEvent>,
141144
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
142-
let response: FacilitapayErrorResponse = res
145+
let status_code = res.status_code;
146+
// Keep the raw bytes in case the first parse fails
147+
let response_body_bytes = res.response.clone();
148+
149+
// First attempt to parse as FacilitapayErrorResponse (tries multiple formats)
150+
match response_body_bytes
151+
.parse_struct::<FacilitapayErrorResponse>("FacilitapayErrorResponse")
152+
{
153+
Ok(error_response) => {
154+
event_builder.map(|i| i.set_response_body(&error_response));
155+
router_env::info!(connector_response = ?error_response);
156+
157+
let (code, message, reason) = match &error_response {
158+
FacilitapayErrorResponse::Simple(simple) => (
159+
consts::NO_ERROR_CODE.to_string(),
160+
simple.error.clone(),
161+
Some(simple.error.clone()),
162+
),
163+
FacilitapayErrorResponse::Structured(field_errors) => {
164+
let error_message = extract_error_message(&field_errors.errors);
165+
(
166+
consts::NO_ERROR_CODE.to_string(),
167+
error_message.clone(),
168+
Some(
169+
serde_json::to_string(&field_errors.errors)
170+
.unwrap_or_else(|_| error_message),
171+
),
172+
)
173+
}
174+
FacilitapayErrorResponse::GenericObject(obj) => {
175+
let error_message = extract_error_message(&obj.0);
176+
(
177+
consts::NO_ERROR_CODE.to_string(),
178+
error_message.clone(),
179+
Some(serde_json::to_string(&obj.0).unwrap_or_else(|_| error_message)),
180+
)
181+
}
182+
FacilitapayErrorResponse::PlainText(text) => (
183+
consts::NO_ERROR_CODE.to_string(),
184+
text.clone(),
185+
Some(text.clone()),
186+
),
187+
};
188+
189+
Ok(ErrorResponse {
190+
status_code,
191+
code,
192+
message,
193+
reason,
194+
attempt_status: None,
195+
connector_transaction_id: None,
196+
network_advice_code: None,
197+
network_decline_code: None,
198+
network_error_message: None,
199+
})
200+
}
201+
// If structured parsing fails, try as a plain string
202+
Err(json_error) => {
203+
router_env::warn!(
204+
initial_parse_error = ?json_error,
205+
"Failed to parse Facilitapay error as JSON object, attempting to parse as String"
206+
);
207+
208+
match response_body_bytes.parse_struct::<String>("PlainTextError") {
209+
Ok(error_string) => {
210+
event_builder.map(|i| i.set_response_body(&error_string));
211+
router_env::info!(connector_response = ?error_string);
212+
213+
Ok(ErrorResponse {
214+
status_code,
215+
code: consts::NO_ERROR_CODE.to_string(),
216+
message: error_string.clone(),
217+
reason: Some(error_string),
218+
attempt_status: None,
219+
connector_transaction_id: None,
220+
network_advice_code: None,
221+
network_decline_code: None,
222+
network_error_message: None,
223+
})
224+
}
225+
Err(string_error) => {
226+
router_env::error!(
227+
string_parse_error = ?string_error,
228+
original_json_error = ?json_error,
229+
"Failed to parse Facilitapay error response as JSON structure or simple String"
230+
);
231+
232+
Err(json_error)
233+
.change_context(errors::ConnectorError::ResponseDeserializationFailed)
234+
}
235+
}
236+
}
237+
}
238+
}
239+
}
240+
241+
// Helper function to extract error messages from JSON values
242+
fn extract_error_message(value: &serde_json::Value) -> String {
243+
if let Some(obj) = value.as_object() {
244+
let error_messages: Vec<String> = obj
245+
.iter()
246+
.flat_map(|(field, error_val)| {
247+
let field_name = field.clone();
248+
249+
if let Some(errors) = error_val.as_array() {
250+
errors
251+
.iter()
252+
.filter_map(|e| e.as_str().map(|s| format!("{}: {}", field_name, s)))
253+
.collect::<Vec<String>>()
254+
} else if let Some(error) = error_val.as_str() {
255+
vec![format!("{}: {}", field_name, error)]
256+
} else {
257+
vec![format!("{}: {}", field_name, error_val)]
258+
}
259+
})
260+
.collect();
261+
262+
if !error_messages.is_empty() {
263+
error_messages.join(", ")
264+
} else {
265+
serde_json::to_string(value).unwrap_or_else(|_| consts::NO_ERROR_MESSAGE.to_string())
266+
}
267+
} else {
268+
serde_json::to_string(value).unwrap_or_else(|_| consts::NO_ERROR_MESSAGE.to_string())
269+
}
270+
}
271+
272+
impl ConnectorIntegration<CreateConnectorCustomer, ConnectorCustomerData, PaymentsResponseData>
273+
for Facilitapay
274+
{
275+
fn get_headers(
276+
&self,
277+
req: &ConnectorCustomerRouterData,
278+
connectors: &Connectors,
279+
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
280+
self.build_headers(req, connectors)
281+
}
282+
283+
fn get_content_type(&self) -> &'static str {
284+
self.common_get_content_type()
285+
}
286+
287+
fn get_url(
288+
&self,
289+
_req: &ConnectorCustomerRouterData,
290+
connectors: &Connectors,
291+
) -> CustomResult<String, errors::ConnectorError> {
292+
Ok(format!(
293+
"{}/{}/{}",
294+
self.base_url(connectors),
295+
"subject",
296+
"people"
297+
))
298+
}
299+
300+
fn get_request_body(
301+
&self,
302+
req: &ConnectorCustomerRouterData,
303+
_connectors: &Connectors,
304+
) -> CustomResult<RequestContent, errors::ConnectorError> {
305+
let connector_req = FacilitapayCustomerRequest::try_from(req)?;
306+
Ok(RequestContent::Json(Box::new(connector_req)))
307+
}
308+
309+
fn build_request(
310+
&self,
311+
req: &ConnectorCustomerRouterData,
312+
connectors: &Connectors,
313+
) -> CustomResult<Option<Request>, errors::ConnectorError> {
314+
Ok(Some(
315+
RequestBuilder::new()
316+
.method(Method::Post)
317+
.url(&types::ConnectorCustomerType::get_url(
318+
self, req, connectors,
319+
)?)
320+
.attach_default_headers()
321+
.headers(types::ConnectorCustomerType::get_headers(
322+
self, req, connectors,
323+
)?)
324+
.set_body(types::ConnectorCustomerType::get_request_body(
325+
self, req, connectors,
326+
)?)
327+
.build(),
328+
))
329+
}
330+
331+
fn handle_response(
332+
&self,
333+
data: &ConnectorCustomerRouterData,
334+
event_builder: Option<&mut ConnectorEvent>,
335+
res: Response,
336+
) -> CustomResult<ConnectorCustomerRouterData, errors::ConnectorError> {
337+
let response: FacilitapayCustomerResponse = res
143338
.response
144-
.parse_struct("FacilitapayErrorResponse")
339+
.parse_struct("FacilitapayCustomerResponse")
145340
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
146341

147342
event_builder.map(|i| i.set_response_body(&response));
148343
router_env::logger::info!(connector_response=?response);
149344

150-
let response_message = response
151-
.message
152-
.as_ref()
153-
.map_or_else(|| consts::NO_ERROR_MESSAGE.to_string(), ToString::to_string);
154-
155-
Ok(ErrorResponse {
156-
status_code: res.status_code,
157-
code: response.code,
158-
message: response_message,
159-
reason: Some(response.error),
160-
attempt_status: None,
161-
connector_transaction_id: None,
162-
network_advice_code: None,
163-
network_decline_code: None,
164-
network_error_message: None,
345+
RouterData::try_from(ResponseRouterData {
346+
response,
347+
data: data.clone(),
348+
http_code: res.status_code,
165349
})
350+
.change_context(errors::ConnectorError::ResponseHandlingFailed)
351+
}
352+
353+
fn get_error_response(
354+
&self,
355+
res: Response,
356+
event_builder: Option<&mut ConnectorEvent>,
357+
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
358+
self.build_error_response(res, event_builder)
166359
}
167360
}
168361

@@ -175,10 +368,10 @@ impl ConnectorValidation for Facilitapay {
175368
) -> CustomResult<(), errors::ConnectorError> {
176369
let capture_method = capture_method.unwrap_or_default();
177370
match capture_method {
178-
enums::CaptureMethod::Automatic
179-
| enums::CaptureMethod::Manual
180-
| enums::CaptureMethod::SequentialAutomatic => Ok(()),
181-
enums::CaptureMethod::ManualMultiple | enums::CaptureMethod::Scheduled => Err(
371+
enums::CaptureMethod::Automatic | enums::CaptureMethod::SequentialAutomatic => Ok(()),
372+
enums::CaptureMethod::Manual
373+
| enums::CaptureMethod::ManualMultiple
374+
| enums::CaptureMethod::Scheduled => Err(
182375
utils::construct_not_implemented_error_report(capture_method, self.id()),
183376
),
184377
}

crates/hyperswitch_connectors/src/connectors/facilitapay/requests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub struct FacilitapayRefundRequest {
7777

7878
#[derive(Debug, Serialize)]
7979
#[serde(rename_all = "camelCase")]
80-
pub struct FacilitapaySubjectPeopleRequest {
80+
pub struct FacilitapayCustomerRequest {
8181
pub person: FacilitapayPerson,
8282
}
8383

crates/hyperswitch_connectors/src/connectors/facilitapay/responses.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ pub struct FacilitapayAuthResponse {
1818
pub enum SubjectKycStatus {
1919
// Customer is able to send/receive money through the platform. No action is needed on your side.
2020
Approved,
21+
2122
// Customer is required to upload documents or uploaded documents have been rejected by KYC.
2223
Reproved,
24+
2325
// Customer has uploaded KYC documents but awaiting analysis from the backoffice. No action is needed on your side.
2426
WaitingApproval,
2527
}
@@ -35,7 +37,7 @@ pub struct FacilitapaySubject {
3537
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
3638
pub updated_at: Option<PrimitiveDateTime>,
3739
pub status: SubjectKycStatus,
38-
pub id: String, // Subject ID
40+
pub id: Secret<String>, // Subject ID
3941
pub birth_date: Option<time::Date>,
4042
pub email: Option<pii::Email>,
4143
pub phone_country_code: Option<Secret<String>>,
@@ -58,7 +60,7 @@ pub struct FacilitapaySubject {
5860

5961
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6062
#[serde(rename_all = "snake_case")]
61-
pub struct FacilitapaySubjectPeopleResponse {
63+
pub struct FacilitapayCustomerResponse {
6264
pub data: FacilitapaySubject,
6365
}
6466

@@ -93,7 +95,7 @@ pub enum FacilitapayPaymentStatus {
9395
Exchanged,
9496
Wired,
9597
Canceled,
96-
#[serde(rename = "other")]
98+
#[serde(other)]
9799
Unknown,
98100
}
99101

@@ -206,9 +208,32 @@ pub struct FacilitapayRefundResponse {
206208
pub data: RefundData,
207209
}
208210

209-
#[derive(Debug, Serialize, Deserialize, PartialEq)]
210-
pub struct FacilitapayErrorResponse {
211-
pub code: String,
211+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
212+
pub struct SimpleError {
212213
pub error: String,
213-
pub message: Option<String>,
214+
}
215+
216+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
217+
pub struct FieldErrors {
218+
pub errors: serde_json::Value,
219+
}
220+
221+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
222+
#[serde(transparent)]
223+
pub struct GenericFieldErrors(pub serde_json::Value);
224+
225+
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
226+
#[serde(untagged)] // Try to deserialize into variants in order
227+
pub enum FacilitapayErrorResponse {
228+
/// Matches structures like `{"errors": {"field": ["message"]}}`
229+
Structured(FieldErrors),
230+
231+
/// Matches structures like `{"error": "invalid_token"}`
232+
Simple(SimpleError),
233+
234+
/// Matches structures like `{"field_name": "error_message"}` or `{"field_name": ["message"]}` or any other JSON object
235+
GenericObject(GenericFieldErrors),
236+
237+
/// Matches plain text errors like `"Internal Server Error"`
238+
PlainText(String),
214239
}

0 commit comments

Comments
 (0)