import uuid from django.contrib.auth.models import AbstractUser from django.db import models # from core.lib.customers import get_or_create, update_customer_fields from core.util import logs log = logs.get_logger(__name__) SERVICE_CHOICES = (("nordigen", "Nordigen"),) PLATFORM_SERVICE_CHOICES = (("agora", "Agora"),) INTERVAL_CHOICES = ( (0, "Never"), (5, "Every 5 seconds"), (15, "Every 15 seconds"), (30, "Every 30 seconds"), (60, "Every minute"), (60 * 5, "Every 5 minutes"), (60 * 10, "Every 10 minutes"), (60 * 60, "Every hour"), (60 * 60 * 4, "Every 4 hours"), (86400, "Every day"), ) TRANSACTION_SOURCE_CHOICES = ( ("booked", "Booked"), ("pending", "Pending"), ) class User(AbstractUser): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) payment_provider_id = models.CharField(max_length=255, null=True, blank=True) billing_provider_id = models.CharField(max_length=255, null=True, blank=True) email = models.EmailField(unique=True) def get_notification_settings(self): return NotificationSettings.objects.get_or_create(user=self)[0] class NotificationSettings(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) ntfy_topic = models.CharField(max_length=255, null=True, blank=True) ntfy_url = models.CharField(max_length=255, null=True, blank=True) def __str__(self): return f"Notification settings for {self.user}" class LinkGroup(models.Model): """ A group linking Aggregators, Platforms and defining a percentage split that the owners of each should receive. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) platform_owner_cut_percentage = models.FloatField(default=0) requisition_owner_cut_percentage = models.FloatField(default=0) operator_cut_percentage = models.FloatField(default=0) enabled = models.BooleanField(default=True) def __str__(self): return self.name def payees(self): payees = {} for platform in self.platform_set.all(): for payee in platform.payees.all(): if "platform" not in payees: payees["platform"] = [] payees["platform"].append(payee) for aggregator in self.aggregator_set.all(): agg_reqs = aggregator.requisition_set.all() for req in agg_reqs: for payee in req.payees.all(): if "requisition" not in payees: payees["requisition"] = [] payees["requisition"].append(payee) return payees @property def platforms(self): return Platform.objects.filter(link_group=self) @property def aggregators(self): return Aggregator.objects.filter(link_group=self) class Aggregator(models.Model): """ A connection to an API aggregator to pull transactions from bank accounts. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) service = models.CharField(max_length=255, choices=SERVICE_CHOICES) secret_id = models.CharField(max_length=1024, null=True, blank=True) secret_key = models.CharField(max_length=1024, null=True, blank=True) access_token = models.CharField(max_length=1024, null=True, blank=True) access_token_expires = models.DateTimeField(null=True, blank=True) poll_interval = models.IntegerField(default=10) account_info = models.JSONField(default=dict) currencies = models.JSONField(default=list) fetch_accounts = models.BooleanField(default=True) link_group = models.ForeignKey( LinkGroup, on_delete=models.CASCADE, null=True, blank=True ) enabled = models.BooleanField(default=True) def __str__(self): return f"{self.name} ({self.get_service_display()})" @classmethod def get_by_id(cls, obj_id, user): return cls.objects.get(id=obj_id, user=user) @property def client(self): pass @classmethod def get_for_platform(cls, platform): # aggregators = [] # linkgroups = LinkGroup.objects.filter( # platforms=platform, # enabled=True, # ) # for link in linkgroups: # for aggregator in link.aggregators.all(): # if aggregator not in aggregators: # aggregators.append(aggregator) platform_link = platform.link_group # return aggregators return cls.objects.filter( link_group=platform_link, ) @property def platforms(self): """ Get platforms for this aggregator. Do this by looking up LinkGroups with the aggregator. Then, join them all together. """ return Platform.objects.filter(link_group=self.link_group) @property def requisitions(self): """ Get requisitions for this aggregator. Do this by looking up LinkGroups with the aggregator. Then, join them all together. """ return Requisition.objects.filter( aggregator=self, ) def get_requisition(self, requisition_id): return Requisition.objects.filter( aggregator=self, requisition_id=requisition_id, ).first() @classmethod def get_currencies_for_platform(cls, platform): # aggregators = Aggregator.get_for_platform(platform) aggregators = platform.aggregators currencies = set() for aggregator in aggregators: for currency in aggregator.currencies: currencies.add(currency) return list(currencies) @classmethod def get_account_info_for_platform(cls, platform): # aggregators = Aggregator.get_for_platform(platform) aggregators = platform.aggregators account_info = {} for agg in aggregators: for bank, accounts in agg.account_info.items(): if bank not in account_info: account_info[bank] = [] for account in accounts: account_info[bank].append(account) return account_info def add_transaction(self, requisition_id, account_id, tx_data): requisition = Requisition.objects.filter( aggregator=self, requisition_id=requisition_id ).first() # if requisition: # tx_data["requisition"] = requisition return Transaction.objects.create( aggregator=self, account_id=account_id, reconciled=False, requisition=requisition, **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 @property def trades(self): """ Get all trades for the platforms of this aggregator's link group. """ trades = [] for platform in self.platforms: platform_trades = platform.trades for trade in platform_trades: trades.append(trade) return trades @property def trades_currencies(self): """ Get all the trade fiat currencies. """ currencies = [] for trade in self.trades: if trade.currency not in currencies: currencies.append(trade.currency) return currencies class Wallet(models.Model): """ A wallet for a user. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) address = models.CharField(max_length=255) def __str__(self): return self.name class Platform(models.Model): """ A connection to an arbitrage platform like AgoraDesk. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) service = models.CharField(max_length=255, choices=PLATFORM_SERVICE_CHOICES) token = models.CharField(max_length=1024) password = models.CharField(max_length=1024) otp_token = models.CharField(max_length=1024, null=True, blank=True) username = models.CharField(max_length=255) send = models.BooleanField(default=True) cheat = models.BooleanField(default=False) dummy = models.BooleanField(default=False) cheat_interval_seconds = models.IntegerField(default=0, choices=INTERVAL_CHOICES) margin = models.FloatField(default=1.20) max_margin = models.FloatField(default=1.30) min_margin = models.FloatField(default=1.15) min_trade_size_usd = models.FloatField(default=10) max_trade_size_usd = models.FloatField(default=4000) accept_within_usd = models.FloatField(default=1) no_reference_amount_check_max_usd = models.FloatField(default=400) last_messages = models.JSONField(default=dict) platform_ad_ids = models.JSONField(default=dict) base_usd = models.FloatField(default=2800) withdrawal_trigger = models.FloatField(default=200) payees = models.ManyToManyField(Wallet, blank=True) link_group = models.ForeignKey( LinkGroup, on_delete=models.CASCADE, null=True, blank=True ) enabled = models.BooleanField(default=True) throughput = models.FloatField(default=0) def __str__(self): return self.name def get_ad(self, platform_ad_id): ad_id = self.platform_ad_ids.get(platform_ad_id, None) if not ad_id: return None ad_object = Ad.objects.filter( id=ad_id, user=self.user, link_group=self.link_group, enabled=True ).first() return ad_object @classmethod def get_for_user(cls, user): return cls.objects.filter(user=user, enabled=True) @property def currencies(self): return Aggregator.get_currencies_for_platform(self) @property def account_info(self): return Aggregator.get_account_info_for_platform(self) @property def ads(self): """ Get all ads linked to this platform. """ return Ad.objects.filter( user=self.user, enabled=True, link_group=self.link_group ) @property def ads_assets(self): """ Get all the assets of all the ads. """ assets = set() for ad in self.ads: for asset in ad.asset_list.all(): assets.add(asset.code) return list(assets) @property def ads_providers(self): """ Get all the providers of all the ads. """ providers = set() for ad in self.ads: for provider in ad.provider_list.all(): providers.add(provider.code) return list(providers) @property def references(self): """ Get references of all our trades that are open. """ references = [] our_trades = Trade.objects.filter(platform=self, open=True) for trade in our_trades: references.append(trade.reference) return references @property def trade_ids(self): """ Get trade IDs of all our trades that are open. """ references = [] our_trades = Trade.objects.filter(platform=self, open=True) for trade in our_trades: references.append(trade.contact_id) return references def get_trade_by_reference(self, reference): return Trade.objects.filter( platform=self, open=True, reference=reference, ).first() @property def trades(self): """ Get all our open trades. """ our_trades = Trade.objects.filter(platform=self, open=True) return our_trades def contact_id_to_reference(self, contact_id): """ Get a reference from a contact_id. """ trade = Trade.objects.filter( platform=self, open=True, contact_id=contact_id ).first() if not trade: return None return trade.reference def get_trade_by_trade_id(self, trade_id): return Trade.objects.filter( platform=self, open=True, contact_id=trade_id, ).first() def new_trade(self, trade_cast): trade = Trade.objects.create( platform=self, **trade_cast, ) return trade def remove_trades_with_reference_not_in(self, reference_list): """ Set trades with reference not in list to open=False. """ trades = Trade.objects.filter(platform=self, open=True) messages = [] for trade in trades: if trade.reference not in reference_list: trade.open = False trade.save() msg = f"[{trade.reference}]: Archiving ID: {trade.contact_id}" messages.append(msg) log.info(msg) return messages @classmethod def get_for_aggregator(cls, aggregator): # platforms = [] # linkgroups = LinkGroup.objects.filter( # aggregators=aggregator, # enabled=True, # ) # for link in linkgroups: # for platform in link.platforms.all(): # if platform not in platforms: # platforms.append(platform) # return platforms aggregator_link = aggregator.link_group return cls.objects.filter( link_group=aggregator_link, ) @property def aggregators(self): """ Get aggregators for this platform. Do this by looking up LinkGroups with the platform. Then, join them all together. """ return Aggregator.objects.filter( link_group=self.link_group, ) @property def platforms(self): """ Get all platforms in this link group. Do this by looking up LinkGroups with the platform. Then, join them all together. """ return Platform.objects.filter( link_group=self.link_group, ) def get_requisition(self, aggregator_id, requisition_id): """ Get a Requisition object with the provided values. """ requisition = Requisition.objects.filter( aggregator_id=aggregator_id, requisition_id=requisition_id, ).first() return requisition class Asset(models.Model): code = models.CharField(max_length=64) name = models.CharField(max_length=255) def __str__(self): return f"{self.name} ({self.code})" class Provider(models.Model): code = models.CharField(max_length=64) name = models.CharField(max_length=255) def __str__(self): return f"{self.name} ({self.code})" class Ad(models.Model): """ An advert definition """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) text = models.TextField() # Shown when the user opens a trade payment_details = models.TextField() # Shown after payment_details_real = models.TextField() payment_method_details = models.CharField(max_length=255) require_feedback_score = models.IntegerField(default=0) dist_list = models.TextField() asset_list = models.ManyToManyField(Asset) provider_list = models.ManyToManyField(Provider) account_map = models.JSONField(default=dict) account_whitelist = models.TextField(null=True, blank=True) send_reference = models.BooleanField(default=True) visible = models.BooleanField(default=True) link_group = models.ForeignKey( LinkGroup, on_delete=models.CASCADE, null=True, blank=True ) enabled = models.BooleanField(default=True) @property def providers(self): return [x.code for x in self.provider_list.all()] @property def assets(self): return [x.code for x in self.asset_list.all()] @classmethod def get_by_id(cls, ad_id, user): return cls.objects.filter(id=ad_id, user=user, enabled=True).first() class Transaction(models.Model): """ A transaction on an aggregator. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE) requisition = models.ForeignKey( "core.Requisition", null=True, on_delete=models.CASCADE ) account_id = models.CharField(max_length=255) transaction_id = models.CharField(max_length=255) ts_added = models.DateTimeField(auto_now_add=True) recipient = models.CharField(max_length=255, null=True, blank=True) sender = models.CharField(max_length=255, null=True, blank=True) amount = models.FloatField() currency = models.CharField(max_length=16) note = models.CharField(max_length=255, null=True, blank=True) reconciled = models.BooleanField(default=False) class Trade(models.Model): """ A trade on a Platform. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) platform = models.ForeignKey(Platform, on_delete=models.CASCADE) contact_id = models.CharField(max_length=255) reference = models.CharField(max_length=255) buyer = models.CharField(max_length=255) amount_fiat = models.FloatField() currency = models.CharField(max_length=16) amount_crypto = models.FloatField() asset = models.CharField(max_length=16) provider = models.CharField(max_length=255) ad_id = models.CharField(max_length=255, null=True, blank=True) open = models.BooleanField(default=True) linked = models.ManyToManyField(Transaction, blank=True) reconciled = models.BooleanField(default=False) released = models.BooleanField(default=False) release_response = models.JSONField(default=dict) class Requisition(models.Model): """ A requisition for an Aggregator """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) aggregator = models.ForeignKey(Aggregator, on_delete=models.CASCADE) requisition_id = models.CharField(max_length=255) payment_details = models.TextField(null=True, blank=True) owner_name = models.CharField(max_length=255, null=True, blank=True) transaction_source = models.CharField( max_length=255, choices=TRANSACTION_SOURCE_CHOICES, default="booked" ) throughput = models.FloatField(default=0) payees = models.ManyToManyField(Wallet, blank=True) def __str__(self): return f"Aggregator: {self.aggregator.name} ID: {self.requisition_id}" class OperatorWallets(models.Model): """ A list of wallets to designate as operator wallets for this user. """ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, on_delete=models.CASCADE) payees = models.ManyToManyField(Wallet, blank=True) assets = { "XMR": "Monero", "BTC": "Bitcoin", } providers = { "REVOLUT": "Revolut", "NATIONAL_BANK": "Bank transfer", "SWISH": "Swish", } for code, name in assets.items(): if not Asset.objects.filter(code=code).exists(): Asset.objects.create(code=code, name=name) for code, name in providers.items(): if not Provider.objects.filter(code=code).exists(): Provider.objects.create(code=code, name=name)