From 13c8436d98b7063052537cbd8385e03440b4ecf6 Mon Sep 17 00:00:00 2001 From: Matthew Northcott Date: Thu, 20 Apr 2023 14:59:00 +1200 Subject: [PATCH] [#41] Allow users to download the user/planng guide for a payment - backend changes to support physical and digital checkouts --- backend/right_tree/api/admin.py | 6 +- backend/right_tree/api/filters.py | 94 +++-------- .../api/migrations/0017_auto_20230420_1457.py | 50 ++++++ backend/right_tree/api/models.py | 23 +++ .../api/resource_generation_utils.py | 84 ++++------ backend/right_tree/api/serializers.py | 2 +- backend/right_tree/api/signals.py | 1 + backend/right_tree/api/views.py | 158 ++++++++++++------ backend/right_tree/settings.py | 5 +- docker-compose.production.yaml | 1 + docker-compose.yaml | 10 -- 11 files changed, 255 insertions(+), 179 deletions(-) create mode 100644 backend/right_tree/api/migrations/0017_auto_20230420_1457.py diff --git a/backend/right_tree/api/admin.py b/backend/right_tree/api/admin.py index 7c017de..e7b3018 100644 --- a/backend/right_tree/api/admin.py +++ b/backend/right_tree/api/admin.py @@ -36,12 +36,12 @@ class ActivationKeySetAdmin(admin.ModelAdmin): class ActivationKeyAdmin(admin.ModelAdmin): - list_display = ['key', 'creation_date', 'key_set', 'remaining_activations'] + list_display = ['key', 'creation_date', 'key_set', 'activations', 'remaining_activations'] list_filter = ['creation_date', 'key_set'] class QuestionnaireAdmin(admin.ModelAdmin): - list_display = ['address_display', 'location_display', 'soil_variant', 'ecological_district_display', 'habitat', 'zone', 'key_set_display'] + list_display = ['address_display', 'location_display', 'soil_variant', 'ecological_district_display', 'habitat', 'zone', 'key_set_display', 'creation_date'] list_filter = [ActivationKeySetFilter] actions = ['export'] @@ -134,3 +134,5 @@ admin.site.register(models.ActivationKey, ActivationKeyAdmin) admin.site.register(models.ActivationKeySet, ActivationKeySetAdmin) admin.site.register(models.Questionnaire, QuestionnaireAdmin) admin.site.register(models.Export, ExportAdmin) +admin.site.register(models.Customer) +admin.site.register(models.CustomerAddress) diff --git a/backend/right_tree/api/filters.py b/backend/right_tree/api/filters.py index ee5f988..96b52c0 100644 --- a/backend/right_tree/api/filters.py +++ b/backend/right_tree/api/filters.py @@ -1,64 +1,7 @@ -import json - -from django.http import Http404 -from django.db.models import Q - -from .models import Plant, EcologicalRegion, EcologicalDistrictLayer, ChristchurchRegion, SoilOrder, SoilVariant +from .models import Plant, EcologicalRegion, EcologicalDistrictLayer, ChristchurchRegion, SoilOrder, SoilVariant, ActivationKey, Questionnaire from .wms_utils import get_point_from_coordinates -def coordinate_filter(request, queryset, ignore_soil_order=False): - coordinates = request.query_params.get('coordinates') - - if coordinates is not None: - pnt = get_point_from_coordinates(coordinates) - filtered_regions = EcologicalRegion.objects.filter( - ecologicaldistrictlayer__geom__intersects=pnt).values_list('id', flat=True) - filtered_soil_orders = SoilOrder.objects.filter( - soillayer__geom__intersects=pnt).values_list('id', flat=True) - - # Filter by ecological regions and soil orders - if ignore_soil_order: - return queryset.filter(ecological_regions__in=filtered_regions).distinct() - else: - return queryset.filter( - Q(ecological_regions__in=filtered_regions) & - Q(soil_order__in=filtered_soil_orders)).distinct() - - return queryset - - -def soil_variant_filter(request, queryset): - soil_variant = request.query_params.get('soilVariant') - - if soil_variant in {"D", "W", "M"}: - soil_variant_ids = SoilVariant.objects.filter(Q(name__startswith=soil_variant) | Q( - name__startswith="M")).values_list('id', flat=True).distinct() - return queryset.filter(soil_variants__in=soil_variant_ids).distinct() - - return queryset - - -def zone_filter(zone_json, queryset): - return queryset.filter(zones__id__contains=zone_json['id']).distinct() - - -def soil_order_coordinate_filter(coordinates): - pnt = get_point_from_coordinates(coordinates) - try: - return SoilOrder.objects.filter(soillayer__geom__intersects=pnt) - except SoilOrder.DoesNotExist: - raise Http404(f"Soil Order cannot be found for point {pnt}") - - -def ecological_district_coordinate_filter(coordinates): - pnt = get_point_from_coordinates(coordinates) - try: - return EcologicalDistrictLayer.objects.filter(geom__intersects=pnt) - except EcologicalDistrictLayer.DoesNotExist: - raise Http404( - f"Ecological district layer cannot be found for point {pnt}") - def is_in_auckland(coordinates): pnt = get_point_from_coordinates(coordinates) eco_district = EcologicalDistrictLayer.objects.filter(geom__intersects=pnt).first() @@ -68,22 +11,31 @@ def is_in_auckland(coordinates): def is_in_christchurch(coordinates): pnt = get_point_from_coordinates(coordinates) in_chch = ChristchurchRegion.objects.filter(geom__intersects=pnt).first() - return in_chch is not None; + return in_chch is not None def get_filtered_plants(request): - filtered_plants = Plant.objects.all() + try: + ak = ActivationKey.objects.get(key=request.query_params['key']) + except (KeyError, ActivationKey.DoesNotExist): + raise ValueError("Invalid key") - zone = request.query_params.get('zone') - if zone != None: - zone_json = json.loads(zone) - filtered_plants = zone_filter(zone_json, filtered_plants) + if not (q := Questionnaire.objects.filter(key=ak).order_by("-creation_date").first()): + return Plant.objects.none() - if not zone_json['ignore_location_filter']: - filtered_plants = coordinate_filter( - request, filtered_plants, ignore_soil_order=zone_json['ignore_soil_order_filter']) - else: - filtered_plants = coordinate_filter(request, filtered_plants) + regions = EcologicalRegion.objects.filter(ecologicaldistrictlayer__geom__intersects=q.location).distinct() + soils = ( + SoilVariant.objects.filter(name="Mesic") | + SoilVariant.objects.filter(name__istartswith=q.soil_variant) + ).distinct() - filtered_plants = soil_variant_filter(request, filtered_plants) + qs = Plant.objects.filter( + zones__in=[q.zone], + ecological_regions__in=regions, + soil_variants__in=soils, + ) - return filtered_plants + if not q.zone.ignore_soil_order_filter: + orders = SoilOrder.objects.filter(soillayer__geom__intersects=q.location).distinct() + qs = qs.filter(soil_order__in=orders).distinct() + + return qs.distinct() diff --git a/backend/right_tree/api/migrations/0017_auto_20230420_1457.py b/backend/right_tree/api/migrations/0017_auto_20230420_1457.py new file mode 100644 index 0000000..13cb9d2 --- /dev/null +++ b/backend/right_tree/api/migrations/0017_auto_20230420_1457.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.17 on 2023-04-20 02:57 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0016_alter_activationkey_key_set'), + ] + + operations = [ + migrations.CreateModel( + name='CustomerAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('city', models.CharField(max_length=100)), + ('line1', models.CharField(max_length=255)), + ('line2', models.CharField(blank=True, max_length=255, null=True)), + ('postal_code', models.SmallIntegerField()), + ], + ), + migrations.AddField( + model_name='activationkey', + name='activations', + field=models.SmallIntegerField(default=0), + ), + migrations.AddField( + model_name='questionnaire', + name='creation_date', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('name', models.CharField(max_length=100)), + ('address', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.customeraddress')), + ], + ), + migrations.AddField( + model_name='activationkey', + name='customer', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='api.customer'), + ), + ] diff --git a/backend/right_tree/api/models.py b/backend/right_tree/api/models.py index 281a348..55a4257 100644 --- a/backend/right_tree/api/models.py +++ b/backend/right_tree/api/models.py @@ -202,6 +202,26 @@ class Address(models.Model): managed = False +class CustomerAddress(models.Model): + city = models.CharField(max_length=100) + line1 = models.CharField(max_length=255) + line2 = models.CharField(max_length=255, blank=True, null=True) + postal_code = models.SmallIntegerField() + + def __str__(self): + return f"{self.line1}, {self.line2}, {self.city} {self.postal_code}" \ + if self.line2 else f"{self.line1}, {self.city} {self.postal_code}" + + +class Customer(models.Model): + email = models.EmailField() + name = models.CharField(max_length=100) + address = models.ForeignKey(CustomerAddress, on_delete=models.PROTECT) + + def __str__(self): + return self.name + + class ActivationKeySet(models.Model): creation_date = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=255, unique=True) @@ -223,8 +243,10 @@ class ActivationKey(models.Model): key = models.CharField(max_length=20, unique=True, default=key_default) key_set = models.ForeignKey(ActivationKeySet, on_delete=models.PROTECT, null=True) + activations = models.SmallIntegerField(default=0) remaining_activations = models.SmallIntegerField(default=1) creation_date = models.DateTimeField(auto_now_add=True) + customer = models.ForeignKey(Customer, on_delete=models.PROTECT, null=True) def __str__(self): return self.key @@ -235,6 +257,7 @@ class Questionnaire(models.Model): soil_variant = models.ForeignKey(SoilVariant, on_delete=models.CASCADE) zone = models.ForeignKey(Zone, on_delete=models.CASCADE) key = models.ForeignKey(ActivationKey, on_delete=models.PROTECT, null=True) + creation_date = models.DateTimeField(auto_now_add=True) @property def habitat(self): diff --git a/backend/right_tree/api/resource_generation_utils.py b/backend/right_tree/api/resource_generation_utils.py index facc39c..5f3b9e1 100644 --- a/backend/right_tree/api/resource_generation_utils.py +++ b/backend/right_tree/api/resource_generation_utils.py @@ -2,8 +2,9 @@ import csv from io import StringIO from os.path import splitext -from .filters import * -from .wms_utils import get_address_from_coordinates, get_point_from_coordinates +from .filters import is_in_christchurch, is_in_auckland +from .models import EcologicalDistrictLayer, SoilOrder, Questionnaire, ActivationKey +from .wms_utils import get_address_from_coordinates import pdfkit import pandas as pd @@ -22,75 +23,58 @@ HEADER_FIELDS = ['Names', 'Growth Form / Max Height (m) / Spacing (m) / Forest P 'Tolerances (Water / Drought / Frost / Salinity)', 'Ecosystem Services', 'Carbon Sequestration Rate', 'Planting Stage'] -def get_location_filters(params): +def get_location_filters(questionnaire): """ Retrives the selected location data from the request. """ filter_rows = [['LOCATION FILTERS:', ' ']] - coordinates = params.get('coordinates') - if coordinates is not None: - eco_district_layer = ecological_district_coordinate_filter( - coordinates).first() - point = get_point_from_coordinates(coordinates) - address = get_address_from_coordinates(coordinates) + eco_district_layer = EcologicalDistrictLayer.objects.filter(geom__intersects=questionnaire.location).first() + address = get_address_from_coordinates(questionnaire.location) - filter_rows.append(['Point coordinates:', point]) - filter_rows.append(['Ecological region:', eco_district_layer.ecologic_1 or '' ]) - filter_rows.append(['Ecological district:', eco_district_layer.ecologic_2 or ' ']) - filter_rows.append(['Property address:', address['full_address'] if address is not None else ' ']) - else: - filter_rows.append(["None specified", " "]) + filter_rows.append(['Point coordinates:', questionnaire.location]) + filter_rows.append(['Ecological region:', eco_district_layer.ecologic_1 or '' ]) + filter_rows.append(['Ecological district:', eco_district_layer.ecologic_2 or ' ']) + filter_rows.append(['Property address:', address['full_address'] if address is not None else ' ']) return filter_rows -def get_soil_filters(params): +def get_soil_filters(questionnaire): """ Retrives the selected soil type data from the request params. """ filter_rows = [['SOIL FILTERS:', ' ']] - soil_variant = params.get('soilVariant') - coordinates = params.get('coordinates') - if soil_variant is not None and coordinates is not None: - soil_order_obj = soil_order_coordinate_filter(coordinates).first() + soil_order = SoilOrder.objects.filter(soillayer__geom__intersects=questionnaire.location).first() - filter_rows.append(['Soil Order:', f"{soil_order_obj.name or ' '} ({soil_order_obj.code or ' '})"]) - filter_rows.append(['Soil Variant:', soil_variant]) - else: - filter_rows.append(["None specified", " "]) + filter_rows.append(['Soil Order:', f"{soil_order.name or ' '} ({soil_order.code or ' '})"]) + filter_rows.append(['Soil Variant:', questionnaire.soil_variant]) return filter_rows -def get_site_filters(params): +def get_site_filters(questionnaire): """ Retrives the selected site data from the request params """ filter_rows = [['SITE FILTERS:', ' ']] - habitat = params.get('habitat') - zone = params.get('zone') - if zone is not None and habitat is not None: - habitat_json = json.loads(habitat) - zone_json = json.loads(zone) - filter_rows.append(['Habitat:', habitat_json.get("name", " ")]) - filter_rows.append(['Zone Name:', zone_json.get("name", " ")]) - filter_rows.append(['Zone Variant:', zone_json.get("variant", " ")]) - filter_rows.append(['Zone Refined Variant:', zone_json.get("refined_variant", " ")]) - else: - filter_rows.append(["None specified", " "]) + zone = questionnaire.zone + habitat = zone.habitat + + filter_rows.append(['Habitat:', habitat.name]) + filter_rows.append(['Zone Name:', zone.name]) + filter_rows.append(['Zone Variant:', zone.variant]) + filter_rows.append(['Zone Refined Variant:', zone.refined_variant]) return filter_rows -def get_additional_region_info(params): +def get_additional_region_info(questionnaire): """ If the location coordinates fall within the CHCH or Auckland regions then return a description of where to find more information. """ - coordinates = params.get('coordinates') - if coordinates is not None: - if is_in_christchurch(coordinates): - return [["Your location falls within the ecosystem type covered by the Christchurch Council ecosystem maps - further information can be obtained from Ōtautahi/Christchurch ecosystems map link to: https://ccc.govt.nz/environment/land/ecosystem-map", " "], [' ', ' ']] - elif is_in_auckland(coordinates): - return [["Your location falls within the ecosystem type covered by the Auckland Council Tiaki Tāmaki Makaurau Conservation map - further information can be obtained from tiaki Tāmaki Makaurau conservation Auckland - link to https://www.tiakitamakimakaurau.nz/conservation-map/", " "], [' ', ' ']] + if is_in_christchurch(questionnaire.location): + return [["Your location falls within the ecosystem type covered by the Christchurch Council ecosystem maps - further information can be obtained from Ōtautahi/Christchurch ecosystems map link to: https://ccc.govt.nz/environment/land/ecosystem-map", " "], [' ', ' ']] + elif is_in_auckland(questionnaire.location): + return [["Your location falls within the ecosystem type covered by the Auckland Council Tiaki Tāmaki Makaurau Conservation map - further information can be obtained from tiaki Tāmaki Makaurau conservation Auckland - link to https://www.tiakitamakimakaurau.nz/conservation-map/", " "], [' ', ' ']] return [] @@ -99,19 +83,25 @@ def get_filter_values(params): """ filter_rows = [] + try: + ak = ActivationKey.objects.get(key=params.get("key", "")) + q = Questionnaire.objects.get(key=ak) + except (ActivationKey.DoesNotExist, Questionnaire.DoesNotExist): + return filter_rows + # Add all the location filters - filter_rows += get_location_filters(params) + filter_rows += get_location_filters(q) filter_rows.append([' ', ' ']) # Add the soil filters - filter_rows += get_soil_filters(params) + filter_rows += get_soil_filters(q) filter_rows.append([' ', ' ']) # Add the project site filters - filter_rows += get_site_filters(params) + filter_rows += get_site_filters(q) filter_rows.append([' ', ' ']) - filter_rows += get_additional_region_info(params) + filter_rows += get_additional_region_info(q) return filter_rows diff --git a/backend/right_tree/api/serializers.py b/backend/right_tree/api/serializers.py index fd49f4a..f8e8802 100644 --- a/backend/right_tree/api/serializers.py +++ b/backend/right_tree/api/serializers.py @@ -104,7 +104,7 @@ class QuestionnaireSerializer(serializers.ModelSerializer): class Meta: model = Questionnaire - fields = '__all__' + exclude = ['id'] def validate_soil_variant(self, value): try: diff --git a/backend/right_tree/api/signals.py b/backend/right_tree/api/signals.py index 3ed6abf..c96d53d 100644 --- a/backend/right_tree/api/signals.py +++ b/backend/right_tree/api/signals.py @@ -32,4 +32,5 @@ def activate_key(sender, instance, created, *args, **kwargs): """Consume one activation on the key associated with the created Questionnaire""" if created and (key := instance.key): key.remaining_activations -= 1 + key.activations += 1 key.save() diff --git a/backend/right_tree/api/views.py b/backend/right_tree/api/views.py index 41baaca..8047d12 100644 --- a/backend/right_tree/api/views.py +++ b/backend/right_tree/api/views.py @@ -4,20 +4,20 @@ import stripe from datetime import timedelta from django.conf import settings -from django.http import HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseBadRequest, FileResponse +from django.contrib.gis.geos import Point +from django.http import JsonResponse, HttpResponseNotAllowed, HttpResponseNotFound, HttpResponseBadRequest, FileResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils import timezone from django.utils.text import slugify from rest_framework import viewsets, permissions -from rest_framework.decorators import action from rest_framework.response import Response -from .models import Habitat, HabitatImage, Plant, EcologicalDistrictLayer, SoilOrder, Zone, Questionnaire, ActivationKey, ActivationKeySet +from .models import Habitat, HabitatImage, Plant, EcologicalDistrictLayer, SoilOrder, Zone, Questionnaire, ActivationKey, ActivationKeySet, Customer, CustomerAddress from .serializers import HabitatImageSerializer, HabitatSerializer, PlantSerializer, SoilOrderSerializer, EcologicalDistrictLayerSerializer, AddressSerializer, ZoneSerializer, QuestionnaireSerializer from .filters import * from .wms_utils import get_address_from_coordinates, search_address -from .resource_generation_utils import generate_csv, get_filter_values, serialize_plants_queryset, create_planting_guide_pdf, PLANTING_GUIDE_PDF_FILENAME, CSV_FILENAME, storage +from .resource_generation_utils import generate_csv, get_filter_values, serialize_plants_queryset, create_planting_guide_pdf, PLANTING_GUIDE_PDF_FILENAME, storage from .redis import redis_client @@ -42,11 +42,14 @@ class SoilOrderViewSet(viewsets.ModelViewSet): def get_queryset(self): """ Filtering soil order query set by coordinate parameters in the URL. """ - coordinates = self.request.query_params.get('coordinates') - if coordinates is not None: - return soil_order_coordinate_filter(coordinates) + try: + lat = float(self.request.query_params["lat"]) + lng = float(self.request.query_params["lng"]) + except (KeyError, ValueError): + return SoilOrder.objects.all() + + return SoilOrder.objects.filter(soillayer__geom__intersects=Point(lng, lat, srid=4326)) - return SoilOrder.objects.all() class EcologicalDistrictViewSet(viewsets.ModelViewSet): @@ -57,11 +60,13 @@ class EcologicalDistrictViewSet(viewsets.ModelViewSet): def get_queryset(self): """ Filtering ecological district/region query set by coordinate parameters in the URL. """ - coordinates = self.request.query_params.get('coordinates') - if coordinates is not None: - return ecological_district_coordinate_filter(coordinates) + try: + lat = float(self.request.query_params["lat"]) + lng = float(self.request.query_params["lng"]) + except (KeyError, ValueError): + return EcologicalDistrictLayer.objects.all() - return EcologicalDistrictLayer.objects.all() + return EcologicalDistrictLayer.objects.filter(geom__intersects=Point(lng, lat, srid=4326)) class LINZPropertyViewSet(viewsets.ViewSet): @@ -69,32 +74,48 @@ class LINZPropertyViewSet(viewsets.ViewSet): """ def list(self, request): - coordinates = self.request.query_params.get('coordinates') + try: + lat = float(self.request.query_params["lat"]) + lng = float(self.request.query_params["lng"]) + except (KeyError, ValueError): + lat = lng = None + address = self.request.query_params.get('search') if address is not None: results = search_address(address) return Response(results) - elif coordinates is not None: - address_data = get_address_from_coordinates(coordinates) + elif lat and lng: + address_data = get_address_from_coordinates(Point(lng, lat, srid=4326)) serializer = AddressSerializer(address_data) return Response(serializer.data) - else: - return HttpResponseBadRequest("No parameters given.") + + return HttpResponseBadRequest("Invalid parameters.") + class AuckCHCHRegionInformation(viewsets.ViewSet): """ Filtered viewset defining if coordinate falls inside auckland and chch regions. """ def list(self, request): - coordinates = self.request.query_params.get('coordinates') - if coordinates is not None: - in_chch = is_in_christchurch(coordinates) - in_auckland = is_in_auckland(coordinates) - region_details = {"in_chch": in_chch, "in_auckland": in_auckland} - return Response(region_details) - else: - return HttpResponseBadRequest("No coordinate given.") + try: + lat = float(self.request.query_params["lat"]) + lng = float(self.request.query_params["lng"]) + except (KeyError, ValueError): + return HttpResponseBadRequest("Missing or invalid coordinates.") + + p = Point(lng, lat, srid=4326) + in_chch = False + in_auckland = False + + # can avoid computing intersections for Auckland if we use a conditional here + if is_in_christchurch(p): + in_chch = True + elif is_in_auckland(p): + in_auckland = True + + return Response({"in_chch": in_chch, "in_auckland": in_auckland}) + class HabitatViewSet(viewsets.ModelViewSet): """ Viewset for all habitats. @@ -188,35 +209,62 @@ def validate_key(request): return HttpResponseBadRequest("'key' not specified") try: - if ActivationKey.objects.get(key=key).remaining_activations > 0: - return HttpResponse() + ak = ActivationKey.objects.get(key=key) except ActivationKey.DoesNotExist: - pass + return HttpResponseNotFound() - return HttpResponseNotFound() + if ak.remaining_activations > 0: + # valid key, permit entry + return JsonResponse({"type": ak.key_set.name}) + elif ak.activations == 1: + # key has been activated, but can return the existing data from that activation + return JsonResponse(QuestionnaireSerializer(ak.questionnaire_set.first()).data) + + # key has multiple activations, but all are expended + # could return most recent questionnaire but user who uses many-use keys probably doesn't care + return HttpResponseBadRequest() def activate_key(request): """Adds a single activation to a given key if a Stripe payment has succeeded""" - redirect_url = "/apply" - try: - key = request.GET['key'] - stripe_session_id = redis_client.getdel(key).decode() - stripe_session = stripe.checkout.Session.retrieve(stripe_session_id) - status = stripe_session.payment_status - except (KeyError, AttributeError): - return redirect(redirect_url) + stripe.api_key = settings.STRIPE_API_KEY - match status: - case "paid": - ActivationKey.objects.create( - key=key, - key_set=ActivationKeySet.objects.get_or_create(name="Stripe", size=0)[0], - ) - redirect_url += "?key=" + key - case "open": - stripe.checkout.Session.expire(stripe_session_id) + redirect_url = "/" + + key = request.GET['key'] + stripe_session_id = redis_client.get(key).decode() + stripe_session = stripe.checkout.Session.retrieve(stripe_session_id) + is_physical = stripe_session.metadata.get("physical") == "true" + kwargs = {} + + if is_physical: + address, _ = CustomerAddress.objects.get_or_create( + city=stripe_session.customer_details.address['city'], + line1=stripe_session.customer_details.address['line1'], + line2=stripe_session.customer_details.address['line2'], + postal_code=int(stripe_session.customer_details.address['postal_code']), + ) + customer, _ = Customer.objects.get_or_create( + email=stripe_session.customer_details.email, + name=stripe_session.customer_details.name, + address=address, + ) + key_set, _ = ActivationKeySet.objects.get_or_create( + name=settings.STRIPE_PHYSICAL_KEY_SET, + size=0, + ) + kwargs["customer"] = customer + else: + key_set, _ = ActivationKeySet.objects.get_or_create( + name=settings.STRIPE_DIGITAL_KEY_SET, + size=0, + ) + + if stripe_session.payment_status == "paid": + ActivationKey.objects.create(key=key, key_set=key_set, **kwargs) + redis_client.delete(key) + redirect_url += "?key=" + key return redirect(redirect_url) @@ -225,13 +273,28 @@ def purchase_key(request): """Generate a prospective key and redirect to the Stripe payment portal""" stripe.api_key = settings.STRIPE_API_KEY + price_id = settings.STRIPE_DIGITAL_PRICE_ID + extra_kwargs = {} key = ActivationKey.key_default() redirect_url = request.build_absolute_uri(reverse(activate_key)) + f"?key={key}" + + # requesting checkout for physical copy + if request.GET.get("physical", "").lower() in {"t", "true", "y", "yes", "1"}: + price_id = settings.STRIPE_PHYSICAL_PRICE_ID + extra_kwargs = { + 'shipping_address_collection': { + 'allowed_countries': ['NZ'], + }, + 'metadata': { + 'physical': 'true', + }, + } + stripe_session = stripe.checkout.Session.create( line_items=[ { - "price": settings.STRIPE_PRICE_ID, + "price": price_id, "quantity": 1, }, ], @@ -247,6 +310,7 @@ def purchase_key(request): mode='payment', success_url=redirect_url, cancel_url=redirect_url, + **extra_kwargs, ) redis_client.setex(key, timedelta(hours=8), stripe_session.id) diff --git a/backend/right_tree/settings.py b/backend/right_tree/settings.py index a5ef862..7631c31 100644 --- a/backend/right_tree/settings.py +++ b/backend/right_tree/settings.py @@ -164,4 +164,7 @@ CELERY_BROKER_URL = REDIS_CELERY_URL # Stripe payment processing STRIPE_API_KEY = os.environ['STRIPE_API_KEY'] -STRIPE_PRICE_ID = os.environ['STRIPE_PRICE_ID'] +STRIPE_DIGITAL_PRICE_ID = os.environ['STRIPE_DIGITAL_PRICE_ID'] +STRIPE_PHYSICAL_PRICE_ID = os.environ['STRIPE_PHYSICAL_PRICE_ID'] +STRIPE_DIGITAL_KEY_SET = "Stripe - digital" +STRIPE_PHYSICAL_KEY_SET = "Stripe - physical" diff --git a/docker-compose.production.yaml b/docker-compose.production.yaml index 7d8e24b..3cdcacf 100644 --- a/docker-compose.production.yaml +++ b/docker-compose.production.yaml @@ -26,6 +26,7 @@ services: - manage.py - collectstatic - --noinput + restart: on-failure backend: <<: *django diff --git a/docker-compose.yaml b/docker-compose.yaml index 5aebc23..4a8551b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,14 +13,6 @@ x-django: &django restart: unless-stopped services: - collectstatic: - <<: *django - container_name: collectstatic - command: - - python - - manage.py - - collectstatic - - --noinput backend: <<: *django @@ -32,8 +24,6 @@ services: condition: service_healthy celery: condition: service_healthy - collectstatic: - condition: service_completed_successfully expose: - "8000" command: