Repository: liberapay/liberapay.com

Issue:
Virtual Debit Card of Airtel Payments Bank is not supported.
I tried to donate, but payment rejected by the software. Here is the actual message:

Payment Required

Your request has been rejected by our software. Please contact us if you need assistance.

Error message:

The payment processor Stripe returned an error: “Your card does not support this type of purchase. (request ID: req_AE5WrjYQmvo3D0)”.


If you decide to contact us please include the following debugging information in your message:

URL: https://liberapay.com/akkcvyas/giving/pay/stripe/?beneficiary=3&method=card Method: POST Referer: b'https://liberapay.com/akkcvyas/giving/pay/stripe/?beneficiary=3&method=card' User-Agent: b'Mozilla/5.0 (Linux; Android 5.1.1; SM-J200G) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.74 Mobile Safari/537.36' Time: 2024-01-27 17:49:34.142992+00:00 Response code: 402 Response message: 'The payment processor Stripe returned an error: “Your card does not support this type of purchase. (request ID: req_AE5WrjYQmvo3D0)”.' Error location: stripe/%payin_id.spt:107 User: <Participant #1844953 'akkcvyas'> Locale: en_US Website version: 725


Discussion:

Resolving patch:
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -3705,6 +3705,14 @@ def _guessed_country(self):
             locale, request = state['locale'], state['request']
             return locale.territory or request.source_country
 
+    @property
+    def can_attempt_payment(self):
+        return (
+            not self.is_suspended and
+            self.status == 'active' and
+            bool(self.get_email_address())
+        )
+
 
 class NeedConfirmation(Exception):
     """Represent the case where we need user confirmation during a merge.
diff --git a/liberapay/payin/cron.py b/liberapay/payin/cron.py
--- a/liberapay/payin/cron.py
+++ b/liberapay/payin/cron.py
@@ -127,7 +127,7 @@ def send_donation_reminder_notifications():
     today = utcnow().date()
     next_payday = compute_next_payday_date()
     for payer, payins in rows:
-        if payer.is_suspended or payer.status != 'active':
+        if not payer.can_attempt_payment:
             continue
         payins.sort(key=itemgetter('execution_date'))
         _check_scheduled_payins(db, payer, payins, automatic=False)
@@ -193,7 +193,7 @@ def send_upcoming_debit_notifications():
       ORDER BY sp.payer, (sp.amount).currency
     """)
     for payer, payins in rows:
-        if payer.is_suspended or payer.status != 'active' or not payer.get_email_address():
+        if not payer.can_attempt_payment:
             continue
         _check_scheduled_payins(db, payer, payins, automatic=True)
         if not payins:
@@ -459,6 +459,8 @@ def _check_scheduled_payins(db, payer, payins, automatic):
 def _filter_transfers(payer, transfers, automatic):
     """Splits scheduled transfers into 4 lists: okay, canceled, impossible, actionable.
     """
+    if not payer.can_attempt_payment:
+        return [], list(transfers), [], []
     canceled_transfers = []
     impossible_transfers = []
     actionable_transfers = []
diff --git a/liberapay/payin/stripe.py b/liberapay/payin/stripe.py
--- a/liberapay/payin/stripe.py
+++ b/liberapay/payin/stripe.py
@@ -95,8 +95,13 @@ def charge(db, payin, payer, route, update_donor=True):
           JOIN participants p On p.id = pt.recipient
          WHERE pt.payin = %(payin)s
     """, dict(payin=payin.id))
+    payer_state = (
+        'blocked' if payer.is_suspended else
+        'invalid' if payer.status != 'active' or not payer.get_email_address() else
+        'okay'
+    )
     new_status = None
-    if payer.is_suspended:
+    if payer_state != 'okay':
         new_status = 'failed'
     elif route.network == 'stripe-sdd':
         for pt in transfers:
@@ -113,9 +118,11 @@ def charge(db, payin, payer, route, update_donor=True):
             payin = update_payin(db, payin.id, None, new_status, new_payin_error)
             for i, pt in enumerate(transfers, 1):
                 new_transfer_error = (
-                    "canceled because payer account is blocked"
-                    if payer.is_suspended else
-                    "canceled because destination account is blocked"
+                    "canceled because the payer's account is blocked"
+                    if payer_state == 'blocked' else
+                    "canceled because the payer's account is in an invalid state"
+                    if payer_state == 'invalid' else
+                    "canceled because the destination account is blocked"
                     if pt.recipient_marked_as in ('fraud', 'spam') else
                     "canceled because another destination account is blocked"
                 ) if new_status == 'failed' else None
@@ -460,7 +467,7 @@ def settle_charge_and_transfers(
         payer = db.Participant.from_id(payin.payer)
         undeliverable_amount = amount_settled.zero()
         for i, pt in enumerate(payin_transfers):
-            if payer.is_suspended:
+            if payer.is_suspended or not payer.get_email_address():
                 if pt.status not in ('failed', 'succeeded'):
                     pt = update_payin_transfer(
                         db, pt.id, None, 'suspended', None,


Test patch:
diff --git a/tests/py/test_payins.py b/tests/py/test_payins.py
--- a/tests/py/test_payins.py
+++ b/tests/py/test_payins.py
@@ -1669,7 +1669,7 @@ def test_09_payin_stripe_sdd_fraud_review(self):
             assert payin.fee is None
             pt = self.db.one("SELECT * FROM payin_transfers")
             assert pt.status == 'failed'
-            assert pt.error == "canceled because destination account is blocked"
+            assert pt.error == "canceled because the destination account is blocked"
 
 
 class TestRefundsStripe(EmailHarness):