import logging import locale import requests import json import stripe from decimal import Decimal from django.conf import settings from django.utils import timezone from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.urls import reverse_lazy from django.core.mail import EmailMessage from django.core.cache import cache from django.contrib.sites.models import Site from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.http import JsonResponse, HttpResponseRedirect from django.views.generic.base import View, RedirectView, TemplateView from django.views.generic.edit import ( FormView, CreateView, UpdateView, DeleteView, FormMixin ) from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.generic.list import ListView from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.messages.views import SuccessMessageMixin from django.contrib import messages from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.forms.models import model_to_dict from django.db.models import ( Exists, OuterRef, Prefetch, Subquery, Count, Sum, Avg, F, Q, Value ) from measurement.measures import Weight from measurement.utils import guess from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from accounts.models import User, Address from accounts.utils import get_or_create_customer from accounts.forms import ( AddressForm as AccountAddressForm, CustomerUpdateForm ) from core.models import ( ProductCategory, Product, ProductVariant, ProductOption, Order, Transaction, OrderLine, Coupon, ShippingRate, Subscription, SiteSettings ) from core.forms import ShippingRateForm from core.shipping import get_shipping_cost from core import OrderStatus, ShippingContainer from .forms import ( AddToCartForm, CartItemUpdateForm, OrderCreateForm, AddressForm, CouponApplyForm, CheckoutShippingForm, SubscriptionForm ) from .cart import CartItem, Cart from .payments import CaptureOrder logger = logging.getLogger(__name__) locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') stripe.api_key = settings.STRIPE_API_KEY class CartView(FormView): template_name = 'storefront/cart_detail.html' form_class = CartItemUpdateForm def get_success_url(self): return reverse('storefront:cart-detail') def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): cart.update_item_quantity( form.cleaned_data['item_index'], form.cleaned_data['quantity'] ) return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['coupon_apply_form'] = CouponApplyForm() return context class CartAddProductView(SingleObjectMixin, FormView): model = Product form_class = AddToCartForm http_method_names = ['post'] def get_success_url(self): return reverse('storefront:cart-detail') def get_form(self, form_class=None): variants = self.get_object().variants.filter( Q(track_inventory=False) | Q( track_inventory=True, stock__gt=0 ) ) options = ProductOption.objects.filter(products__pk=self.get_object().pk) if form_class is None: form_class = self.get_form_class() return form_class(variants, options, **self.get_form_kwargs()) def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): cleaned_data = form.cleaned_data cart.add_item( CartItem({ 'variant_pk': cleaned_data.pop('variant'), 'quantity': cleaned_data.pop('quantity'), 'options': cleaned_data }) ) return self.form_valid(form) else: return self.form_invalid(form) class CartItemUpdateView(FormView): form_class = CartItemUpdateForm http_method_names = ['post'] def get_success_url(self): return reverse('storefront:cart-detail') def post(self, request, *args, **kwargs): cart = Cart(request) form = self.get_form() if form.is_valid(): cart.update_item_quantity( form.cleaned_data['item_index'], form.cleaned_data['quantity'] ) return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): return super().form_valid(form) def cart_remove_product_view(request, pk): cart = Cart(request) cart.remove_item(pk) return redirect('storefront:cart-detail') class CouponApplyView(FormView): form_class = CouponApplyForm success_url = reverse_lazy('storefront:cart-detail') http_method_names = ['post'] def form_valid(self, form): today = timezone.localtime(timezone.now()).date() try: coupon = Coupon.objects.get(code=form.cleaned_data['code']) except Coupon.DoesNotExist: messages.warning(self.request, 'Coupon does not exist.') else: if coupon.is_valid: cart = Cart(self.request) cart.add_coupon(coupon) else: messages.warning(self.request, 'Coupon is invalid.') return super().form_valid(form) class ProductCategoryDetailView(DetailView): model = ProductCategory template_name = 'storefront/category_detail.html' context_object_name = 'category' def get_queryset(self): object_list = ProductCategory.objects.prefetch_related( Prefetch( 'product_set', queryset=Product.objects.filter( Q(visible_in_listings=True), Q(variants__track_inventory=False) | Q(variants__track_inventory=True) & Q(variants__stock__gt=0) ).prefetch_related( Prefetch( 'variants', queryset=ProductVariant.objects.all().order_by('sorting', 'weight') ) ).distinct() ) ) return object_list class ProductListView(ListView): model = Product template_name = 'storefront/product_list.html' ordering = 'sorting' queryset = Product.objects.filter( visible_in_listings=True, category__main_category=True ).prefetch_related( Prefetch( 'variants', queryset=ProductVariant.objects.all().order_by('sorting', 'weight') ) ) class ProductDetailView(FormMixin, DetailView): model = Product template_name = 'storefront/product_detail.html' form_class = AddToCartForm def get_form(self, form_class=None): variants = self.object.variants.filter( Q(track_inventory=False) | Q( track_inventory=True, stock__gt=0 ) ).order_by('sorting', 'name') options = ProductOption.objects.filter(products__pk=self.object.pk) if form_class is None: form_class = self.get_form_class() return form_class(variants, options, **self.get_form_kwargs()) class CheckoutAddressView(FormView): template_name = 'storefront/checkout_address.html' form_class = AddressForm success_url = reverse_lazy('storefront:order-create') def get_initial(self): user = self.request.user initial = None if user.is_authenticated and user.default_shipping_address: address = user.default_shipping_address initial = { 'full_name': address.first_name + ' ' + address.last_name, 'email': user.email, 'street_address_1': address.street_address_1, 'street_address_2': address.street_address_2, 'city': address.city, 'state': address.state, 'postal_code': address.postal_code } elif self.request.session.get('shipping_address'): address = self.request.session.get('shipping_address') initial = { 'full_name': address['first_name'] + ' ' + address['last_name'], 'email': address['email'], 'street_address_1': address['street_address_1'], 'street_address_2': address['street_address_2'], 'city': address['city'], 'state': address['state'], 'postal_code': address['postal_code'] } return initial def form_valid(self, form): # save address data to session cleaned_data = form.cleaned_data first_name, last_name = form.process_full_name( cleaned_data.get('full_name') ) address = { 'first_name': first_name, 'last_name': last_name, 'email': cleaned_data['email'], 'street_address_1': cleaned_data['street_address_1'], 'street_address_2': cleaned_data['street_address_2'], 'city': cleaned_data['city'], 'state': cleaned_data['state'], 'postal_code': cleaned_data['postal_code'] } self.request.session['shipping_address'] = address return super().form_valid(form) class CheckoutShippingView(FormView): template_name = 'storefront/checkout_shipping_form.html' form_class = CheckoutShippingForm success_url = reverse_lazy('storefront:order-create') containers = None def get_containers(self, request): if self.containers is None: cart = Cart(request) self.containers = cart.get_shipping_container() return self.containers def get(self, request, *args, **kwargs): if not self.request.session.get('shipping_address'): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( reverse('storefront:checkout-address') ) site_settings = cache.get('SiteSettings') cart = Cart(self.request) if len(self.get_containers(request)) == 0: self.request.session['shipping_container'] = site_settings.default_shipping_rate return HttpResponseRedirect(self.success_url) elif len(self.get_containers(request)) == 1: self.request.session['shipping_container'] = self.get_containers(request)[0] return HttpResponseRedirect(self.success_url) return super().get(request, *args, **kwargs) def get_form(self, form_class=None): cart = Cart(self.request) for container in self.get_containers(self.request): container.s_cost = cart.get_shipping_price(container.container) if form_class is None: form_class = self.get_form_class() return form_class(self.get_containers(self.request), **self.get_form_kwargs()) def form_valid(self, form): shipping_container = ShippingRate.objects.get( pk=form.cleaned_data.get('shipping_method') ) self.request.session['shipping_container'] = shipping_container return super().form_valid(form) class OrderCreateView(CreateView): model = Order template_name = 'storefront/order_form.html' form_class = OrderCreateForm success_url = reverse_lazy('storefront:payment-done') def get(self, request, *args, **kwargs): if not self.request.session.get('shipping_address'): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( reverse('storefront:checkout-address') ) cart = Cart(request) if cart.coupon is not None: try: user = User.objects.get( email=request.session.get('shipping_address').get('email') ) except User.DoesNotExist: user = None if user in cart.coupon.users.all(): cart.remove_coupon() messages.warning(request, 'Coupon already used.') return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['shipping_address'] = self.request.session.get('shipping_address') context['PAYPAL_CLIENT_ID'] = settings.PAYPAL_CLIENT_ID return context def form_valid(self, form): cart = Cart(self.request) form.instance.subtotal_amount = cart.subtotal_price_after_discount form.instance.coupon = cart.coupon form.instance.coupon_amount = cart.discount_amount form.instance.total_amount = cart.total_price form.instance.weight = cart.total_weight shipping_container = cart.get_shipping_container() form.instance.shipping_total = cart.get_shipping_price(shipping_container) shipping_address = self.request.session.get('shipping_address') form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, shipping_address) form.instance.status = OrderStatus.DRAFT self.object = form.save() bulk_list = cart.build_bulk_list(self.object) objs = OrderLine.objects.bulk_create(bulk_list) response = cart.create_order() data = response.result.__dict__['_dict'] self.request.session['order_id'] = self.object.pk return JsonResponse(data) @csrf_exempt @require_POST def paypal_order_transaction_capture(request, transaction_id): if request.method == "POST": data = CaptureOrder().capture_order(transaction_id) cart = Cart(request) order = Order.objects.get(pk=request.session.get('order_id')) order.status = OrderStatus.UNFULFILLED order.minus_stock() try: coupon = Coupon.objects.get( code=request.session.get('coupon_code') ) except ObjectDoesNotExist: coupon = None if coupon: order.coupon = coupon coupon.users.add(order.customer) order.save() transaction = Transaction.objects.get(order=order) transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id'] transaction.status = data['status'] transaction.save() cart.clear() logger.debug(f'\nPayPal Response data: {data}\n') return JsonResponse(data) else: return JsonResponse({'details': 'invalid request'}) class PaymentDoneView(TemplateView): template_name = 'storefront/payment_done.html' class PaymentCanceledView(TemplateView): template_name = 'storefront/payment_canceled.html' class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView): model = User template_name = 'storefront/customer_detail.html' context_object_name = 'customer' permission_denied_message = 'Not authorized.' raise_exception = True def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['order_list'] = Order.objects.without_drafts().filter( customer=self.object ).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 def test_func(self): return self.request.user.pk == self.get_object().pk class CustomerUpdateView(UserPassesTestMixin, LoginRequiredMixin, UpdateView): model = User template_name = 'storefront/customer_form.html' context_object_name = 'customer' form_class = CustomerUpdateForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.get_object().pk def get_success_url(self): return reverse( 'storefront:customer-detail', kwargs={'pk': self.object.pk} ) class OrderDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView): model = Order template_name = 'storefront/order_detail.html' pk_url_kwarg = 'order_pk' permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.get_object().customer.pk def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context class CustomerAddressCreateView( UserPassesTestMixin, LoginRequiredMixin, CreateView ): model = Address template_name = 'storefront/address_create_form.html' form_class = AccountAddressForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.kwargs['pk'] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context def form_valid(self, form): customer = User.objects.get(pk=self.kwargs['pk']) self.object = form.save() customer.addresses.add(self.object) return super().form_valid(form) def get_success_url(self): return reverse( 'storefront:customer-detail', kwargs={'pk': self.kwargs['pk']} ) class CustomerAddressUpdateView( UserPassesTestMixin, LoginRequiredMixin, UpdateView ): model = Address pk_url_kwarg = 'address_pk' template_name = 'storefront/address_form.html' form_class = AccountAddressForm permission_denied_message = 'Not authorized.' raise_exception = True def test_func(self): return self.request.user.pk == self.kwargs['pk'] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['customer'] = User.objects.get(pk=self.kwargs['pk']) return context def get_success_url(self): return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']}) class AboutView(TemplateView): template_name = 'storefront/about.html' class FairTradeView(TemplateView): template_name = 'storefront/fairtrade.html' class ReviewListView(TemplateView): template_name = 'storefront/reviews.html' class SubscriptionFormView(FormView): template_name = 'storefront/subscription/form.html' form_class = SubscriptionForm success_url = reverse_lazy('storefront:subscription-address') def get_stripe_products(self): # id, name product_list = [{ 'id': product['id'], 'name': product['name'], 'created': product['created'], 'weight_per_item': product['metadata']['weight_per_item'], 'cost': product['metadata']['cost'], 'prices': [] } for product in stripe.Product.list(active=True)] # id, product, recurring.interval_count, recurring.interval, unit_amount price_list = [{ 'id': price['id'], 'product': price['product'], 'interval_count': price.recurring.interval_count, 'interval': price.recurring.interval, 'unit_amount': price['unit_amount'] } for price in stripe.Price.list(active=True)] for prod in product_list: prod['prices'] = list(filter( lambda p: True if p['product'] == prod['id'] else False, price_list )) return sorted(product_list, key=lambda p: p['created']) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['stripe_products'] = self.get_stripe_products() context['product_list'] = Product.objects.filter( visible_in_listings=True, category__name='Coffee' ) return context def form_valid(self, form): self.request.session['subscription'] = { 'items': [{ 'price': form.cleaned_data['stripe_price_id'], 'quantity': form.cleaned_data['total_quantity'] }], 'metadata': { 'grind': form.cleaned_data['grind'], 'total_weight': form.cleaned_data['total_weight'], 'products_and_quantities': json.loads(form.cleaned_data['products_and_quantities']) } } return super().form_valid(form) class SubscriptionAddAddressView(CheckoutAddressView): template_name = 'storefront/subscription/address.html' success_url = reverse_lazy('storefront:subscription-create') class SubscriptionCreateView(SuccessMessageMixin, CreateView): model = Subscription success_message = 'Subscription created.' template_name = 'storefront/subscription/create_form.html' fields = [] def get_item_list(self): item_list = [{ 'product': Product.objects.get(pk=item['pk']), 'quantity': item['quantity'] } for item in self.request.session['subscription']['metadata']['products_and_quantities']] return item_list def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) subscription = self.request.session['subscription'] metadata = subscription['metadata'] price = stripe.Price.retrieve( subscription['items'][0].get('price'), expand=['product'] ) shipping_address = self.request.session.get('shipping_address') weight, unit = metadata['total_weight'].split(':') total_weight = guess(float(weight), unit, measures=[Weight]) subtotal_price = (Decimal(price.unit_amount) * subscription['items'][0]['quantity']) / 100 shipping_cost = get_shipping_cost( total_weight, shipping_address['postal_code'] ) total_price = subtotal_price + shipping_cost context['sub_cart'] = { 'items': self.get_item_list(), 'size': price.product.name, 'grind': metadata['grind'], 'schedule': f'Every {price.recurring.interval_count} / {price.recurring.interval}', 'subtotal_price': locale.currency(subtotal_price), 'shipping_cost': shipping_cost, 'total_price': total_price, 'total_weight': guess(float(weight), unit, measures=[Weight]) } context['shipping_address'] = shipping_address return context def get_line_items(self): line_items = self.object.items recurring = stripe.Price.retrieve( line_items[0].get('price') ).get('recurring') shipping_cost = get_shipping_cost( self.object.total_weight, self.object.shipping_address.postal_code ) * 100 line_items.append({ 'price_data': { 'currency': settings.DEFAULT_CURRENCY.lower(), 'unit_amount': int(shipping_cost), 'product_data': { 'name': 'Shipping' }, 'recurring': { 'interval': recurring.interval, 'interval_count': recurring.interval_count } }, 'quantity': 1 }) return line_items def get_success_url(self): session = stripe.checkout.Session.create( customer=self.object.customer.get_or_create_stripe_id(), success_url='http://' + Site.objects.get_current().domain + reverse( 'storefront:subscription-done' ) + '?session_id={CHECKOUT_SESSION_ID}', cancel_url='http://' + Site.objects.get_current().domain + reverse( 'storefront:subscription-create'), mode='subscription', line_items=self.get_line_items(), subscription_data={'metadata': self.object.format_metadata()} ) return session.url def form_valid(self, form): shipping_address = self.request.session.get('shipping_address') subscription = self.request.session.get('subscription') form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, shipping_address) weight, unit = subscription['metadata']['total_weight'].split(':') form.instance.total_weight = guess( float(weight), unit, measures=[Weight] ) form.instance.items = subscription['items'] form.instance.metadata = subscription['metadata'] return super().form_valid(form) class SubscriptionDoneView(TemplateView): template_name = 'storefront/subscription/done.html' # stripe listen --forward-to localhost:8000/stripe-webhook @csrf_exempt @require_POST def stripe_webhook(request): # You can use webhooks to receive information about asynchronous payment events. # For more about our webhook events check out https://stripe.com/docs/webhooks. webhook_secret = None request_data = json.loads(request.body) if webhook_secret: # Retrieve the event by verifying the signature using the raw body # and secret if webhook signing is configured. signature = request.headers.get('stripe-signature') try: event = stripe.Webhook.construct_event( payload=request.data, sig_header=signature, secret=webhook_secret) data = event['data'] except Exception as e: return e # Get the type of webhook event sent - used to check the status # of PaymentIntents. event_type = event['type'] else: data = request_data['data'] event_type = request_data['type'] data_object = data['object'] # logger.warning('\n') # logger.warning(event_type.upper() + ':\n') # logger.warning(data) # logger.warning('\n') if event_type == 'checkout.session.completed': # Payment is successful and the subscription is created. # You should provision the subscription and save the customer ID to your database. pass if event_type == 'customer.subscription.created': try: subscription = Subscription.objects.get( pk=data_object['metadata'].get('subscription_pk') ) except Subscription.DoesNotExist: logger.warning('Subscription does not exist') raise else: subscription.stripe_id = data_object['id'] subscription.is_active = True subscription.save() if event_type == 'invoice.paid': # Continue to provision the subscription as payments continue to be made. # Store the status in your database and check when a user accesses your service. # This approach helps you avoid hitting rate limits. try: subscription = Subscription.objects.get( stripe_id=data_object['subscription'] ) except Subscription.DoesNotExist: logger.warning('Subscription does not exist') raise else: subscription.create_order(data_object) if event_type == 'invoice.payment_failed': # The payment failed or the customer does not have a valid payment method. # The subscription becomes past_due. Notify your customer and send them to the # customer portal to update their payment information. pass if event_type == 'invoice.created': # Add shipping cost as an item on the invoice # shipping_cost = get_shipping_cost( # self.object.total_weight, # self.request.user.default_shipping_address.postal_code # ) * 100 # stripe.InvoiceItem.create( # customer=data_object['customer'], # subscription=data_object['subscription'], # description='Shipping', # unit_amount=1234, # currency=settings.DEFAULT_CURRENCY.lower() # ) pass # # if event_type == 'checkout.session.completed': # if event_type == 'invoice.paid': # # Used to provision services after the trial has ended. # # The status of the invoice will show up as paid. Store the status in your # # database to reference when a user accesses your service to avoid hitting rate # # limits. # messages.success(request, 'Paid') # logger.warning(data) # if event_type == 'invoice.payment_failed': # # If the payment fails or the customer does not have a valid payment method, # # an invoice.payment_failed event is sent, the subscription becomes past_due. # # Use this webhook to notify your user that their payment has # # failed and to retrieve new card details. # messages.warning(request, 'Payment failed') # logger.warning(data) # if event_type == 'customer.subscription.deleted': # # handle subscription canceled automatically based # # upon your subscription settings. Or if the user cancels it. # messages.error(request, 'Deleted') # logger.warning(data) return JsonResponse({'status': 'success'})