diff --git a/stregsystem/management/commands/importmobilepaypayments.py b/stregsystem/management/commands/importmobilepaypayments.py index 266b4ad7..2691e55b 100644 --- a/stregsystem/management/commands/importmobilepaypayments.py +++ b/stregsystem/management/commands/importmobilepaypayments.py @@ -1,9 +1,10 @@ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta, date from django.core.management.base import BaseCommand from django.utils.dateparse import parse_datetime from pathlib import Path from requests import HTTPError +from requests.auth import HTTPBasicAuth from stregsystem.models import MobilePayment import json @@ -16,12 +17,15 @@ class Command(BaseCommand): help = 'Imports the latest payments from MobilePay' - api_endpoint = 'https://api.mobilepay.dk' + api_endpoint = 'https://api.vipps.no' # Saves secret tokens to the file "tokens.json" right next to this file. # Important to use a separate file since the tokens can change and is thus not suitable for django settings. tokens_file = (Path(__file__).parent / 'tokens.json').as_posix() tokens_file_backup = (Path(__file__).parent / 'tokens.json.bak').as_posix() tokens = None + # Cutoff for when this iteration of the Mobilepay-API (Vipps) is deployed + manual_cutoff_date = date(2024, 4, 9) + myshop_number = 90601 logger = logging.getLogger(__name__) days_back = None @@ -73,68 +77,81 @@ def update_token_storage(self): # Fetches a new access token using the refresh token. def refresh_access_token(self): - url = f"{self.api_endpoint}/merchant-authentication-openidconnect/connect/token" + url = f"{self.api_endpoint}/miami/v1/token" payload = { - "grant_type": "refresh_token", - "refresh_token": self.tokens['refresh_token'], - "client_id": self.tokens['zip-client-id'], - "client_secret": self.tokens['zip-client-secret'], + "grant_type": "client_credentials", } - response = requests.post(url, data=payload) + + auth = HTTPBasicAuth(self.tokens['client_id'], self.tokens['client_secret']) + + response = requests.post(url, data=payload, auth=auth) response.raise_for_status() json_response = response.json() # Calculate when the token expires expire_time = datetime.now() + timedelta(seconds=json_response['expires_in'] - 1) self.tokens['access_token_timeout'] = expire_time.isoformat(timespec='milliseconds') self.tokens['access_token'] = json_response['access_token'] - self.update_token_storage() - # Format to timestamp format. Source: - # https://github.com/MobilePayDev/MobilePay-TransactionReporting-API/blob/master/docs/api/types.md#timestamp - @staticmethod - def format_datetime(inputdatetime): - return f"{inputdatetime.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]}Z" + def refresh_ledger_id(self): + self.tokens['ledger_id'] = self.get_ledger_id(self.myshop_number) # Fetches the transactions for a given payment-point (MobilePay phone-number) in a given period (from-to) - def get_transactions(self): - url = f"{self.api_endpoint}/transaction-reporting/api/merchant/v1/paymentpoints/{self.tokens['paymentpoint']}/transactions" - current_time = datetime.now(timezone.utc) + def get_transactions(self, transaction_date: date): + ledger_date = transaction_date.strftime('%Y-%m-%d') + + url = f"{self.api_endpoint}/report/v2/ledgers/{self.tokens['ledger_id']}/funds/dates/{ledger_date}" + params = { - 'from': self.format_datetime(current_time - timedelta(days=self.days_back)), - 'to': self.format_datetime(current_time), + 'includeGDPRSensitiveData': "true", } headers = { - 'x-ibm-client-secret': self.tokens['ibm-client-secret'], - 'x-ibm-client-id': self.tokens['ibm-client-id'], 'authorization': 'Bearer {}'.format(self.tokens['access_token']), } response = requests.get(url, params=params, headers=headers) response.raise_for_status() - return response.json()['transactions'] + return response.json()['items'] # Client side check if the token has expired. def refresh_expired_token(self): self.read_token_storage() + + if 'access_token_timeout' not in self.tokens: + self.refresh_access_token() + expire_time = parse_datetime(self.tokens['access_token_timeout']) if datetime.now() >= expire_time: self.refresh_access_token() - def fetch_transactions(self): + if 'ledger_id' not in self.tokens: + self.refresh_ledger_id() + + self.update_token_storage() + + def fetch_transactions(self) -> list: # Do a client side check if token is good. If not - fetch another token. try: self.refresh_expired_token() assert self.days_back is not None - return self.get_transactions() + + transactions = [] + + for i in range(self.days_back): + past_date = date.today() - timedelta(days=i) + if past_date < self.manual_cutoff_date: + break + + transactions.extend(self.get_transactions(past_date)) + + return transactions except HTTPError as e: self.write_error(f"Got an HTTP error when trying to fetch transactions: {e.response}") except Exception as e: - self.write_error(f'Got an error when trying to fetch transactions.') - pass + self.write_error(f'Got an error when trying to fetch transactions: {e}') def import_mobilepay_payments(self): transactions = self.fetch_transactions() - if transactions is None: + if len(transactions) == 0: self.write_info(f'Ran, but no transactions found') return @@ -144,28 +161,48 @@ def import_mobilepay_payments(self): self.write_info('Successfully ran MobilePayment API import') def import_mobilepay_payment(self, transaction): - if transaction['type'] != 'Payment': + """ + Example of a transaction: + { + "pspReference": "32212390715", + "time": "2024-04-05T07:19:26.528092Z", + "ledgerDate": "2024-04-05", + "entryType": "capture", + "reference": "10113143347", + "currency": "DKK", + "amount": 20000, + "recipientHandle": "DK:90601", + "balanceAfter": 110000, + "balanceBefore": 90000, + "name": "Jakob Topper", + "maskedPhoneNo": "xxxx 1234", + "message": "Topper" + } + :param transaction: + :return: + """ + if transaction['entryType'] != 'capture': return - trans_id = transaction['paymentTransactionId'] + trans_id = transaction['pspReference'] if MobilePayment.objects.filter(transaction_id=trans_id).exists(): - self.write_debug(f'Skipping transaction since it already exists (Transaction ID: {trans_id})') + self.write_debug(f'Skipping transaction since it already exists (PSP-Reference: {trans_id})') return - currency_code = transaction['currencyCode'] + currency_code = transaction['currency'] if currency_code != 'DKK': self.write_warning(f'Does ONLY support DKK (Transaction ID: {trans_id}), was {currency_code}') return amount = transaction['amount'] - comment = strip_emoji(transaction['senderComment']) + comment = strip_emoji(transaction['message']) - payment_datetime = parse_datetime(transaction['timestamp']) + payment_datetime = parse_datetime(transaction['time']) MobilePayment.objects.create( - amount=amount * 100, # convert to streg-ører + amount=amount, # already in streg-ører member=mobile_payment_exact_match_member(comment), comment=comment, timestamp=payment_datetime, @@ -174,3 +211,40 @@ def import_mobilepay_payment(self, transaction): ) self.write_info(f'Imported transaction id: {trans_id} for amount: {amount}') + + def get_ledger_info(self, myshop_number: int): + """ + { + "ledgerId": "123456", + "currency": "DKK", + "payoutBankAccount": { + "scheme": "BBAN:DK", + "id": "123412341234123412" + }, + "owner": { + "scheme": "business:DK:CVR", + "id": "16427888" + }, + "settlesForRecipientHandles": [ + "DK:90601" + ] + } + :param myshop_number: + :return: + """ + url = f"{self.api_endpoint}/settlement/v1/ledgers" + params = {'settlesForRecipientHandles': 'DK:{}'.format(myshop_number)} + headers = { + 'authorization': 'Bearer {}'.format(self.tokens['access_token']), + } + response = requests.get(url, params=params, headers=headers) + response.raise_for_status() + + ledger_info = response.json()["items"] + + assert len(ledger_info) != 0 + + return ledger_info[0] + + def get_ledger_id(self, myshop_number: int) -> int: + return int(self.get_ledger_info(myshop_number)["ledgerId"])