From 4d2989e74c56685e7ccf40d2e0d5f1a43164fb2c Mon Sep 17 00:00:00 2001 From: Mark Sheppard Date: Mon, 28 Aug 2023 22:18:55 +0000 Subject: [PATCH] Added the "add_payee" action --- starling | 142 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 126 insertions(+), 16 deletions(-) diff --git a/starling b/starling index 9173c34..6b1be8b 100755 --- a/starling +++ b/starling @@ -5,6 +5,7 @@ import argparse import io import json +import re import requests import sys import types @@ -13,8 +14,82 @@ base_url = 'https://api.starlingbank.com/api/v2' #-----------------------------------------------------------------------------# +class Question: + + def __init__(self, name, description, type, values): + self.name = name + self.description = description + self.type = type + self.values = values + self.answer = None + + def ask(self): + while True: + sys.stdout.write(f'{self.description} ({self.type_description()}): ') + self.answer = sys.stdin.readline().rstrip('\r\n') + if self.valid_answer(): + break + print(f'Invalid answer {self.answer!r}, please try again') + + def type_description(self): + if self.type == 'string': + return f'{self.values[0]} - {self.values[1]} characters' + if self.type == 'digits': + return f'exactly {self.values} digits' + if self.type == 'enum': + return ', '.join(self.values) + else: + raise Exception(f'Unknown value type {self.type} for "{self.description}"') + + def valid_answer(self): + if self.type == 'string': + size = len(self.answer) + return size >= self.values[0] and size <= self.values[1] + if self.type == 'digits': + size = len(self.answer) + return size == self.values and re.match('^\d+$', self.answer) + if self.type == 'enum': + return self.answer in self.values + else: + raise Exception(f'Unknown value type {self.type} for "{self.description}"') + +class Form: + + def __init__(self, topic, *questions): + self.topic = topic + self.questions = questions + + def complete(self): + print(f'Please enter the details for the {self.topic}:') + print('') + for question in self.questions: + question.ask() + print('') + print(f'Please check the details for the {self.topic}:') + print('') + for question in self.questions: + print(f'{question.description}: {question.answer}') + print('') + while True: + sys.stdout.write('Are these details corrent (y/n)? ') + answer = sys.stdin.readline().rstrip('\r\n') + if answer == 'y': + break + if answer == 'n': + sys.exit(1) + return dict(map(lambda q: (q.name, q.answer), self.questions)) + class StarlingClient: + new_payee_form = Form( + 'new payee', + Question('payee_type', 'Payee type', 'enum', ('BUSINESS', 'INDIVIDUAL',)), + Question('payee_name', 'Payee\'s name', 'string', (1, 255,)), + Question('sort_code', 'Sort code', 'digits', 6), + Question('account_number', 'Account number', 'digits', 8), + Question('account_description', 'Account description', 'string', (1, 255,)), + ) + def __init__(self): self.tokens = self.read_tokens('main', 'payments') @@ -34,6 +109,15 @@ class StarlingClient: response.raise_for_status() return json.loads(response.text, object_hook=lambda obj: types.SimpleNamespace(**obj)) + def put(self, path, data, token='main'): + url = f'{base_url}/{path}' + headers = { + 'Authorization': f'Bearer {self.tokens[token]}', + } + response = requests.put(url, json=data, headers=headers) + response.raise_for_status() + return json.loads(response.text, object_hook=lambda obj: types.SimpleNamespace(**obj)) + ### Low-level API wrappers ### def account_holder(self): @@ -48,8 +132,10 @@ class StarlingClient: def balance(self, accountUid): return self.get(f'accounts/{accountUid}/balance') - def payees(self): - return self.get('payees') + def payees(self, data=None): + if data is None: + return self.get('payees') + return self.put('payees', data) ### Mid-level methods to munge the data from the low-level calls ### @@ -104,6 +190,29 @@ class StarlingClient: for payee in payees: print(f' {payee.payeeName} ({payee.payeeType})') + def add_payee(self): + details = self.new_payee_form.complete() + data = { + 'payeeName': details['payee_name'], + 'payeeType': details['payee_type'], + 'accounts': [ + { + 'description': details['account_description'], + 'defaultAccount': True, + 'countryCode': 'GB', + 'accountIdentifier': details['account_number'], + 'bankIdentifier': details['sort_code'], + 'bankIdentifierType': 'SORT_CODE', + }, + ], + } + response = self.payees(data) + if response.success: + print(f'Successfully created payee with UID {response.payeeUid}') + else: + print(f'Failed to create payee: {response}') + sys.exit(1) + #-----------------------------------------------------------------------------# parser = argparse.ArgumentParser( @@ -111,13 +220,9 @@ parser = argparse.ArgumentParser( description='Interacts with Starling Bank\'s API to manage bank accounts.', formatter_class=argparse.RawDescriptionHelpFormatter, epilog='''recognised actions and args: - - show customer and account details + - show customer and account details payees - list the customer's payees - contacts [] - get/set contacts for domain - expire - allow domain to auto-expire - expiry - list domains with expiry info - -To show the current lock state use "%(prog)s show status". + add_payee - adds a new payee ''') parser.add_argument('-t', '--test', action='store_true', default=False, @@ -132,14 +237,19 @@ client = StarlingClient() action_args = args.action if len(action_args) == 0: client.default_action() + sys.exit(0) + +action = action_args.pop(0) +count = len(action_args) +if action == 'payees': + if count > 0: + parser.error(f'Too many arguments for "{action}" action') + client.list_payees() +elif action == 'add_payee': + if count > 0: + parser.error(f'Too many arguments for "{action}" action') + client.add_payee() else: - action = action_args.pop(0) - count = len(action_args) - if action == 'payees': - if count > 0: - parser.error(f'Too many arguments for "{action}" action') - client.list_payees() - else: - parser.error(f'Unknown action "{action}"') + parser.error(f'Unknown action "{action}"') #-----------------------------------------------------------------------------#