Coverage for functions \ flipdare \ payments \ data \ payment_intent_codes.py: 72%
76 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
13from __future__ import annotations
15from typing import Any, ClassVar, Literal, get_args
16from stripe import PaymentIntent as StripePaymentIntent
17from flipdare.generated.shared.payment.payment_event_status import PaymentEventStatus
19__all__ = ["StripeDeclineCode", "StripeCancellationCode", "StripeIntentErrorCode"]
22class StripeDeclineCode:
23 __slots__ = ()
25 @staticmethod
26 def from_intent(
27 intent: StripePaymentIntent,
28 ) -> PaymentEventStatus | None:
29 error = getattr(intent, "last_payment_error", None)
30 if error is None:
31 return None
32 code = getattr(error, "code", None)
33 if code is None:
34 return None
35 decline_code = getattr(error, "decline_code", None)
36 if decline_code is None:
37 return None
39 message = getattr(error, "message", "") or ""
40 # Check for insufficient funds indicators
41 if decline_code == "insufficient_funds" or (
42 code == "card_declined" and "insufficient" in message.lower()
43 ):
45 # Suggest customer try a different payment method
46 # or contact their bank
47 return PaymentEventStatus.INSUFFICIENT_FUNDS
49 elif code == "balance_insufficient":
50 # This applies when charging from Stripe balance
51 return PaymentEventStatus.INSUFFICIENT_FUNDS
53 # we recheck on error,
54 # for the other codes, retrying would be pointless.
55 if code in ["card_declined", "bank_account_declined"]:
56 # Check the specific decline reason
57 if decline_code == "insufficient_funds":
58 return PaymentEventStatus.INSUFFICIENT_FUNDS
59 elif decline_code == "generic_decline":
60 return PaymentEventStatus.ERROR
61 elif decline_code in {"incorrect_cvc", "expired_card"}:
62 return PaymentEventStatus.CARD_ISSUE
63 else:
64 return PaymentEventStatus.ERROR
66 return None
69class StripeCancellationCode:
70 __slots__ = ()
72 EXPIRED: ClassVar = Literal[
73 "abandoned",
74 "automatic",
75 ]
76 DUPLICATE: ClassVar = Literal["duplicate",]
78 FRAUD: ClassVar = Literal["fraudulent",]
80 @staticmethod
81 def from_intent(
82 intent: StripePaymentIntent,
83 check_for_expired: bool = True,
84 check_for_duplicate: bool = True,
85 check_for_fraud: bool = True,
86 ) -> PaymentEventStatus | None:
87 cancellation_reason = getattr(intent, "cancellation_reason", None)
88 if cancellation_reason is None:
89 return None
91 if check_for_expired and _check(cancellation_reason, StripeCancellationCode.EXPIRED):
92 return PaymentEventStatus.EXPIRED
94 if check_for_duplicate and _check(cancellation_reason, StripeCancellationCode.DUPLICATE):
95 return PaymentEventStatus.DUPLICATE
97 if check_for_fraud and _check(cancellation_reason, StripeCancellationCode.FRAUD):
98 return PaymentEventStatus.FRAUDULENT
100 return None
103class StripeIntentErrorCode:
104 __slots__ = ()
106 REFUND: ClassVar = Literal["charge_already_refunded",]
108 CAPTURED: ClassVar = Literal["charge_already_captured",]
110 DISPUTE: ClassVar = Literal["charge_disputed",]
112 DECLINED: ClassVar = Literal[
113 "payment_method_customer_decline",
114 "payment_method_provider_decline",
115 "card_declined",
116 "bank_account_declined",
117 "insufficient_funds",
118 "lost_card",
119 "stolen_card",
120 "expired_card",
121 "processing_error",
122 "balance_insufficient",
123 ]
125 EXPIRED: ClassVar = Literal[
126 "payment_method_provider_timeout",
127 "payment_intent_payment_attempt_expired",
128 "payment_intent_payment_attempt_failed",
129 "abandoned",
130 "charge_expired_for_capture",
131 "capture_charge_authorization_expired",
132 "setup_intent_setup_attempt_expired",
133 ]
135 API: ClassVar = Literal[
136 "setup_attempt_failed",
137 "setup_intent_authentication_failure",
138 "setup_intent_invalid_parameter",
139 "setup_intent_mandate_invalid",
140 "setup_intent_mobile_wallet_unsupported",
141 "setup_intent_setup_attempt_expired",
142 "setup_intent_unexpected_state",
143 "secret_key_required",
144 "rate_limit",
145 "livemode_mismatch",
146 "card_decline_rate_limit_exceeded",
147 "capture_unauthorized_payment",
148 "payment_intent_mandate_invalid",
149 "payment_intent_rate_limit_exceeded",
150 "payment_intent_unexpected_state",
151 "amount_too_large",
152 "amount_too_small",
153 "api_key_expired",
154 "platform_account_required",
155 "platform_api_key_expired",
156 ]
158 @staticmethod
159 def from_intent(
160 intent: StripePaymentIntent,
161 check_for_refund: bool = True,
162 check_for_capture: bool = True,
163 check_for_dispute: bool = True,
164 check_for_declined: bool = True,
165 check_for_expired: bool = True,
166 ) -> PaymentEventStatus | None:
167 error = getattr(intent, "last_payment_error", None)
168 code = getattr(error, "code", None) if error else None
170 if check_for_refund and _check(code, StripeIntentErrorCode.REFUND):
171 return PaymentEventStatus.REFUNDED
173 if check_for_capture and _check(code, StripeIntentErrorCode.CAPTURED):
174 return PaymentEventStatus.ALREADY_CAPTURED
176 if check_for_dispute and _check(code, StripeIntentErrorCode.DISPUTE):
177 return PaymentEventStatus.DISPUTED
179 if check_for_declined and _check(code, StripeIntentErrorCode.DECLINED):
180 return PaymentEventStatus.DECLINED
182 if check_for_expired and _check(code, StripeIntentErrorCode.EXPIRED):
183 return PaymentEventStatus.EXPIRED
185 return None
188def _check(last_payment_error: str | None, codes: Any) -> bool:
189 if last_payment_error is None:
190 return False
192 return last_payment_error in get_args(codes)