1use std::fmt;
9use std::str::FromStr;
10
11#[derive(Debug, thiserror::Error)]
13pub enum Error {
14 #[error("HTTP error: {0}")]
16 Http(#[from] reqwest::Error),
17
18 #[error("failed to decode response: {0}")]
20 Decode(#[from] serde_json::Error),
21
22 #[error("bKash API error: {code} — {message}")]
25 Api {
26 code: String,
28 message: String,
30 status: u16,
32 },
33
34 #[error("bKash auth error: {0}")]
37 Auth(String),
38
39 #[error("invalid configuration: {0}")]
41 Config(String),
42
43 #[error("webhook signature verification failed")]
45 InvalidSignature,
46
47 #[error("URL parse error: {0}")]
49 Url(#[from] url::ParseError),
50
51 #[error("token endpoint error: {0}")]
53 Token(String),
54}
55
56impl Error {
57 #[must_use]
60 pub fn is_transient(&self) -> bool {
61 match self {
62 Self::Http(_) => true,
63 Self::Api { code, status, .. } => *status >= 500 || code == "503",
64 _ => false,
65 }
66 }
67
68 #[must_use]
71 pub fn is_auth(&self) -> bool {
72 match self {
73 Self::Auth(_) => true,
74 Self::Api { code, .. } => ErrorCode::from_code(code).is_auth(),
75 _ => false,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub enum ErrorCode {
86 InvalidAppKey,
88 InvalidPaymentId,
90 ProcessFailed,
92 InvalidFirstPaymentDate,
94 InvalidFrequency,
96 InvalidAmount,
98 InvalidCurrency,
100 InvalidIntent,
102 InvalidWallet,
104 InvalidOtp,
106 InvalidPin,
108 InvalidReceiverMsisdn,
110 ResendLimitExceeded,
112 WrongPin,
114 WrongPinCountExceeded,
116 WrongVerificationCode,
118 WrongVerificationLimit,
120 OtpVerificationTimeExpired,
122 PinVerificationTimeExpired,
124 ExceptionOccurred,
126 InvalidMandateId,
128 MissingMandateId,
130 InsufficientBalance,
132 ExceptionOccurredAlt,
134 InvalidRequestBody,
136 ReversalAmountExceedsOriginal,
138 MandateAlreadyExists,
140 ReversalError,
142 DuplicateRequest,
144 MandateTypeError,
146 InvalidMerchantInvoiceNumber,
148 InvalidTransferType,
150 TransactionNotFound,
152 ReversalNotAllowed,
154 AccountStateError,
156 AccountPermissionError,
158 AccountStateErrorAlt,
160 AccountStateErrorAlt2,
162 AccountStateErrorAlt3,
164 AccountStateErrorAlt4,
166 AccountStateErrorAlt5,
168 AccountStateErrorAlt6,
170 SecurityError,
172 SecurityErrorAlt,
174 SubscriptionError,
176 SubscriptionErrorAlt,
178 TlvFormat,
180 InvalidPayerReference,
182 InvalidMerchantCallbackUrl,
184 AgreementAlreadyExists,
186 InvalidAgreementId,
188 AgreementStateError,
190 AgreementStateErrorAlt,
192 AgreementStateErrorAlt2,
194 AgreementStateErrorAlt3,
196 InvalidPaymentState,
198 NotBkashAccount,
200 CustomerWalletError,
202 MultipleOtpRequestDenied,
204 PaymentExecutionPrerequisiteNotMet,
206 InitiatorOnlyAction,
208 PaymentAlreadyCompleted,
210 ModeNotValid,
212 ProductModeUnavailable,
214 MandatoryFieldMissing,
216 AgreementSharingNotAllowed,
218 AgreementPermissionError,
220 PaymentAlreadyCompletedAlt,
222 AgreementAlreadyCancelled,
224 RefundInvalidAmount,
226 RefundAmountExceedsAvailable,
228 RefundTransactionNotEligible,
230 RefundAlreadyFullyRefunded,
232 RefundDuplicate,
234 RefundChargeExceedsOriginal,
236 RefundNotPermitted,
238 RefundMaxRefundsReached,
240 RefundInvalidReference,
242 RefundNotFound,
244 RefundInvalidState,
246 MerchantNotPermitted,
248 AgreementExecutionAlreadyCompleted,
250 PaymentExecutionAlreadyCompleted,
252 InvalidPlatform,
254 AuthorizedPaymentAlreadyProcessed,
256 TransactionNotYetCompleted,
258 SystemUndergoingMaintenance,
260 Other(String),
262}
263
264impl ErrorCode {
265 #[must_use]
267 pub fn from_code(code: &str) -> Self {
268 match code {
269 "2001" => Self::InvalidAppKey,
270 "2002" => Self::InvalidPaymentId,
271 "2003" => Self::ProcessFailed,
272 "2004" => Self::InvalidFirstPaymentDate,
273 "2005" => Self::InvalidFrequency,
274 "2006" => Self::InvalidAmount,
275 "2007" => Self::InvalidCurrency,
276 "2008" => Self::InvalidIntent,
277 "2009" => Self::InvalidWallet,
278 "2010" => Self::InvalidOtp,
279 "2011" => Self::InvalidPin,
280 "2012" => Self::InvalidReceiverMsisdn,
281 "2013" => Self::ResendLimitExceeded,
282 "2014" => Self::WrongPin,
283 "2015" => Self::WrongPinCountExceeded,
284 "2016" => Self::WrongVerificationCode,
285 "2017" => Self::WrongVerificationLimit,
286 "2018" => Self::OtpVerificationTimeExpired,
287 "2019" => Self::PinVerificationTimeExpired,
288 "2020" => Self::ExceptionOccurred,
289 "2021" => Self::InvalidMandateId,
290 "2022" => Self::MissingMandateId,
291 "2023" => Self::InsufficientBalance,
292 "2024" => Self::ExceptionOccurredAlt,
293 "2025" => Self::InvalidRequestBody,
294 "2026" => Self::ReversalAmountExceedsOriginal,
295 "2027" => Self::MandateAlreadyExists,
296 "2028" => Self::ReversalError,
297 "2029" => Self::DuplicateRequest,
298 "2030" => Self::MandateTypeError,
299 "2031" => Self::InvalidMerchantInvoiceNumber,
300 "2032" => Self::InvalidTransferType,
301 "2033" => Self::TransactionNotFound,
302 "2034" => Self::ReversalNotAllowed,
303 "2035" => Self::AccountStateError,
304 "2036" => Self::AccountPermissionError,
305 "2037" => Self::AccountStateErrorAlt,
306 "2038" => Self::AccountStateErrorAlt2,
307 "2039" => Self::AccountStateErrorAlt3,
308 "2040" => Self::AccountStateErrorAlt4,
309 "2041" => Self::AccountStateErrorAlt5,
310 "2042" => Self::AccountStateErrorAlt6,
311 "2043" => Self::SecurityError,
312 "2044" => Self::SecurityErrorAlt,
313 "2045" => Self::SubscriptionError,
314 "2046" => Self::SubscriptionErrorAlt,
315 "2047" => Self::TlvFormat,
316 "2048" => Self::InvalidPayerReference,
317 "2049" => Self::InvalidMerchantCallbackUrl,
318 "2050" => Self::AgreementAlreadyExists,
319 "2051" => Self::InvalidAgreementId,
320 "2052" => Self::AgreementStateError,
321 "2053" => Self::AgreementStateErrorAlt,
322 "2054" => Self::AgreementStateErrorAlt2,
323 "2055" => Self::AgreementStateErrorAlt3,
324 "2056" => Self::InvalidPaymentState,
325 "2057" => Self::NotBkashAccount,
326 "2058" => Self::CustomerWalletError,
327 "2059" => Self::MultipleOtpRequestDenied,
328 "2060" => Self::PaymentExecutionPrerequisiteNotMet,
329 "2061" => Self::InitiatorOnlyAction,
330 "2062" => Self::PaymentAlreadyCompleted,
331 "2063" => Self::ModeNotValid,
332 "2064" => Self::ProductModeUnavailable,
333 "2065" => Self::MandatoryFieldMissing,
334 "2066" => Self::AgreementSharingNotAllowed,
335 "2067" => Self::AgreementPermissionError,
336 "2068" => Self::PaymentAlreadyCompletedAlt,
337 "2069" => Self::AgreementAlreadyCancelled,
338 "2071" => Self::RefundInvalidAmount,
339 "2072" => Self::RefundAmountExceedsAvailable,
340 "2073" => Self::RefundTransactionNotEligible,
341 "2074" => Self::RefundAlreadyFullyRefunded,
342 "2075" => Self::RefundDuplicate,
343 "2076" => Self::RefundChargeExceedsOriginal,
344 "2077" => Self::RefundNotPermitted,
345 "2078" => Self::RefundMaxRefundsReached,
346 "2079" => Self::RefundInvalidReference,
347 "2080" => Self::RefundNotFound,
348 "2081" => Self::RefundInvalidState,
349 "2082" => Self::MerchantNotPermitted,
350 "2116" => Self::AgreementExecutionAlreadyCompleted,
351 "2117" => Self::PaymentExecutionAlreadyCompleted,
352 "2118" => Self::InvalidPlatform,
353 "2119" => Self::AuthorizedPaymentAlreadyProcessed,
354 "2127" => Self::TransactionNotYetCompleted,
355 "503" => Self::SystemUndergoingMaintenance,
356 other => Self::Other(other.to_string()),
357 }
358 }
359
360 #[must_use]
362 pub fn as_code(&self) -> &str {
363 match self {
364 Self::InvalidAppKey => "2001",
365 Self::InvalidPaymentId => "2002",
366 Self::ProcessFailed => "2003",
367 Self::InvalidFirstPaymentDate => "2004",
368 Self::InvalidFrequency => "2005",
369 Self::InvalidAmount => "2006",
370 Self::InvalidCurrency => "2007",
371 Self::InvalidIntent => "2008",
372 Self::InvalidWallet => "2009",
373 Self::InvalidOtp => "2010",
374 Self::InvalidPin => "2011",
375 Self::InvalidReceiverMsisdn => "2012",
376 Self::ResendLimitExceeded => "2013",
377 Self::WrongPin => "2014",
378 Self::WrongPinCountExceeded => "2015",
379 Self::WrongVerificationCode => "2016",
380 Self::WrongVerificationLimit => "2017",
381 Self::OtpVerificationTimeExpired => "2018",
382 Self::PinVerificationTimeExpired => "2019",
383 Self::ExceptionOccurred => "2020",
384 Self::InvalidMandateId => "2021",
385 Self::MissingMandateId => "2022",
386 Self::InsufficientBalance => "2023",
387 Self::ExceptionOccurredAlt => "2024",
388 Self::InvalidRequestBody => "2025",
389 Self::ReversalAmountExceedsOriginal => "2026",
390 Self::MandateAlreadyExists => "2027",
391 Self::ReversalError => "2028",
392 Self::DuplicateRequest => "2029",
393 Self::MandateTypeError => "2030",
394 Self::InvalidMerchantInvoiceNumber => "2031",
395 Self::InvalidTransferType => "2032",
396 Self::TransactionNotFound => "2033",
397 Self::ReversalNotAllowed => "2034",
398 Self::AccountStateError => "2035",
399 Self::AccountPermissionError => "2036",
400 Self::AccountStateErrorAlt => "2037",
401 Self::AccountStateErrorAlt2 => "2038",
402 Self::AccountStateErrorAlt3 => "2039",
403 Self::AccountStateErrorAlt4 => "2040",
404 Self::AccountStateErrorAlt5 => "2041",
405 Self::AccountStateErrorAlt6 => "2042",
406 Self::SecurityError => "2043",
407 Self::SecurityErrorAlt => "2044",
408 Self::SubscriptionError => "2045",
409 Self::SubscriptionErrorAlt => "2046",
410 Self::TlvFormat => "2047",
411 Self::InvalidPayerReference => "2048",
412 Self::InvalidMerchantCallbackUrl => "2049",
413 Self::AgreementAlreadyExists => "2050",
414 Self::InvalidAgreementId => "2051",
415 Self::AgreementStateError => "2052",
416 Self::AgreementStateErrorAlt => "2053",
417 Self::AgreementStateErrorAlt2 => "2054",
418 Self::AgreementStateErrorAlt3 => "2055",
419 Self::InvalidPaymentState => "2056",
420 Self::NotBkashAccount => "2057",
421 Self::CustomerWalletError => "2058",
422 Self::MultipleOtpRequestDenied => "2059",
423 Self::PaymentExecutionPrerequisiteNotMet => "2060",
424 Self::InitiatorOnlyAction => "2061",
425 Self::PaymentAlreadyCompleted => "2062",
426 Self::ModeNotValid => "2063",
427 Self::ProductModeUnavailable => "2064",
428 Self::MandatoryFieldMissing => "2065",
429 Self::AgreementSharingNotAllowed => "2066",
430 Self::AgreementPermissionError => "2067",
431 Self::PaymentAlreadyCompletedAlt => "2068",
432 Self::AgreementAlreadyCancelled => "2069",
433 Self::RefundInvalidAmount => "2071",
434 Self::RefundAmountExceedsAvailable => "2072",
435 Self::RefundTransactionNotEligible => "2073",
436 Self::RefundAlreadyFullyRefunded => "2074",
437 Self::RefundDuplicate => "2075",
438 Self::RefundChargeExceedsOriginal => "2076",
439 Self::RefundNotPermitted => "2077",
440 Self::RefundMaxRefundsReached => "2078",
441 Self::RefundInvalidReference => "2079",
442 Self::RefundNotFound => "2080",
443 Self::RefundInvalidState => "2081",
444 Self::MerchantNotPermitted => "2082",
445 Self::AgreementExecutionAlreadyCompleted => "2116",
446 Self::PaymentExecutionAlreadyCompleted => "2117",
447 Self::InvalidPlatform => "2118",
448 Self::AuthorizedPaymentAlreadyProcessed => "2119",
449 Self::TransactionNotYetCompleted => "2127",
450 Self::SystemUndergoingMaintenance => "503",
451 Self::Other(s) => s.as_str(),
452 }
453 }
454
455 #[must_use]
458 pub fn is_transient(&self) -> bool {
459 matches!(self, Self::SystemUndergoingMaintenance)
460 }
461
462 #[must_use]
465 pub fn is_auth(&self) -> bool {
466 matches!(self, Self::InvalidAppKey)
467 }
468}
469
470impl fmt::Display for ErrorCode {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 f.write_str(self.as_code())
473 }
474}
475
476impl FromStr for ErrorCode {
477 type Err = std::convert::Infallible;
478
479 fn from_str(s: &str) -> Result<Self, Self::Err> {
480 Ok(Self::from_code(s))
481 }
482}
483
484impl serde::Serialize for ErrorCode {
485 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
486 s.serialize_str(self.as_code())
487 }
488}
489
490impl<'de> serde::Deserialize<'de> for ErrorCode {
491 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
492 let s = String::deserialize(d)?;
493 Ok(Self::from_code(&s))
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 #[test]
502 fn from_str_parses_known_codes() {
503 assert_eq!(ErrorCode::from_code("2001"), ErrorCode::InvalidAppKey);
504 assert_eq!(
505 ErrorCode::from_code("503"),
506 ErrorCode::SystemUndergoingMaintenance
507 );
508 assert_eq!(
509 ErrorCode::from_code("2127"),
510 ErrorCode::TransactionNotYetCompleted
511 );
512 }
513
514 #[test]
515 fn from_str_falls_back_to_other() {
516 assert_eq!(
517 ErrorCode::from_code("99999"),
518 ErrorCode::Other("99999".to_string())
519 );
520 }
521
522 #[test]
523 fn is_transient_for_503() {
524 assert!(ErrorCode::SystemUndergoingMaintenance.is_transient());
525 assert!(ErrorCode::from_code("503").is_transient());
526 }
527
528 #[test]
529 fn is_transient_false_for_2001() {
530 assert!(!ErrorCode::InvalidAppKey.is_transient());
531 }
532
533 #[test]
534 fn is_auth_for_2001() {
535 assert!(ErrorCode::InvalidAppKey.is_auth());
536 assert!(ErrorCode::from_code("2001").is_auth());
537 }
538
539 #[test]
540 fn is_auth_false_for_503() {
541 assert!(!ErrorCode::SystemUndergoingMaintenance.is_auth());
542 }
543
544 #[test]
545 fn display_returns_code_string() {
546 assert_eq!(ErrorCode::InvalidAppKey.to_string(), "2001");
547 assert_eq!(ErrorCode::SystemUndergoingMaintenance.to_string(), "503");
548 }
549
550 #[test]
551 fn as_code_round_trips() {
552 for variant in [
553 ErrorCode::InvalidAppKey,
554 ErrorCode::SystemUndergoingMaintenance,
555 ErrorCode::TransactionNotYetCompleted,
556 ] {
557 let code = variant.as_code();
558 assert_eq!(ErrorCode::from_code(code), variant);
559 }
560 }
561
562 #[test]
563 fn error_display_does_not_leak_credentials() {
564 let e = Error::Api {
565 code: "2001".to_string(),
566 message: "Invalid App Key".to_string(),
567 status: 200,
568 };
569 let s = e.to_string();
570 assert!(s.contains("2001"));
571 assert!(s.contains("Invalid App Key"));
572 }
573
574 #[test]
575 fn error_is_transient_for_503() {
576 let e = Error::Api {
577 code: "503".to_string(),
578 message: "maintenance".to_string(),
579 status: 200,
580 };
581 assert!(e.is_transient());
582 }
583
584 #[test]
585 fn error_is_auth_for_2001() {
586 let e = Error::Api {
587 code: "2001".to_string(),
588 message: "bad key".to_string(),
589 status: 200,
590 };
591 assert!(e.is_auth());
592 }
593}