Implement transaction handling
This commit is contained in:
parent
780adf3bc1
commit
7d1bd75f48
|
@ -1,10 +1,8 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
|
||||||
import orjson
|
from core.clients.platforms.agora import AgoraClient
|
||||||
|
from core.lib import notify
|
||||||
from core.lib import db, notify
|
from core.lib.money import money
|
||||||
|
|
||||||
# from core.lib.money import money
|
|
||||||
from core.util import logs
|
from core.util import logs
|
||||||
|
|
||||||
log = logs.get_logger("aggregator")
|
log = logs.get_logger("aggregator")
|
||||||
|
@ -53,29 +51,55 @@ class AggregatorClient(ABC):
|
||||||
async def process_transactions(self, account_id, transactions):
|
async def process_transactions(self, account_id, transactions):
|
||||||
if not transactions:
|
if not transactions:
|
||||||
return False
|
return False
|
||||||
transaction_ids = [x["transaction_id"] for x in transactions]
|
|
||||||
new_key_name = f"new.transactions.{self.instance.id}.{self.name}.{account_id}"
|
|
||||||
old_key_name = f"transactions.{self.instance.id}.{self.name}.{account_id}"
|
|
||||||
# for transaction_id in transaction_ids:
|
|
||||||
if not transaction_ids:
|
|
||||||
return
|
|
||||||
await db.r.sadd(new_key_name, *transaction_ids)
|
|
||||||
|
|
||||||
difference = list(await db.r.sdiff(new_key_name, old_key_name))
|
platforms = self.platforms
|
||||||
|
for transaction in transactions:
|
||||||
|
transaction_id = transaction["transaction_id"]
|
||||||
|
tx_obj = self.instance.get_transaction(
|
||||||
|
account_id,
|
||||||
|
transaction_id,
|
||||||
|
)
|
||||||
|
if tx_obj is None:
|
||||||
|
tx_cast = {
|
||||||
|
"transaction_id": transaction_id,
|
||||||
|
"recipient": transaction["creditorName"],
|
||||||
|
"sender": transaction["debtorName"],
|
||||||
|
"amount": transaction["amount"],
|
||||||
|
"currency": transaction["currency"],
|
||||||
|
"note": transaction["reference"],
|
||||||
|
}
|
||||||
|
tx_obj = self.instance.add_transaction(
|
||||||
|
account_id,
|
||||||
|
tx_cast,
|
||||||
|
)
|
||||||
|
# New transaction
|
||||||
|
await self.transaction(platforms, tx_obj)
|
||||||
|
else:
|
||||||
|
# Transaction exists
|
||||||
|
continue
|
||||||
|
# transaction_ids = [x["transaction_id"] for x in transactions]
|
||||||
|
# new_key_name = f"new.transactions.{self.instance.id}.{self.name}.{account_id}"
|
||||||
|
# old_key_name = f"transactions.{self.instance.id}.{self.name}.{account_id}"
|
||||||
|
# # for transaction_id in transaction_ids:
|
||||||
|
# if not transaction_ids:
|
||||||
|
# return
|
||||||
|
# await db.r.sadd(new_key_name, *transaction_ids)
|
||||||
|
|
||||||
difference = db.convert(difference)
|
# difference = list(await db.r.sdiff(new_key_name, old_key_name))
|
||||||
|
|
||||||
new_transactions = [
|
# difference = db.convert(difference)
|
||||||
x for x in transactions if x["transaction_id"] in difference
|
|
||||||
]
|
|
||||||
|
|
||||||
# Rename the new key to the old key so we can run the diff again
|
# new_transactions = [
|
||||||
await db.r.rename(new_key_name, old_key_name)
|
# x for x in transactions if x["transaction_id"] in difference
|
||||||
for transaction in new_transactions:
|
# ]
|
||||||
transaction["subclass"] = self.name
|
|
||||||
# self.tx.transaction(transaction)
|
|
||||||
|
|
||||||
def valid_transaction(self, data):
|
# # Rename the new key to the old key so we can run the diff again
|
||||||
|
# await db.r.rename(new_key_name, old_key_name)
|
||||||
|
# for transaction in new_transactions:
|
||||||
|
# transaction["subclass"] = self.name
|
||||||
|
# # self.tx.transaction(transaction)
|
||||||
|
|
||||||
|
def valid_transaction(self, tx_obj):
|
||||||
"""
|
"""
|
||||||
Determine if a given transaction object is valid.
|
Determine if a given transaction object is valid.
|
||||||
:param data: a transaction cast
|
:param data: a transaction cast
|
||||||
|
@ -83,56 +107,58 @@ class AggregatorClient(ABC):
|
||||||
:return: whether the transaction is valid
|
:return: whether the transaction is valid
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
txid = data["transaction_id"]
|
txid = tx_obj.transaction_id
|
||||||
if "amount" not in data:
|
if tx_obj.amount is None:
|
||||||
return False
|
return False
|
||||||
if "currency" not in data:
|
if tx_obj.currency is None:
|
||||||
return False
|
return False
|
||||||
amount = data["amount"]
|
amount = tx_obj.amount
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
log.info(f"Ignoring transaction with negative/zero amount: {txid}")
|
log.info(f"Ignoring transaction with negative/zero amount: {txid}")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def extract_reference(self, data):
|
# def extract_reference(self, data):
|
||||||
"""
|
# """
|
||||||
Extract a reference from the transaction cast.
|
# Extract a reference from the transaction cast.
|
||||||
:param data: a transaction cast
|
# :param data: a transaction cast
|
||||||
:type data: dict
|
# :type data: dict
|
||||||
:return: the extracted reference or not_set
|
# :return: the extracted reference or not_set
|
||||||
:rtype: str
|
# :rtype: str
|
||||||
"""
|
# """
|
||||||
if "reference" in data:
|
# if "reference" in data:
|
||||||
return data["reference"]
|
# return data["reference"]
|
||||||
elif "meta" in data:
|
# elif "meta" in data:
|
||||||
if "provider_reference" in data["meta"]:
|
# if "provider_reference" in data["meta"]:
|
||||||
return data["meta"]["provider_reference"]
|
# return data["meta"]["provider_reference"]
|
||||||
return "not_set"
|
# return "not_set"
|
||||||
|
|
||||||
def extract_sender(self, data):
|
# def extract_sender(self, data):
|
||||||
"""
|
# """
|
||||||
Extract a sender name from the transaction cast.
|
# Extract a sender name from the transaction cast.
|
||||||
:param data: a transaction cast
|
# :param data: a transaction cast
|
||||||
:type data: dict
|
# :type data: dict
|
||||||
:return: the sender name or not_set
|
# :return: the sender name or not_set
|
||||||
:rtype: str
|
# :rtype: str
|
||||||
"""
|
# """
|
||||||
if "debtorName" in data:
|
# if "debtorName" in data:
|
||||||
return data["debtorName"]
|
# return data["debtorName"]
|
||||||
elif "meta" in data:
|
# elif "meta" in data:
|
||||||
if "debtor_account_name" in data["meta"]:
|
# if "debtor_account_name" in data["meta"]:
|
||||||
return data["meta"]["debtor_account_name"]
|
# return data["meta"]["debtor_account_name"]
|
||||||
elif " " in data["reference"]:
|
# elif " " in data["reference"]:
|
||||||
refsplit = data["reference"].split(" ")
|
# refsplit = data["reference"].split(" ")
|
||||||
if not len(refsplit) == 2:
|
# if not len(refsplit) == 2:
|
||||||
log.error(f"Sender cannot be extracted: {data}")
|
# log.error(f"Sender cannot be extracted: {data}")
|
||||||
return "not_set"
|
# return "not_set"
|
||||||
realname, part2 = data["reference"].split(" ")
|
# realname, part2 = data["reference"].split(" ")
|
||||||
return realname
|
# return realname
|
||||||
|
|
||||||
return "not_set"
|
# return "not_set"
|
||||||
|
|
||||||
async def reference_partial_check(self, reference, txid, currency, amount):
|
async def reference_partial_check(
|
||||||
|
self, platform, reference, txid, currency, amount
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Perform a partial check by intersecting all parts of the split of the
|
Perform a partial check by intersecting all parts of the split of the
|
||||||
reference against the existing references, and returning a set of the matches.
|
reference against the existing references, and returning a set of the matches.
|
||||||
|
@ -146,9 +172,10 @@ class AggregatorClient(ABC):
|
||||||
# Split the reference into parts
|
# Split the reference into parts
|
||||||
ref_split = reference.split(" ")
|
ref_split = reference.split(" ")
|
||||||
# Get all existing references
|
# Get all existing references
|
||||||
existing_refs = await db.get_refs()
|
existing_refs = platform.references
|
||||||
# Get all parts of the given reference split that match the existing references
|
# Get all parts of the given reference split that match the existing references
|
||||||
stored_trade_reference = set(existing_refs).intersection(set(ref_split))
|
# stored_trade_reference = set(existing_refs).intersection(set(ref_split))
|
||||||
|
stored_trade_reference = [x for x in existing_refs if x in ref_split]
|
||||||
if len(stored_trade_reference) > 1:
|
if len(stored_trade_reference) > 1:
|
||||||
message = (
|
message = (
|
||||||
f"Multiple references valid for TXID {txid}: {reference}"
|
f"Multiple references valid for TXID {txid}: {reference}"
|
||||||
|
@ -162,38 +189,59 @@ class AggregatorClient(ABC):
|
||||||
return stored_trade_reference.pop()
|
return stored_trade_reference.pop()
|
||||||
|
|
||||||
# TODO: pass platform here
|
# TODO: pass platform here
|
||||||
# async def can_alt_lookup(self, amount, currency, reference):
|
async def can_alt_lookup(self, platform, amount, currency, reference):
|
||||||
# amount_usd = self.money.to_usd(amount, currency)
|
amount_usd = await money.to_usd(amount, currency)
|
||||||
# # Amount is reliable here as it is checked by find_trade,
|
# Amount is reliable here as it is checked by find_trade,
|
||||||
# # so no need for stored_trade["amount"]
|
# so no need for stored_trade["amount"]
|
||||||
# if float(amount_usd) > float(settings.Agora.AcceptableAltLookupUSD):
|
if amount_usd > platform.no_reference_amount_check_max_usd:
|
||||||
# message = (
|
message = (
|
||||||
# f"Amount exceeds max for {reference}"
|
f"Amount exceeds max for {reference}"
|
||||||
# f"Currency: {currency} | Amount: {amount}"
|
f"Currency: {currency} | Amount: {amount}"
|
||||||
# )
|
)
|
||||||
# title = "Amount exceeds max for {reference}"
|
title = "Amount exceeds max for {reference}"
|
||||||
# await notify.sendmsg(self.instance.user, message, title=title)
|
await notify.sendmsg(self.instance.user, message, title=title)
|
||||||
# return False
|
return False
|
||||||
# return True
|
return True
|
||||||
|
|
||||||
async def amount_currency_lookup(self, amount, currency, txid, reference):
|
def find_trade(self, platform, txid, currency, amount):
|
||||||
log.info(f"No reference in DB refs for {reference}")
|
"""
|
||||||
self.irc.sendmsg(f"No reference in DB refs for {reference}")
|
Get a trade reference that matches the given currency and amount.
|
||||||
# Try checking just amount and currency, as some people
|
Only works if there is one result.
|
||||||
# (usually people buying small amounts)
|
:param txid: Sink transaction ID
|
||||||
# are unable to put in a reference properly.
|
:param currency: currency
|
||||||
|
:param amount: amount
|
||||||
|
:type txid: string
|
||||||
|
:type currency: string
|
||||||
|
:type amount: int
|
||||||
|
:return: matching trade object or False
|
||||||
|
:rtype: dict or bool
|
||||||
|
"""
|
||||||
|
refs = platform.references
|
||||||
|
matching_refs = []
|
||||||
|
# TODO: use get_ref_map in this function instead of calling get_ref multiple
|
||||||
|
# times
|
||||||
|
for ref in refs:
|
||||||
|
stored_trade = platform.get_trade_by_reference(ref)
|
||||||
|
if stored_trade.currency == currency and stored_trade.amount_fiat == amount:
|
||||||
|
matching_refs.append(stored_trade)
|
||||||
|
if len(matching_refs) != 1:
|
||||||
|
log.error(
|
||||||
|
f"Find trade returned multiple results for TXID {txid}: {matching_refs}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return matching_refs[0]
|
||||||
|
|
||||||
log.info(f"Checking against amount and currency for TXID {txid}")
|
async def amount_currency_lookup(self, platform, amount, currency, txid, ref):
|
||||||
self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}")
|
|
||||||
title = f"Checking against amount and currency for TXID {txid}"
|
title = f"Checking against amount and currency for TXID {txid}"
|
||||||
message = (
|
message = (
|
||||||
f"Checking against amount and currency for TXID {txid}"
|
f"Checking against amount and currency for TXID {txid}"
|
||||||
f"Currency: {currency} | Amount: {amount}"
|
f"Currency: {currency} | Amount: {amount}"
|
||||||
)
|
)
|
||||||
await notify.sendmsg(self.instance.user, message, title=title)
|
await notify.sendmsg(self.instance.user, message, title=title)
|
||||||
if not await self.can_alt_lookup(amount, currency, reference):
|
|
||||||
|
if not await self.can_alt_lookup(platform, amount, currency, ref):
|
||||||
return False
|
return False
|
||||||
stored_trade = await self.find_trade(txid, currency, amount)
|
stored_trade = self.find_trade(platform, txid, currency, amount)
|
||||||
if not stored_trade:
|
if not stored_trade:
|
||||||
title = f"Failed to get reference by amount and currency: {txid}"
|
title = f"Failed to get reference by amount and currency: {txid}"
|
||||||
message = (
|
message = (
|
||||||
|
@ -202,11 +250,12 @@ class AggregatorClient(ABC):
|
||||||
)
|
)
|
||||||
await notify.sendmsg(self.instance.user, message, title=title)
|
await notify.sendmsg(self.instance.user, message, title=title)
|
||||||
return None
|
return None
|
||||||
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
|
||||||
return stored_trade
|
return stored_trade
|
||||||
|
|
||||||
async def normal_lookup(self, stored_trade_reference, reference, currency, amount):
|
async def normal_lookup(
|
||||||
stored_trade = await db.get_ref(stored_trade_reference)
|
self, platform, stored_trade_reference, reference, currency, amount
|
||||||
|
):
|
||||||
|
stored_trade = platform.get_trade_by_reference(stored_trade_reference)
|
||||||
if not stored_trade:
|
if not stored_trade:
|
||||||
title = f"No reference in DB for {reference}"
|
title = f"No reference in DB for {reference}"
|
||||||
message = (
|
message = (
|
||||||
|
@ -215,27 +264,25 @@ class AggregatorClient(ABC):
|
||||||
)
|
)
|
||||||
await notify.sendmsg(self.instance.user, message, title=title)
|
await notify.sendmsg(self.instance.user, message, title=title)
|
||||||
return False
|
return False
|
||||||
stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
# stored_trade["amount"] = float(stored_trade["amount"]) # convert to float
|
||||||
return stored_trade
|
return stored_trade
|
||||||
|
|
||||||
async def currency_check(self, currency, amount, reference, stored_trade):
|
async def currency_check(self, currency, stored_trade):
|
||||||
if not stored_trade["currency"] == currency:
|
if not stored_trade.currency == currency:
|
||||||
title = "Currency mismatch"
|
title = "Currency mismatch"
|
||||||
message = (
|
message = (
|
||||||
f"Currency mismatch, Agora: {stored_trade['currency']} "
|
f"Currency mismatch, Agora: {stored_trade.currency} "
|
||||||
f"/ Sink: {currency}"
|
f"/ Sink: {currency}"
|
||||||
)
|
)
|
||||||
await notify.sendmsg(self.instance.user, message, title=title)
|
await notify.sendmsg(self.instance.user, message, title=title)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def alt_amount_check(
|
async def alt_amount_check(self, platform, amount, currency, stored_trade):
|
||||||
self, platform, amount, currency, reference, stored_trade
|
|
||||||
):
|
|
||||||
# If the amount does not match exactly, get the min and max values for our
|
# If the amount does not match exactly, get the min and max values for our
|
||||||
# given acceptable margins for trades
|
# given acceptable margins for trades
|
||||||
min_amount, max_amount = await self.money.get_acceptable_margins(
|
min_amount, max_amount = await money.get_acceptable_margins(
|
||||||
platform, currency, stored_trade["amount"]
|
platform, currency, stored_trade.amount_fiat
|
||||||
)
|
)
|
||||||
log.info(
|
log.info(
|
||||||
(
|
(
|
||||||
|
@ -243,7 +290,6 @@ class AggregatorClient(ABC):
|
||||||
f" / max: {max_amount}"
|
f" / max: {max_amount}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.irc.sendmsg()
|
|
||||||
title = "Amount does not match exactly"
|
title = "Amount does not match exactly"
|
||||||
message = (
|
message = (
|
||||||
f"Amount does not match exactly, trying with margins: min: "
|
f"Amount does not match exactly, trying with margins: min: "
|
||||||
|
@ -253,14 +299,14 @@ class AggregatorClient(ABC):
|
||||||
if not min_amount < amount < max_amount:
|
if not min_amount < amount < max_amount:
|
||||||
title = "Amount mismatch - not in margins"
|
title = "Amount mismatch - not in margins"
|
||||||
message = (
|
message = (
|
||||||
f"Amount mismatch - not in margins: {stored_trade['amount']} "
|
f"Amount mismatch - not in margins: {stored_trade.amount_fiat} "
|
||||||
f"(min: {min_amount} / max: {max_amount}"
|
f"(min: {min_amount} / max: {max_amount}"
|
||||||
)
|
)
|
||||||
await notify.sendmsg(self.instance.user, message, title=title)
|
await notify.sendmsg(self.instance.user, message, title=title)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def transaction(self, data):
|
async def transaction(self, platforms, tx_obj):
|
||||||
"""
|
"""
|
||||||
Store details of transaction and post notifications to IRC.
|
Store details of transaction and post notifications to IRC.
|
||||||
Matches it up with data stored in Redis to attempt to reconcile with an Agora
|
Matches it up with data stored in Redis to attempt to reconcile with an Agora
|
||||||
|
@ -268,50 +314,38 @@ class AggregatorClient(ABC):
|
||||||
:param data: details of transaction
|
:param data: details of transaction
|
||||||
:type data: dict
|
:type data: dict
|
||||||
"""
|
"""
|
||||||
valid = self.valid_transaction(data)
|
valid = self.valid_transaction(tx_obj)
|
||||||
if not valid:
|
if not valid:
|
||||||
return False
|
return False
|
||||||
ts = data["timestamp"]
|
txid = tx_obj.transaction_id
|
||||||
txid = data["transaction_id"]
|
amount = tx_obj.amount
|
||||||
amount = float(data["amount"])
|
currency = tx_obj.currency
|
||||||
currency = data["currency"]
|
|
||||||
|
|
||||||
reference = self.extract_reference(data)
|
reference = tx_obj.note
|
||||||
sender = self.extract_sender(data)
|
|
||||||
|
|
||||||
subclass = data["subclass"]
|
# reference = self.extract_reference(data)
|
||||||
to_store = {
|
# sender = tx_obj.sender
|
||||||
"subclass": subclass,
|
|
||||||
"ts": ts,
|
|
||||||
"txid": txid,
|
|
||||||
"reference": reference,
|
|
||||||
"amount": amount,
|
|
||||||
"currency": currency,
|
|
||||||
"sender": sender,
|
|
||||||
}
|
|
||||||
db.r.hmset(f"tx.{txid}", to_store)
|
|
||||||
|
|
||||||
log.info(f"Transaction processed: {orjson.dumps(to_store, indent=2)}")
|
log.info(f"Transaction processed: {tx_obj}")
|
||||||
self.irc.sendmsg(
|
await notify.sendmsg(
|
||||||
(
|
self.instance.user,
|
||||||
f"AUTO Incoming transaction on {subclass}: {txid} {amount}{currency} "
|
(f"Transaction: {txid} {amount}{currency}: {reference}"),
|
||||||
f"({reference})"
|
title="Incoming transaction",
|
||||||
)
|
)
|
||||||
)
|
for platform in platforms:
|
||||||
|
stored_trade_reference = await self.reference_partial_check(
|
||||||
stored_trade_reference = self.reference_partial_check(
|
platform, reference, txid, currency, amount
|
||||||
reference, txid, currency, amount
|
|
||||||
)
|
)
|
||||||
if stored_trade_reference is False: # can be None though
|
if stored_trade_reference is False: # can be None though
|
||||||
return
|
continue
|
||||||
|
|
||||||
stored_trade = False
|
stored_trade = False
|
||||||
looked_up_without_reference = False
|
looked_up_without_reference = False
|
||||||
|
|
||||||
# Normal implementation for when we have a reference
|
# Normal implementation for when we have a reference
|
||||||
if stored_trade_reference:
|
if stored_trade_reference:
|
||||||
stored_trade = self.normal_lookup(
|
stored_trade = await self.normal_lookup(
|
||||||
stored_trade_reference, reference, currency, amount
|
platform, stored_trade_reference, reference, currency, amount
|
||||||
)
|
)
|
||||||
# if not stored_trade:
|
# if not stored_trade:
|
||||||
# return
|
# return
|
||||||
|
@ -319,52 +353,53 @@ class AggregatorClient(ABC):
|
||||||
# Amount/currency lookup implementation for when we have no reference
|
# Amount/currency lookup implementation for when we have no reference
|
||||||
else:
|
else:
|
||||||
if not stored_trade: # check we don't overwrite the lookup above
|
if not stored_trade: # check we don't overwrite the lookup above
|
||||||
stored_trade = self.amount_currency_lookup(
|
stored_trade = await self.amount_currency_lookup(
|
||||||
amount, currency, txid, reference
|
platform, amount, currency, txid, reference
|
||||||
)
|
)
|
||||||
if stored_trade is False:
|
if stored_trade is False:
|
||||||
return
|
continue
|
||||||
if stored_trade:
|
if stored_trade:
|
||||||
# Note that we have looked it up without reference so we don't use
|
# Note that we have looked it up without reference so we don't
|
||||||
# +- below
|
# use +- below
|
||||||
# This might be redundant given the amount checks in find_trade,
|
# This might be redundant given the checks in find_trade,
|
||||||
# but better safe than sorry!
|
# but better safe than sorry!
|
||||||
looked_up_without_reference = True
|
looked_up_without_reference = True
|
||||||
else:
|
else:
|
||||||
return
|
continue
|
||||||
else:
|
else:
|
||||||
# Stored trade reference is none, the checks below will do nothing
|
# Stored trade reference is none, the checks below will do nothing
|
||||||
return
|
continue
|
||||||
|
|
||||||
# Make sure it was sent in the expected currency
|
# Make sure it was sent in the expected currency
|
||||||
if not self.currency_check(currency, amount, reference, stored_trade):
|
if not await self.currency_check(currency, stored_trade):
|
||||||
return
|
continue
|
||||||
|
|
||||||
# Make sure the expected amount was sent
|
# Make sure the expected amount was sent
|
||||||
if not stored_trade["amount"] == amount:
|
if not stored_trade.amount_fiat == amount:
|
||||||
if looked_up_without_reference:
|
if looked_up_without_reference:
|
||||||
return
|
continue
|
||||||
platform = stored_trade["subclass"]
|
if not await self.alt_amount_check(
|
||||||
if not self.alt_amount_check(
|
platform, amount, currency, stored_trade
|
||||||
platform, amount, currency, reference, stored_trade
|
|
||||||
):
|
):
|
||||||
return
|
continue
|
||||||
platform = stored_trade["subclass"]
|
|
||||||
# platform_buyer = stored_trade["buyer"]
|
# platform_buyer = stored_trade["buyer"]
|
||||||
|
|
||||||
# Check sender - we don't do anything with this yet
|
# Check sender - we don't do anything with this yet
|
||||||
# sender_valid = antifraud.check_valid_sender(
|
# sender_valid = antifraud.check_valid_sender(
|
||||||
# reference, platform, sender, platform_buyer
|
# reference, platform, sender, platform_buyer
|
||||||
# )
|
# )
|
||||||
# log.info(f"Trade {reference} buyer {platform_buyer} valid: {sender_valid}")
|
# log.info(f"Trade {reference} buyer {platform_buyer}
|
||||||
# trade_released = self.release_map_trade(reference, txid)
|
# valid: {sender_valid}")
|
||||||
|
instance = await AgoraClient(platform)
|
||||||
|
rtrn = await instance.release_map_trade(stored_trade, tx_obj)
|
||||||
# if trade_released:
|
# if trade_released:
|
||||||
# self.ux.notify.notify_complete_trade(amount, currency)
|
# self.ux.notify.notify_complete_trade(amount, currency)
|
||||||
# else:
|
# else:
|
||||||
# log.error(f"Cannot release trade {reference}.")
|
# log.error(f"Cannot release trade {reference}.")
|
||||||
# return
|
# return
|
||||||
|
|
||||||
rtrn = await self.release_funds(stored_trade["id"], stored_trade["reference"])
|
# rtrn = await platform.release_funds(stored_trade["id"],
|
||||||
|
# stored_trade["reference"])
|
||||||
if rtrn:
|
if rtrn:
|
||||||
title = "Trade complete"
|
title = "Trade complete"
|
||||||
message = f"Trade complete: {amount}{currency}"
|
message = f"Trade complete: {amount}{currency}"
|
||||||
|
|
|
@ -306,7 +306,7 @@ class NordigenClient(BaseClient, AggregatorClient):
|
||||||
self.normalise_transactions(parsed, state="booked")
|
self.normalise_transactions(parsed, state="booked")
|
||||||
|
|
||||||
if process:
|
if process:
|
||||||
await self.process_transactions(parsed)
|
await self.process_transactions(account_id, parsed)
|
||||||
if pending:
|
if pending:
|
||||||
parsed_pending = response["pending"]
|
parsed_pending = response["pending"]
|
||||||
self.normalise_transactions(parsed_pending, state="pending")
|
self.normalise_transactions(parsed_pending, state="pending")
|
||||||
|
|
|
@ -778,72 +778,68 @@ class LocalPlatformClient(ABC):
|
||||||
|
|
||||||
return all(actioned)
|
return all(actioned)
|
||||||
|
|
||||||
async def release_funds(self, trade_id, reference):
|
async def release_trade_escrow(self, trade_id, reference):
|
||||||
# stored_trade = await db.get_ref(reference)
|
# stored_trade = await db.get_ref(reference)
|
||||||
logmessage = f"All checks passed, releasing funds for {trade_id} {reference}"
|
logmessage = f"All checks passed, releasing funds for {trade_id} {reference}"
|
||||||
log.info(logmessage)
|
log.info(logmessage)
|
||||||
title = "Releasing escrow"
|
title = "Releasing escrow"
|
||||||
await notify.sendmsg(self.instance.user, logmessage, title=title)
|
await notify.sendmsg(self.instance.user, logmessage, title=title)
|
||||||
release = self.release_funds
|
|
||||||
post_message = self.api.contact_message_post
|
|
||||||
|
|
||||||
rtrn = await release(trade_id)
|
# THIS IS NOT A COMMENT
|
||||||
if rtrn["message"] == "OK":
|
# THIS IS FOR SECURITY
|
||||||
post_message(trade_id, "Thanks! Releasing now :)")
|
# WHEN IT HAS BEEN CONFIRMED TO WORK
|
||||||
return True
|
# THIS CAN BE UNCOMMENTED
|
||||||
else:
|
# rtrn = await self.release_funds(trade_id)
|
||||||
logmessage = f"Release funds unsuccessful: {rtrn['message']}"
|
# if rtrn["message"] == "OK":
|
||||||
title = "Release unsuccessful"
|
# await self.api.contact_message_post(trade_id, "Thanks! Releasing now :)")
|
||||||
log.error(logmessage)
|
# return True
|
||||||
await notify.sendmsg(self.instance.user, logmessage, title=title)
|
# else:
|
||||||
return
|
# logmessage = f"Release funds unsuccessful: {rtrn['message']}"
|
||||||
|
# title = "Release unsuccessful"
|
||||||
|
# log.error(logmessage)
|
||||||
|
# await notify.sendmsg(self.instance.user, logmessage, title=title)
|
||||||
|
# return
|
||||||
|
# UNCOMMENT TO HERE
|
||||||
|
|
||||||
# # Parse the escrow release response
|
async def update_trade_tx(self, stored_trade, tx_obj):
|
||||||
# message = rtrn["message"]
|
|
||||||
# # message_long = rtrn["response"]["data"]["message"]
|
|
||||||
# self.irc.sendmsg(f"{dumps(message)}")
|
|
||||||
|
|
||||||
async def update_trade_tx(self, reference, txid):
|
|
||||||
"""
|
"""
|
||||||
Update a trade to point to a given transaction ID.
|
Update a trade to point to a given transaction ID.
|
||||||
Return False if the trade already has a mapped transaction.
|
Return False if the transaction already has a mapped trade.
|
||||||
"""
|
"""
|
||||||
existing_tx = await db.r.hget(f"trade.{reference}", "tx")
|
|
||||||
if existing_tx is None:
|
if tx_obj.reconciled:
|
||||||
return None
|
|
||||||
elif existing_tx == b"":
|
|
||||||
await db.r.hset(f"trade.{reference}", "tx", txid)
|
|
||||||
return True
|
|
||||||
else: # Already a mapped transaction
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def release_map_trade(self, reference, tx):
|
if tx_obj in stored_trade.linked.all():
|
||||||
|
return False
|
||||||
|
|
||||||
|
stored_trade.linked.add(tx_obj)
|
||||||
|
stored_trade.save()
|
||||||
|
|
||||||
|
tx_obj.reconciled = True
|
||||||
|
tx_obj.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def release_map_trade(self, stored_trade, tx_obj):
|
||||||
"""
|
"""
|
||||||
Map a trade to a transaction and release if no other TX is
|
Map a trade to a transaction and release if no other TX is
|
||||||
mapped to the same trade.
|
mapped to the same trade.
|
||||||
"""
|
"""
|
||||||
stored_trade = await db.get_ref(reference)
|
platform_buyer = stored_trade.buyer
|
||||||
if not stored_trade:
|
bank_sender = tx_obj.sender
|
||||||
log.error(f"Could not get stored trade for {reference}.")
|
trade_id = stored_trade.contact_id
|
||||||
return None
|
is_updated = await self.update_trade_tx(stored_trade, tx_obj)
|
||||||
tx_obj = await db.get_tx(tx)
|
if is_updated is True:
|
||||||
if not tx_obj:
|
|
||||||
log.error(f"Could not get TX for {tx}.")
|
|
||||||
return None
|
|
||||||
platform_buyer = stored_trade["buyer"]
|
|
||||||
bank_sender = tx_obj["sender"]
|
|
||||||
trade_id = stored_trade["id"]
|
|
||||||
is_updated = await self.update_trade_tx(reference, tx)
|
|
||||||
if is_updated is None:
|
|
||||||
return None
|
|
||||||
elif is_updated is True:
|
|
||||||
# We mapped the trade successfully
|
# We mapped the trade successfully
|
||||||
self.release_funds(trade_id, reference)
|
await self.release_trade_escrow(trade_id, stored_trade.reference)
|
||||||
antifraud.add_bank_sender(platform_buyer, bank_sender)
|
await antifraud.add_bank_sender(platform_buyer, bank_sender)
|
||||||
return True
|
return True
|
||||||
elif is_updated is False:
|
else:
|
||||||
# Already mapped
|
# Already mapped
|
||||||
log.error(f"Trade {reference} already has a TX mapped, cannot map {tx}.")
|
log.error(
|
||||||
|
f"Trade {stored_trade} already has a TX mapped, cannot map {tx_obj}."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def new_trade(
|
async def new_trade(
|
||||||
|
|
|
@ -3,6 +3,9 @@ from pyotp import TOTP
|
||||||
|
|
||||||
from core.clients.base import BaseClient
|
from core.clients.base import BaseClient
|
||||||
from core.clients.platform import LocalPlatformClient
|
from core.clients.platform import LocalPlatformClient
|
||||||
|
from core.util import logs
|
||||||
|
|
||||||
|
log = logs.get_logger("agora")
|
||||||
|
|
||||||
|
|
||||||
class AgoraClient(LocalPlatformClient, BaseClient):
|
class AgoraClient(LocalPlatformClient, BaseClient):
|
||||||
|
@ -20,15 +23,12 @@ class AgoraClient(LocalPlatformClient, BaseClient):
|
||||||
"""
|
"""
|
||||||
print("CALLING RELEASE FUNDS", contact_id)
|
print("CALLING RELEASE FUNDS", contact_id)
|
||||||
if self.instance.dummy:
|
if self.instance.dummy:
|
||||||
self.log.error(
|
log.error(f"Running in dummy mode, not releasing funds for {contact_id}")
|
||||||
f"Running in dummy mode, not releasing funds for {contact_id}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
payload = {"tradeId": contact_id, "password": self.sets.Pass}
|
|
||||||
rtrn = await self.api._api_call(
|
rtrn = await self.api.contact_release(
|
||||||
api_method=f"contact_release/{contact_id}",
|
contact_id,
|
||||||
http_method="POST",
|
self.instance.password,
|
||||||
query_values=payload,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if we can withdraw funds
|
# Check if we can withdraw funds
|
||||||
|
|
|
@ -88,7 +88,6 @@ class AgoraDesk:
|
||||||
async with session.post(api_call_url, **cast) as response_raw:
|
async with session.post(api_call_url, **cast) as response_raw:
|
||||||
response = await response_raw.json()
|
response = await response_raw.json()
|
||||||
status_code = response_raw.status
|
status_code = response_raw.status
|
||||||
|
|
||||||
else:
|
else:
|
||||||
cast["params"] = query_values
|
cast["params"] = query_values
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
|
@ -226,11 +225,23 @@ class AgoraDesk:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Todo:
|
# Todo:
|
||||||
# post/trade/contact_release/{trade_id} • Release trade escrow
|
|
||||||
# post/contact_fund/{trade_id} • Fund a trade
|
# post/contact_fund/{trade_id} • Fund a trade
|
||||||
# post/contact_dispute/{trade_id} • Start a trade dispute
|
# post/contact_dispute/{trade_id} • Start a trade dispute
|
||||||
|
|
||||||
# post/contact_mark_as_paid/{trade_id} • Mark a trade as paid
|
# post/contact_mark_as_paid/{trade_id} • Mark a trade as paid
|
||||||
|
|
||||||
|
async def contact_release(self, trade_id: str, password: str) -> Dict[str, Any]:
|
||||||
|
"""See Agoradesk API documentation.
|
||||||
|
|
||||||
|
https://agoradesk.com/api-docs/v1#operation/releaseEscrow
|
||||||
|
"""
|
||||||
|
payload = {"tradeId": trade_id, "password": password}
|
||||||
|
return await self._api_call(
|
||||||
|
api_method=f"contact_release/{trade_id}",
|
||||||
|
http_method="POST",
|
||||||
|
query_values=payload,
|
||||||
|
)
|
||||||
|
|
||||||
async def contact_mark_as_paid(self, trade_id: str) -> Dict[str, Any]:
|
async def contact_mark_as_paid(self, trade_id: str) -> Dict[str, Any]:
|
||||||
"""See Agoradesk API.
|
"""See Agoradesk API.
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
# Project imports
|
# Project imports
|
||||||
from core.lib import db # , notify
|
# from core.lib import db # , notify
|
||||||
from core.util import logs
|
from core.util import logs
|
||||||
|
|
||||||
log = logs.get_logger("antifraud")
|
log = logs.get_logger("antifraud")
|
||||||
|
|
||||||
|
|
||||||
class AntiFraud(object):
|
class AntiFraud(object):
|
||||||
async def add_bank_sender(self, platform, platform_buyer, bank_sender):
|
async def add_bank_sender(self, platform_buyer, bank_sender):
|
||||||
"""
|
"""
|
||||||
Add the bank senders into Redis.
|
Add the bank senders into Redis.
|
||||||
:param platform: name of the platform - freeform
|
:param platform: name of the platform - freeform
|
||||||
:param platform_buyer: the username of the buyer on the platform
|
:param platform_buyer: the username of the buyer on the platform
|
||||||
:param bank_sender: the sender name from the bank
|
:param bank_sender: the sender name from the bank
|
||||||
"""
|
"""
|
||||||
key = f"namemap.{platform}.{platform_buyer}"
|
# key = f"namemap.{platform}.{platform_buyer}"
|
||||||
await db.r.sadd(key, bank_sender)
|
# await db.r.sadd(key, bank_sender)
|
||||||
|
# TODO
|
||||||
|
|
||||||
async def get_previous_senders(self, platform, platform_buyer):
|
async def get_previous_senders(self, platform, platform_buyer):
|
||||||
"""
|
"""
|
||||||
|
@ -24,12 +25,13 @@ class AntiFraud(object):
|
||||||
:return: set of previous buyers
|
:return: set of previous buyers
|
||||||
:rtype: set
|
:rtype: set
|
||||||
"""
|
"""
|
||||||
key = f"namemap.{platform}.{platform_buyer}"
|
# key = f"namemap.{platform}.{platform_buyer}"
|
||||||
senders = await db.r.smembers(key)
|
# senders = await db.r.smembers(key)
|
||||||
if not senders:
|
# if not senders:
|
||||||
return None
|
# return None
|
||||||
senders = db.convert(senders)
|
# senders = db.convert(senders)
|
||||||
return senders
|
# return senders
|
||||||
|
# TODO
|
||||||
|
|
||||||
async def check_valid_sender(
|
async def check_valid_sender(
|
||||||
self, reference, platform, bank_sender, platform_buyer
|
self, reference, platform, bank_sender, platform_buyer
|
||||||
|
@ -45,22 +47,23 @@ class AntiFraud(object):
|
||||||
:return: whether the sender is valid
|
:return: whether the sender is valid
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
senders = await self.get_previous_senders(platform, platform_buyer)
|
# senders = await self.get_previous_senders(platform, platform_buyer)
|
||||||
if senders is None: # no senders yet, assume it's valid
|
# if senders is None: # no senders yet, assume it's valid
|
||||||
return True
|
# return True
|
||||||
if platform_buyer in senders:
|
# if platform_buyer in senders:
|
||||||
return True
|
# return True
|
||||||
self.ux.notify.notify_sender_name_mismatch(
|
# self.ux.notify.notify_sender_name_mismatch(
|
||||||
reference, platform_buyer, bank_sender
|
# reference, platform_buyer, bank_sender
|
||||||
)
|
|
||||||
# title = "Sender name mismatch"
|
|
||||||
# message = (
|
|
||||||
# f"Sender name mismatch for {reference}:\n"
|
|
||||||
# f"Platform buyer: {platform_buyer}"
|
|
||||||
# f"Bank sender: {bank_sender}"
|
|
||||||
# )
|
# )
|
||||||
# await notify.sendmsg(self.instance.) # TODO
|
# # title = "Sender name mismatch"
|
||||||
return False
|
# # message = (
|
||||||
|
# # f"Sender name mismatch for {reference}:\n"
|
||||||
|
# # f"Platform buyer: {platform_buyer}"
|
||||||
|
# # f"Bank sender: {bank_sender}"
|
||||||
|
# # )
|
||||||
|
# # await notify.sendmsg(self.instance.) # TODO
|
||||||
|
# return False
|
||||||
|
# TODO
|
||||||
|
|
||||||
async def check_tx_sender(self, tx, reference):
|
async def check_tx_sender(self, tx, reference):
|
||||||
"""
|
"""
|
||||||
|
@ -69,21 +72,22 @@ class AntiFraud(object):
|
||||||
:param tx: the transaction ID
|
:param tx: the transaction ID
|
||||||
:param reference: the trade reference
|
:param reference: the trade reference
|
||||||
"""
|
"""
|
||||||
stored_trade = await db.get_ref(reference)
|
# stored_trade = await db.get_ref(reference)
|
||||||
if not stored_trade:
|
# if not stored_trade:
|
||||||
return None
|
# return None
|
||||||
stored_tx = await db.get_tx(tx)
|
# stored_tx = await db.get_tx(tx)
|
||||||
if not stored_tx:
|
# if not stored_tx:
|
||||||
return None
|
# return None
|
||||||
bank_sender = stored_tx["sender"]
|
# bank_sender = stored_tx["sender"]
|
||||||
platform_buyer = stored_trade["buyer"]
|
# platform_buyer = stored_trade["buyer"]
|
||||||
platform = stored_trade["subclass"]
|
# platform = stored_trade["subclass"]
|
||||||
is_allowed = await self.check_valid_sender(
|
# is_allowed = await self.check_valid_sender(
|
||||||
reference, platform, bank_sender, platform_buyer
|
# reference, platform, bank_sender, platform_buyer
|
||||||
)
|
# )
|
||||||
if is_allowed is True:
|
# if is_allowed is True:
|
||||||
return True
|
# return True
|
||||||
return False
|
# return False
|
||||||
|
# TODO
|
||||||
|
|
||||||
# def user_verification_successful(self, uid):
|
# def user_verification_successful(self, uid):
|
||||||
# """
|
# """
|
||||||
|
|
|
@ -111,26 +111,25 @@ class Money(object):
|
||||||
return rates
|
return rates
|
||||||
|
|
||||||
# TODO: pass platform
|
# TODO: pass platform
|
||||||
# async def get_acceptable_margins(self, platform, currency, amount):
|
async def get_acceptable_margins(self, platform, currency, amount):
|
||||||
# """
|
"""
|
||||||
# Get the minimum and maximum amounts we would accept a trade for.
|
Get the minimum and maximum amounts we would accept a trade for.
|
||||||
# :param currency: currency code
|
:param currency: currency code
|
||||||
# :param amount: amount
|
:param amount: amount
|
||||||
# :return: (min, max)
|
:return: (min, max)
|
||||||
# :rtype: tuple
|
:rtype: tuple
|
||||||
# """
|
"""
|
||||||
# sets = util.get_settings(platform)
|
rates = await self.get_rates_all()
|
||||||
# rates = await self.get_rates_all()
|
if currency == "USD":
|
||||||
# if currency == "USD":
|
min_amount = amount - platform.accept_within_usd
|
||||||
# min_amount = amount - float(sets.AcceptableUSDMargin)
|
max_amount = amount + platform.accept_within_usd
|
||||||
# max_amount = amount + float(sets.AcceptableUSDMargin)
|
return (min_amount, max_amount)
|
||||||
# return (min_amount, max_amount)
|
amount_usd = amount / rates[currency]
|
||||||
# amount_usd = amount / rates[currency]
|
min_usd = amount_usd - platform.accept_within_usd
|
||||||
# min_usd = amount_usd - float(sets.AcceptableUSDMargin)
|
max_usd = amount_usd + platform.accept_within_usd
|
||||||
# max_usd = amount_usd + float(sets.AcceptableUSDMargin)
|
min_local = min_usd * rates[currency]
|
||||||
# min_local = min_usd * rates[currency]
|
max_local = max_usd * rates[currency]
|
||||||
# max_local = max_usd * rates[currency]
|
return (min_local, max_local)
|
||||||
# return (min_local, max_local)
|
|
||||||
|
|
||||||
async def get_minmax(self, min_usd, max_usd, asset, currency):
|
async def get_minmax(self, min_usd, max_usd, asset, currency):
|
||||||
rates = await self.get_rates_all()
|
rates = await self.get_rates_all()
|
||||||
|
|
|
@ -16,11 +16,20 @@ INTERVALS_PLATFORM = [x[0] for x in INTERVAL_CHOICES]
|
||||||
|
|
||||||
|
|
||||||
async def aggregator_job():
|
async def aggregator_job():
|
||||||
aggregators = Aggregator.objects.filter(enabled=True, fetch_accounts=True)
|
aggregators = Aggregator.objects.filter(enabled=True)
|
||||||
for aggregator in aggregators:
|
for aggregator in aggregators:
|
||||||
if aggregator.service == "nordigen":
|
if aggregator.service == "nordigen":
|
||||||
instance = await NordigenClient(aggregator)
|
instance = await NordigenClient(aggregator)
|
||||||
|
if aggregator.fetch_accounts is True:
|
||||||
await instance.get_all_account_info(store=True)
|
await instance.get_all_account_info(store=True)
|
||||||
|
|
||||||
|
fetch_tasks = []
|
||||||
|
for bank, accounts in aggregator.account_info.items():
|
||||||
|
for account in accounts:
|
||||||
|
account_id = account["account_id"]
|
||||||
|
task = instance.get_transactions(account_id, process=True)
|
||||||
|
fetch_tasks.append(task)
|
||||||
|
await asyncio.gather(*fetch_tasks)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"No such client library: {aggregator.service}")
|
raise NotImplementedError(f"No such client library: {aggregator.service}")
|
||||||
aggregator.fetch_accounts = False
|
aggregator.fetch_accounts = False
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-12 19:21
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0021_alter_trade_ad_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='transaction_id',
|
||||||
|
field=models.CharField(default='NONE', max_length=255),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='ts_added',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.7 on 2023-03-13 09:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0022_transaction_transaction_id_transaction_ts_added'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='trade',
|
||||||
|
name='linked',
|
||||||
|
field=models.ManyToManyField(blank=True, to='core.transaction'),
|
||||||
|
),
|
||||||
|
]
|
101
core/models.py
101
core/models.py
|
@ -80,7 +80,40 @@ class Aggregator(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_for_platform(cls, platform):
|
def get_for_platform(cls, platform):
|
||||||
return cls.objects.filter(user=platform.user, enabled=True)
|
aggregators = []
|
||||||
|
ads = Ad.objects.filter(
|
||||||
|
platforms=platform,
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
print("ADS", ads)
|
||||||
|
for ad in ads:
|
||||||
|
for aggregator in ad.aggregators.all():
|
||||||
|
if aggregator not in aggregators:
|
||||||
|
aggregators.append(aggregator)
|
||||||
|
|
||||||
|
print("RET", aggregators)
|
||||||
|
return aggregators
|
||||||
|
|
||||||
|
@property
|
||||||
|
def platforms(self):
|
||||||
|
"""
|
||||||
|
Get platforms for this aggregator.
|
||||||
|
Do this by looking up Ads with the aggregator.
|
||||||
|
Then, join them all together.
|
||||||
|
"""
|
||||||
|
platforms = []
|
||||||
|
ads = Ad.objects.filter(
|
||||||
|
aggregators=self,
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
print("ADS", ads)
|
||||||
|
for ad in ads:
|
||||||
|
for platform in ad.platforms.all():
|
||||||
|
if platform not in platforms:
|
||||||
|
platforms.append(platform)
|
||||||
|
|
||||||
|
print("RET", platforms)
|
||||||
|
return platforms
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_currencies_for_platform(cls, platform):
|
def get_currencies_for_platform(cls, platform):
|
||||||
|
@ -104,6 +137,23 @@ class Aggregator(models.Model):
|
||||||
account_info[bank].append(account)
|
account_info[bank].append(account)
|
||||||
return account_info
|
return account_info
|
||||||
|
|
||||||
|
def add_transaction(self, account_id, tx_data):
|
||||||
|
return Transaction.objects.create(
|
||||||
|
aggregator=self,
|
||||||
|
account_id=account_id,
|
||||||
|
reconciled=False,
|
||||||
|
**tx_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_transaction(self, account_id, tx_id):
|
||||||
|
transaction = Transaction.objects.filter(
|
||||||
|
account_id=account_id,
|
||||||
|
transaction_id=tx_id,
|
||||||
|
).first()
|
||||||
|
if not transaction:
|
||||||
|
return None
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
|
||||||
class Platform(models.Model):
|
class Platform(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -208,6 +258,13 @@ class Platform(models.Model):
|
||||||
|
|
||||||
return references
|
return references
|
||||||
|
|
||||||
|
def get_trade_by_reference(self, reference):
|
||||||
|
return Trade.objects.filter(
|
||||||
|
platform=self,
|
||||||
|
open=True,
|
||||||
|
reference=reference,
|
||||||
|
).first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trades(self):
|
def trades(self):
|
||||||
"""
|
"""
|
||||||
|
@ -249,6 +306,43 @@ class Platform(models.Model):
|
||||||
log.info(msg)
|
log.info(msg)
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_aggregator(cls, aggregator):
|
||||||
|
platforms = []
|
||||||
|
ads = Ad.objects.filter(
|
||||||
|
aggregators=aggregator,
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
print("ADS", ads)
|
||||||
|
for ad in ads:
|
||||||
|
for platform in ad.platforms.all():
|
||||||
|
if platform not in platforms:
|
||||||
|
platforms.append(platform)
|
||||||
|
|
||||||
|
print("RET", platforms)
|
||||||
|
return platforms
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aggregators(self):
|
||||||
|
"""
|
||||||
|
Get aggregators for this platform.
|
||||||
|
Do this by looking up Ads with the platform.
|
||||||
|
Then, join them all together.
|
||||||
|
"""
|
||||||
|
aggregators = []
|
||||||
|
ads = Ad.objects.filter(
|
||||||
|
platforms=self,
|
||||||
|
enabled=True,
|
||||||
|
)
|
||||||
|
print("ADS", ads)
|
||||||
|
for ad in ads:
|
||||||
|
for aggregator in ad.aggregators.all():
|
||||||
|
if aggregator not in aggregators:
|
||||||
|
aggregators.append(aggregator)
|
||||||
|
|
||||||
|
print("RET", aggregators)
|
||||||
|
return aggregators
|
||||||
|
|
||||||
|
|
||||||
class Asset(models.Model):
|
class Asset(models.Model):
|
||||||
code = models.CharField(max_length=64)
|
code = models.CharField(max_length=64)
|
||||||
|
@ -320,8 +414,9 @@ class Transaction(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
|
aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE)
|
||||||
account_id = models.CharField(max_length=255)
|
account_id = models.CharField(max_length=255)
|
||||||
|
transaction_id = models.CharField(max_length=255)
|
||||||
|
|
||||||
ts_added = ...
|
ts_added = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
recipient = models.CharField(max_length=255, null=True, blank=True)
|
recipient = models.CharField(max_length=255, null=True, blank=True)
|
||||||
sender = models.CharField(max_length=255, null=True, blank=True)
|
sender = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
@ -354,7 +449,7 @@ class Trade(models.Model):
|
||||||
|
|
||||||
open = models.BooleanField(default=True)
|
open = models.BooleanField(default=True)
|
||||||
|
|
||||||
linked = models.ManyToManyField(Transaction)
|
linked = models.ManyToManyField(Transaction, blank=True)
|
||||||
reconciled = models.BooleanField(default=False)
|
reconciled = models.BooleanField(default=False)
|
||||||
|
|
||||||
released = models.BooleanField(default=False)
|
released = models.BooleanField(default=False)
|
||||||
|
|
|
@ -0,0 +1,587 @@
|
||||||
|
import logging
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from core.clients.aggregator import AggregatorClient
|
||||||
|
from core.models import Aggregator, Platform, Trade, Transaction, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransactions(TransactionTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="testuser", email="test@example.com", password="test"
|
||||||
|
)
|
||||||
|
self.aggregator = Aggregator.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
name="Test",
|
||||||
|
service="nordigen",
|
||||||
|
secret_id="a",
|
||||||
|
secret_key="a",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.agg_client = AggregatorClient()
|
||||||
|
self.agg_client.instance = self.aggregator
|
||||||
|
|
||||||
|
self.platform = Platform.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
name="Test",
|
||||||
|
service="agora",
|
||||||
|
token="a",
|
||||||
|
password="a",
|
||||||
|
otp_token="a",
|
||||||
|
username="myuser",
|
||||||
|
)
|
||||||
|
self.transaction = Transaction.objects.create(
|
||||||
|
aggregator=self.aggregator,
|
||||||
|
account_id="my account id",
|
||||||
|
transaction_id="BANKTX",
|
||||||
|
amount=1,
|
||||||
|
currency="GBP",
|
||||||
|
note="TEST-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.trades = {
|
||||||
|
1: {
|
||||||
|
"contact_id": "uuid1",
|
||||||
|
"buyer": "test_buyer_1",
|
||||||
|
"currency": "GBP",
|
||||||
|
"asset": "XMR",
|
||||||
|
"amount_fiat": 1,
|
||||||
|
"amount_crypto": 0.3,
|
||||||
|
"reference": "TEST-1",
|
||||||
|
"provider": "REVOLUT",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
"contact_id": "uuid2",
|
||||||
|
"buyer": "test_buyer_2",
|
||||||
|
"currency": "GBP",
|
||||||
|
"asset": "XMR",
|
||||||
|
"amount_fiat": 1,
|
||||||
|
"amount_crypto": 0.3,
|
||||||
|
"reference": "TEST-2",
|
||||||
|
"provider": "REVOLUT",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
"contact_id": "uuid3",
|
||||||
|
"buyer": "test_buyer_3",
|
||||||
|
"currency": "GBP",
|
||||||
|
"asset": "XMR",
|
||||||
|
"amount_fiat": 1000,
|
||||||
|
"amount_crypto": 3,
|
||||||
|
"reference": "TEST-3",
|
||||||
|
"provider": "REVOLUT",
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
"contact_id": "uuid4",
|
||||||
|
"buyer": "test_buyer_4",
|
||||||
|
"currency": "GBP",
|
||||||
|
"asset": "XMR",
|
||||||
|
"amount_fiat": 10,
|
||||||
|
"amount_crypto": 0.5,
|
||||||
|
"reference": "TEST-4",
|
||||||
|
"provider": "REVOLUT",
|
||||||
|
},
|
||||||
|
5: { # to conflict with 1
|
||||||
|
"contact_id": "uuid1",
|
||||||
|
"buyer": "test_buyer_2",
|
||||||
|
"currency": "GBP",
|
||||||
|
"asset": "XMR",
|
||||||
|
"amount_fiat": 1,
|
||||||
|
"amount_crypto": 0.3,
|
||||||
|
"reference": "TEST-1",
|
||||||
|
"provider": "REVOLUT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_trades(self, *numbers):
|
||||||
|
for trade_key in self.trades.keys():
|
||||||
|
if trade_key in numbers:
|
||||||
|
Trade.objects.create(
|
||||||
|
platform=self.platform,
|
||||||
|
# open=True,
|
||||||
|
**self.trades[trade_key],
|
||||||
|
)
|
||||||
|
|
||||||
|
def mock_to_usd(self, amount, currency):
|
||||||
|
if currency == "GBP":
|
||||||
|
return amount * 1.3
|
||||||
|
elif currency == "USD":
|
||||||
|
return amount
|
||||||
|
# fuck it who cares
|
||||||
|
elif currency == "SEK":
|
||||||
|
return 100
|
||||||
|
elif currency == "EUR":
|
||||||
|
return 10
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_reference_partial_check(self, _):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
|
||||||
|
result = await self.agg_client.reference_partial_check(
|
||||||
|
self.platform,
|
||||||
|
"TEST-1",
|
||||||
|
"for notifications only",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, "TEST-1")
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_reference_partial_check_subset(self, _):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
|
||||||
|
result = await self.agg_client.reference_partial_check(
|
||||||
|
self.platform,
|
||||||
|
"the TEST-1 in string",
|
||||||
|
"for notifications only",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, "TEST-1")
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_reference_partial_check_multiple_match(self, _):
|
||||||
|
self.create_trades(1, 2, 3, 4, 5)
|
||||||
|
|
||||||
|
result = await self.agg_client.reference_partial_check(
|
||||||
|
self.platform,
|
||||||
|
"TEST-1",
|
||||||
|
"for notifications only",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_reference_partial_check_none(self, _):
|
||||||
|
# self.create_trades(1, 2, 3, 4, 5)
|
||||||
|
|
||||||
|
result = await self.agg_client.reference_partial_check(
|
||||||
|
self.platform,
|
||||||
|
"TEST-1",
|
||||||
|
"for notifications only",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_reference_partial_check_none_match(self, _):
|
||||||
|
self.create_trades(1, 2, 3, 4, 5)
|
||||||
|
|
||||||
|
result = await self.agg_client.reference_partial_check(
|
||||||
|
self.platform,
|
||||||
|
"NOWHERE",
|
||||||
|
"for notifications only",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_valid_transaction(self):
|
||||||
|
result = self.agg_client.valid_transaction(self.transaction)
|
||||||
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
|
def test_valid_transaction_fail(self):
|
||||||
|
self.transaction.amount = -100
|
||||||
|
result = self.agg_client.valid_transaction(self.transaction)
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
async def test_can_alt_lookup(self, *args):
|
||||||
|
result = await self.agg_client.can_alt_lookup(
|
||||||
|
self.platform,
|
||||||
|
999999999, # ignored
|
||||||
|
"GBP",
|
||||||
|
"IGNORED",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=500)
|
||||||
|
async def test_can_alt_lookup_fail(self, *args):
|
||||||
|
result = await self.agg_client.can_alt_lookup(
|
||||||
|
self.platform,
|
||||||
|
999999999, # ignored
|
||||||
|
"GBP",
|
||||||
|
"IGNORED",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
def test_find_trade(self):
|
||||||
|
self.create_trades(1, 3, 4)
|
||||||
|
|
||||||
|
result = self.agg_client.find_trade(
|
||||||
|
self.platform,
|
||||||
|
"BANKTX",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.reference, "TEST-1")
|
||||||
|
self.assertEqual(result.currency, "GBP")
|
||||||
|
self.assertEqual(result.amount_fiat, 1)
|
||||||
|
|
||||||
|
def test_find_trade_fail_two_match(self):
|
||||||
|
self.create_trades(1, 2, 3, 4) # 2 trades with same amount and currency
|
||||||
|
result = self.agg_client.find_trade(
|
||||||
|
self.platform,
|
||||||
|
"BANKTX",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
def test_find_trade_fail_two_match_alt(self):
|
||||||
|
self.create_trades(1, 3, 4)
|
||||||
|
result = self.agg_client.find_trade(
|
||||||
|
self.platform,
|
||||||
|
"BANKTX",
|
||||||
|
"GBP",
|
||||||
|
88,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
async def test_amount_currency_lookup(self, *args):
|
||||||
|
self.create_trades(1, 3, 4)
|
||||||
|
result = await self.agg_client.amount_currency_lookup(
|
||||||
|
self.platform,
|
||||||
|
1,
|
||||||
|
"GBP",
|
||||||
|
"BANKTX",
|
||||||
|
"TEST-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.reference, "TEST-1")
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
async def test_amount_currency_lookup_fail(self, *args):
|
||||||
|
self.create_trades(1, 3, 4)
|
||||||
|
result = await self.agg_client.amount_currency_lookup(
|
||||||
|
self.platform,
|
||||||
|
88,
|
||||||
|
"GBP",
|
||||||
|
"BANKTX",
|
||||||
|
"TEST-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=500)
|
||||||
|
async def test_amount_currency_lookup_fail_too_high(self, *args):
|
||||||
|
self.create_trades(1, 3, 4)
|
||||||
|
result = await self.agg_client.amount_currency_lookup(
|
||||||
|
self.platform,
|
||||||
|
1,
|
||||||
|
"GBP",
|
||||||
|
"BANKTX",
|
||||||
|
"TEST-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_normal_lookup(self, _):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
|
||||||
|
result = await self.agg_client.normal_lookup(
|
||||||
|
self.platform,
|
||||||
|
"TEST-1",
|
||||||
|
"BANKTX",
|
||||||
|
"GBP",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result.reference, "TEST-1")
|
||||||
|
self.assertEqual(result.currency, "GBP")
|
||||||
|
self.assertEqual(result.amount_fiat, 1)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_currency_check(self, _):
|
||||||
|
self.create_trades(1)
|
||||||
|
trade = Trade.objects.all().first()
|
||||||
|
result = await self.agg_client.currency_check(
|
||||||
|
"GBP",
|
||||||
|
trade,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
async def test_currency_check_fail(self, _):
|
||||||
|
self.create_trades(1)
|
||||||
|
trade = Trade.objects.all().first()
|
||||||
|
result = await self.agg_client.currency_check(
|
||||||
|
"AYZ",
|
||||||
|
trade,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
async def test_alt_amount_check(self, *args):
|
||||||
|
self.create_trades(1)
|
||||||
|
trade = Trade.objects.all().first()
|
||||||
|
|
||||||
|
result = await self.agg_client.alt_amount_check(
|
||||||
|
self.platform,
|
||||||
|
1.123,
|
||||||
|
"GBP",
|
||||||
|
trade,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
async def test_alt_amount_check_fail(self, *args):
|
||||||
|
self.create_trades(1)
|
||||||
|
trade = Trade.objects.all().first()
|
||||||
|
|
||||||
|
result = await self.agg_client.alt_amount_check(
|
||||||
|
self.platform,
|
||||||
|
1.501,
|
||||||
|
"GBP",
|
||||||
|
trade,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_called_once_with("uuid1", "TEST-1")
|
||||||
|
self.assertEqual(self.transaction.reconciled, True)
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_second(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.note = "TEST-2"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_called_once_with("uuid2", "TEST-2")
|
||||||
|
self.assertEqual(self.transaction.reconciled, True)
|
||||||
|
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_invalid(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.amount = -1
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
||||||
|
|
||||||
|
# def test_transaction_malformed(self):
|
||||||
|
# malformed_data = self.test_data_copy
|
||||||
|
# del malformed_data["amount"]
|
||||||
|
# self.transactions.transaction(malformed_data)
|
||||||
|
# self.transactions.release_funds.assert_not_called()
|
||||||
|
|
||||||
|
# malformed_data = self.test_data_copy
|
||||||
|
# del malformed_data["currency"]
|
||||||
|
# self.transactions.transaction(malformed_data)
|
||||||
|
# self.transactions.release_funds.assert_not_called()
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_no_reference_fail(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.note = "none"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1.5)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_no_reference_pass(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1)
|
||||||
|
self.transaction.note = "none"
|
||||||
|
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_called_with("uuid1", "TEST-1")
|
||||||
|
self.assertEqual(self.transaction.reconciled, True)
|
||||||
|
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1000)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_large(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.amount = 1000
|
||||||
|
self.transaction.note = "TEST-3"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_called_once_with("uuid3", "TEST-3")
|
||||||
|
self.assertEqual(self.transaction.reconciled, True)
|
||||||
|
|
||||||
|
self.transaction_amount_fiat = 1
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=1000)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_no_reference_exceeds_max(
|
||||||
|
self, release, gam, to_usd, sendmsg
|
||||||
|
):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.amount = 1000
|
||||||
|
self.transaction.note = "noref"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_wrong_currency(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.currency = "EUR"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.currency = "GBP"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_wrong_currency_noref(
|
||||||
|
self, release, gam, to_usd, sendmsg
|
||||||
|
):
|
||||||
|
self.transaction.currency = "EUR"
|
||||||
|
self.transaction.note = "none"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.currency = "GBP"
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.8, 1.8)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_wrong_amount(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.amount = 10
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.8, 1.8)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_wrong_amount_noref(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.transaction.amount = 10
|
||||||
|
self.transaction.note = "none"
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
||||||
|
self.transaction.note = "TEST-1"
|
||||||
|
|
||||||
|
# def test_transaction_pending(self):
|
||||||
|
# pending_tx = self.test_data_copy
|
||||||
|
# pending_tx["data"]["state"] = "pending"
|
||||||
|
# self.transactions.transaction(pending_tx)
|
||||||
|
# self.transactions.release_funds.assert_not_called()
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_too_low(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
|
||||||
|
self.transaction.amount = 5
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
||||||
|
|
||||||
|
@patch("core.lib.notify.sendmsg")
|
||||||
|
@patch("core.clients.aggregator.money.to_usd", return_value=100)
|
||||||
|
@patch(
|
||||||
|
"core.clients.aggregator.money.get_acceptable_margins", return_value=(0.5, 1)
|
||||||
|
)
|
||||||
|
@patch("core.clients.aggregator.AgoraClient.release_trade_escrow")
|
||||||
|
async def test_transaction_too_high(self, release, gam, to_usd, sendmsg):
|
||||||
|
self.create_trades(1, 2, 3, 4)
|
||||||
|
self.transaction.amount = 15
|
||||||
|
await self.agg_client.transaction([self.platform], self.transaction)
|
||||||
|
release.assert_not_called()
|
||||||
|
self.assertEqual(self.transaction.reconciled, False)
|
||||||
|
|
||||||
|
self.transaction.amount = 1
|
Loading…
Reference in New Issue