diff --git a/prices-live.json b/prices-live.json new file mode 100644 index 0000000000000000000000000000000000000000..111c7b5a90f39152f76bd964e9bfae4ab6919f6f --- /dev/null +++ b/prices-live.json @@ -0,0 +1,122 @@ +[ + { + "fields" : { + "display_name" : "TNC25 Regular Pass", + "kind" : "REGISTRATION_TYPE", + "name" : "regular_pass", + "stripe_product_id" : "prod_RZw4xezI1PkDWK", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "1zdvtaxrc" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 1 + }, + { + "fields" : { + "display_name" : "TNC25 Speaker Pass", + "kind" : "REGISTRATION_TYPE", + "name" : "speaker_pass", + "stripe_product_id" : "prod_RZw4iAk7qjAeaT", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "1zdvtaxrf" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 2 + }, + { + "fields" : { + "display_name" : "TNC25 Sponsor Pass", + "kind" : "REGISTRATION_TYPE", + "name" : "sponsor_pass", + "stripe_product_id" : "prod_RZw44phJtblEZv", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "1zegwg39v" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 4 + }, + { + "fields" : { + "display_name" : "Side meeting (Friday)", + "kind" : "EXTRA", + "name" : "side_meeting_pass_friday", + "stripe_product_id" : "prod_RZw4eSzXLC1A2N", + "visit_checkout_answer_id" : "1zeb3k6yy", + "visit_checkout_question_id" : "1zeb2vc8p", + "visit_paid_answer_id" : "1zen12x8z", + "visit_paid_question_id" : "1zen0dy45", + "visit_registration_id" : "" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 5 + }, + { + "fields" : { + "display_name" : "Side meeting (Monday)", + "kind" : "EXTRA", + "name" : "side_meeting_pass_monday", + "stripe_product_id" : "prod_RPrt4MzT2m98d2", + "visit_checkout_answer_id" : "1zeb3k6yx", + "visit_checkout_question_id" : "1zeb2vc8p", + "visit_paid_answer_id" : "1zen12x8y", + "visit_paid_question_id" : "1zen0dy45", + "visit_registration_id" : "" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 6 + }, + { + "fields" : { + "display_name" : "Social Pass (Tuesday)", + "kind" : "EXTRA", + "name" : "social_pass_tuesday", + "stripe_product_id" : "price_1QX2JCDSpyjzuj5pMEiQwFuO", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 7 + }, + { + "fields" : { + "display_name" : "Social Pass (Wednesday)", + "kind" : "EXTRA", + "name" : "social_pass_wednesday", + "stripe_product_id" : "price_1QX2KNDSpyjzuj5pDNrkoH0x", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 8 + }, + { + "fields" : { + "display_name" : "Social Pass (Combined)", + "kind" : "EXTRA", + "name" : "social_pass_all", + "stripe_product_id" : "price_1QX2MJDSpyjzuj5pFUbQUyV4", + "visit_checkout_answer_id" : "", + "visit_checkout_question_id" : "", + "visit_paid_answer_id" : "", + "visit_paid_question_id" : "", + "visit_registration_id" : "" + }, + "model" : "stripe_checkout.priceditem", + "pk" : 9 + } +] diff --git a/prices.json b/prices-test.json similarity index 100% rename from prices.json rename to prices-test.json diff --git a/requirements.txt b/requirements.txt index ca5218b4fbaa21b27fd49630bcb92ac9fe72e11e..4826df6874c9c38f4ad1cd354eceb9446a2d5552 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django<5.1 +django~=4.2.18 dj_database_url==2.3.0 # pin for security jsonschema stripe diff --git a/setup.py b/setup.py index 6ac984a2af5a53e5b45af7a94162ae0531861d02..61aa704f4bd11d4bd4959b064f34b1771324be53 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,14 @@ from setuptools import setup, find_packages setup( name="stripe-checkout", - version="0.5", + version="0.6", author="GEANT", author_email="swd@geant.org", description="Stripe custom checkout support service", url=("TBD"), packages=find_packages(), install_requires=[ - "django<5.1", + "django~=4.2.18", "dj_database_url==2.3.0", # pin for security "jsonschema", "stripe", diff --git a/stripe_checkout/stripe_checkout/management/commands/processevents.py b/stripe_checkout/stripe_checkout/management/commands/processevents.py index fb0a0b5f0fabc4a86a25d04346ac7718930dda28..d9be070e716d8576e4d63d4358fd823a25aec5c2 100644 --- a/stripe_checkout/stripe_checkout/management/commands/processevents.py +++ b/stripe_checkout/stripe_checkout/management/commands/processevents.py @@ -4,10 +4,11 @@ from django.db import transaction from stripe_checkout.stripe_checkout.models import Event, ItemKind, Order from stripe_checkout.stripe_checkout.visit import VisitorAPI -SUCCESS_EVENTS = [ - "invoice.paid", - "payment_intent.succeeded", - "checkout.session.completed", +PAYMENT_INTENT_SUCCEEDED = "payment_intent.succeeded" +PAYMENT_INTENT_CANCELED = "payment_intent.canceled" +VALID_EVENTS = [ + PAYMENT_INTENT_SUCCEEDED, + PAYMENT_INTENT_CANCELED, ] RAISE_EXCEPTIONS = True @@ -37,7 +38,9 @@ class Command(BaseCommand): self.stdout.write(msg) def _handle_event(self, event: Event) -> bool: - if event.type not in SUCCESS_EVENTS: + if event.type not in VALID_EVENTS: + return False + if not event.object: return False order = ( Order.objects.filter(stripe_id=event.stripe_id) @@ -46,12 +49,15 @@ class Command(BaseCommand): ) if order is None: return False - self.update_visit_paid(order) - order.paid = True + + if event.type == PAYMENT_INTENT_SUCCEEDED: + self._handle_pi_succeeded(event.object, order) + if event.type == PAYMENT_INTENT_CANCELED: + self._handle_pi_canceled(event.object, order) order.save() return True - def update_visit_paid(self, order: Order): + def _handle_pi_succeeded(self, payment_intent: dict, order: Order): api = VisitorAPI() visitor = api.get_visitor(order.visitor_id) for item in order.items.all(): @@ -63,13 +69,10 @@ class Command(BaseCommand): visitor.add_answer(question_id, answer_id) api.update_visitor(visitor) + order.paid = True - @staticmethod - def prepare_answers(answers: dict): - return [ - { - "id": question, - "answers": [{"id": a} for a in answers], - } - for question, answers in answers.items() - ] + def _handle_pi_canceled(self, invoice: dict, order: Order): + api = VisitorAPI() + visitor = api.get_visitor(order.visitor_id) + visitor.canceled = True + api.update_visitor(visitor) diff --git a/stripe_checkout/stripe_checkout/migrations/0002_event_alter_priceditem_stripe_id_order.py b/stripe_checkout/stripe_checkout/migrations/0002_event_alter_priceditem_stripe_id_order.py index 59cdb1075f277d27d9c256f303d4f93686ac0d42..94a59bab6232814328d2eed49f9494a12b52ecf1 100644 --- a/stripe_checkout/stripe_checkout/migrations/0002_event_alter_priceditem_stripe_id_order.py +++ b/stripe_checkout/stripe_checkout/migrations/0002_event_alter_priceditem_stripe_id_order.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ), ), ("payload", models.JSONField()), - ("handled", models.BooleanField(db_default=False, default=False)), + ("handled", models.BooleanField(default=False)), ], ), migrations.AlterField( @@ -45,7 +45,7 @@ class Migration(migrations.Migration): ), ("visitor_id", models.CharField(max_length=40)), ("stripe_id", models.CharField(max_length=255)), - ("paid", models.BooleanField(db_default=False, default=False)), + ("paid", models.BooleanField(default=False)), ( "items", models.ManyToManyField( diff --git a/stripe_checkout/stripe_checkout/models.py b/stripe_checkout/stripe_checkout/models.py index b81a4d5947198e6be4a4d8a1d1de1a5f22182874..9fdaf242c8a175f82b63a5ee5cfe76a751e5afe6 100644 --- a/stripe_checkout/stripe_checkout/models.py +++ b/stripe_checkout/stripe_checkout/models.py @@ -23,7 +23,14 @@ class PricedItem(models.Model): name = models.CharField(max_length=255, blank=False, unique=True) display_name = models.CharField(max_length=255, blank=False) - kind = models.CharField(max_length=40, choices=ItemKind, blank=False) + kind = models.CharField( + max_length=40, + choices=[ + ("REGISTRATION_TYPE", "Registration Type"), + ("EXTRA", "Extra"), + ], + blank=False, + ) stripe_product_id = models.CharField(max_length=40, blank=False, unique=True) # The following items is required for ItemKind.REGISTRATION_TYPE @@ -65,7 +72,7 @@ class Order(models.Model): visitor_id = models.CharField(max_length=40, blank=False) stripe_id = models.CharField(max_length=255, blank=False, unique=True) items = models.ManyToManyField(to=PricedItem, related_name="order") - paid = models.BooleanField(default=False, db_default=False) + paid = models.BooleanField(default=False) exchange_rate = models.ForeignKey( ExchangeRate, on_delete=models.SET_NULL, @@ -77,7 +84,7 @@ class Order(models.Model): class Event(models.Model): payload = models.JSONField(blank=False) - handled = models.BooleanField(default=False, db_default=False) + handled = models.BooleanField(default=False) @property def type(self): @@ -88,6 +95,10 @@ class Event(models.Model): if (created := self.payload.get("created")) is not None: return datetime.datetime.fromtimestamp(created) + @property + def object(self): + return self.payload.get("data", {}).get("object", {}) + @property def stripe_id(self): - return self.payload.get("data", {}).get("object", {}).get("id") + return self.object.get("id") diff --git a/stripe_checkout/stripe_checkout/shopping_cart.py b/stripe_checkout/stripe_checkout/shopping_cart.py index 5ddc8430fca47b0f488e69a89601e8df3f7cf8d7..84b3753dbba76930586381a547eb5bcd88efac52 100644 --- a/stripe_checkout/stripe_checkout/shopping_cart.py +++ b/stripe_checkout/stripe_checkout/shopping_cart.py @@ -24,8 +24,11 @@ class ShoppingCart: return cls(items=to_pay) def create_order(self, visitor: dict, stripe_obj: dict): + payment_intent = stripe_obj["payment_intent"] + assert payment_intent is not None + order = models.Order.objects.create( - visitor_id=visitor["id"], stripe_id=stripe_obj["id"] + visitor_id=visitor["id"], stripe_id=payment_intent ) order.items.set(sc_item.item for sc_item in self) return order diff --git a/stripe_checkout/stripe_checkout/stripe.py b/stripe_checkout/stripe_checkout/stripe.py index 9a8fcc386baf4ed9339c4097ea3ea51bdcba6300..5cbe462e3c99494e4764beabfa5eccb94c8fd0c3 100644 --- a/stripe_checkout/stripe_checkout/stripe.py +++ b/stripe_checkout/stripe_checkout/stripe.py @@ -34,12 +34,35 @@ def get_or_create_customer(visitor: Visitor) -> Optional[str]: customer = stripe.Customer.create( name=visitor.full_name, email=visitor.email, - country=visitor["contact"].get("country"), + address=parse_address(visitor.billing_address), ) return customer["id"] + customer_id = result[0]["id"] + stripe.Customer.modify( + customer_id, + name=visitor.full_name, + address=parse_address(visitor.billing_address), + ) + return result[0]["id"] +def parse_address(visit_address: dict): + address_line = visit_address.get("address", "") + if house_number := visit_address.get("houseNumber", ""): + address_line += " " + house_number + if house_number_suffix := visit_address.get("houseNumberSuffix", ""): + address_line += " " + house_number_suffix + + return { + "line1": address_line, + "postal_code": visit_address.get("postalCode", ""), + "city": visit_address.get("city", ""), + "state": visit_address.get("state", ""), + "country": visit_address.get("country", ""), + } + + def create_invoice( shopping_cart: ShoppingCart, customer_id, @@ -57,7 +80,7 @@ def create_invoice( rate = gbp_exchange_rate.rate vat = shopping_cart.vat * rate custom_fields.append( - {"name": "GBP VAT Rate", "value": f"GBP {vat:.2f} ({rate:.4f})"} + {"name": "GBP VAT", "value": f"GBP {vat:.2f} ({rate:.4f})"} ) invoice = stripe.Invoice.create( diff --git a/stripe_checkout/stripe_checkout/visit.py b/stripe_checkout/stripe_checkout/visit.py index 3fbdd320983b8648ca746f8db25f47fbb04e7856..b53950e54164b2bd68f01bfde35f398fbcfa9528 100644 --- a/stripe_checkout/stripe_checkout/visit.py +++ b/stripe_checkout/stripe_checkout/visit.py @@ -1,5 +1,6 @@ from __future__ import annotations from typing import Optional, Union +from django.http import Http404 import requests from requests.auth import HTTPBasicAuth from django.conf import settings @@ -30,10 +31,18 @@ class VisitorAPI: ] def get_visitor(self, visitor_id: str): - return Visitor.from_api( - self._request("get", f"{BASE_URL}/visitors/{self.expo_id}/{visitor_id}") + result = self._request( + "get", f"{BASE_URL}/visitors/{self.expo_id}/{visitor_id}" ) + # Visit has at least one special, fake, visitor ids that instead of returning + # 404, falls back to return the whole visitor list. We need to catch it and + # properly raise + if not isinstance(result, dict): + raise Http404() + + return Visitor.from_api(result) + def update_visitor( self, visitor: Union[str, Visitor], payload: Optional[dict] = None ): @@ -47,9 +56,12 @@ class VisitorAPI: class Visitor(dict): PAID_TAG = "PAID" + CANCELED_TAG = "CANCELLED" # double L since we're british @classmethod def from_api(cls, data): + if isinstance(data, Visitor): + return data return cls(**data) @property @@ -68,17 +80,35 @@ class Visitor(dict): def email(self): return self["contact"].get("email") + @property + def billing_address(self): + for addr in self["contact"]["addresses"]: + if addr.get("type") == "billing": + return addr + return {} + @property def paid(self): return self.PAID_TAG in self.get("tags") @paid.setter def paid(self, value: bool): + self.set_tag(self.PAID_TAG, value) + + @property + def canceled(self): + return self.CANCELED_TAG in self.get("tags") + + @canceled.setter + def canceled(self, value: bool): + self.set_tag(self.CANCELED_TAG, value) + + def set_tag(self, tag: str, active: bool): current_tags = set(self.get("tags", [])) - if value: - self["tags"] = list(current_tags | {self.PAID_TAG}) + if active: + self["tags"] = list(current_tags | {tag}) else: - self["tags"] = list(current_tags - {self.PAID_TAG}) + self["tags"] = list(current_tags - {tag}) def set_registration_type(self, registration_type_id): self["registrationType"] = {"id": registration_type_id} diff --git a/test/conftest.py b/test/conftest.py index c49d56d1ecb0c096758c1e829868017de31534c9..740bfd5d45d666bf34fbc3cf3be591657cf20ba9 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,6 +1,5 @@ import itertools import json -import pathlib import re from unittest.mock import patch @@ -10,18 +9,14 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.test import Client from django.utils import timezone + from stripe_checkout import config from stripe_checkout.stripe_checkout.models import ExchangeRate, ItemKind, PricedItem - -VISIT_RESPONSES_DIR = pathlib.Path(__file__).parent / "visit-responses" +from stripe_checkout.stripe_checkout.visit import Visitor User = get_user_model() -def _load_test_data(filename): - return json.loads((VISIT_RESPONSES_DIR / filename).read_text()) - - @pytest.fixture def uses_db(request): result = any(m.name == "django_db" for m in request.node.own_markers) @@ -70,9 +65,11 @@ def item_data(): @pytest.fixture(autouse=True) def setup_django(uses_db, item_data, config_file): + config.load_config(config_file, settings) + if not uses_db: return - config.load_config(config_file, settings) + for item in item_data: PricedItem.objects.create(**item) @@ -94,12 +91,18 @@ def create_visitor(mock_visit): "firstName": "Some", "lastName": "Visitor", "email": f"{visitor_id}@example.com", - "country": "NL", + "addresses": [ + { + "type": "billing", + "country": "NL", + } + ], }, "tags": [], } - mock_visit.append(payload) - return payload + visitor = Visitor.from_api(payload) + mock_visit.append(visitor) + return visitor return _create_visitor @@ -173,13 +176,16 @@ def mock_stripe(item_data): patch( "stripe.Customer.search", return_value={"data": [{"id": "stripe-cus-id"}]} ), + patch("stripe.Customer.create", return_value={"id": "stripe-cus-id"}), + patch("stripe.Customer.modify", return_value={"id": "stripe-cus-id"}), patch("stripe.Invoice.create", return_value={"id": "stripe-inv-id"}), patch("stripe.InvoiceItem.create", return_value={"id": "stripe-invitem-id"}), patch( "stripe.Invoice.send_invoice", return_value={ - "id": "stripe-invitem-id", + "id": "inv_stripe-inv-id", "hosted_invoice_url": "http://boguspath", + "payment_intent": "pi_some_payment_intent", }, ), patch("stripe.Invoice.add_lines"), @@ -225,9 +231,9 @@ def config_file(stripe_api_key, visit_api_key, visit_expo_id, tmp_path): file.write_text( json.dumps( { - "STRIPE_API_KEY": stripe_api_key, "VISIT_API_KEY": visit_api_key, "VISIT_EXPO_ID": visit_expo_id, + "STRIPE_API_KEY": stripe_api_key, "STRIPE_SIGNING_SECRET": "stripe-signing-secret", "STRIPE_INVOICE_TEMPLATE_ID": "stripe-invoice-template-id", "STRIPE_TAX_RATE_ID": "stripe-tax-rate-id", @@ -238,7 +244,7 @@ def config_file(stripe_api_key, visit_api_key, visit_expo_id, tmp_path): @pytest.fixture -def client(setup_django, mock_visit, mock_stripe): +def client(mock_visit, mock_stripe): return Client() diff --git a/test/test_processevent.py b/test/test_processevent.py index 84f0d4f0d551335876bda545ea6141073bdd06c3..b95ae79daac92d248f2d29bb2fd72d51b9874b83 100644 --- a/test/test_processevent.py +++ b/test/test_processevent.py @@ -6,6 +6,14 @@ from stripe_checkout.stripe_checkout.models import Event, Order, PricedItem from stripe_checkout.stripe_checkout.shopping_cart import ShoppingCart +def a_payment_intent(): + return {"id": "pi_12345"} + + +def an_invoice(): + return {"id": "stripe-invoice", "payment_intent": a_payment_intent()["id"]} + + @pytest.fixture def order(default_visitor): item = PricedItem.objects.get(name="side-meeting-friday") @@ -16,8 +24,7 @@ def order(default_visitor): } ] cart = ShoppingCart.from_visitor(default_visitor) - stripe_obj = {"id": "stripe-invoice"} - order = cart.create_order(default_visitor, stripe_obj=stripe_obj) + order = cart.create_order(default_visitor, stripe_obj=an_invoice()) order.save() return order @@ -32,12 +39,19 @@ def test_ignores_order_without_callback_event(order): @responses.activate @pytest.mark.django_db -def test_processes_order_with_callback_event(order, default_visitor): +@pytest.mark.parametrize( + "event, paid", + [ + ("payment_intent.succeeded", True), + ("payment_intent.canceled", False), + ], +) +def test_processes_order_with_callback_event(event, paid, order, default_visitor): assert not order.paid event = Event.objects.create( payload={ - "type": "invoice.paid", - "data": {"object": {"id": "stripe-invoice"}}, + "type": event, + "data": {"object": a_payment_intent()}, } ) assert not event.handled @@ -47,18 +61,18 @@ def test_processes_order_with_callback_event(order, default_visitor): event = Event.objects.first() assert event.handled order = Order.objects.get(pk=order.pk) - assert order.paid + assert order.paid == paid @responses.activate @pytest.mark.django_db -def test_updates_visitor(order, default_visitor): +def test_updates_visitor_paid(order, default_visitor): assert "PAID" not in default_visitor["tags"] assert len(default_visitor["questions"]) == 1 Event.objects.create( payload={ - "type": "invoice.paid", - "data": {"object": {"id": "stripe-invoice"}}, + "type": "payment_intent.succeeded", + "data": {"object": a_payment_intent()}, } ) call_command("processevents") @@ -69,3 +83,21 @@ def test_updates_visitor(order, default_visitor): "id": "side_meeting_paid", "answers": [{"id": "friday-paid"}], } + + +@responses.activate +@pytest.mark.django_db +def test_updates_visitor_canceled(order, default_visitor): + assert "CANCELLED" not in default_visitor["tags"] + assert "PAID" not in default_visitor["tags"] + + Event.objects.create( + payload={ + "type": "payment_intent.canceled", + "data": {"object": a_payment_intent()}, + } + ) + call_command("processevents") + + assert "PAID" not in default_visitor["tags"] + assert "CANCELLED" in default_visitor["tags"] diff --git a/test/test_shopping_cart.py b/test/test_shopping_cart.py index 622e83515da34f77d5d4d11aff3acda1b4d8edbb..f253cc9fc5208bcaa0155d4ba857fe5aa4d5fcd4 100644 --- a/test/test_shopping_cart.py +++ b/test/test_shopping_cart.py @@ -27,13 +27,13 @@ def test_create_order_with_items(default_visitor): } ] cart = ShoppingCart.from_visitor(default_visitor) - stripe_obj = {"id": "stripe-invoice"} + stripe_obj = {"id": "stripe-invoice", "payment_intent": "pi_12345"} cart.create_order(default_visitor, stripe_obj=stripe_obj) all_orders = Order.objects.all() assert len(all_orders) == 1 order = all_orders[0] assert order.visitor_id == default_visitor["id"] - assert order.stripe_id == stripe_obj["id"] + assert order.stripe_id == stripe_obj["payment_intent"] assert len(order.items.all()) == 2 assert not order.paid diff --git a/test/test_stripe.py b/test/test_stripe.py new file mode 100644 index 0000000000000000000000000000000000000000..694ea2d64651d0baeea036a70da4fd0aa4e08df4 --- /dev/null +++ b/test/test_stripe.py @@ -0,0 +1,61 @@ +import pytest +import stripe +from stripe_checkout.stripe_checkout.stripe import get_or_create_customer + + +@pytest.mark.parametrize( + "visit_addresses, stripe_address", + [ + ([], {}), + ([{"country": "NL", "type": "postal"}], {}), + ( + [ + {"country": "NL", "type": "postal"}, + {"country": "BE", "type": "billing"}, + ], + {"country": "BE"}, + ), + ( + [ + { + "address": "line 1\nline 2", + "city": "cityasdf", + "country": "NL", + "houseNumber": "1234", + "houseNumberSuffix": "A", + "postalCode": "1235AF", + "state": "state-asdf", + "type": "billing", + } + ], + { + "line1": "line 1\nline 2 1234 A", + "postal_code": "1235AF", + "city": "cityasdf", + "state": "state-asdf", + "country": "NL", + }, + ), + ], +) +def test_create_customer_with_billing_address( + visit_addresses, stripe_address, default_visitor, mock_stripe +): + + default_visitor["contact"]["addresses"] = visit_addresses + stripe.Customer.search.return_value = {"data": []} + get_or_create_customer(default_visitor) + assert stripe.Customer.create.call_count == 1 + + full_result = stripe.Customer.create.call_args.kwargs["address"] + non_empty = {key: val for key, val in full_result.items() if val} + assert non_empty == stripe_address + + +def test_updates_visitor_with_address(default_visitor, mock_stripe): + default_visitor["contact"]["addresses"] = [{"country": "BE", "type": "billing"}] + get_or_create_customer(default_visitor) + assert stripe.Customer.modify.call_count == 1 + new_address = stripe.Customer.modify.call_args.kwargs["address"] + non_empty = {key: val for key, val in new_address.items() if val} + assert non_empty == {"country": "BE"} diff --git a/test/test_visit.py b/test/test_visit.py index 535a93d969f115157e260432a486b8d9cd7ee308..4f0206da7f274c7a447156ba14a4514c76bf1ca4 100644 --- a/test/test_visit.py +++ b/test/test_visit.py @@ -26,14 +26,16 @@ def test_update_visitor(client, user, visitor_id): @responses.activate @pytest.mark.django_db def test_create_invoice(client, visitor_id): - """ - test that the create-intent endpoint returns a redirect url - """ assert not Order.objects.count() rv = client.post(f"/checkout/{visitor_id}/", data={"payment_method": "invoice"}) assert rv.status_code == 303 - assert Order.objects.count() == 1 + + orders = Order.objects.all() + assert len(orders) == 1 + + order = orders[0] + assert order.stripe_id.startswith("pi_") @responses.activate @@ -42,7 +44,7 @@ def test_exchange_rate(client, default_exchange_rate, visitor_id): client.post(f"/checkout/{visitor_id}/", data={"payment_method": "invoice"}) call_args = stripe.Invoice.create.call_args[1] assert call_args["custom_fields"][0] == { - "name": "GBP VAT Rate", + "name": "GBP VAT", "value": "GBP 1.60 (0.8000)", } diff --git a/test/test_visit_client.py b/test/test_visit_client.py index 1504bca383abc3aed157f4c4ce734caa69ebf65a..1e00118e0e9aacb66fa8ab460841b348b8d3ac0b 100644 --- a/test/test_visit_client.py +++ b/test/test_visit_client.py @@ -1,4 +1,6 @@ import base64 +import re +from django.http import Http404 import pytest import responses @@ -13,8 +15,17 @@ def api(mock_visit, visit_expo_id, visit_api_key): @responses.activate def test_sends_authorization_header(api, visit_api_key): api.list_visitors() - header = ( - "Basic " - + base64.b64encode(f"{visit_api_key}:".encode()).decode() - ) + header = "Basic " + base64.b64encode(f"{visit_api_key}:".encode()).decode() assert responses.calls[0].request.headers["Authorization"] == header + + +@responses.activate +def test_special_fake_visitor_id_that_return_list(api): + responses.reset() + responses.add( + responses.GET, + re.compile(r"https://api.visitcloud.com/create/v2/visitors/[^/]+/[^/]+$"), + json=[{"id": "12345"}], + ) + with pytest.raises(Http404): + api.get_visitor("fakevisitor")