Skip to content

Commit 3080f26

Browse files
authored
Validate base membership exists for lab access purchase (#525)
* Validate base membership exists for lab access purchase * Add tests * :Fix tests * Fix nitpick commit, because coderabbit had a good point * Fix base membership check, move validation logic
1 parent 47f4c1e commit 3080f26

File tree

2 files changed

+126
-2
lines changed

2 files changed

+126
-2
lines changed

api/src/shop/transactions.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from datetime import datetime, timezone
2+
from datetime import date, datetime, timezone
33
from decimal import Decimal, Rounded, localcontext
44
from logging import getLogger
55
from typing import Any, Dict, List, Optional, Tuple
@@ -342,11 +342,14 @@ def payment_success(transaction: Transaction) -> None:
342342

343343
def process_cart(member_id: int, cart: List[CartItem]) -> Tuple[Decimal, List[TransactionContent]]:
344344
contents = []
345+
labaccess_in_cart = False
346+
base_membership_in_cart = False
345347

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

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

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

394+
special_product_id = product.get_metadata(MakerspaceMetadataKeys.SPECIAL_PRODUCT_ID, None)
395+
if subscription_type == SubscriptionType.LAB or special_product_id == "single_labaccess_month":
396+
labaccess_in_cart = True
397+
398+
if subscription_type == SubscriptionType.MEMBERSHIP or special_product_id == "single_membership_year":
399+
base_membership_in_cart = True
400+
390401
discount = get_discount_for_product(product, price_level)
391402

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

409+
validate_labaccess_puchase_with_valid_base_membership(
410+
labaccess_in_cart,
411+
member_has_base_membership,
412+
base_membership_in_cart,
413+
)
414+
398415
if ctx.flags[Rounded]:
399416
# This can possibly happen with huge values, I suppose they will be caught below anyway but it's good to
400417
# catch in any case.
@@ -431,3 +448,12 @@ def validate_order(
431448
convert_to_stripe_amount(total_amount)
432449

433450
return total_amount, unsaved_contents
451+
452+
453+
def validate_labaccess_puchase_with_valid_base_membership(
454+
labaccess_in_cart, member_has_base_membership, base_membership_in_cart
455+
):
456+
if labaccess_in_cart and not (member_has_base_membership or base_membership_in_cart):
457+
raise BadRequest(
458+
"Could not purchase selected subscription. Please buy the base annual membership before lab access."
459+
)

api/src/systest/api/purchase_test.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from datetime import datetime, timedelta
12
from logging import getLogger
23

34
import stripe
5+
from membership.models import Span
46
from shop.transactions import CartItem, Purchase
57
from test_aid.systest_base import VALID_3DS_CARD_NO, VALID_NON_3DS_CARD_NO, ApiShopTestMixin, ApiTest, retry
68

@@ -94,3 +96,99 @@ def test_empty_cart_fails_purchase(self) -> None:
9496
)
9597

9698
self.post(f"/webshop/pay", purchase.to_dict(), token=self.token).expect(code=400, what="empty_cart")
99+
100+
def test_labaccess_purchase_fails_without_base_membership(self):
101+
category = self.db.create_category()
102+
lab_access_product = self.db.create_product(
103+
price=575,
104+
unit="mån",
105+
category_id=category.id,
106+
product_metadata={
107+
"allowed_price_levels": ["low_income_discount"],
108+
"special_product_id": "single_labaccess_month",
109+
},
110+
)
111+
purchase = Purchase(
112+
cart=[CartItem(lab_access_product.id, 1)],
113+
expected_sum=lab_access_product.price,
114+
stripe_payment_method_id="not_used",
115+
)
116+
self.db.create_member()
117+
self.post("/webshop/pay", purchase.to_dict(), token=self.token).expect(code=400)
118+
119+
def test_labaccess_purchase_allowed_together_with_base_membership(self):
120+
category = self.db.create_category()
121+
base_membership = self.db.create_product(
122+
price=200,
123+
unit="år",
124+
category_id=category.id,
125+
product_metadata={
126+
"allowed_price_levels": ["low_income_discount"],
127+
"special_product_id": "single_membership_year",
128+
},
129+
)
130+
lab_access_product = self.db.create_product(
131+
price=575,
132+
unit="mån",
133+
category_id=category.id,
134+
product_metadata={
135+
"allowed_price_levels": ["low_income_discount"],
136+
"special_product_id": "single_labaccess_month",
137+
},
138+
)
139+
payment_method = stripe.PaymentMethod.create(type="card", card=self.card(VALID_NON_3DS_CARD_NO))
140+
purchase = Purchase(
141+
cart=[CartItem(lab_access_product.id, 1), CartItem(base_membership.id, 1)],
142+
expected_sum=lab_access_product.price + base_membership.price,
143+
stripe_payment_method_id=payment_method.id,
144+
)
145+
self.db.create_member()
146+
self.post("/webshop/pay", purchase.to_dict(), token=self.token).expect(code=200)
147+
148+
def test_labaccess_purchase_allowed_while_membership_active(self):
149+
category = self.db.create_category()
150+
lab_access_product = self.db.create_product(
151+
price=575,
152+
unit="mån",
153+
category_id=category.id,
154+
product_metadata={
155+
"allowed_price_levels": ["low_income_discount"],
156+
"special_product_id": "single_labaccess_month",
157+
},
158+
)
159+
payment_method = stripe.PaymentMethod.create(type="card", card=self.card(VALID_NON_3DS_CARD_NO))
160+
purchase = Purchase(
161+
cart=[CartItem(lab_access_product.id, 1)],
162+
expected_sum=lab_access_product.price,
163+
stripe_payment_method_id=payment_method.id,
164+
)
165+
member = self.db.create_member()
166+
token = self.db.create_access_token(user_id=member.member_id)
167+
startdate = datetime.utcnow()
168+
enddate = startdate + timedelta(days=30)
169+
self.db.create_span(member=member, type=Span.MEMBERSHIP, startdate=startdate, enddate=enddate)
170+
self.post("/webshop/pay", purchase.to_dict(), token=token.access_token).expect(code=200)
171+
172+
def test_labaccess_purchase_not_allowed_when_membership_expired(self):
173+
category = self.db.create_category()
174+
lab_access_product = self.db.create_product(
175+
price=575,
176+
unit="mån",
177+
category_id=category.id,
178+
product_metadata={
179+
"allowed_price_levels": ["low_income_discount"],
180+
"special_product_id": "single_labaccess_month",
181+
},
182+
)
183+
payment_method = stripe.PaymentMethod.create(type="card", card=self.card(VALID_NON_3DS_CARD_NO))
184+
purchase = Purchase(
185+
cart=[CartItem(lab_access_product.id, 1)],
186+
expected_sum=lab_access_product.price,
187+
stripe_payment_method_id=payment_method.id,
188+
)
189+
member = self.db.create_member()
190+
token = self.db.create_access_token(user_id=member.member_id)
191+
startdate = datetime.utcnow()
192+
enddate = startdate - timedelta(days=30)
193+
self.db.create_span(member=member, type=Span.MEMBERSHIP, startdate=startdate, enddate=enddate)
194+
self.post("/webshop/pay", purchase.to_dict(), token=token.access_token).expect(code=400)

0 commit comments

Comments
 (0)