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

feat(economy): ghost product mutation, tests and other misc. #371

Merged
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
5 changes: 1 addition & 4 deletions .github/workflows/test_on_push.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Run tests on PRs

on:
push:
branches:
- develop
on: push

jobs:
test:
Expand Down
55 changes: 25 additions & 30 deletions admissions/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,46 +135,41 @@ def test__before_interview_time__returns_no_available_locations(self):

def test__start_of_interview_time__returns_3_available_locations(self):
now = timezone.datetime.now()
datetime_from = timezone.make_aware(
timezone.datetime(now.year, now.month, now.day, hour=12, minute=00),
timezone=pytz.timezone(settings.TIME_ZONE),
)
datetime_to = timezone.make_aware(
timezone.datetime(now.year, now.month, now.day, hour=12, minute=45),
timezone=pytz.timezone(settings.TIME_ZONE),
)

locations = get_available_interview_locations(
datetime_from=timezone.datetime(
now.year,
now.month,
now.day,
hour=12,
minute=00,
tzinfo=pytz.timezone(settings.TIME_ZONE),
),
datetime_to=timezone.datetime(
now.year,
now.month,
now.day,
hour=12,
minute=45,
tzinfo=pytz.timezone(settings.TIME_ZONE),
),
datetime_from=datetime_from, datetime_to=datetime_to
)
self.assertEqual(locations.count(), 3)

