Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate base membership exists for lab access purchase #525

Merged
merged 5 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions api/src/shop/transactions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from decimal import Decimal, Rounded, localcontext
from logging import getLogger
from typing import Any, Dict, List, Optional, Tuple
Expand Down Expand Up @@ -342,11 +342,14 @@ def payment_success(transaction: Transaction) -> None:

def process_cart(member_id: int, cart: List[CartItem]) -> Tuple[Decimal, List[TransactionContent]]:
contents = []
labaccess_in_cart = False
base_membership_in_cart = False

member = db_session.get(Member, member_id)
if member is None:
raise NotFound(message=f"Could not find member with id {member_id}.")
price_level = get_price_level_for_member(member)
member_has_base_membership = Span.MEMBERSHIP in [span.type for span in member.spans if span.enddate >= date.today()]

with localcontext() as ctx:
ctx.clear_flags()
Expand All @@ -370,7 +373,8 @@ def process_cart(member_id: int, cart: List[CartItem]) -> Tuple[Decimal, List[Tr
what=NEGATIVE_ITEM_COUNT,
)

if product.get_metadata(MakerspaceMetadataKeys.SUBSCRIPTION_TYPE, None) is not None:
subscription_type = product.get_metadata(MakerspaceMetadataKeys.SUBSCRIPTION_TYPE, None)
if subscription_type is not None:
if count != product.smallest_multiple:
raise BadRequest(
f"Bad count for subscription product {product_id}. Count must be exactly {product.smallest_multiple}, was {count}.",
Expand All @@ -387,6 +391,13 @@ def process_cart(member_id: int, cart: List[CartItem]) -> Tuple[Decimal, List[Tr
if product.filter:
PRODUCT_FILTERS[product.filter](item, member_id)

special_product_id = product.get_metadata(MakerspaceMetadataKeys.SPECIAL_PRODUCT_ID, None)
if subscription_type == SubscriptionType.LAB or special_product_id == "single_labaccess_month":
labaccess_in_cart = True

if subscription_type == SubscriptionType.MEMBERSHIP or special_product_id == "single_membership_year":
base_membership_in_cart = True

discount = get_discount_for_product(product, price_level)

amount = Decimal(product.price) * Decimal(count) * (1 - discount.fraction_off)
Expand All @@ -395,6 +406,12 @@ def process_cart(member_id: int, cart: List[CartItem]) -> Tuple[Decimal, List[Tr
content = TransactionContent(product_id=product_id, count=count, amount=amount)
contents.append(content)

validate_labaccess_puchase_with_valid_base_membership(
labaccess_in_cart,
member_has_base_membership,
base_membership_in_cart,
)

if ctx.flags[Rounded]:
# This can possibly happen with huge values, I suppose they will be caught below anyway but it's good to
# catch in any case.
Expand Down Expand Up @@ -431,3 +448,12 @@ def validate_order(
convert_to_stripe_amount(total_amount)

return total_amount, unsaved_contents


def validate_labaccess_puchase_with_valid_base_membership(
labaccess_in_cart, member_has_base_membership, base_membership_in_cart
):
if labaccess_in_cart and not (member_has_base_membership or base_membership_in_cart):
raise BadRequest(
"Could not purchase selected subscription. Please buy the base annual membership before lab access."
)
98 changes: 98 additions & 0 deletions api/src/systest/api/purchase_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from datetime import datetime, timedelta
from logging import getLogger

import stripe
from membership.models import Span
from shop.transactions import CartItem, Purchase
from test_aid.systest_base import VALID_3DS_CARD_NO, VALID_NON_3DS_CARD_NO, ApiShopTestMixin, ApiTest, retry

Expand Down Expand Up @@ -94,3 +96,99 @@ def test_empty_cart_fails_purchase(self) -> None:
)

self.post(f"/webshop/pay", purchase.to_dict(), token=self.token).expect(code=400, what="empty_cart")

def test_labaccess_purchase_fails_without_base_membership(self):
category = self.db.create_category()
lab_access_product = self.db.create_product(
price=575,
unit="mån",
category_id=category.id,
product_metadata={
"allowed_price_levels": ["low_income_discount"],
"special_product_id": "single_labaccess_month",
},
)
purchase = Purchase(
cart=[CartItem(lab_access_product.id, 1)],
expected_sum=lab_access_product.price,
stripe_payment_method_id="not_used",
)
self.db.create_member()
self.post("/webshop/pay", purchase.to_dict(), token=self.token).expect(code=400)

def test_labaccess_purchase_allowed_together_with_base_membership(self):
category = self.db.create_category()
base_membership = self.db.create_product(
price=200,
unit="år",
category_id=category.id,
product_metadata={
"allowed_price_levels": ["low_income_discount"],
"special_product_id": "single_membership_year",
},
)
lab_access_product = self.db.create_product(
price=575,
unit="mån",
category_id=category.id,
product_metadata={
"allowed_price_levels": ["low_income_discount"],
"special_product_id": "single_labaccess_month",
},
)
payment_method = stripe.PaymentMethod.create(type="card", card=self.card(VALID_NON_3DS_CARD_NO))
purchase = Purchase(
cart=[CartItem(lab_access_product.id, 1), CartItem(base_membership.id, 1)],
expected_sum=lab_access_product.price + base_membership.price,
stripe_payment_method_id=payment_method.id,
)
self.db.create_member()
self.post("/webshop/pay", purchase.to_dict(), token=self.token).expect(code=200)

def test_labaccess_purchase_allowed_while_membership_active(self):
category = self.db.create_category()
lab_access_product = self.db.create_product(
price=575,
unit="mån",
category_id=category.id,
product_metadata={
"allowed_price_levels": ["low_income_discount"],
"special_product_id": "single_labaccess_month",
},
)
payment_method = stripe.PaymentMethod.create(type="card", card=self.card(VALID_NON_3DS_CARD_NO))
purchase = Purchase(
cart=[CartItem(lab_access_product.id, 1)],
expected_sum=lab_access_product.price,
stripe_payment_method_id=payment_method.id,
)
member = self.db.create_member()
token = self.db.create_access_token(user_id=member.member_id)
startdate = datetime.utcnow()
enddate = startdate + timedelta(days=30)
self.db.create_span(member=member, type=Span.MEMBERSHIP, startdate=startdate, enddate=enddate)
self.post("/webshop/pay", purchase.to_dict(), token=token.access_token).expect(code=200)

def test_labaccess_purchase_not_allowed_when_membership_expired(self):
category = self.db.create_category()
lab_access_product = self.db.create_product(
price=575,
unit="mån",
category_id=category.id,
product_metadata={
"allowed_price_levels": ["low_income_discount"],
"special_product_id": "single_labaccess_month",
},
)
payment_method = stripe.PaymentMethod.create(type="card", card=self.card(VALID_NON_3DS_CARD_NO))
purchase = Purchase(
cart=[CartItem(lab_access_product.id, 1)],
expected_sum=lab_access_product.price,
stripe_payment_method_id=payment_method.id,
)
member = self.db.create_member()
token = self.db.create_access_token(user_id=member.member_id)
startdate = datetime.utcnow()
enddate = startdate - timedelta(days=30)
self.db.create_span(member=member, type=Span.MEMBERSHIP, startdate=startdate, enddate=enddate)
self.post("/webshop/pay", purchase.to_dict(), token=token.access_token).expect(code=400)
Loading