Skip to main content

kamu_snap_response/
error.rs

1//! SNAP BI error taxonomy.
2//!
3//! Every variant carries a fixed `(http_status, case_code, category)` triple
4//! exposed via [`Error::http_status`], [`Error::case_code`], and
5//! [`Error::category`]. The full wire `responseCode` is composed via
6//! [`Error::response_code`].
7//!
8//! Compared to v1.x:
9//!
10//! - `Unathorized` is renamed to [`Error::Unauthorized`] (hard rename).
11//! - `#[non_exhaustive]` is present — new variants are non-breaking.
12//! - The `#[default]` derive on `GeneralError` is dropped — callers must
13//!   construct an `Error` explicitly. No silent 500 fallback.
14//! - Method names lose their `get_` prefix.
15//! - Returns [`http::StatusCode`], not `actix_web::http::StatusCode`. Actix
16//!   re-exports the same upstream type, so existing consumers see no change.
17
18use crate::category::Category;
19use crate::response_code::{ResponseCode, ServiceCode};
20
21/// All known SNAP BI error variants plus a feature-gated `Crypto` bridge.
22#[derive(Debug, thiserror::Error)]
23#[non_exhaustive]
24pub enum Error {
25    /// General request failed error, including message parsing failed.
26    #[error("Bad Request")]
27    BadRequest,
28    /// Invalid format
29    #[error("Invalid Field Format {0}")]
30    InvalidFieldFormat(String),
31    /// Missing or invalid format on mandatory field
32    #[error("Invalid Mandatory Field {0}")]
33    InvalidMandatoryField(String),
34    /// General unauthorized error (No Interface Def, API is Invalid, Oauth
35    /// Failed, Verify Client Secret Fail, Client Forbidden Access API, Unknown
36    /// Client, Key not Found).
37    #[error("Unauthorized. {0}")]
38    Unauthorized(String),
39    /// Token found in request is invalid (Access Token Not Exist, Access Token
40    /// Expiry)
41    #[error("Invalid Token (B2B)")]
42    InvalidTokenB2B,
43    /// Token found in request is invalid (Access Token Not Exist, Access Token
44    /// Expiry)
45    #[error("Invalid Customer Token")]
46    InvalidCustomerToken,
47    /// Token not found in the system. This occurs on any API that requires
48    /// token as input parameter
49    #[error("Token Not Found (B2B)")]
50    TokenNotFoundB2B,
51    /// Token not found in the system. This occurs on any API that requires
52    /// token as input parameter
53    #[error("Customer Token Not Found")]
54    CustomerTokenNotFound,
55    /// Transaction expired
56    #[error("Transaction Expired")]
57    TransactionExpired,
58    /// This merchant is not allowed to call Direct Debit APIs
59    #[error("Feature Not Allowed {0}")]
60    FeatureNotAllowed(String),
61    /// Exceeds Transaction Amount Limit
62    #[error("Exceeds Transaction Amount Limit")]
63    ExceedsTransactionAmountLimit,
64    /// Suspected Fraud
65    #[error("Suspected Fraud")]
66    SuspectedFraud,
67    /// Too many requests, exceeds transaction frequency limit
68    #[error("Activity Count Limit Exceeded")]
69    ActivityCountLimitExceeded,
70    /// Account or User status is abnormal
71    #[error("Do Not Honor")]
72    DoNotHonor,
73    /// Cut off in progress
74    #[error("Feature Not Allowed At This Time. {0}")]
75    FeatureNotAllowedAtThisTime(String),
76    /// The payment card is blocked
77    #[error("Card Blocked")]
78    CardBlocked,
79    /// The payment card is expired
80    #[error("Card Expired")]
81    CardExpired,
82    /// The account is dormant
83    #[error("Dormant Account")]
84    DormantAccount,
85    /// Need to set token limit
86    #[error("Need To Set Token Limit")]
87    NeedToSetTokenLimit,
88    /// OTP has been blocked
89    #[error("OTP Blocked")]
90    OTPBlocked,
91    /// OTP has been expired
92    #[error("OTP Lifetime Expired")]
93    OTPLifetimeExpired,
94    /// Initiates request OTP to the issuer
95    #[error("OTP Sent To Cardholder")]
96    OTPSentToCardholder,
97    /// Insufficient funds
98    #[error("Insufficient Funds")]
99    InsufficientFunds,
100    /// Transaction not permitted
101    #[error("Transaction Not Permitted. {0}")]
102    TransactionNotPermitted(String),
103    /// Suspend transaction
104    #[error("Suspend Transaction")]
105    SuspendTransaction,
106    /// Purchase amount exceeds the token limit set prior
107    #[error("Token Limit Exceeded")]
108    TokenLimitExceeded,
109    /// Inactive account
110    #[error("Inactive Card/Account/Customer")]
111    InactiveCardOrAccountOrCustomer,
112    /// Merchant is suspended from calling any APIs
113    #[error("Merchant Blacklisted")]
114    MerchantBlacklisted,
115    /// Merchant aggregated purchase amount on that day exceeds the agreed limit
116    #[error("Merchant Limit Exceed")]
117    MerchantLimitExceed,
118    /// Set limit not allowed on particular token
119    #[error("Set Limit Not Allowed")]
120    SetLimitNotAllowed,
121    /// The token limit desired by the merchant is not within the agreed range
122    /// between the merchant and the Issuer
123    #[error("Token Limit Invalid")]
124    TokenLimitInvalid,
125    /// Account aggregated purchase amount on that day exceeds the agreed limit
126    #[error("Account Limit Exceed")]
127    AccountLimitExceed,
128    /// Invalid transaction status
129    #[error("Invalid Transaction Status")]
130    InvalidTransactionStatus,
131    /// Transaction not found
132    #[error("Transaction Not Found")]
133    TransactionNotFound,
134    /// Invalid routing
135    #[error("Invalid Routing")]
136    InvalidRouting,
137    /// Bank not supported by switch
138    #[error("Bank Not Supported By Switch")]
139    BankNotSupportedBySwitch,
140    /// Transaction is cancelled by customer
141    #[error("Transaction Cancelled")]
142    TransactionCancelled,
143    /// Merchant is not registered for Card Registration services
144    #[error("Merchant Is Not Registered For Card Registration Services")]
145    MerchantNotRegisteredForCardRegistrationServices,
146    /// Need to request OTP
147    #[error("Need To Request OTP")]
148    NeedToRequestOTP,
149    /// The journeyId cannot be found in the system
150    #[error("Journey Not Found")]
151    JourneyNotFound,
152    /// Merchant does not exist or status abnormal
153    #[error("Invalid Merchant")]
154    InvalidMerchant,
155    /// No issuer
156    #[error("No Issuer")]
157    NoIssuer,
158    /// Invalid API transition within a journey
159    #[error("Invalid API Transition")]
160    InvalidAPITransition,
161    /// Card information may be invalid, or the card account may be blacklisted,
162    /// or Virtual Account number maybe invalid.
163    #[error("Invalid Card/Account/Customer {0}/Virtual Account")]
164    InvalidCardOrAccountOrCustomerOrVirtualAccount(String),
165    /// The bill or Virtual account is blocked/ suspended/not found.
166    #[error("Invalid Bill/Virtual Account {0}")]
167    InvalidBillOrVirtualAccountWithReason(String),
168    /// The amount doesn't match with what supposed to
169    #[error("Invalid Amount")]
170    InvalidAmount,
171    /// The bill has been paid
172    #[error("Paid Bill")]
173    PaidBill,
174    /// OTP is incorrect
175    #[error("Invalid OTP")]
176    InvalidOTP,
177    /// Partner number can't be found
178    #[error("Partner Not Found")]
179    PartnerNotFound,
180    /// Terminal does not exist in the system
181    #[error("Invalid Terminal")]
182    InvalidTerminal,
183    /// Inconsistent request parameter found for the same partner reference
184    /// number/transaction id.
185    #[error("Inconsistent Request")]
186    InconsistentRequest,
187    /// The bill is expired.
188    #[error("Invalid Bill/Virtual Account")]
189    InvalidBillOrVirtualAccount,
190    /// Requested function is not supported
191    #[error("Requested Function Is Not Supported")]
192    RequestedFunctionIsNotSupported,
193    /// Requested operation to cancel/refund transaction is not allowed at this
194    /// time.
195    #[error("Requested Operation Is Not Allowed")]
196    RequestedOperationIsNotAllowed,
197    /// Cannot use same X-EXTERNAL-ID in same day
198    #[error("Conflict")]
199    Conflict,
200    /// Transaction has previously been processed indicates the same
201    /// partnerReferenceNo already success
202    #[error("Duplicate partnerReferenceNo")]
203    DuplicatePartnerReferenceNo,
204    /// Maximum transaction limit exceeded
205    #[error("Too Many Requests")]
206    TooManyRequests,
207    /// General Error
208    #[error("General Error")]
209    GeneralError,
210    /// Unknown internal server failure, please retry the process again
211    #[error("Internal Server Error")]
212    InternalServerError,
213    /// Backend system failure, etc.
214    #[error("External Server Error")]
215    ExternalServerError,
216    /// Timeout from the issuer
217    #[error("Timeout")]
218    Timeout,
219
220    /// Bridge variant carrying a [`kamu_snap_crypto::Error`]. Only present
221    /// when the `crypto` feature is enabled.
222    #[cfg(feature = "crypto")]
223    #[error("crypto error: {0}")]
224    Crypto(#[from] kamu_snap_crypto::Error),
225}
226
227impl Error {
228    /// Coarse [`Category`] for retry-policy / logging consumers.
229    pub fn category(&self) -> Category {
230        match self {
231            Self::BadRequest => Category::System,
232            Self::InvalidFieldFormat(_) => Category::Message,
233            Self::InvalidMandatoryField(_) => Category::Message,
234            Self::Unauthorized(_) => Category::System,
235            Self::InvalidTokenB2B => Category::System,
236            Self::InvalidCustomerToken => Category::System,
237            Self::TokenNotFoundB2B => Category::System,
238            Self::CustomerTokenNotFound => Category::System,
239            Self::TransactionExpired => Category::Business,
240            Self::FeatureNotAllowed(_) => Category::System,
241            Self::ExceedsTransactionAmountLimit => Category::Business,
242            Self::SuspectedFraud => Category::Business,
243            Self::ActivityCountLimitExceeded => Category::Business,
244            Self::DoNotHonor => Category::Business,
245            Self::FeatureNotAllowedAtThisTime(_) => Category::System,
246            Self::CardBlocked => Category::Business,
247            Self::CardExpired => Category::Business,
248            Self::DormantAccount => Category::Business,
249            Self::NeedToSetTokenLimit => Category::Business,
250            Self::OTPBlocked => Category::System,
251            Self::OTPLifetimeExpired => Category::System,
252            Self::OTPSentToCardholder => Category::System,
253            Self::InsufficientFunds => Category::Business,
254            Self::TransactionNotPermitted(_) => Category::Business,
255            Self::SuspendTransaction => Category::Business,
256            Self::TokenLimitExceeded => Category::Business,
257            Self::InactiveCardOrAccountOrCustomer => Category::Business,
258            Self::MerchantBlacklisted => Category::Business,
259            Self::MerchantLimitExceed => Category::Business,
260            Self::SetLimitNotAllowed => Category::Business,
261            Self::TokenLimitInvalid => Category::Business,
262            Self::AccountLimitExceed => Category::Business,
263            Self::InvalidTransactionStatus => Category::Business,
264            Self::TransactionNotFound => Category::Business,
265            Self::InvalidRouting => Category::System,
266            Self::BankNotSupportedBySwitch => Category::System,
267            Self::TransactionCancelled => Category::Business,
268            Self::MerchantNotRegisteredForCardRegistrationServices => Category::Business,
269            Self::NeedToRequestOTP => Category::System,
270            Self::JourneyNotFound => Category::System,
271            Self::InvalidMerchant => Category::Business,
272            Self::NoIssuer => Category::Business,
273            Self::InvalidAPITransition => Category::System,
274            Self::InvalidCardOrAccountOrCustomerOrVirtualAccount(_) => Category::Business,
275            Self::InvalidBillOrVirtualAccountWithReason(_) => Category::Business,
276            Self::InvalidAmount => Category::Business,
277            Self::PaidBill => Category::Business,
278            Self::InvalidOTP => Category::System,
279            Self::PartnerNotFound => Category::Business,
280            Self::InvalidTerminal => Category::Business,
281            Self::InconsistentRequest => Category::Business,
282            Self::InvalidBillOrVirtualAccount => Category::Business,
283            Self::RequestedFunctionIsNotSupported => Category::System,
284            Self::RequestedOperationIsNotAllowed => Category::Business,
285            Self::Conflict => Category::System,
286            Self::DuplicatePartnerReferenceNo => Category::System,
287            Self::TooManyRequests => Category::System,
288            Self::GeneralError => Category::System,
289            Self::InternalServerError => Category::System,
290            Self::ExternalServerError => Category::System,
291            Self::Timeout => Category::System,
292            #[cfg(feature = "crypto")]
293            Self::Crypto(_) => Category::System,
294        }
295    }
296
297    /// HTTP status code mapped per the SNAP BI taxonomy.
298    pub fn http_status(&self) -> http::StatusCode {
299        match self {
300            Self::BadRequest | Self::InvalidFieldFormat(_) | Self::InvalidMandatoryField(_) => {
301                http::StatusCode::BAD_REQUEST
302            }
303
304            Self::Unauthorized(_)
305            | Self::InvalidTokenB2B
306            | Self::InvalidCustomerToken
307            | Self::TokenNotFoundB2B
308            | Self::CustomerTokenNotFound => http::StatusCode::UNAUTHORIZED,
309
310            Self::TransactionExpired
311            | Self::FeatureNotAllowed(_)
312            | Self::ExceedsTransactionAmountLimit
313            | Self::SuspectedFraud
314            | Self::ActivityCountLimitExceeded
315            | Self::DoNotHonor
316            | Self::FeatureNotAllowedAtThisTime(_)
317            | Self::CardBlocked
318            | Self::CardExpired
319            | Self::DormantAccount
320            | Self::NeedToSetTokenLimit
321            | Self::OTPBlocked
322            | Self::OTPLifetimeExpired
323            | Self::OTPSentToCardholder
324            | Self::InsufficientFunds
325            | Self::TransactionNotPermitted(_)
326            | Self::SuspendTransaction
327            | Self::TokenLimitExceeded
328            | Self::InactiveCardOrAccountOrCustomer
329            | Self::MerchantBlacklisted
330            | Self::MerchantLimitExceed
331            | Self::SetLimitNotAllowed
332            | Self::TokenLimitInvalid
333            | Self::AccountLimitExceed => http::StatusCode::FORBIDDEN,
334
335            Self::InvalidOTP
336            | Self::InvalidTransactionStatus
337            | Self::TransactionCancelled
338            | Self::MerchantNotRegisteredForCardRegistrationServices
339            | Self::PaidBill
340            | Self::PartnerNotFound
341            | Self::JourneyNotFound
342            | Self::InvalidMerchant
343            | Self::NoIssuer
344            | Self::TransactionNotFound
345            | Self::InconsistentRequest
346            | Self::InvalidAmount
347            | Self::InvalidAPITransition
348            | Self::InvalidRouting
349            | Self::BankNotSupportedBySwitch
350            | Self::InvalidTerminal
351            | Self::InvalidCardOrAccountOrCustomerOrVirtualAccount(_)
352            | Self::InvalidBillOrVirtualAccountWithReason(_)
353            | Self::InvalidBillOrVirtualAccount
354            | Self::NeedToRequestOTP => http::StatusCode::NOT_FOUND,
355
356            Self::RequestedFunctionIsNotSupported | Self::RequestedOperationIsNotAllowed => {
357                http::StatusCode::METHOD_NOT_ALLOWED
358            }
359
360            Self::Conflict | Self::DuplicatePartnerReferenceNo => http::StatusCode::CONFLICT,
361
362            Self::TooManyRequests => http::StatusCode::TOO_MANY_REQUESTS,
363
364            Self::GeneralError | Self::InternalServerError | Self::ExternalServerError => {
365                http::StatusCode::INTERNAL_SERVER_ERROR
366            }
367
368            Self::Timeout => http::StatusCode::GATEWAY_TIMEOUT,
369
370            #[cfg(feature = "crypto")]
371            Self::Crypto(_) => http::StatusCode::UNAUTHORIZED,
372        }
373    }
374
375    /// 2-digit case code embedded in the `responseCode`.
376    pub fn case_code(&self) -> u8 {
377        match self {
378            Self::BadRequest => 0,
379            Self::InvalidFieldFormat(_) => 1,
380            Self::InvalidMandatoryField(_) => 2,
381            Self::Unauthorized(_) => 0,
382            Self::InvalidTokenB2B => 1,
383            Self::InvalidCustomerToken => 2,
384            Self::TokenNotFoundB2B => 3,
385            Self::CustomerTokenNotFound => 4,
386            Self::TransactionExpired => 0,
387            Self::FeatureNotAllowed(_) => 1,
388            Self::ExceedsTransactionAmountLimit => 2,
389            Self::SuspectedFraud => 3,
390            Self::ActivityCountLimitExceeded => 4,
391            Self::DoNotHonor => 5,
392            Self::FeatureNotAllowedAtThisTime(_) => 6,
393            Self::CardBlocked => 7,
394            Self::CardExpired => 8,
395            Self::DormantAccount => 9,
396            Self::NeedToSetTokenLimit => 10,
397            Self::OTPBlocked => 11,
398            Self::OTPLifetimeExpired => 12,
399            Self::OTPSentToCardholder => 13,
400            Self::InsufficientFunds => 14,
401            Self::TransactionNotPermitted(_) => 15,
402            Self::SuspendTransaction => 16,
403            Self::TokenLimitExceeded => 17,
404            Self::InactiveCardOrAccountOrCustomer => 18,
405            Self::MerchantBlacklisted => 19,
406            Self::MerchantLimitExceed => 20,
407            Self::SetLimitNotAllowed => 21,
408            Self::TokenLimitInvalid => 22,
409            Self::AccountLimitExceed => 23,
410            Self::InvalidTransactionStatus => 0,
411            Self::TransactionNotFound => 1,
412            Self::InvalidRouting => 2,
413            Self::BankNotSupportedBySwitch => 3,
414            Self::TransactionCancelled => 4,
415            Self::MerchantNotRegisteredForCardRegistrationServices => 5,
416            Self::NeedToRequestOTP => 6,
417            Self::JourneyNotFound => 7,
418            Self::InvalidMerchant => 8,
419            Self::NoIssuer => 9,
420            Self::InvalidAPITransition => 10,
421            Self::InvalidCardOrAccountOrCustomerOrVirtualAccount(_) => 11,
422            Self::InvalidBillOrVirtualAccountWithReason(_) => 12,
423            Self::InvalidAmount => 13,
424            Self::PaidBill => 14,
425            Self::InvalidOTP => 15,
426            Self::PartnerNotFound => 16,
427            Self::InvalidTerminal => 17,
428            Self::InconsistentRequest => 18,
429            Self::InvalidBillOrVirtualAccount => 19,
430            Self::RequestedFunctionIsNotSupported => 0,
431            Self::RequestedOperationIsNotAllowed => 1,
432            Self::Conflict => 0,
433            Self::DuplicatePartnerReferenceNo => 1,
434            Self::TooManyRequests => 0,
435            Self::GeneralError => 0,
436            Self::InternalServerError => 1,
437            Self::ExternalServerError => 2,
438            Self::Timeout => 0,
439            #[cfg(feature = "crypto")]
440            Self::Crypto(_) => 0,
441        }
442    }
443
444    /// Compose the full wire `responseCode` for this variant under `service`.
445    pub fn response_code(&self, service: ServiceCode) -> ResponseCode {
446        ResponseCode::from_parts(self.http_status().as_u16(), service, self.case_code())
447    }
448
449    /// Inverse classifier — given a received `(http, case)`, return the
450    /// matching variant with empty payload for string-bearing variants.
451    /// `None` for unknown pairs.
452    pub fn from_http_and_case(http: u16, case: u8) -> Option<Self> {
453        use Error::*;
454        Some(match (http, case) {
455            (400, 0) => BadRequest,
456            (400, 1) => InvalidFieldFormat(String::new()),
457            (400, 2) => InvalidMandatoryField(String::new()),
458
459            (401, 0) => Unauthorized(String::new()),
460            (401, 1) => InvalidTokenB2B,
461            (401, 2) => InvalidCustomerToken,
462            (401, 3) => TokenNotFoundB2B,
463            (401, 4) => CustomerTokenNotFound,
464
465            (403, 0) => TransactionExpired,
466            (403, 1) => FeatureNotAllowed(String::new()),
467            (403, 2) => ExceedsTransactionAmountLimit,
468            (403, 3) => SuspectedFraud,
469            (403, 4) => ActivityCountLimitExceeded,
470            (403, 5) => DoNotHonor,
471            (403, 6) => FeatureNotAllowedAtThisTime(String::new()),
472            (403, 7) => CardBlocked,
473            (403, 8) => CardExpired,
474            (403, 9) => DormantAccount,
475            (403, 10) => NeedToSetTokenLimit,
476            (403, 11) => OTPBlocked,
477            (403, 12) => OTPLifetimeExpired,
478            (403, 13) => OTPSentToCardholder,
479            (403, 14) => InsufficientFunds,
480            (403, 15) => TransactionNotPermitted(String::new()),
481            (403, 16) => SuspendTransaction,
482            (403, 17) => TokenLimitExceeded,
483            (403, 18) => InactiveCardOrAccountOrCustomer,
484            (403, 19) => MerchantBlacklisted,
485            (403, 20) => MerchantLimitExceed,
486            (403, 21) => SetLimitNotAllowed,
487            (403, 22) => TokenLimitInvalid,
488            (403, 23) => AccountLimitExceed,
489
490            (404, 0) => InvalidTransactionStatus,
491            (404, 1) => TransactionNotFound,
492            (404, 2) => InvalidRouting,
493            (404, 3) => BankNotSupportedBySwitch,
494            (404, 4) => TransactionCancelled,
495            (404, 5) => MerchantNotRegisteredForCardRegistrationServices,
496            (404, 6) => NeedToRequestOTP,
497            (404, 7) => JourneyNotFound,
498            (404, 8) => InvalidMerchant,
499            (404, 9) => NoIssuer,
500            (404, 10) => InvalidAPITransition,
501            (404, 11) => InvalidCardOrAccountOrCustomerOrVirtualAccount(String::new()),
502            (404, 12) => InvalidBillOrVirtualAccountWithReason(String::new()),
503            (404, 13) => InvalidAmount,
504            (404, 14) => PaidBill,
505            (404, 15) => InvalidOTP,
506            (404, 16) => PartnerNotFound,
507            (404, 17) => InvalidTerminal,
508            (404, 18) => InconsistentRequest,
509            (404, 19) => InvalidBillOrVirtualAccount,
510
511            (405, 0) => RequestedFunctionIsNotSupported,
512            (405, 1) => RequestedOperationIsNotAllowed,
513
514            (409, 0) => Conflict,
515            (409, 1) => DuplicatePartnerReferenceNo,
516
517            (429, 0) => TooManyRequests,
518
519            (500, 0) => GeneralError,
520            (500, 1) => InternalServerError,
521            (500, 2) => ExternalServerError,
522
523            (504, 0) => Timeout,
524
525            _ => return None,
526        })
527    }
528}