diff --git a/bids/views.py b/bids/views.py index 3b07f62..fa69b44 100644 --- a/bids/views.py +++ b/bids/views.py @@ -160,7 +160,7 @@ def accept_bid(request, bid_id): ) sale.save() - return HttpResponseRedirect(reverse('get_sale', args=[bid.id])) + return HttpResponseRedirect(reverse('get_sale', args=[sale.id])) @login_required def delete_bid(request, bid_id): diff --git a/items/views.py b/items/views.py index 877a271..95ca946 100644 --- a/items/views.py +++ b/items/views.py @@ -40,11 +40,13 @@ def get_item(request, item_id): item = Item.objects.get(id=item_id) item_images = item.images.all() item_bids = item.bids.all().order_by('-bid_price_xmr') + sale = ItemSale.objects.filter(bid__in=item_bids, sale_cancelled=False).first() context = { 'item': item, 'item_images': item_images, - 'item_bids': item_bids + 'item_bids': item_bids, + 'sale': sale } return render(request, 'items/get_item.html', context) diff --git a/sales/migrations/0003_itemsale_sale_cancelled.py b/sales/migrations/0003_itemsale_sale_cancelled.py new file mode 100644 index 0000000..067b57d --- /dev/null +++ b/sales/migrations/0003_itemsale_sale_cancelled.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.8 on 2020-01-10 22:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sales', '0002_auto_20200107_0910'), + ] + + operations = [ + migrations.AddField( + model_name='itemsale', + name='sale_cancelled', + field=models.BooleanField(default=False), + ), + ] diff --git a/sales/models.py b/sales/models.py index 7511a4a..0ac88a8 100644 --- a/sales/models.py +++ b/sales/models.py @@ -30,6 +30,7 @@ class ItemSale(models.Model): seller_notified_of_payout = models.BooleanField(default=False) platform_paid = models.BooleanField(default=False) sale_finalized = models.BooleanField(default=False) + sale_cancelled = models.BooleanField(default=False) def __str__(self): return f"{self.id} - {self.item.name} - {self.bid.bidder} > {self.item.owner}" diff --git a/sales/tasks.py b/sales/tasks.py index 2e9dd32..28e0f0c 100644 --- a/sales/tasks.py +++ b/sales/tasks.py @@ -16,10 +16,7 @@ class EmailTemplate: 'sale': item, 'site_name': settings.SITE_NAME, 'site_url': settings.SITE_URL, - 'sale_path': reverse('get_sale', args=[item.bid.id]), - 'shipping_address': UserShippingAddress.objects.filter( - user=item.bid.bidder - ).first() + 'sale_path': reverse('get_sale', args=[item.id]) } subject = render_to_string( template_name=f'sales/notify/{scenario}/{role}/subject.txt', @@ -53,7 +50,7 @@ class EmailTemplate: @periodic_task(crontab(minute='*/3')) def notify_buyer_of_pending_sale(): - item_sales = ItemSale.objects.filter(buyer_notified=False) + item_sales = ItemSale.objects.filter(buyer_notified=False, sale_cancelled=False) for sale in item_sales: email_template = EmailTemplate( item=sale, @@ -157,7 +154,7 @@ def pay_sellers_on_sold_items(): sale.seller_notified_of_payout = True sale.save() -@periodic_task(crontab(hour='*/2')) +@periodic_task(crontab(hour='*/3')) def pay_platform_on_sold_items(): aw = AuctionWallet() if aw.connected is False: @@ -208,4 +205,29 @@ def poll_for_buyer_escrow_payments(): sale.id, sale.received_payment_xmr, sale.payment_received )) -# TODO - close out old sales +@periodic_task(crontab(hour='*/8')) +def close_completed_items_sales(): + item_sales = ItemSale.objects.filter(platform_paid=True, sale_finalized=True) + for sale in item_sales: + print(f'deleting item #{sale.item.id} and all accompanying bids, sales, meta') + sale.item.delete() + +@periodic_task(crontab(minute='*/6')) +def closed_cancelled_sales(): + aw = AuctionWallet() + if aw.connected is False: + return False + + item_sales = ItemSale.objects.filter(sale_cancelled=True) + for sale in item_sales: + print(f'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.balance() > Decimal(0.0): + try: + sale_account.sweep_all(sale.bid.return_address) + sale.delete() + except Exception as e: + print('unable to sweep all: ', e) + return False + else: + sale.delete() diff --git a/sales/urls.py b/sales/urls.py index c7c2c2c..ba969dc 100644 --- a/sales/urls.py +++ b/sales/urls.py @@ -3,7 +3,8 @@ from . import views urlpatterns = [ - path('/', views.get_sale, name='get_sale'), + path('/', views.get_sale, name='get_sale'), + path('/cancel', views.cancel_sale, name='cancel_sale'), path('/confirm_shipment/', views.confirm_shipment, name='confirm_shipment'), path('/confirm_receipt/', views.confirm_receipt, name='confirm_receipt') ] diff --git a/sales/views.py b/sales/views.py index 0ae70eb..6cff05f 100644 --- a/sales/views.py +++ b/sales/views.py @@ -10,9 +10,9 @@ from sales.models import ItemSale @login_required -def get_sale(request, bid_id): - bid = ItemBid.objects.get(id=bid_id) - sale = ItemSale.objects.get(bid=bid) +def get_sale(request, sale_id): + sale = ItemSale.objects.get(id=sale_id) + bid = ItemBid.objects.get(id=sale.bid.id) qr_uri = 'monero:{}?tx_amount={}&tx_description="xmrauctions_sale_{}"'.format( sale.escrow_address, sale.expected_payment_xmr, sale.id ) @@ -22,6 +22,11 @@ def get_sale(request, bid_id): messages.error(request, "You can't view a sale you are not involved in.") return HttpResponseRedirect(reverse('home')) + # Do not proceed if sale is cancelled + if sale.sale_cancelled: + messages.error(request, 'That sale has been cancelled and is no longer available.') + return HttpResponseRedirect(reverse('get_item', args=[sale.item.id])) + _address_qr = BytesIO() address_qr = qrcode_make(qr_uri).save(_address_qr) @@ -35,6 +40,38 @@ def get_sale(request, bid_id): return render(request, 'sales/get_sale.html', context) +@login_required +def cancel_sale(request, sale_id): + sale = ItemSale.objects.filter(id=sale_id).first() + bid = ItemBid.objects.get(id=sale.bid.id) + + if sale is None: + messages.error(request, "That sale doesn't exist.") + return HttpResponseRedirect(reverse('home')) + + # Do not proceed unless current user is a buyer or seller + if request.user != sale.bid.bidder and request.user != sale.item.owner: + messages.error(request, "You can't view a sale you are not involved in.") + return HttpResponseRedirect(reverse('home')) + + if sale.payment_received: + messages.error(request, "You can't cancel a sale which has already received funds.") + return HttpResponseRedirect(reverse('get_sale', args=[sale.bid.id])) + + # Item becomes available + sale.item.available = True + sale.item.save() + + # Bid becomes not accepted + sale.bid.accepted = False + sale.bid.save() + + # Sale gets cancelled + sale.sale_cancelled = True + sale.save() + return HttpResponseRedirect(reverse('get_item', args=[sale.item.id])) + + @login_required def confirm_shipment(request, sale_id): sale = ItemSale.objects.get(id=sale_id) diff --git a/web/templates/items/get_item.html b/web/templates/items/get_item.html index d6b9dde..7cdaaf0 100644 --- a/web/templates/items/get_item.html +++ b/web/templates/items/get_item.html @@ -51,7 +51,7 @@ {% if bid.accepted %} {% if bid.bidder == request.user or bid.item.owner == request.user %} - View Sale + View Sale {% endif %} {% else %} {% if bid.bidder == request.user %} diff --git a/web/templates/sales/get_sale.html b/web/templates/sales/get_sale.html index 3877dc0..c243d99 100644 --- a/web/templates/sales/get_sale.html +++ b/web/templates/sales/get_sale.html @@ -28,6 +28,9 @@
  • Monerujo (Android)
  • MyMonero (Desktop, Web)
  • +

    Change your Mind?

    +

    You can cancel the sale and reopen the item for bidding. Any funds sent will be transferred again to your return address.

    +

    Cancel Sale

    {% elif sale.payment_received and sale.item_shipped == False %}

    Congratulations {{ sale.bid.bidder.username }},

    @@ -80,7 +83,6 @@ We are waiting for the buyer to send funds to the escrow address. No action is needed from you at this time, but you will be notified you when there is.

    -

    Congratulations on the sale!

    {% elif sale.payment_received and sale.item_shipped == False %}

    Congratulations {{ sale.item.owner.username }},