Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new Mobilepay reporting API #416

Merged
merged 21 commits into from
Apr 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 108 additions & 34 deletions stregsystem/management/commands/importmobilepaypayments.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
BoAnd marked this conversation as resolved.
Show resolved Hide resolved
krestenlaust marked this conversation as resolved.
Show resolved Hide resolved

BoAnd marked this conversation as resolved.
Show resolved Hide resolved
logger = logging.getLogger(__name__)
days_back = None
Expand Down Expand Up @@ -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",
}
krestenlaust marked this conversation as resolved.
Show resolved Hide resolved
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

Expand All @@ -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,
Expand All @@ -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"])
Loading