diff --git a/.env.example b/.env.example index 25a6d04..4669b2a 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,4 @@ POS_TOKEN='123456' PRODUCTION=1 CURRENCY='RUS' COUNTRY='RU' -DRY_RUN=1 \ No newline at end of file +DRY_RUN= \ No newline at end of file diff --git a/app.py b/app.py index d6390fb..d79c76e 100644 --- a/app.py +++ b/app.py @@ -1,140 +1,247 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # __author__ = 'szhdanoff@gmail.com' +__version__ = '1.0.1' import os import time import csv import phonenumbers - +import apscheduler +# +from apscheduler.schedulers.background import BackgroundScheduler from email_validator import validate_email, EmailNotValidError from dotenv import load_dotenv # local imports from dinect_api import get_user -from dinect_api import new_user from dinect_api import new_user_by_card +from dinect_api import bonuses_update load_dotenv() -is_prod = bool(os.getenv('PRODUCTION', False)) COUNTRY = os.getenv('COUNTRY', 'RU') - +DRY_RUN = bool(os.getenv('DRY_RUN', False)) csv_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'csv') -files = [] -# r=root, d=directories, f = files -for r, d, f in os.walk(csv_path): - for file in f: - filename, file_extension = os.path.splitext(file) - if file_extension == '.csv': - files.append(os.path.join(r, file)) +print('Script version:', __version__, '| CSV path:', csv_path, '| COUNTRY:', COUNTRY, '| DRY_RUN:', DRY_RUN) -for f in files: - with open(f + f'-{time.strftime("%Y%m%d-%H%M%S", time.localtime())}.log', "w", encoding="utf-8") as log_file: - with open(f, "r", encoding="utf-8") as csv_file: - file_name = os.path.basename(f) - # USERS file - if 'users' in file_name: - print(f'Processing "users" file: {f}') - csv_reader = csv.reader(csv_file, delimiter=',') - line_count = 0 - for row in csv_reader: - if line_count == 0: - line_count += 1 - else: - try: - print(f'Processing line {line_count}: {row}') - nickname, full_name, card, phone, email, gender = row - # strip whitespaces - nickname = nickname.strip() - full_name = full_name.strip() - card = card.strip() - # validate phone - phone = phone.strip() - try: - parsed_phone = phonenumbers.parse(phone, region=COUNTRY) - if phonenumbers.is_valid_number(parsed_phone): - phone = phonenumbers.format_number(parsed_phone, phonenumbers.PhoneNumberFormat.E164) - except: - 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') - # validate email - email = email.strip() - try: - email_info = validate_email(email, check_deliverability=False) - email = email_info.normalized - except EmailNotValidError as e: - print(f'error in line: [{line_count}]- Invalid email: {email}') - log_file.write(f'error in line: [{line_count}]- Invalid email: {email}\n') - email = None - # set email if not found - if email is None: - email = f'{card}@user.dinect.com' - # validate / set gender - gender = gender.strip() - if gender not in ['M', 'F']: - gender = 1 - else: - if gender == 'M': +def validate_phone(raw_phone): + """ + Validates a given phone number. + + Args: + raw_phone (str): The phone number to be validated. + + Returns: + str: The validated phone number in E164 format if valid, otherwise an empty string. + """ + raw_phone = raw_phone.strip() + try: + raw_phone = phonenumbers.parse(raw_phone, COUNTRY) + if phonenumbers.is_valid_number(raw_phone): + return phonenumbers.format_number(raw_phone, phonenumbers.PhoneNumberFormat.E164) + else: + return '' + except phonenumbers.NumberParseException: + return '' + + +def run_import(): + """ + Imports users and transactions data from CSV files, validates the data, + creates new users if necessary, and updates user bonuses. + + Parameters: + None + + Returns: + None + """ + files = [] + # r=root, d=directories, f = files + for r, d, f in os.walk(csv_path): + for file in f: + filename, file_extension = os.path.splitext(file) + if file_extension == '.csv': + files.append(os.path.join(r, file)) + + for f in files: + with open(f + f'-{time.strftime("%Y%m%d-%H%M%S", time.localtime())}.log', "w", encoding="utf-8") as log_file: + with open(f, "r", encoding="utf-8") as csv_file: + file_name = os.path.basename(f) + # USERS file + start_time = time.time() + if 'users' in file_name: + print(f'Processing "users" file: {f}') + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + for row in csv_reader: + if line_count == 0: + line_count += 1 + else: + line_count += 1 + try: + print(f'Processing line {line_count}: {row}') + nickname, full_name, card, phone, email, gender = row + + # strip whitespaces + nickname = nickname.strip() + full_name = full_name.strip() + card = card.strip() + + # validate phone + phone = validate_phone(phone) + 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') + + # validate email + email = email.strip() + try: + email_info = validate_email(email, check_deliverability=False) + email = email_info.normalized + except EmailNotValidError as e: + print(f'error in line: [{line_count}]- Invalid email: {email}') + log_file.write(f'error in line: [{line_count}]- Invalid email: {email}\n') + email = None + # set email if not found + if email is None: + email = f'{card}@user.dinect.com' + + # validate / set gender + gender = gender.strip() + if gender not in ['M', 'F']: gender = 1 else: - gender = 2 + if gender == 'M': + gender = 1 + else: + gender = 2 - line_count += 1 - except ValueError as e: - ret = f'Unexpected error in line: [{line_count}] {repr(e)}' - log_file.write(f'{ret}\n') + except ValueError as e: + ret = f'Unexpected error in line: [{line_count}] {repr(e)}' + log_file.write(f'{ret}\n') - # Find user via the API - # by card - user_found_by_card, user_id, user_card, purchases_url, data = get_user(card) - print(f'user_found_by_card {card}', user_found_by_card) - # by phone - phone = phone.replace('+', '') - if not user_found_by_card: - user_found_by_phone, user_id, user_card, purchases_url, data = get_user(phone, get_type='phone') - print(f'user_found_by_phone {phone}', user_found_by_phone) + # Find user via the API + # by card + user_found_by_card, user_id, user_card, purchases_url, data = get_user(card) + print(f'user_found_by_card {card}', user_found_by_card) + # by phone + phone = phone.replace('+', '') + if not user_found_by_card: + user_found_by_phone, user_id, user_card, purchases_url, data = get_user(phone, + get_type='phone') + print(f'user_found_by_phone {phone}', user_found_by_phone) - user_found = user_found_by_card or user_found_by_phone + user_found = user_found_by_card or user_found_by_phone - # create new user if not found - if not user_found: - # user_created, data = new_user( - # full_name=nickname, phone=phone, gender=1, foreign_card=None, email=email, - # ) - user_created, data = new_user_by_card( - full_name=nickname, phone=phone, gender=1, external_card=card, email=email, use_existing=False - ) - if user_created: - print('User created with', data['ID'], data['DIN']) + # create new user if not found + if not user_found: + # user_created, data = new_user( + # full_name=nickname, phone=phone, gender=1, foreign_card=None, email=email, + # ) + user_created, data = new_user_by_card( + full_name=nickname, phone=phone, gender=1, external_card=card, email=email, + use_existing=False + ) + if user_created: + print('User created with', data['ID'], data['DIN']) + else: + log_file.write(f'error in line: [{line_count}]- Invalid user data: {data}\n') else: - log_file.write(f'error in line: [{line_count}]- Invalid user data: {data}\n') - else: - print('User found with', user_id, user_card, purchases_url) + print('User found with', data['DIN'], user_card) + end_time = time.time() + print(f'Elapsed time of USERS file processing : {end_time - start_time} seconds') - - - # TRANSACTIONS file - if 'transaction' in file_name: - print(f'Processing "transaction" file: {f}') - csv_reader = csv.reader(csv_file, delimiter=',') - line_count = 0 - for row in csv_reader: - if line_count == 0: - line_count += 1 - else: - try: - print(f'Processing line {line_count}: {row}') - user_id, card, phone, summ_total, summ_discount, sum_with_discount, bonus_amount, transaction_date, transaction_time = row + # TRANSACTIONS files + start_time = time.time() + if 'transaction' in file_name: + print(f'Processing "transaction" file: {f}') + csv_reader = csv.reader(csv_file, delimiter=',') + line_count = 0 + for row in csv_reader: + if line_count == 0: line_count += 1 + else: + try: + line_count += 1 + 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 - except ValueError as e: - ret = f'error in line: [{line_count}] {repr(e)}' - log_file.write(f'{ret}\n') + if card.strip() != '': + user_found_by_card, din_id, _, _, _ = get_user(card, get_type='auto') + if not user_found_by_card: + print(f'error in line: [{line_count}]- Invalid card: {card}') + log_file.write(f'error in line: [{line_count}]- Invalid card: {card}\n') + else: + print('User found with', user_id, card) + else: + user_found_by_card = False - csv_file.close() - log_file.close() + if validate_phone(phone) != '': + phone = phone.replace('+', '') + user_found_by_phone, din_id, _, _, _ = get_user(phone, get_type='phone') - # os.rename(f, f + '.old') + if not user_found_by_phone: + print(f'error in line: [{line_count}]- Invalid phone: {phone}') + log_file.write(f'error in line: [{line_count}]- Invalid phone: {phone}\n') + else: + print('User found with', din_id, phone) + else: + user_found_by_phone = False + + 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, + bonus_amount=bonus_amount, + doc_id=doc_id, + sum_with_discount=sum_with_discount, + sum_discount=summ_discount, + dry_run=DRY_RUN + ) + + if not result: + print(f'error in line: [{line_count}]- bonuses_update: {data}') + log_file.write(f'error in line: [{line_count}]- bonuses_update: {data}\n') + else: + print(f'Bonuses updated, user_id={din_id}') + + else: + print(f'error in line: [{line_count}]- Invalid user: {user_id}') + log_file.write(f'error in line: [{line_count}]- Invalid user: {user_id}\n') + + except ValueError as e: + ret = f'error in line: [{line_count}] {repr(e)}' + log_file.write(f'{ret}\n') + + 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') + + +scheduler = BackgroundScheduler() +scheduler.start() + +try: + job = scheduler.add_job(run_import, 'interval', minutes=1) + print('Import running every 1 minute. Place *.csv files in "csv" directory') + print('Press Ctrl+{0} to exit'.format('Break' if apscheduler.__version__ >= '3.0.0' else 'C')) + # This is here to simulate application activity (which keeps the main thread alive). + while True: + time.sleep(10) +except (KeyboardInterrupt, SystemExit): + # Not strictly necessary if daemonic mode is enabled but should be done if possible + scheduler.shutdown() diff --git a/dinect_api.py b/dinect_api.py index 10f0fe7..b6674bb 100644 --- a/dinect_api.py +++ b/dinect_api.py @@ -1,29 +1,24 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # __author__ = 'szhdanoff@gmail.com' -__version__ = '1.0.1' - +__version__ = '1.0.3' import os import requests import json from dotenv import load_dotenv - load_dotenv() -# local imports -# import app - is_prod = bool(os.getenv('PRODUCTION', False)) if is_prod: url = 'https://pos-api.dinect.com/20130701/' else: url = 'https://pos-api-ote.dinect.com/20130701/' -print(is_prod, url) +print('PRODUCTION:', is_prod, '| API URL:', url) -APP_TOKEN = os.getenv('APP_TOKEN') -POS_TOKEN = os.getenv('POS_TOKEN') +# APP_TOKEN = os.getenv('APP_TOKEN') +# POS_TOKEN = os.getenv('POS_TOKEN') app_token = os.getenv('APP_TOKEN') pos_token = os.getenv('POS_TOKEN') @@ -42,8 +37,6 @@ HEADERS = { } -# GET /20130701/tokens/?next=/20130701/logon -# https://pos-api.dinect.com/20130701/tokens/3b01228843d115ae8c03a4d3b20dcb545dbb228c def get_user(search_id, get_type='auto', headers=None) -> tuple: """ A function to get user information based on the search_id and get_type. @@ -118,57 +111,27 @@ def new_user(full_name, phone, gender=1, foreign_card=None, email=None, headers= return False, r.json() -def bonuses_update( - user_id, - summ_total, - bonus_amount, - sum_with_discount, - sum_discount, - doc_id, - headers=None): +def new_user_by_card(external_card, full_name, phone, gender=1, email=None, headers=None, use_existing=True): """ - Updates the bonuses for a user. + Creates a new user by card. Args: - user_id (int): The ID of the user. - summ_total (float): The total amount. - bonus_amount (float): The bonus amount. - sum_with_discount (float): The amount with discount. - sum_discount (float): The discount amount. - doc_id (str): The document ID. + external_card (str): The external card code. + full_name (str): The full name of the user. + phone (str): The phone number of the user. + gender (int, optional): The gender of the user. Defaults to 1. + email (str, optional): The email of the user. Defaults to None. headers (dict, optional): The headers to include in the request. Defaults to None. + use_existing (bool, optional): Whether to bind the user to an existing card. Defaults to True. Returns: tuple: A tuple containing a boolean indicating the success of the request and the JSON response. 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 JSON response is returned. + If the request is unsuccessful, the boolean is False and the error message is returned. """ if headers is None: headers = HEADERS - base_url = url + 'users/' + str(user_id) + '/purchases/' - params = { - "doc_id": doc_id, - "bonus_amount": bonus_amount, - "sum_total": summ_total, - "sum_discount": sum_discount, - "sum_with_discount ": sum_with_discount, - "commit": 'True', - "curr_iso_name": 'RUB', - "override": 'True', - # "date": '2024-08-03 12:53:07', - } - r = requests.post(base_url, headers=headers, json=params) - if r.status_code == 201: - return True, r.json() - else: - return False, r.json() - - -def new_user_by_card(external_card, full_name, phone, gender=1, email=None, headers=None, use_existing=True): - if headers is None: - headers = HEADERS - base_url = url + 'cards/' params = { 'full_name': full_name, @@ -212,46 +175,33 @@ def add_external_card(user_id, card, headers=None): return True, r.json() -# user_date = new_user('Test2', '79039426493', email='YlEJp@example.com') -# print(user_date) +def bonuses_update( + user_id, + summ_total, + bonus_amount, + sum_with_discount, + sum_discount, + doc_id, + headers=None, + dry_run=False +): + if headers is None: + headers = HEADERS -# 79039426498 -# (True, {'DIN': 232113, 'ID': '4620011139016260791309380'}) - -# result, user_id, user_card, purchases_url, data = get_user('1234567890123') -# result, user_id, user_card, purchases_url, data = get_user('79039426493', get_type='phone') -# print(result, data) -# if result: -# # user_id = data[0].get('id') -# # user_card = data[0].get('card') -# # purchases_url = data[0].get('purchases_url') -# print('user_id', user_id) -# print('user_card', user_card) -# print('purchases_url', purchases_url) - - -# добавление внешней карты лояльности - - -# print(get_user('4620011139016689273132009')) -# print(get_user('1234567890123')) -# print(get_user('+79039406889')) -# print(new_user()) -# (True, '{"DIN":3152300,"ID":"4620011139016570939672611"}') - - -# print(bonuses_update( -# user_id=int('1002'), -# summ_total=100.00, -# bonus_amount=10.00, -# doc_id='test12', -# sum_with_discount=90.00, -# sum_discount=10.00, -# )) -# print(get_user('79039426493', get_type='phone')) -# (True, {'DIN': 3155239, 'ID': '4620011139016802073627661'}) - - -# print(new_user_by_card( -# external_card='1234567891235', full_name='Test', phone='79039426495', email='123321@example.com', use_existing=False -# )) + base_url = url + 'users/' + str(user_id) + '/purchases/' + params = { + "doc_id": doc_id, + "bonus_amount": bonus_amount, + "sum_total": summ_total, + "sum_discount": sum_discount, + "sum_with_discount ": sum_with_discount, + "commit": 'True', + "curr_iso_name": 'RUB', + "override": 'True' + # "date": '2024-08-03 12:53:07', + } + r = requests.post(base_url, headers=headers, json=params) + if r.status_code == 201: + return True, r.json() + else: + return False, r.text diff --git a/examples/transaction-example.csv b/examples/transaction-example.csv index 919b2c9..0492a9b 100644 --- a/examples/transaction-example.csv +++ b/examples/transaction-example.csv @@ -1,3 +1,3 @@ -user_id, card, phone, summ_total, summ_discount, sum_with_discount, bonus_amount, transaction_date, transaction_time -15689, 654897321321,+78906543210,12345.67,123.56,12222.11,121,2002-03-11,21:05:36 -6578, 654897321123,+78906233212,345.67,45.00,300.67,12,2002-03-12,01:05:36 \ No newline at end of file +user_id, card, phone, summ_total, summ_discount, sum_with_discount, bonus_amount, transaction_date, transaction_time, doc_id +15689, 654897321321,+78906543210,12345.67,123.56,12222.11,121,2002-03-11,21:05:36,п123 +6578, 654897321123,+78906233212,345.67,45.00,300.67,12,2002-03-12,01:05:36, \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f605720..8fb8c92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ python-dotenv~=1.0.0 requests~=2.32.3 -phonenumbers~=8.13.42 -email-validator \ No newline at end of file +phonenumbers==8.13.43 +APScheduler~=3.10.4 +email_validator~=2.2.0 \ No newline at end of file