Add error handling and retry logic to API calls

- Add retry decorator for transient errors in dinect_api.py
- Add error handling decorator for consistent error responses
- Update bonuses_update to handle numeric bonus amounts properly
- Fix parameter name typo in bonuses_update
- Skip invalid phone numbers and rows in app.py
- Add duplicate doc_id detection in transaction processing
- Improve logging of successful transactions
- Remove redundant doc_id generation code
- Fix file closing logic in app.py
This commit is contained in:
Sergey_Z
2026-02-26 17:34:43 +03:00
parent b8e9b16bd9
commit 2df74566aa
2 changed files with 151 additions and 12 deletions

View File

@@ -1,14 +1,66 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# __author__ = 'szhdanoff@gmail.com'
__version__ = '1.0.3'
__version__ = '1.0.4'
import os
import requests
import json
import time
from functools import wraps
from dotenv import load_dotenv
load_dotenv()
def retry_on_error(max_retries=3, delay=1, backoff=2):
"""
Decorator for retrying API calls on transient errors.
Args:
max_retries: Maximum number of retry attempts
delay: Initial delay between retries in seconds
backoff: Multiplier for delay after each retry
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
current_delay = delay
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except (requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
requests.exceptions.HTTPError) as e:
last_exception = e
if attempt < max_retries:
print(f"Retry {attempt + 1}/{max_retries} after {current_delay}s - Error: {e}")
time.sleep(current_delay)
current_delay *= backoff
else:
print(f"All {max_retries} retries exhausted. Last error: {e}")
raise last_exception
return wrapper
return decorator
def handle_api_error(func):
"""
Decorator for handling API errors consistently.
Returns tuple (False, error_message) on any exception.
"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except requests.exceptions.RequestException as e:
return False, f"Network error: {str(e)}"
except (ValueError, KeyError) as e:
return False, f"Data error: {str(e)}"
except Exception as e:
return False, f"Unexpected error: {str(e)}"
return wrapper
is_prod = bool(os.getenv('PRODUCTION', False))
if is_prod:
url = 'https://pos-api.dinect.com/20130701/'
@@ -51,6 +103,12 @@ def get_user(search_id, get_type='auto', headers=None) -> tuple:
- The boolean value indicating success or failure.
- The response data based on the request made.
"""
return _get_user_impl(search_id, get_type, headers)
@retry_on_error(max_retries=3, delay=1, backoff=2)
@handle_api_error
def _get_user_impl(search_id, get_type='auto', headers=None) -> tuple:
if headers is None:
headers = HEADERS
@@ -129,6 +187,12 @@ def new_user_by_card(external_card, full_name, phone, gender=1, email=None, head
If the request is successful, the boolean is True and the JSON response is returned.
If the request is unsuccessful, the boolean is False and the error message is returned.
"""
return _new_user_by_card_impl(external_card, full_name, phone, gender, email, headers, use_existing)
@retry_on_error(max_retries=3, delay=1, backoff=2)
@handle_api_error
def _new_user_by_card_impl(external_card, full_name, phone, gender=1, email=None, headers=None, use_existing=True):
if headers is None:
headers = HEADERS
@@ -149,6 +213,26 @@ def new_user_by_card(external_card, full_name, phone, gender=1, email=None, head
return False, r.text
if headers is None:
headers = HEADERS
base_url = url + '/users/'
params = {
# "short_name": nickname,
"full_name": full_name,
"phone": phone,
"email": email,
"gender": gender,
}
r = requests.post(base_url, headers=headers, json=params)
if r.status_code == 201:
return True, r.json()
else:
return False, r.json()
def add_external_card(user_id, card, headers=None):
"""
Adds an external card to a user.
@@ -173,6 +257,8 @@ def add_external_card(user_id, card, headers=None):
r = requests.post(base_url, headers=headers, json=params)
if r.status_code == 201:
return True, r.json()
else:
return False, r.text if r.text else f"HTTP {r.status_code}"
def bonuses_update(
@@ -184,6 +270,37 @@ def bonuses_update(
doc_id,
headers=None,
dry_run=False
):
"""
Updates user bonuses (adds or subtracts bonus points).
Args:
user_id: User ID in Dinect system
summ_total: Total sum of transaction
bonus_amount: Bonus amount to add (positive) or pay with bonuses (negative)
sum_with_discount: Sum after discount
sum_discount: Discount amount
doc_id: Document ID for the transaction
headers: Optional request headers
dry_run: If True, simulates the transaction without actual processing
Returns:
tuple: (success: bool, response_data: dict|str)
"""
return _bonuses_update_impl(user_id, summ_total, bonus_amount, sum_with_discount, sum_discount, doc_id, headers, dry_run)
@retry_on_error(max_retries=3, delay=1, backoff=2)
@handle_api_error
def _bonuses_update_impl(
user_id,
summ_total,
bonus_amount,
sum_with_discount,
sum_discount,
doc_id,
headers=None,
dry_run=False
):
if headers is None:
headers = HEADERS
@@ -194,7 +311,7 @@ def bonuses_update(
# "bonus_amount": bonus_amount,
"sum_total": summ_total,
"sum_discount": sum_discount,
"sum_with_discount ": sum_with_discount,
"sum_with_discount": sum_with_discount,
"commit": 'True',
"curr_iso_name": currency,
"override": 'True'
@@ -202,9 +319,10 @@ def bonuses_update(
}
bonus_amount_numeric = float(bonus_amount)
if bonus_amount_numeric >= 0:
params["bonus_amount"] = bonus_amount
params["bonus_amount"] = str(int(bonus_amount_numeric))
else:
params["bonus_payment"] = bonus_amount[1:]
# Negative value - pay with bonuses (use absolute value)
params["bonus_payment"] = str(int(abs(bonus_amount_numeric)))
r = requests.post(base_url, headers=headers, json=params)
if r.status_code == 201: