import logging 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 moneyed import Money, USD 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__) 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__visible_in_listings=True, variants__track_inventory=False) | Q(variants__visible_in_listings=True, variants__track_inventory=True, variants__stock__gt=0) ).prefetch_related( Prefetch( 'variants', queryset=ProductVariant.objects.filter( visible_in_listings=True ).order_by('sorting', 'weight') ) ).distinct() ) ) return object_list class ProductListView(ListView): model = Product template_name = 'storefront/product_list.html' ordering = ['category', 'sorting'] queryset = Product.objects.filter( Q(visible_in_listings=True, category__main_category=True), Q(variants__visible_in_listings=True, variants__track_inventory=False) | Q(variants__visible_in_listings=True, variants__track_inventory=True, variants__stock__gt=0) ).prefetch_related( Prefetch( 'variants', queryset=ProductVariant.objects.filter( visible_in_listings=True ).order_by('sorting', 'weight') ) ).distinct() 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, visible_in_listings=True) | Q( visible_in_listings=True, 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 SubscriptionAdView(TemplateView): template_name = 'storefront/subscription/ad.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(self, request, *args, **kwargs): if not self.request.session.get('subscription'): return HttpResponseRedirect(reverse('storefront:subscription-form')) return super().get(request, *args, **kwargs) 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': Money(subtotal_price, USD), '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. endpoint_secret = settings.STRIPE_WEBHOOK_SECRET payload = request.body sig_header = request.META['HTTP_STRIPE_SIGNATURE'] event = None try: event = stripe.Webhook.construct_event( payload, sig_header, endpoint_secret ) except ValueError as e: # Invalid payload logger.warning('Stripe Webhook: Invalid payload') return JsonResponse({'status': 'ERROR: Invalid payload'}) except stripe.error.SignatureVerificationError as e: # Invalid signature logger.warning('Stripe Webhook: Invalid signature') return JsonResponse({'status': 'ERROR: Invalid signature'}) if event.type == 'customer.subscription.created': try: subscription = Subscription.objects.get( pk=event.data.object['metadata'].get('subscription_pk') ) except Subscription.DoesNotExist: logger.warning('Subscription does not exist') raise else: subscription.stripe_id = event.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=event.data.object['subscription'] ) except Subscription.DoesNotExist: logger.warning('Subscription does not exist') raise else: subscription.create_order(event.data.object) return JsonResponse({'status': 'success'})