Skip to content

Commit e9af678

Browse files
authored
Correctly changing deal currency (#2837)
1 parent e5bde9b commit e9af678

File tree

11 files changed

+126
-17
lines changed

11 files changed

+126
-17
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ compilemessages:
55
$(manage) compilemessages
66

77
fmt:
8+
uv run ruff format src
9+
# Run formatter for the second time to fix errors left after first fixing
810
uv run ruff format src
911
uv run ruff check src --fix --unsafe-fixes
1012
uv run toml-sort pyproject.toml

src/apps/b2b/admin/deals/forms.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.utils.translation import gettext_lazy as _
55

66
from apps.b2b.models import Deal
7-
from apps.b2b.services import BulkStudentCreator, DealCreator
7+
from apps.b2b.services import BulkStudentCreator, DealCreator, DealCurrencyChanger
88
from core.admin import ModelForm
99

1010

@@ -50,10 +50,20 @@ class Meta:
5050
fields = "__all__"
5151

5252
def save(self, commit: bool = False) -> Deal:
53+
self._change_currency(deal=self.instance)
54+
5355
deal = super().save(commit=commit)
5456

57+
self._create_students(deal)
58+
59+
return deal
60+
61+
def _change_currency(self, deal: Deal) -> None:
62+
currency = self.cleaned_data["currency"]
63+
if self.initial["currency"] != currency:
64+
DealCurrencyChanger(deal, new_currency_code=currency)()
65+
66+
def _create_students(self, deal: Deal) -> None:
5567
student_list = self.cleaned_data["students"]
5668
if len(student_list) > 0:
5769
BulkStudentCreator(user_input=student_list, deal=deal)()
58-
59-
return deal

src/apps/b2b/factory.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any
33

44
from apps.b2b.models import Customer, Deal
5+
from apps.banking import currency
56
from apps.products.models import Course
67
from apps.users.models import User
78
from core.test.factory import FixtureFactory, register
@@ -14,21 +15,23 @@ def deal(
1415
author: User | None = None,
1516
course: Course | None = None,
1617
price: Decimal | None = None,
17-
currency: str | None = None,
18+
currency_code: str | None = None,
1819
student_count: int | None = 0,
1920
**kwargs: dict[str, Any],
2021
) -> Deal:
2122
customer = self.mixer.blend("b2b.Customer") if customer is None else customer
2223
author = self.mixer.blend("users.User") if author is None else author
2324
course = self.course() if course is None else course
2425
price = price if price is not None else Decimal(self.price())
26+
currency_code = currency_code if currency_code is not None else "RUB"
2527

2628
deal = Deal.objects.create(
2729
customer=customer,
2830
course=course,
2931
price=price,
3032
author=author,
31-
currency=currency if currency is not None else "RUB",
33+
currency=currency_code,
34+
currency_rate_on_creation=currency.get_rate_or_default(currency_code),
3235
**kwargs,
3336
)
3437

src/apps/b2b/services/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from apps.b2b.services.bulk_student_creator import BulkStudentCreator
22
from apps.b2b.services.deal_completer import DealCompleter
33
from apps.b2b.services.deal_creator import DealCreator
4+
from apps.b2b.services.deal_currency_changer import DealCurrencyChanger
45

56
__all__ = [
67
"BulkStudentCreator",
78
"DealCompleter",
89
"DealCreator",
10+
"DealCurrencyChanger",
911
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from dataclasses import dataclass
2+
from typing import Callable
3+
4+
from django.contrib.admin.models import CHANGE
5+
6+
from apps.b2b.models import Deal
7+
from apps.banking import currency
8+
from core.current_user import get_current_user
9+
from core.services import BaseService
10+
from core.tasks import write_admin_log
11+
12+
13+
@dataclass
14+
class DealCurrencyChanger(BaseService):
15+
"""Changes deal currency and currency rate on creation"""
16+
17+
deal: Deal
18+
new_currency_code: str
19+
20+
def act(self) -> None:
21+
self.deal.currency = self.new_currency_code.upper()
22+
self.deal.currency_rate_on_creation = currency.get_rate_or_default(self.new_currency_code)
23+
self.deal.save(update_fields=["currency", "currency_rate_on_creation", "modified"])
24+
25+
self.write_auditlog()
26+
27+
def get_validators(self) -> list[Callable]:
28+
return [
29+
self.validate_currency_code,
30+
]
31+
32+
def write_auditlog(self) -> None:
33+
user = get_current_user()
34+
if user is None:
35+
raise RuntimeError("Cannot determine user")
36+
37+
write_admin_log.delay(
38+
action_flag=CHANGE,
39+
change_message=f"Deal currency changed to {self.deal.currency}",
40+
model="b2b.Deal",
41+
object_id=self.deal.id,
42+
user_id=user.id,
43+
)
44+
45+
def validate_currency_code(self) -> None:
46+
if currency.get_rate(self.new_currency_code.upper()) is None:
47+
raise TypeError("Non-existant currency code")

src/apps/b2b/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def course(factory):
1313

1414
@pytest.fixture
1515
def deal(factory, customer, course):
16-
return factory.deal(customer=customer, course=course)
16+
return factory.deal(customer=customer, course=course, currency_code="RUB")
1717

1818

1919
@pytest.fixture

src/apps/b2b/tests/deal_completer/test_deal_completer_new_order_creation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ def test_price_calculation(completer, factory, student_count, single_order_price
6666

6767
@pytest.mark.usefixtures("usd")
6868
@pytest.mark.parametrize(
69-
("currency", "single_order_price"),
69+
("currency_code", "single_order_price"),
7070
[
7171
("RUB", "50.00"),
7272
("USD", "5000.00"),
7373
("NNE", "50.00"), # for nonexistant currencis the default rate is used
7474
],
7575
)
76-
def test_currency_price_calculation(completer, factory, currency, single_order_price):
77-
deal = factory.deal(student_count=2, price=Decimal("100.00"), currency=currency)
76+
def test_currency_price_calculation(completer, factory, currency_code, single_order_price):
77+
deal = factory.deal(student_count=2, price=Decimal("100.00"), currency_code=currency_code)
7878

7979
completer(deal=deal)()
8080
order = Order.objects.first()

src/apps/b2b/tests/test_deal_creator_service.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@
66

77
pytestmark = [
88
pytest.mark.django_db,
9-
pytest.mark.usefixtures("_set_current_user"),
9+
pytest.mark.usefixtures("_set_current_user", "kzt"),
1010
]
1111

1212

13-
@pytest.fixture(autouse=True)
14-
def _kzt(factory):
15-
factory.currency(name="KZT", rate=Decimal("0.2"))
16-
17-
1813
def test_deal_is_created(customer, course):
1914
deal = DealCreator(
2015
customer=customer,
@@ -34,7 +29,7 @@ def test_deal_is_created(customer, course):
3429
[
3530
("RUB", "1.0"),
3631
("NNE", "1.0"), # unknown currency
37-
("KZT", "0.2"),
32+
("KZT", "5.0"),
3833
],
3934
)
4035
def test_currency_is_saved(customer, course, currency, expected_rate):
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from decimal import Decimal
2+
3+
import pytest
4+
from django.contrib.admin.models import CHANGE, LogEntry
5+
from django.contrib.contenttypes.models import ContentType
6+
7+
from apps.b2b.services import DealCurrencyChanger
8+
9+
pytestmark = [
10+
pytest.mark.django_db,
11+
pytest.mark.usefixtures("_set_current_user", "usd"),
12+
]
13+
14+
15+
@pytest.mark.parametrize("new_currency_code", ["usd", "UsD"])
16+
def test(deal, new_currency_code):
17+
assert deal.currency == "RUB"
18+
19+
DealCurrencyChanger(deal=deal, new_currency_code=new_currency_code)()
20+
deal.refresh_from_db()
21+
22+
assert deal.currency == "USD"
23+
assert deal.currency_rate_on_creation == Decimal(100)
24+
25+
26+
def test_wrong_currency_code(deal):
27+
with pytest.raises(TypeError):
28+
DealCurrencyChanger(deal=deal, new_currency_code="NONEXISTANT-STUPID-CODE")()
29+
30+
31+
@pytest.mark.auditlog
32+
def test_auditlog(deal, user):
33+
DealCurrencyChanger(deal=deal, new_currency_code="usd")()
34+
35+
log = LogEntry.objects.filter(
36+
content_type=ContentType.objects.get_for_model(deal).id,
37+
).last()
38+
39+
assert log.action_flag == CHANGE
40+
assert "currency" in log.change_message
41+
assert "USD" in log.change_message
42+
43+
assert log.user == user
44+
assert log.object_id == str(deal.id)
45+
assert log.object_repr == str(deal)

src/apps/banking/currency.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_rate(name: str) -> Decimal | None:
3737
Currency = apps.get_model("banking.Currency")
3838

3939
with contextlib.suppress(Currency.DoesNotExist):
40-
return Currency.objects.get(name=name).rate
40+
return Currency.objects.get(name__iexact=name).rate
4141

4242

4343
def get_rate_or_default(name: str) -> Decimal:

0 commit comments

Comments
 (0)