Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
24acc6a
payments: begin updating Wise implementation to match API updates
jayaddison Aug 28, 2025
944fe52
payments: initial functional (sandbox-tested) updated Wise `BankAccou…
jayaddison Aug 30, 2025
eb155bd
payments: raise exception when requested detail attribute is not found
jayaddison Aug 30, 2025
b211ab5
tests: payments: add Wise Sandbox `account-details` VCR recording
jayaddison Sep 8, 2025
7b7166e
payments: apply `ruff format`
jayaddison Sep 8, 2025
23e7491
payments: refactor preparation: add basic account-details test coverage
jayaddison Sep 8, 2025
ede71c6
payments: naive refactor for Wise account detail aggregation
jayaddison Sep 8, 2025
02ba153
payments: preparation: declare (as-yet-unused) `RecipientDetails` dat…
jayaddison Sep 8, 2025
d5a0937
payments: preparation: consolidate bank name/address fields in dataclass
jayaddison Sep 8, 2025
fd91cdf
payments: run `ruff format`
jayaddison Sep 8, 2025
71df64d
payments: add some `TODO` notes for pending refactoring
jayaddison Sep 8, 2025
04e36bc
payments: refactor: single-pass bank detail aggregation
jayaddison Sep 9, 2025
bff957e
payments: fixup: construct `BankAccount` instance using correct varia…
jayaddison Sep 9, 2025
902cb35
payments: rectify some naming, and refactor to reduce line length
jayaddison Sep 9, 2025
98850c3
payments: fixup: ensure `bank_details` variable is assigned prior to …
jayaddison Sep 9, 2025
69b0844
payments: fixup: variable/attribute names
jayaddison Sep 9, 2025
1cc942c
payments: fixup: default `RecipientDetails` attribute values
jayaddison Sep 9, 2025
76ed50e
payments: fixup: parse/clean sort code and account number
jayaddison Sep 9, 2025
3269de1
payments: apply `ruff format`
jayaddison Sep 9, 2025
579129e
payments: rename method for brevity
jayaddison Sep 9, 2025
a616d9e
payments: refactor / relocate validation logic
jayaddison Sep 9, 2025
4ce979d
payments: use assertions for brevity
jayaddison Sep 9, 2025
4a0a2b1
payments: factor-out `wise_balance_id` variable
jayaddison Sep 9, 2025
2f11c07
payments: cleanup: remove a few temporary/workaround `import` statements
jayaddison Sep 9, 2025
9632cb9
payments: nitpick / rectify a method name
jayaddison Sep 10, 2025
4bba914
tests: payments: resolve Wise `FIXME` item
jayaddison Sep 10, 2025
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
129 changes: 90 additions & 39 deletions apps/payments/wise.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from dataclasses import dataclass
from datetime import timedelta

from flask import abort, request
Expand Down Expand Up @@ -203,52 +204,102 @@ def wise_business_profile():
return id


def wise_retrieve_accounts(profile_id):
# Wise creates the concept of a multi-currency account by calling normal
# bank accounts "balances". As far as we're concerned, "balances" are bank
# accounts, as that's what people will be sending money to.
for account in wise.balances.list(profile_id=profile_id):
try:
if not account.bankDetails:
continue
if not account.bankDetails.bankAddress:
continue
except AttributeError:
continue
@dataclass
class WiseRecipient:
account_holder: str
name_and_address: str
sort_code: str | None = None
account_number: str | None = None
swift: str | None = None
iban: str | None = None

@property
def bank_info(self):
assert self.name_and_address, "Bank name/address information not found"
return self.name_and_address

@property
def name(self):
bank_name, _, _ = self.bank_info.partition("\n")
assert bank_name, "Bank name is empty"
return bank_name

@property
def address(self):
_, _, bank_address = self.bank_info.partition("\n")
assert bank_address, "Bank address is empty"
return bank_address

@property
def parsed_account_number(self):
return self.account_number.replace(" ", "")[-8:]

@property
def parsed_sort_code(self):
return self.sort_code.replace("-", "")


def _recipient_details_adapter(receive_options):
"""Helper method to adapt Wise receive options response data into a WiseRecipient instance"""
field_mappings = {
"ACCOUNT_HOLDER": "account_holder",
"BANK_NAME_AND_ADDRESS": "name_and_address",
"BANK_CODE": "sort_code",
"ACCOUNT_NUMBER": "account_number",
"SWIFT_CODE": "swift",
"IBAN": "iban",
}
return WiseRecipient(
**{
field_mappings.get(detail.type): detail.body
for detail in receive_options.details
if detail.type in field_mappings
}
)

address = ", ".join(
[
account.bankDetails.bankAddress.addressFirstLine,
account.bankDetails.bankAddress.city + " " + (account.bankDetails.bankAddress.postCode or ""),
account.bankDetails.bankAddress.country,
]
)

sort_code = account_number = None
def _merge_recipient_details(account):
existing_details = None
for receive_options in account.receiveOptions:
recipient_details = _recipient_details_adapter(receive_options)

if existing_details:
# coalesce translated account info into the existing bank details
for field in ("sort_code", "account_number", "swift", "iban"):
existing_value = getattr(existing_details, field)
updated_value = getattr(recipient_details, field)
combined_value = existing_value if existing_value is not None else updated_value
setattr(existing_details, field, combined_value)
else:
existing_details = recipient_details

return existing_details


def wise_retrieve_accounts(profile_id):
from main import wise

for account in wise.account_details.list(profile_id=profile_id):
if account.currency.code != "GBP":
# TODO: support other host currencies
continue

if account.bankDetails.currency == "GBP":
# bankCode is the SWIFT code for non-GBP accounts.
sort_code = account.bankDetails.bankCode.replace("-", "")
bank_details = _merge_recipient_details(account)

if len(account.bankDetails.accountNumber) == 8:
account_number = account.bankDetails.accountNumber
else:
# Wise bug:
# accountNumber is sometimes erroneously the IBAN for GBP accounts.
# Extract the account number from the IBAN.
account_number = account.bankDetails.accountNumber.replace(" ", "")[-8:]
# Workaround: the Wise Sandbox API returns a null/empty account ID; populate a value
if account.id is None and app.config.get("TRANSFERWISE_ENVIRONMENT") == "sandbox":
account.id = 0

yield BankAccount(
sort_code=sort_code,
acct_id=account_number,
currency=account.bankDetails.currency,
sort_code=bank_details.parsed_sort_code,
acct_id=bank_details.parsed_account_number,
currency=account.currency.code,
active=False,
payee_name=account.bankDetails.get("accountHolderName"),
institution=account.bankDetails.bankName,
address=address,
swift=account.bankDetails.get("swift"),
iban=account.bankDetails.get("iban"),
# Webhooks only include the borderlessAccountId
payee_name=bank_details.account_holder,
institution=bank_details.name,
address=bank_details.address,
swift=bank_details.swift,
iban=bank_details.iban,
wise_balance_id=account.id,
)

Expand Down
Loading
Loading