diff --git a/app.py b/app.py index c1327e7..be1ac61 100644 --- a/app.py +++ b/app.py @@ -104,6 +104,7 @@ def run_import(): if phone == '': print(f'error in line: [{line_count}]- Invalid phone number: {phone}') log_file.write(f'error in line: [{line_count}]- Invalid phone number: {phone}\n') + continue # Skip invalid phone # validate email email = email.strip() @@ -131,6 +132,7 @@ def run_import(): except ValueError as e: ret = f'Unexpected error in line: [{line_count}] {repr(e)}' log_file.write(f'{ret}\n') + continue # Skip invalid rows # Find user via the API # by card @@ -176,6 +178,7 @@ def run_import(): print(f'Processing "transaction" file: {f}') csv_reader = csv.reader(csv_file, delimiter=',') line_count = 0 + processed_doc_ids = set() # Track processed doc_ids to detect duplicates for row in csv_reader: if line_count == 0: line_count += 1 @@ -185,6 +188,30 @@ def run_import(): print(f'Processing line {line_count}: {row}') user_id, card, phone, summ_total, summ_discount, sum_with_discount, bonus_amount, transaction_date, transaction_time, doc_id = row + # Validate numeric fields + try: + float(summ_total) if summ_total.strip() else 0.0 + float(summ_discount) if summ_discount.strip() else 0.0 + float(sum_with_discount) if sum_with_discount.strip() else 0.0 + float(bonus_amount) if bonus_amount.strip() else 0.0 + except ValueError as ve: + print(f'error in line: [{line_count}]- Invalid numeric value: {ve}') + log_file.write(f'error in line: [{line_count}]- Invalid numeric value: {ve}\n') + continue + + # Check for duplicate doc_id + if doc_id.strip() == '': + doc_id = f'{file_name[0:20]}-{line_count}-{card}' + else: + doc_id = doc_id.strip() + + # Check for duplicate doc_id in current run + if doc_id in processed_doc_ids: + print(f'error in line: [{line_count}]- Duplicate doc_id: {doc_id}') + log_file.write(f'error in line: [{line_count}]- Duplicate doc_id: {doc_id}\n') + continue + processed_doc_ids.add(doc_id) + if card.strip() != '': user_found_by_card, din_id, _, _, _ = get_user(card, get_type='auto') if not user_found_by_card: @@ -210,11 +237,6 @@ def run_import(): user_found = user_found_by_card or user_found_by_phone if user_found: - if doc_id.strip() == '': - doc_id = f'{file_name[0:20]}-{line_count}-{card}' - else: - doc_id = doc_id.strip() - result, data = bonuses_update( user_id=din_id, summ_total=summ_total, @@ -230,6 +252,7 @@ def run_import(): log_file.write(f'error in line: [{line_count}]- bonuses_update: {data}\n') else: print(f'RESULT=OK, Bonuses updated, user_id={din_id}') + log_file.write(f'Line: [{line_count}]- Success: doc_id={doc_id}, bonus={bonus_amount}\n') else: print(f'error in line: [{line_count}]- Invalid user: {user_id}') @@ -238,13 +261,11 @@ def run_import(): except ValueError as e: ret = f'error in line: [{line_count}] {repr(e)}' log_file.write(f'{ret}\n') + continue end_time = time.time() print(f'Elapsed time of TRANSACTIONS file processing : {end_time - start_time} seconds') - csv_file.close() - log_file.close() - os.rename(f, f + '.' + time.strftime("%Y%m%d-%H%M%S") + '.old') diff --git a/dinect_api.py b/dinect_api.py index 45e917c..2aaba0f 100644 --- a/dinect_api.py +++ b/dinect_api.py @@ -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: