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"), ) 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 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) 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 = [] ads = Ad.objects.filter( platforms=platform, enabled=True, ) for ad in ads: for aggregator in ad.aggregators.all(): if aggregator not in aggregators: aggregators.append(aggregator) 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, ) for ad in ads: for platform in ad.platforms.all(): if platform not in platforms: platforms.append(platform) return platforms @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, 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): """ 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) enabled = models.BooleanField(default=True) 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, 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, platforms=self) @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 = [] ads = Ad.objects.filter( aggregators=aggregator, enabled=True, ) for ad in ads: for platform in ad.platforms.all(): if platform not in platforms: platforms.append(platform) 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, ) for ad in ads: for aggregator in ad.aggregators.all(): if aggregator not in aggregators: aggregators.append(aggregator) return aggregators 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) dist_list = models.TextField() asset_list = models.ManyToManyField(Asset) provider_list = models.ManyToManyField(Provider) platforms = models.ManyToManyField(Platform) aggregators = models.ManyToManyField(Aggregator) 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) 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) 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) 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)