* refactor sale creation and url schema

* add `sale_cancelled` to ItemSale model
* add new tasks for closing completed and cancelled sales
* add `cancel_sale` url/view/logic
pull/3/head
lalanza808 5 years ago
parent 059da3bcf6
commit 0d8def3859

@ -160,7 +160,7 @@ def accept_bid(request, bid_id):
) )
sale.save() sale.save()
return HttpResponseRedirect(reverse('get_sale', args=[bid.id])) return HttpResponseRedirect(reverse('get_sale', args=[sale.id]))
@login_required @login_required
def delete_bid(request, bid_id): def delete_bid(request, bid_id):

@ -40,11 +40,13 @@ def get_item(request, item_id):
item = Item.objects.get(id=item_id) item = Item.objects.get(id=item_id)
item_images = item.images.all() item_images = item.images.all()
item_bids = item.bids.all().order_by('-bid_price_xmr') item_bids = item.bids.all().order_by('-bid_price_xmr')
sale = ItemSale.objects.filter(bid__in=item_bids, sale_cancelled=False).first()
context = { context = {
'item': item, 'item': item,
'item_images': item_images, 'item_images': item_images,
'item_bids': item_bids 'item_bids': item_bids,
'sale': sale
} }
return render(request, 'items/get_item.html', context) return render(request, 'items/get_item.html', context)

@ -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),
),
]

@ -30,6 +30,7 @@ class ItemSale(models.Model):
seller_notified_of_payout = models.BooleanField(default=False) seller_notified_of_payout = models.BooleanField(default=False)
platform_paid = models.BooleanField(default=False) platform_paid = models.BooleanField(default=False)
sale_finalized = models.BooleanField(default=False) sale_finalized = models.BooleanField(default=False)
sale_cancelled = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"{self.id} - {self.item.name} - {self.bid.bidder} > {self.item.owner}" return f"{self.id} - {self.item.name} - {self.bid.bidder} > {self.item.owner}"

@ -16,10 +16,7 @@ class EmailTemplate:
'sale': item, 'sale': item,
'site_name': settings.SITE_NAME, 'site_name': settings.SITE_NAME,
'site_url': settings.SITE_URL, 'site_url': settings.SITE_URL,
'sale_path': reverse('get_sale', args=[item.bid.id]), 'sale_path': reverse('get_sale', args=[item.id])
'shipping_address': UserShippingAddress.objects.filter(
user=item.bid.bidder
).first()
} }
subject = render_to_string( subject = render_to_string(
template_name=f'sales/notify/{scenario}/{role}/subject.txt', template_name=f'sales/notify/{scenario}/{role}/subject.txt',
@ -53,7 +50,7 @@ class EmailTemplate:
@periodic_task(crontab(minute='*/3')) @periodic_task(crontab(minute='*/3'))
def notify_buyer_of_pending_sale(): 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: for sale in item_sales:
email_template = EmailTemplate( email_template = EmailTemplate(
item=sale, item=sale,
@ -157,7 +154,7 @@ def pay_sellers_on_sold_items():
sale.seller_notified_of_payout = True sale.seller_notified_of_payout = True
sale.save() sale.save()
@periodic_task(crontab(hour='*/2')) @periodic_task(crontab(hour='*/3'))
def pay_platform_on_sold_items(): def pay_platform_on_sold_items():
aw = AuctionWallet() aw = AuctionWallet()
if aw.connected is False: if aw.connected is False:
@ -208,4 +205,29 @@ def poll_for_buyer_escrow_payments():
sale.id, sale.received_payment_xmr, sale.payment_received 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()

@ -3,7 +3,8 @@ from . import views
urlpatterns = [ urlpatterns = [
path('<int:bid_id>/', views.get_sale, name='get_sale'), path('<int:sale_id>/', views.get_sale, name='get_sale'),
path('<int:sale_id>/cancel', views.cancel_sale, name='cancel_sale'),
path('<int:sale_id>/confirm_shipment/', views.confirm_shipment, name='confirm_shipment'), path('<int:sale_id>/confirm_shipment/', views.confirm_shipment, name='confirm_shipment'),
path('<int:sale_id>/confirm_receipt/', views.confirm_receipt, name='confirm_receipt') path('<int:sale_id>/confirm_receipt/', views.confirm_receipt, name='confirm_receipt')
] ]

@ -10,9 +10,9 @@ from sales.models import ItemSale
@login_required @login_required
def get_sale(request, bid_id): def get_sale(request, sale_id):
bid = ItemBid.objects.get(id=bid_id) sale = ItemSale.objects.get(id=sale_id)
sale = ItemSale.objects.get(bid=bid) bid = ItemBid.objects.get(id=sale.bid.id)
qr_uri = 'monero:{}?tx_amount={}&tx_description="xmrauctions_sale_{}"'.format( qr_uri = 'monero:{}?tx_amount={}&tx_description="xmrauctions_sale_{}"'.format(
sale.escrow_address, sale.expected_payment_xmr, sale.id 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.") messages.error(request, "You can't view a sale you are not involved in.")
return HttpResponseRedirect(reverse('home')) 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 = BytesIO()
address_qr = qrcode_make(qr_uri).save(_address_qr) 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) 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 @login_required
def confirm_shipment(request, sale_id): def confirm_shipment(request, sale_id):
sale = ItemSale.objects.get(id=sale_id) sale = ItemSale.objects.get(id=sale_id)

@ -51,7 +51,7 @@
<td> <td>
{% if bid.accepted %} {% if bid.accepted %}
{% if bid.bidder == request.user or bid.item.owner == request.user %} {% if bid.bidder == request.user or bid.item.owner == request.user %}
<a href="{% url 'get_sale' bid.id %}" class="button alt">View Sale</a> <a href="{% url 'get_sale' sale.id %}" class="button alt">View Sale</a>
{% endif %} {% endif %}
{% else %} {% else %}
{% if bid.bidder == request.user %} {% if bid.bidder == request.user %}

@ -28,6 +28,9 @@
<li><a href="https://www.monerujo.io/" target="_blank">Monerujo (Android)</a></li> <li><a href="https://www.monerujo.io/" target="_blank">Monerujo (Android)</a></li>
<li><a href="https://mymonero.com/" target="_blank">MyMonero (Desktop, Web)</a></li> <li><a href="https://mymonero.com/" target="_blank">MyMonero (Desktop, Web)</a></li>
</ul> </ul>
<h2>Change your Mind?</h2>
<p>You can cancel the sale and reopen the item for bidding. Any funds sent will be transferred again to your return address.</p>
<p><a href="{% url 'cancel_sale' sale.id %}" class="button">Cancel Sale</a></p>
</span> </span>
{% elif sale.payment_received and sale.item_shipped == False %} {% elif sale.payment_received and sale.item_shipped == False %}
<p class="sale-info">Congratulations {{ sale.bid.bidder.username }},</p> <p class="sale-info">Congratulations {{ sale.bid.bidder.username }},</p>
@ -80,7 +83,6 @@
We are waiting for the buyer to send funds to the escrow address. 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. No action is needed from you at this time, but you will be notified you when there is.
</p> </p>
<p class="sale-info">Congratulations on the sale!</p>
{% elif sale.payment_received and sale.item_shipped == False %} {% elif sale.payment_received and sale.item_shipped == False %}
<p class="sale-info">Congratulations {{ sale.item.owner.username }},</p> <p class="sale-info">Congratulations {{ sale.item.owner.username }},</p>
<p class="sale-info"> <p class="sale-info">