You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
8.6 KiB
Python
230 lines
8.6 KiB
Python
import logging
|
|
from decimal import Decimal
|
|
from huey import crontab
|
|
from huey.contrib.djhuey import periodic_task
|
|
from django.conf import settings
|
|
from django.core.mail import send_mail
|
|
from django.template.loader import render_to_string
|
|
from django.urls import reverse
|
|
from core.monero import AuctionWallet
|
|
from core.models import UserShippingAddress
|
|
from sales.models import ItemSale
|
|
|
|
|
|
logger = logging.getLogger('django.server')
|
|
|
|
class EmailTemplate:
|
|
def __init__(self, item, scenario, role):
|
|
context = {
|
|
'sale': item,
|
|
'site_name': settings.SITE_NAME,
|
|
'site_url': settings.SITE_URL,
|
|
'sale_path': reverse('get_sale', args=[item.id])
|
|
}
|
|
subject = render_to_string(
|
|
template_name=f'sales/notify/{scenario}/{role}/subject.txt',
|
|
context=context,
|
|
request=None
|
|
)
|
|
body = render_to_string(
|
|
template_name=f'sales/notify/{scenario}/{role}/body.txt',
|
|
context=context,
|
|
request=None
|
|
)
|
|
self.subject = ''.join(subject.splitlines())
|
|
self.body = body
|
|
self.role = role
|
|
self.item = item
|
|
|
|
def send(self):
|
|
if self.role == 'buyer':
|
|
to_address = self.item.bid.bidder.email
|
|
else:
|
|
to_address = self.item.item.owner.email
|
|
|
|
res = send_mail(
|
|
self.subject,
|
|
self.body,
|
|
settings.DEFAULT_FROM_EMAIL,
|
|
[to_address]
|
|
)
|
|
return res
|
|
|
|
|
|
### Notifications
|
|
|
|
|
|
@periodic_task(crontab(minute='*'))
|
|
def notify_buyer_of_pending_sale():
|
|
item_sales = ItemSale.objects.filter(buyer_notified=False, sale_cancelled=False)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Sale #{sale.id} just created, notifying buyer.')
|
|
email_template = EmailTemplate(
|
|
item=sale,
|
|
scenario='sale_created',
|
|
role='buyer'
|
|
)
|
|
email_template.send()
|
|
sale.buyer_notified = True
|
|
sale.save()
|
|
|
|
@periodic_task(crontab(minute='*'))
|
|
def notify_seller_of_funds_received():
|
|
item_sales = ItemSale.objects.filter(seller_notified=False, buyer_notified=True).filter(payment_received=True)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Funds received from buyer for sale #{sale.id}, notifying seller.')
|
|
email_template = EmailTemplate(
|
|
item=sale,
|
|
scenario='funds_received',
|
|
role='seller'
|
|
)
|
|
email_template.send()
|
|
sale.seller_notified = True
|
|
sale.save()
|
|
|
|
@periodic_task(crontab(minute='*'))
|
|
def notify_buyer_of_shipment_confirmation():
|
|
item_sales = ItemSale.objects.filter(item_shipped=True).filter(buyer_notified_of_shipment=False)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Item shipped for sale #{sale.id}, notifying buyer.')
|
|
email_template = EmailTemplate(
|
|
item=sale,
|
|
scenario='item_shipped',
|
|
role='buyer'
|
|
)
|
|
email_template.send()
|
|
bidder_profile = UserShippingAddress.objects.get(user=sale.bid.bidder)
|
|
bidder_profile.delete()
|
|
logger.info(f'[INFO] Buyer shipping info wiped for sale #{sale.id}')
|
|
sale.buyer_notified_of_shipment = True
|
|
sale.save()
|
|
|
|
|
|
### Payments
|
|
|
|
@periodic_task(crontab(minute='*'))
|
|
def poll_for_buyer_escrow_payments():
|
|
aw = AuctionWallet()
|
|
if aw.connected is False:
|
|
logging.error('Auction wallet is not connected. Quitting.')
|
|
return False
|
|
|
|
item_sales = ItemSale.objects.filter(payment_received=False)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Polling escrow address #{sale.escrow_account_index} for sale #{sale.id} for new funds.')
|
|
sale_account = aw.wallet.accounts[sale.escrow_account_index]
|
|
unlocked = sale_account.balances()[1]
|
|
sale.received_payment_xmr = unlocked
|
|
if unlocked >= Decimal(str(sale.expected_payment_xmr)):
|
|
logger.info(f'[INFO] Found payment of {sale.received_payment_xmr} XMR for sale #{sale.id}.')
|
|
sale.payment_received = True
|
|
|
|
sale.save()
|
|
|
|
|
|
@periodic_task(crontab(minute='*'))
|
|
def pay_sellers_on_sold_items():
|
|
aw = AuctionWallet()
|
|
if aw.connected is False:
|
|
logging.error('Auction wallet is not connected. Quitting.')
|
|
return False
|
|
|
|
item_sales = ItemSale.objects.filter(item_received=True, payment_received=True).filter(seller_paid=False)
|
|
for sale in item_sales:
|
|
# Take platform fees from the sale - the 50:50 split between buyer/seller
|
|
sale_total = sale.agreed_price_xmr - sale.platform_fee_xmr
|
|
sale_account = aw.wallet.accounts[sale.escrow_account_index]
|
|
|
|
if sale_account.balances()[1] >= Decimal(sale.agreed_price_xmr):
|
|
try:
|
|
# Construct a transaction so we can get current fee and subtract from the total
|
|
_tx = sale_account.transfer(
|
|
sale.item.payout_address, Decimal(.01), relay=False
|
|
)
|
|
new_total = sale_total - float(_tx[0].fee)
|
|
|
|
logger.info(f'[INFO] Sending {new_total} XMR from wallet account #{sale.escrow_account_index} to item owner\'s payout address for sale #{sale.id}.')
|
|
# Make the transaction with network fee removed
|
|
tx = sale_account.transfer(
|
|
sale.item.payout_address, new_total, relay=True
|
|
)
|
|
sale.network_fee_xmr = _tx[0].fee
|
|
sale.seller_payout_transaction = tx[0]
|
|
sale.seller_paid = True
|
|
sale.escrow_complete = True
|
|
sale.save()
|
|
except Exception as e:
|
|
logger.error(f'[ERROR] Unable to pay seller for sale #{sale.id}: ')
|
|
else:
|
|
logger.warning(f'[WARNING] Not enough unlocked funds available in account #{sale.escrow_account_index} for sale #{sale.id}.')
|
|
|
|
if sale.seller_paid and sale.seller_notified_of_payout is False:
|
|
email_template = EmailTemplate(
|
|
item=sale,
|
|
scenario='sale_completed',
|
|
role='seller'
|
|
)
|
|
sent = email_template.send()
|
|
sale.seller_notified_of_payout = True
|
|
sale.save()
|
|
|
|
@periodic_task(crontab(minute='*/30'))
|
|
def pay_platform_on_sold_items():
|
|
aw = AuctionWallet()
|
|
if aw.connected is False:
|
|
logging.error('Auction wallet is not connected. Quitting.')
|
|
return False
|
|
|
|
aof = settings.PLATFORM_WALLET_ADDRESS
|
|
if aof is None:
|
|
aof = str(aw.wallet.accounts[0].address())
|
|
|
|
item_sales = ItemSale.objects.filter(escrow_complete=True, seller_paid=True, item_received=True).filter(platform_paid=False)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Paying platform fees for sale #{sale.id} to wallet {aof}.')
|
|
sale_account = aw.wallet.accounts[sale.escrow_account_index]
|
|
bal = sale_account.balances()[1]
|
|
if bal >= 0:
|
|
try:
|
|
if settings.PLATFORM_FEE_PERCENT > 0:
|
|
logger.info(f'[INFO] Getting platform fees of {bal} XMR')
|
|
sale_account.sweep_all(aof)
|
|
else:
|
|
logging.info('No platform fees are set - proceeding without taking fees.')
|
|
|
|
sale.platform_paid = True
|
|
sale.sale_finalized = True
|
|
sale.save()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f'[ERROR] Unable to pay platform for sale #{sale.id} - trying again')
|
|
else:
|
|
logger.warning(f'[WARNING] Not enough unlocked funds available in account #{sale.escrow_account_index} for sale #{sale.id}.')
|
|
|
|
@periodic_task(crontab(minute='0', hour='*/12'))
|
|
def close_completed_items_sales():
|
|
item_sales = ItemSale.objects.filter(platform_paid=True, sale_finalized=True)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Deleting item #{sale.item.id} and all accompanying bids, sales, meta, etc.')
|
|
sale.item.delete()
|
|
|
|
@periodic_task(crontab(minute='*'))
|
|
def closed_cancelled_sales():
|
|
aw = AuctionWallet()
|
|
if aw.connected is False:
|
|
logging.error('Auction wallet is not connected. Quitting.')
|
|
return False
|
|
|
|
item_sales = ItemSale.objects.filter(sale_cancelled=True)
|
|
for sale in item_sales:
|
|
logger.info(f'[INFO] Deleting sale #{sale.id} and transferring back any sent funds to the buyer.')
|
|
sale_account = aw.wallet.accounts[sale.escrow_account_index]
|
|
if sale_account.balances()[0] > Decimal(0.0):
|
|
try:
|
|
sale_account.sweep_all(sale.bid.return_address)
|
|
sale.delete()
|
|
except Exception as e:
|
|
logger.error(f'[ERROR] Unable to return funds to use for sale #{sale.id}.')
|
|
else:
|
|
sale.delete()
|