Finish first iteration of subscriptions
This commit is contained in:
parent
467e736147
commit
14246afd19
19
src/core/migrations/0027_order_subscription.py
Normal file
19
src/core/migrations/0027_order_subscription.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.0.2 on 2022-12-30 16:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0026_orderline_product'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='subscription',
|
||||||
|
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='core.subscription'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -382,6 +382,13 @@ class Order(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
subscription = models.ForeignKey(
|
||||||
|
'Subscription',
|
||||||
|
related_name='orders',
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@ -548,13 +555,59 @@ class Subscription(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
created_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def convert_int_to_decimal(self, price):
|
||||||
|
return Decimal(str(price)[:-2] + '.' + str(price)[-2:])
|
||||||
|
|
||||||
|
def format_product(self, data):
|
||||||
|
return {
|
||||||
|
'product': Product.objects.get(pk=data['pk']),
|
||||||
|
'quantity': data['quantity']
|
||||||
|
}
|
||||||
|
|
||||||
|
def deserialize_subscription(self, data):
|
||||||
|
subscription = {}
|
||||||
|
|
||||||
|
for x in data:
|
||||||
|
if 'products_and_quantities' in x['metadata']:
|
||||||
|
subscription['unit_price'] = self.convert_int_to_decimal(x['price']['unit_amount'])
|
||||||
|
|
||||||
|
if 'Shipping' in x['description']:
|
||||||
|
subscription['shipping_cost'] = self.convert_int_to_decimal(x['amount'])
|
||||||
|
|
||||||
|
return subscription
|
||||||
|
|
||||||
def create_order(self, data_object):
|
def create_order(self, data_object):
|
||||||
pass
|
subscription = self.deserialize_subscription(data_object['lines']['data'])
|
||||||
|
subscription['items'] = map(self.format_product, self.metadata['products_and_quantities'])
|
||||||
|
subscription['customer_note'] = f"Grind: {self.metadata['grind']}"
|
||||||
|
|
||||||
|
order = Order.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
status=OrderStatus.UNFULFILLED,
|
||||||
|
shipping_address=self.shipping_address,
|
||||||
|
subtotal_amount=self.convert_int_to_decimal(data_object['subtotal']),
|
||||||
|
shipping_total=subscription['shipping_cost'],
|
||||||
|
total_amount=self.convert_int_to_decimal(data_object['total']),
|
||||||
|
weight=self.total_weight,
|
||||||
|
subscription=self
|
||||||
|
)
|
||||||
|
|
||||||
|
bulk_lines = [OrderLine(
|
||||||
|
order=order,
|
||||||
|
product=item['product'],
|
||||||
|
quantity=item['quantity'],
|
||||||
|
customer_note=subscription['customer_note'],
|
||||||
|
unit_price=subscription['unit_price']
|
||||||
|
) for item in subscription['items']]
|
||||||
|
OrderLine.objects.bulk_create(bulk_lines)
|
||||||
|
|
||||||
def format_metadata(self):
|
def format_metadata(self):
|
||||||
metadata = {}
|
metadata = {}
|
||||||
for key, value in self.metadata.items():
|
for key, value in self.metadata.items():
|
||||||
|
if 'products_and_quantities' in key:
|
||||||
metadata[key] = json.dumps(value)
|
metadata[key] = json.dumps(value)
|
||||||
|
else:
|
||||||
|
metadata[key] = value
|
||||||
metadata['subscription_pk'] = self.pk
|
metadata['subscription_pk'] = self.pk
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|||||||
@ -28,38 +28,28 @@ def format_product(data, unit_price):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def find_products(data):
|
|
||||||
for x in data:
|
|
||||||
if 'products_and_quantities' in x['metadata']:
|
|
||||||
return map(format_product, x['metadata']['products_and_quantities'])
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
shipping_cost = None
|
|
||||||
unit_price = None
|
|
||||||
items = None
|
|
||||||
customer_note = ''
|
|
||||||
|
|
||||||
|
|
||||||
def deserialize_subscription(data):
|
def deserialize_subscription(data):
|
||||||
|
sub_data = {}
|
||||||
|
|
||||||
for x in data:
|
for x in data:
|
||||||
if 'products_and_quantities' in x['metadata']:
|
if 'products_and_quantities' in x['metadata']:
|
||||||
customer_note = f"Grind: {x['metadata']['grind']}"
|
sub_data['customer_note'] = f"Grind: {x['metadata']['grind']}"
|
||||||
unit_price = convert_int_to_decimal(x['price']['unit_amount'])
|
sub_data['unit_price'] = convert_int_to_decimal(x['price']['unit_amount'])
|
||||||
items = map(format_product, x['metadata']['products_and_quantities'])
|
sub_data['items'] = map(format_product, x['metadata']['products_and_quantities'])
|
||||||
|
sub_data['total_weight'] = x['metadata']['total_weight']
|
||||||
|
|
||||||
if x['description'] == 'Shipping':
|
if x['description'] == 'Shipping':
|
||||||
shipping_cost = convert_int_to_decimal(x['amount'])
|
sub_data['shipping_cost'] = convert_int_to_decimal(x['amount'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
return sub_data
|
||||||
|
|
||||||
|
|
||||||
# shipping_cost = find_shipping_cost(data_object['lines']['data'])
|
# shipping_cost = find_shipping_cost(data_object['lines']['data'])
|
||||||
# items = find_products(data_object['lines']['data'])
|
# items = find_products(data_object['lines']['data'])
|
||||||
# unit_price = find_unit_price(data_object['lines']['data'])
|
# unit_price = find_unit_price(data_object['lines']['data'])
|
||||||
|
|
||||||
deserialize_subscription(data_object['lines']['data'])
|
sub_data = deserialize_subscription(data_object['lines']['data'])
|
||||||
|
|
||||||
order = Order.objects.create(
|
order = Order.objects.create(
|
||||||
customer=,
|
customer=,
|
||||||
@ -76,9 +66,9 @@ order.lines.add(
|
|||||||
[OrderLine(
|
[OrderLine(
|
||||||
product=item['product'],
|
product=item['product'],
|
||||||
quantity=item['quantity'],
|
quantity=item['quantity'],
|
||||||
customer_note='Grind: ',
|
customer_note=sub_data['customer_note'],
|
||||||
unit_price=unit_price
|
unit_price=sub_data['unit_price']
|
||||||
) for item in items]
|
) for item in sub_data['items']]
|
||||||
)
|
)
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% for item in order.lines.all %}
|
{% for item in order.lines.all %}
|
||||||
<div class="object__item object__item--col5">
|
<div class="object__item object__item--col5">
|
||||||
|
{% if item.variant %}
|
||||||
{% with product=item.variant.product %}
|
{% with product=item.variant.product %}
|
||||||
<figure class="item__figure">
|
<figure class="item__figure">
|
||||||
{% if item.variant.image %}
|
{% if item.variant.image %}
|
||||||
@ -37,6 +38,22 @@
|
|||||||
<span>${{item.unit_price}}</span>
|
<span>${{item.unit_price}}</span>
|
||||||
<span>${{item.get_total}}</span>
|
<span>${{item.get_total}}</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% elif item.product %}
|
||||||
|
{% with product=item.product %}
|
||||||
|
<figure class="item__figure">
|
||||||
|
{% if item.variant.image %}
|
||||||
|
<img class="item__image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
|
<img class="item__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
|
<figcaption><strong>{{item.product}}</strong><br>{{item.customer_note}}</figcaption>
|
||||||
|
</figure>
|
||||||
|
<span>{{product.sku}}</span>
|
||||||
|
<span>{{item.quantity}}</span>
|
||||||
|
<span>${{item.unit_price}}</span>
|
||||||
|
<span>${{item.get_total}}</span>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p>No items in order yet.</p>
|
<p>No items in order yet.</p>
|
||||||
@ -114,6 +131,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if order.subscription %}
|
||||||
|
<section class="object__panel">
|
||||||
|
<div class="object__item panel__header">
|
||||||
|
<h4>Subscription</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel__item">
|
||||||
|
<p>{{ order.subscription.stripe_id }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
<section class="object__panel">
|
<section class="object__panel">
|
||||||
<div class="object__item panel__header">
|
<div class="object__item panel__header">
|
||||||
<h4>Transaction</h4>
|
<h4>Transaction</h4>
|
||||||
@ -124,5 +151,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@ -57,6 +57,29 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{% if customer.subscriptions.count > 0 %}
|
||||||
|
<section>
|
||||||
|
<h3>Your subscriptions</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Subscription #</th>
|
||||||
|
<th colspan="2">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for subscription in subscriptions %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ subscription.id }}</td>
|
||||||
|
<td>{{ subscription.status }}</td>
|
||||||
|
<td><a href="https://dashboard.stripe.com/test/subscriptions/{{ subscription.id }}">manage subscription ↗</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
<section>
|
<section>
|
||||||
<h3>Your orders</h3>
|
<h3>Your orders</h3>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for item in order.lines.all %}
|
{% for item in order.lines.all %}
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if item.variant %}
|
||||||
{% with product=item.variant.product %}
|
{% with product=item.variant.product %}
|
||||||
<td>
|
<td>
|
||||||
{% if item.variant.image %}
|
{% if item.variant.image %}
|
||||||
@ -37,6 +38,24 @@
|
|||||||
<td>${{item.unit_price}}</td>
|
<td>${{item.unit_price}}</td>
|
||||||
<td>${{item.get_total}}</td>
|
<td>${{item.get_total}}</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
{% elif item.product %}
|
||||||
|
{% with product=item.product %}
|
||||||
|
<td>
|
||||||
|
{% if item.variant.image %}
|
||||||
|
<img class="line__image" src="{{item.variant.image.image.url}}" alt="{{item.variant.image.image}}">
|
||||||
|
{% else %}
|
||||||
|
<img class="line__image" src="{{product.get_first_img.image.url}}" alt="{{product.get_first_img.image}}">
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{{ item.product }}</strong><br>
|
||||||
|
{{item.customer_note}}
|
||||||
|
</td>
|
||||||
|
<td>{{item.quantity}}</td>
|
||||||
|
<td>${{item.unit_price}}</td>
|
||||||
|
<td>${{item.get_total}}</td>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -127,22 +127,5 @@ urlpatterns = [
|
|||||||
views.SubscriptionDoneView.as_view(),
|
views.SubscriptionDoneView.as_view(),
|
||||||
name='subscription-done'
|
name='subscription-done'
|
||||||
),
|
),
|
||||||
path('<int:pk>/', include([
|
|
||||||
path(
|
|
||||||
'',
|
|
||||||
views.SubscriptionDetailView.as_view(),
|
|
||||||
name='subscription-detail'
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
'update/',
|
|
||||||
views.SubscriptionUpdateView.as_view(),
|
|
||||||
name='subscription-update'
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
'delete/',
|
|
||||||
views.SubscriptionDeleteView.as_view(),
|
|
||||||
name='subscription-delete'
|
|
||||||
),
|
|
||||||
])),
|
|
||||||
])),
|
])),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -437,6 +437,10 @@ class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
|
|||||||
context['order_list'] = Order.objects.without_drafts().filter(
|
context['order_list'] = Order.objects.without_drafts().filter(
|
||||||
customer=self.object
|
customer=self.object
|
||||||
).prefetch_related('lines')
|
).prefetch_related('lines')
|
||||||
|
subscriptions = []
|
||||||
|
if self.object.stripe_id is not None:
|
||||||
|
subscriptions = stripe.Subscription.list(customer=self.object.stripe_id)['data']
|
||||||
|
context['subscriptions'] = subscriptions
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
@ -720,7 +724,7 @@ class SubscriptionCreateView(SuccessMessageMixin, CreateView):
|
|||||||
session = stripe.checkout.Session.create(
|
session = stripe.checkout.Session.create(
|
||||||
customer=self.object.customer.get_or_create_stripe_id(),
|
customer=self.object.customer.get_or_create_stripe_id(),
|
||||||
success_url='http://' + Site.objects.get_current().domain + reverse(
|
success_url='http://' + Site.objects.get_current().domain + reverse(
|
||||||
'storefront:subscription-detail', kwargs={'pk': self.object.pk}
|
'storefront:subscription-done'
|
||||||
) + '?session_id={CHECKOUT_SESSION_ID}',
|
) + '?session_id={CHECKOUT_SESSION_ID}',
|
||||||
cancel_url='http://' + Site.objects.get_current().domain + reverse(
|
cancel_url='http://' + Site.objects.get_current().domain + reverse(
|
||||||
'storefront:subscription-create'),
|
'storefront:subscription-create'),
|
||||||
@ -743,27 +747,10 @@ class SubscriptionCreateView(SuccessMessageMixin, CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionDetailView(DetailView):
|
|
||||||
model = Subscription
|
|
||||||
template_name = 'storefront/subscription/detail.html'
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionDoneView(TemplateView):
|
class SubscriptionDoneView(TemplateView):
|
||||||
template_name = 'storefront/subscription/done.html'
|
template_name = 'storefront/subscription/done.html'
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionUpdateView(SuccessMessageMixin, UpdateView):
|
|
||||||
model = Subscription
|
|
||||||
success_message = 'Subscription saved.'
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionDeleteView(SuccessMessageMixin, DeleteView):
|
|
||||||
model = Subscription
|
|
||||||
success_message = 'Subscription deleted.'
|
|
||||||
success_url = reverse_lazy('subscription-list')
|
|
||||||
|
|
||||||
|
|
||||||
# stripe listen --forward-to localhost:8000/stripe-webhook
|
# stripe listen --forward-to localhost:8000/stripe-webhook
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@require_POST
|
@require_POST
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user