def test__remove_1_location_availability__returns_2_available_locations(self):
self.bodegaen_day_2.delete()
now = timezone.datetime.now() + timezone.timedelta(days=1)
locations = get_available_interview_locations(
datetime_from=timezone.datetime(
now.year,
now.month,
now.day,
hour=12,
minute=00,
tzinfo=pytz.timezone(settings.TIME_ZONE),
tomorrow = timezone.datetime.now() + timezone.timedelta(days=1)
tomorrow_datetime_from = timezone.make_aware(
timezone.datetime(
tomorrow.year, tomorrow.month, tomorrow.day, hour=12, minute=00
),
datetime_to=timezone.datetime(
now.year,
now.month,
now.day,
timezone=pytz.timezone(settings.TIME_ZONE),
)
tomorrow_datetime_to = timezone.make_aware(
timezone.datetime(
tomorrow.year,
tomorrow.month,
tomorrow.day,
hour=12,
minute=45,
tzinfo=pytz.timezone(settings.TIME_ZONE),
),
timezone=pytz.timezone(settings.TIME_ZONE),
)
locations = get_available_interview_locations(
datetime_from=tomorrow_datetime_from, datetime_to=tomorrow_datetime_to
)
self.assertEqual(locations.count(), 2)

Expand Down
4 changes: 2 additions & 2 deletions api/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ def setUp(self):
self.initial_funds = 1000
self.user_account.add_funds(self.initial_funds)
self.flag = FeatureFlagFactory.create(
name=settings.X_APP_AUCTION_MODE, enabled=True
name=settings.X_APP_STOCK_MARKET_MODE, enabled=True
)

def test__no_existing_sales__charge_purchase_price(self):
Expand Down Expand Up @@ -377,7 +377,7 @@ def setUp(self):
self.initial_funds = 1000
self.user_account.add_funds(self.initial_funds)
self.flag = FeatureFlagFactory.create(
name=settings.X_APP_AUCTION_MODE, enabled=False
name=settings.X_APP_STOCK_MARKET_MODE, enabled=False
)

def test__stock_mode_disabled__charge_ordinary_price(self):
Expand Down
2 changes: 1 addition & 1 deletion api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def post(self, request, *args, **kwargs):
order_size = product_order["order_size"]

if (
check_feature_flag(settings.X_APP_AUCTION_MODE, fail_silently=True)
check_feature_flag(settings.X_APP_STOCK_MARKET_MODE, fail_silently=True)
and product.purchase_price
): # Stock mode is enabled and the product has a registered purchase price
product_price = calculate_stock_price_for_product(product.id)
Expand Down
14 changes: 10 additions & 4 deletions economy/price_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
from django.conf import settings


def calculate_stock_price_for_product(product_id):
def calculate_stock_price_for_product(product_id: int, fail_silently=True):
"""
Calculates the price of a product provided a specific product id.
"""
product = SociProduct.objects.get(id=product_id)

if not product.purchase_price:
raise RuntimeError(
"product has no purchase price. Cannot calculate stock price"
)
if fail_silently:
return product.price
else:
raise RuntimeError(
f"Cannot calculate stock price for product without purchase price: {product}"
)

purchase_window = timezone.now() - settings.STOCK_MODE_PRICE_WINDOW
purchases_made_in_window = ProductOrder.objects.filter(
Expand Down
65 changes: 21 additions & 44 deletions economy/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,13 @@
ValidationError,
)
from django.db import transaction
from django.forms import FloatField
from graphene import Node
from django.db.models import (
Q,
Sum,
Count,
F,
Avg,
Subquery,
OuterRef,
Value,
Case,
When,
BooleanField,
)
from django.db.models.functions import Coalesce, TruncDay, TruncDate
from django.db.models.functions import Coalesce, TruncDate
from graphene_django import DjangoObjectType
from django.utils import timezone
from graphene_django_cud.mutations import (
Expand Down Expand Up @@ -54,6 +45,7 @@
ProductOrder,
SociOrderSession,
SociOrderSessionOrder,
ProductGhostOrder,
)
from economy.price_strategies import calculate_stock_price_for_product
from schedules.models import Schedule
Expand Down Expand Up @@ -606,36 +598,6 @@ def resolve_stock_market_products(self, info, *args, **kwargs):
return data


class CreateSociProductMutation(DjangoCreateMutation):
class Meta:
model = SociProduct


class PatchSociProductMutation(DjangoPatchMutation):
class Meta:
model = SociProduct


class DeleteSociProductMutation(DjangoDeleteMutation):
class Meta:
model = SociProduct


class CreateProductOrderMutation(DjangoCreateMutation):
class Meta:
model = ProductOrder


class PatchProductOrderMutation(DjangoPatchMutation):
class Meta:
model = ProductOrder


class DeleteProductOrderMutation(DjangoDeleteMutation):
class Meta:
model = ProductOrder


class UndoProductOrderMutation(graphene.Mutation):
class Arguments:
id = graphene.ID(required=True)
Expand Down Expand Up @@ -1214,11 +1176,24 @@ def mutate(self, info, amount, deposit_method, description, *args, **kwargs):
return CreateDepositMutation(deposit=deposit)


class EconomyMutations(graphene.ObjectType):
create_soci_product = CreateSociProductMutation.Field()
patch_soci_product = PatchSociProductMutation.Field()
delete_soci_product = DeleteSociProductMutation.Field()
class IncrementProductGhostOrderMutation(graphene.Mutation):
class Arguments:
product_id = graphene.ID()

success = graphene.Boolean()

@gql_has_permissions("economy.add_productghostorder")
def mutate(self, info, product_id, *args, **kwargs):
product_id = disambiguate_id(product_id)

product = SociProduct.objects.get(id=product_id)

ProductGhostOrder.objects.create(product=product)

return IncrementProductGhostOrderMutation(success=True)


class EconomyMutations(graphene.ObjectType):
place_product_order = PlaceProductOrderMutation.Field()
undo_product_order = UndoProductOrderMutation.Field()

Expand All @@ -1241,3 +1216,5 @@ class EconomyMutations(graphene.ObjectType):
)
soci_order_session_next_status = SociOrderSessionNextStatusMutation.Field()
invite_users_to_order_session = InviteUsersToSociOrderSessionMutation.Field()

increment_product_ghost_order = IncrementProductGhostOrderMutation.Field()
51 changes: 51 additions & 0 deletions economy/tests/test_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.test import TestCase
from addict import Dict
from graphene.test import Client
from ksg_nett.schema import schema
from economy.tests.factories import SociProductFactory
from economy.models import ProductGhostOrder
from users.tests.factories import UserWithPermissionsFactory, UserFactory


class TestIncrementProductGhostOrderMutation(TestCase):
def setUp(self) -> None:
self.graphql_client = Client(schema)
self.product = SociProductFactory.create(
name="Smirnoff Ice", price=30, purchase_price=20
)
self.user_with_perm = UserWithPermissionsFactory.create(
permissions="economy.add_productghostorder"
)
self.user_without_perm = UserFactory.create()

self.mutation = """
mutation IncrementGhostOrderMutation($productId: ID) {
incrementProductGhostOrder(productId: $productId) {
success
}
}
"""

def test__correct_input_and_has_permission__creates_new_object(self):
pre_count = ProductGhostOrder.objects.all().count()
self.graphql_client.execute(
self.mutation,
variables={"productId": self.product.id},
context=Dict(user=self.user_with_perm),
)
post_count = ProductGhostOrder.objects.all().count()
diff = post_count - pre_count
self.assertEqual(diff, 1)

def test__correct_input_without_permission__returns_error(self):
pre_count = ProductGhostOrder.objects.all().count()
executed = self.graphql_client.execute(
self.mutation,
variables={"productId": self.product.id},
context=Dict(user=self.user_without_perm),
)
result = Dict(executed)
post_count = ProductGhostOrder.objects.all().count()
diff = post_count - pre_count
self.assertEqual(diff, 0)
self.assertIsNotNone(result.data.errors)
17 changes: 15 additions & 2 deletions economy/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,28 @@ def test__price_calculation__returns_expected_price(self):
calculated_price = calculate_stock_price_for_product(self.tuborg.id)
self.assertEqual(expected, calculated_price)

def test__product_has_no_purchase_price__raises_error(self):
def test__product_has_no_purchase_price_no_silent_fail__raises_error(self):
no_purchase_price = SociProductFactory.create(
price=20, purchase_price=None, name="tuborg 2"
)

self.assertRaises(
RuntimeError, calculate_stock_price_for_product, no_purchase_price.id
RuntimeError,
calculate_stock_price_for_product,
no_purchase_price.id,
fail_silently=False,
)

def test__product_has_no_purchase_price_silent_failing__returns_normal_price(self):
no_purchase_price = SociProductFactory.create(
price=20, purchase_price=None, name="tuborg 2"
)

result = calculate_stock_price_for_product(
no_purchase_price.id, fail_silently=True
)
self.assertEqual(result, no_purchase_price.price)

def test__product_orders_outside_price_window__not_included_in_calculation(self):
outside_window = (
timezone.now()
Expand Down
2 changes: 1 addition & 1 deletion ksg_nett/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@
BANK_TRANSFER_DEPOSIT_FEATURE_FLAG = "bank_transfer_deposit"
DEPOSIT_TIME_RESTRICTIONS_FEATURE_FLAG = "deposit_time_restrictions"
EXTERNAL_CHARGING_FEATURE_FLAG = "external_charging"
X_APP_AUCTION_MODE = "x-app-auction-mode"
X_APP_STOCK_MARKET_MODE = "x-app-stock-market-mode"

EXTERNAL_CHARGE_MAX_AMOUNT = os.environ.get("EXTERNAL_CHARGE_MAX_AMOUNT", 300)

Expand Down
Loading