wbcommission 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbcommission might be problematic. Click here for more details.
- wbcommission/__init__.py +1 -0
- wbcommission/admin/__init__.py +4 -0
- wbcommission/admin/accounts.py +22 -0
- wbcommission/admin/commission.py +85 -0
- wbcommission/admin/rebate.py +7 -0
- wbcommission/analytics/__init__.py +0 -0
- wbcommission/analytics/marginality.py +181 -0
- wbcommission/apps.py +5 -0
- wbcommission/dynamic_preferences_registry.py +0 -0
- wbcommission/factories/__init__.py +9 -0
- wbcommission/factories/commission.py +100 -0
- wbcommission/factories/rebate.py +16 -0
- wbcommission/filters/__init__.py +7 -0
- wbcommission/filters/rebate.py +187 -0
- wbcommission/filters/signals.py +44 -0
- wbcommission/generators/__init__.py +2 -0
- wbcommission/generators/rebate_generator.py +93 -0
- wbcommission/migrations/0001_initial.py +299 -0
- wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
- wbcommission/migrations/0003_alter_commission_account.py +24 -0
- wbcommission/migrations/0004_rebate_audit_log.py +19 -0
- wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
- wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
- wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
- wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
- wbcommission/migrations/__init__.py +0 -0
- wbcommission/models/__init__.py +9 -0
- wbcommission/models/account_service.py +217 -0
- wbcommission/models/commission.py +679 -0
- wbcommission/models/rebate.py +319 -0
- wbcommission/models/signals.py +45 -0
- wbcommission/permissions.py +6 -0
- wbcommission/reports/__init__.py +0 -0
- wbcommission/reports/audit_report.py +51 -0
- wbcommission/reports/customer_report.py +299 -0
- wbcommission/reports/utils.py +30 -0
- wbcommission/serializers/__init__.py +3 -0
- wbcommission/serializers/commissions.py +26 -0
- wbcommission/serializers/rebate.py +87 -0
- wbcommission/serializers/signals.py +27 -0
- wbcommission/tests/__init__.py +0 -0
- wbcommission/tests/analytics/__init__.py +0 -0
- wbcommission/tests/analytics/test_marginality.py +253 -0
- wbcommission/tests/conftest.py +89 -0
- wbcommission/tests/models/__init__.py +0 -0
- wbcommission/tests/models/mixins.py +22 -0
- wbcommission/tests/models/test_account_service.py +293 -0
- wbcommission/tests/models/test_commission.py +587 -0
- wbcommission/tests/models/test_rebate.py +136 -0
- wbcommission/tests/signals.py +0 -0
- wbcommission/tests/test_permissions.py +66 -0
- wbcommission/tests/viewsets/__init__.py +0 -0
- wbcommission/tests/viewsets/test_rebate.py +76 -0
- wbcommission/urls.py +42 -0
- wbcommission/viewsets/__init__.py +7 -0
- wbcommission/viewsets/buttons/__init__.py +2 -0
- wbcommission/viewsets/buttons/rebate.py +46 -0
- wbcommission/viewsets/buttons/signals.py +53 -0
- wbcommission/viewsets/commissions.py +21 -0
- wbcommission/viewsets/display/__init__.py +5 -0
- wbcommission/viewsets/display/commissions.py +21 -0
- wbcommission/viewsets/display/rebate.py +117 -0
- wbcommission/viewsets/endpoints/__init__.py +4 -0
- wbcommission/viewsets/endpoints/commissions.py +0 -0
- wbcommission/viewsets/endpoints/rebate.py +21 -0
- wbcommission/viewsets/menu/__init__.py +1 -0
- wbcommission/viewsets/menu/commissions.py +0 -0
- wbcommission/viewsets/menu/rebate.py +13 -0
- wbcommission/viewsets/mixins.py +39 -0
- wbcommission/viewsets/rebate.py +481 -0
- wbcommission/viewsets/titles/__init__.py +1 -0
- wbcommission/viewsets/titles/commissions.py +0 -0
- wbcommission/viewsets/titles/rebate.py +11 -0
- wbcommission-2.2.1.dist-info/METADATA +11 -0
- wbcommission-2.2.1.dist-info/RECORD +76 -0
- wbcommission-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.contrib.auth.models import Permission
|
|
7
|
+
from faker import Faker
|
|
8
|
+
from pandas.tseries.offsets import BDay
|
|
9
|
+
from psycopg.types.range import DateRange, NumericRange
|
|
10
|
+
from wbcommission.models import Commission, CommissionRule, CommissionType
|
|
11
|
+
from wbportfolio.models import Claim, Trade
|
|
12
|
+
from wbportfolio.models.roles import PortfolioRole
|
|
13
|
+
|
|
14
|
+
fake = Faker()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.django_db
|
|
18
|
+
class TestCommissionModel:
|
|
19
|
+
def test_init(self, commission):
|
|
20
|
+
assert commission.id is not None
|
|
21
|
+
|
|
22
|
+
def test_init_account_role_type_commission(self, account_role_type_commission):
|
|
23
|
+
assert account_role_type_commission.id is not None
|
|
24
|
+
|
|
25
|
+
def test_init_portfolio_role_commission(self, portfolio_role_commission):
|
|
26
|
+
assert portfolio_role_commission.id is not None
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
"validity_date, is_superuser",
|
|
30
|
+
[
|
|
31
|
+
(fake.date_object(), True),
|
|
32
|
+
(fake.date_object(), False),
|
|
33
|
+
],
|
|
34
|
+
)
|
|
35
|
+
def test_filter_for_user(
|
|
36
|
+
self,
|
|
37
|
+
user,
|
|
38
|
+
commission_factory,
|
|
39
|
+
commission_role_factory,
|
|
40
|
+
account_factory,
|
|
41
|
+
account_role_factory,
|
|
42
|
+
validity_date,
|
|
43
|
+
is_superuser,
|
|
44
|
+
):
|
|
45
|
+
# Create a public account without any account role. The linked commission rule cannot be seen by the user
|
|
46
|
+
public_account_without_role = account_factory.create(is_public=True)
|
|
47
|
+
public_account_without_role_commission = commission_factory.create( # noqa
|
|
48
|
+
is_hidden=fake.pybool(), account=public_account_without_role
|
|
49
|
+
) # noqa
|
|
50
|
+
|
|
51
|
+
recipient_commission = commission_factory.create(is_hidden=fake.pybool(), crm_recipient=user.profile.entry_ptr)
|
|
52
|
+
|
|
53
|
+
# Create a public account with a account role for the user, the underlying commision line can be seen by the user
|
|
54
|
+
public_account_with_role = account_factory.create(is_public=True)
|
|
55
|
+
account_role_factory.create(account=public_account_with_role, entry=user.profile.entry_ptr)
|
|
56
|
+
public_account_with_role_commission = commission_factory.create(
|
|
57
|
+
is_hidden=False, account=public_account_with_role
|
|
58
|
+
) # noqa
|
|
59
|
+
# any public sub accounts can be seen by the user, as any commission lines attached to these sub account but as it is hidden, it will not show
|
|
60
|
+
sub_public_account_with_parent_role = account_factory.create(parent=public_account_with_role)
|
|
61
|
+
sub_public_account_with_parent_role_commission = commission_factory.create( # noqa
|
|
62
|
+
is_hidden=True, account=sub_public_account_with_parent_role
|
|
63
|
+
)
|
|
64
|
+
# Private account without role, so cannot be seen by the user, as any commission line
|
|
65
|
+
private_account_without_role = account_factory.create(is_public=False)
|
|
66
|
+
private_account_without_role_commission = commission_factory.create( # noqa
|
|
67
|
+
is_hidden=False, account=private_account_without_role
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Private account with a role, so the underlying commission line can be seen by the user
|
|
71
|
+
private_account_with_role = account_factory.create(is_public=False)
|
|
72
|
+
account_role_factory.create(account=private_account_with_role, entry=user.profile.entry_ptr)
|
|
73
|
+
unhidden_commission_with_role_on_private_account = commission_factory.create(
|
|
74
|
+
is_hidden=False, account=private_account_with_role
|
|
75
|
+
) # noqa
|
|
76
|
+
|
|
77
|
+
# Private account with a role, but expired, so the commission line cannot be seen by the user
|
|
78
|
+
private_account_with_unvalid_role = account_factory.create( # noqa
|
|
79
|
+
is_public=False,
|
|
80
|
+
)
|
|
81
|
+
account_role_factory.create(
|
|
82
|
+
account=private_account_with_unvalid_role,
|
|
83
|
+
entry=user.profile.entry_ptr,
|
|
84
|
+
visibility_daterange=DateRange(date.min, fake.past_date()), # type: ignore
|
|
85
|
+
)
|
|
86
|
+
private_account_with_unvalid_role_commission = commission_factory.create( # noqa
|
|
87
|
+
is_hidden=False, account=private_account_with_unvalid_role
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Valid role an private account but the commission line is hidden so the user cannot see this commission line
|
|
91
|
+
private_account_with_valid_role_hidden_commission = commission_factory.create( # noqa
|
|
92
|
+
is_hidden=True, account=private_account_with_role
|
|
93
|
+
) # noqa Valid account role but commission line hidden, so won't show up
|
|
94
|
+
|
|
95
|
+
# Commission line on account without any role, but the user as direct commission role so they can see it
|
|
96
|
+
commission_with_direct_role = commission_role_factory.create(person=user.profile).commission
|
|
97
|
+
if is_superuser:
|
|
98
|
+
user.user_permissions.add(
|
|
99
|
+
Permission.objects.get(content_type__app_label="wbcommission", codename="administrate_commission")
|
|
100
|
+
)
|
|
101
|
+
user = get_user_model().objects.get(id=user.id)
|
|
102
|
+
if is_superuser:
|
|
103
|
+
assert set(Commission.objects.filter_for_user(user, validity_date)) == set(Commission.objects.all())
|
|
104
|
+
else:
|
|
105
|
+
assert set(Commission.objects.filter_for_user(user)) == {
|
|
106
|
+
public_account_with_role_commission,
|
|
107
|
+
recipient_commission,
|
|
108
|
+
unhidden_commission_with_role_on_private_account,
|
|
109
|
+
commission_with_direct_role,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize("val_date", [fake.date_object()])
|
|
113
|
+
def test_get_recipients(self, commission, product, val_date):
|
|
114
|
+
assert set(commission.get_recipients(commission.account, product, val_date)) == {
|
|
115
|
+
(commission.crm_recipient, Decimal(1.0))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize("val_date", [fake.date_object()])
|
|
119
|
+
def test_get_recipients_portfolio_role(
|
|
120
|
+
self,
|
|
121
|
+
portfolio_role_commission_factory,
|
|
122
|
+
product,
|
|
123
|
+
val_date,
|
|
124
|
+
product_portfolio_role_factory,
|
|
125
|
+
):
|
|
126
|
+
# Create valid portfolio roles
|
|
127
|
+
valid_role1 = product_portfolio_role_factory.create(instrument=product, start=val_date)
|
|
128
|
+
valid_role2 = product_portfolio_role_factory.create(
|
|
129
|
+
instrument=None, start=val_date, role_type=valid_role1.role_type
|
|
130
|
+
)
|
|
131
|
+
valid_role3 = product_portfolio_role_factory.create(
|
|
132
|
+
instrument=product, start=val_date - timedelta(days=1), end=val_date, role_type=valid_role1.role_type
|
|
133
|
+
)
|
|
134
|
+
# create invalid portfolio roles
|
|
135
|
+
unvalid_role1 = product_portfolio_role_factory.create( # noqa
|
|
136
|
+
instrument=product, end=val_date + timedelta(days=1)
|
|
137
|
+
) # noqa
|
|
138
|
+
unvalid_role2 = product_portfolio_role_factory.create( # noqa
|
|
139
|
+
instrument=product,
|
|
140
|
+
start=val_date,
|
|
141
|
+
role_type=PortfolioRole.RoleType.ANALYST
|
|
142
|
+
if valid_role1.role_type == PortfolioRole.RoleType.PORTFOLIO_MANAGER
|
|
143
|
+
else PortfolioRole.RoleType.PORTFOLIO_MANAGER,
|
|
144
|
+
) # noqa
|
|
145
|
+
|
|
146
|
+
# create commission of type portfolio role whose type correspond to valid_role1's type
|
|
147
|
+
commission = portfolio_role_commission_factory.create(portfolio_role_recipient=valid_role1.role_type)
|
|
148
|
+
total_weighting = valid_role1.weighting + valid_role2.weighting + valid_role3.weighting
|
|
149
|
+
# We expect all recipients who have a portfolio role of type role1 to get a share of that commission line
|
|
150
|
+
res = {k: v for k, v in commission.get_recipients(commission.account, product, val_date)}
|
|
151
|
+
assert res[valid_role1.person.entry_ptr] == pytest.approx(
|
|
152
|
+
Decimal(valid_role1.weighting / total_weighting), rel=Decimal(1e-6)
|
|
153
|
+
)
|
|
154
|
+
assert res[valid_role2.person.entry_ptr] == pytest.approx(
|
|
155
|
+
Decimal(valid_role2.weighting / total_weighting), rel=Decimal(1e-6)
|
|
156
|
+
)
|
|
157
|
+
assert res[valid_role3.person.entry_ptr] == pytest.approx(
|
|
158
|
+
Decimal(valid_role3.weighting / total_weighting), rel=Decimal(1e-6)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parametrize("val_date", [fake.date_object()])
|
|
162
|
+
def test_get_recipients_account_role(
|
|
163
|
+
self, account_type_role_commission_factory, account_factory, account_role_factory, product, val_date
|
|
164
|
+
):
|
|
165
|
+
parent_account = account_factory.create()
|
|
166
|
+
valid_parent_role1 = account_role_factory.create(
|
|
167
|
+
account=parent_account, weighting=0.5
|
|
168
|
+
) # parent account role but not direct account role, thus invalid
|
|
169
|
+
unvalid_role1 = account_role_factory.create( # noqa
|
|
170
|
+
account=parent_account,
|
|
171
|
+
visibility_daterange=DateRange(date.min, val_date), # type: ignore
|
|
172
|
+
) # parent account role but not direct account role, thus invalid
|
|
173
|
+
child_account = account_factory.create(parent=parent_account)
|
|
174
|
+
|
|
175
|
+
# create valid account role for that child account
|
|
176
|
+
valid_role1 = account_role_factory.create(
|
|
177
|
+
account=child_account, role_type=valid_parent_role1.role_type, weighting=0.4
|
|
178
|
+
)
|
|
179
|
+
valid_role2 = account_role_factory.create(
|
|
180
|
+
account=child_account, role_type=valid_parent_role1.role_type, weighting=0.1
|
|
181
|
+
)
|
|
182
|
+
account_role_factory.create(account=child_account) # noqa Other role type, therefore unvalid
|
|
183
|
+
|
|
184
|
+
# Create commission of type account type role for that accout role type of unvalid_role1
|
|
185
|
+
account_role_type_commission = account_type_role_commission_factory.create(
|
|
186
|
+
account=parent_account, account_role_type_recipient=valid_parent_role1.role_type
|
|
187
|
+
)
|
|
188
|
+
res = {k: v for k, v in account_role_type_commission.get_recipients(child_account, product, val_date)}
|
|
189
|
+
# we expect every profile who have an account role fo type unvalid_role1.role_type to gain from this commission line
|
|
190
|
+
assert res[valid_role1.entry] == pytest.approx(Decimal(0.4), rel=Decimal(1e-6))
|
|
191
|
+
assert res[valid_role2.entry] == pytest.approx(Decimal(0.1), rel=Decimal(1e-6))
|
|
192
|
+
assert res[valid_parent_role1.entry] == pytest.approx(Decimal(0.5), rel=Decimal(1e-6))
|
|
193
|
+
|
|
194
|
+
assert (
|
|
195
|
+
next(account_role_type_commission.get_recipients(parent_account, product, val_date))[0]
|
|
196
|
+
== valid_parent_role1.entry
|
|
197
|
+
)
|
|
198
|
+
assert next(account_role_type_commission.get_recipients(parent_account, product, val_date))[
|
|
199
|
+
1
|
|
200
|
+
] == pytest.approx(Decimal(0.5 / 0.5), rel=Decimal(1e-6))
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize("validity_date, min_aum", [(fake.date_object(), fake.pydecimal(min_value=0))])
|
|
203
|
+
def test_is_valid(self, commission, product, validity_date, min_aum):
|
|
204
|
+
# basic test for is_valid. We expect the "validated_percent" and "validated_net_commission" to be set properly upon validation
|
|
205
|
+
rule = commission.rules.first()
|
|
206
|
+
rule.percent = Decimal(0.2)
|
|
207
|
+
rule.save()
|
|
208
|
+
rule.refresh_from_db()
|
|
209
|
+
assert commission.is_valid(validity_date, product, min_aum)
|
|
210
|
+
assert commission.validated_percent == rule.percent
|
|
211
|
+
assert commission.validated_net_commission == commission.net_commission
|
|
212
|
+
|
|
213
|
+
@pytest.mark.parametrize("validity_date, min_aum", [(fake.date_object(), fake.pydecimal(min_value=0))])
|
|
214
|
+
def test_is_valid_with_exclusion_rule(
|
|
215
|
+
self, commission, commission_exclusion_rule_factory, product, validity_date, min_aum, account_role_type
|
|
216
|
+
):
|
|
217
|
+
# test overriding of commission rule by exclusion rule on a specific product
|
|
218
|
+
rule = commission.rules.first()
|
|
219
|
+
rule.percent = Decimal(0.2)
|
|
220
|
+
rule.save()
|
|
221
|
+
rule.refresh_from_db()
|
|
222
|
+
exclusion_rule = commission_exclusion_rule_factory.create(
|
|
223
|
+
product=product,
|
|
224
|
+
commission_type=commission.commission_type,
|
|
225
|
+
)
|
|
226
|
+
exclusion_rule.refresh_from_db()
|
|
227
|
+
assert commission.is_valid(validity_date, product, min_aum)
|
|
228
|
+
assert commission.validated_percent == exclusion_rule.overriding_percent
|
|
229
|
+
assert commission.validated_net_commission == exclusion_rule.get_net_or_gross(commission.net_commission)
|
|
230
|
+
|
|
231
|
+
# we assign a explicit account type for the commission
|
|
232
|
+
commission.exclusion_rule_account_role_type = account_role_type
|
|
233
|
+
commission.save()
|
|
234
|
+
exclusion_rule_for_specific_account_type = commission_exclusion_rule_factory.create(
|
|
235
|
+
product=product, commission_type=commission.commission_type, account_role_type=account_role_type
|
|
236
|
+
)
|
|
237
|
+
exclusion_rule_for_specific_account_type.refresh_from_db()
|
|
238
|
+
assert commission.is_valid(validity_date, product, min_aum)
|
|
239
|
+
assert commission.validated_percent == exclusion_rule_for_specific_account_type.overriding_percent
|
|
240
|
+
assert commission.validated_net_commission == exclusion_rule_for_specific_account_type.get_net_or_gross(
|
|
241
|
+
commission.net_commission
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@pytest.mark.parametrize(
|
|
245
|
+
"validity_date, min_aum", [(fake.date_object(), fake.pydecimal(min_value=10, max_value=100))]
|
|
246
|
+
)
|
|
247
|
+
def test_is_invalid_aum(self, commission, product, validity_date, min_aum):
|
|
248
|
+
rule = commission.rules.first()
|
|
249
|
+
rule.assets_under_management_range = NumericRange(min_aum, None) # type: ignore
|
|
250
|
+
rule.save()
|
|
251
|
+
assert not commission.is_valid(validity_date, product, min_aum - Decimal(1))
|
|
252
|
+
|
|
253
|
+
@pytest.mark.parametrize("validity_date, min_aum", [(fake.date_object(), fake.pydecimal(min_value=1000000))])
|
|
254
|
+
def test_is_invalid_date(self, commission, product, validity_date, min_aum):
|
|
255
|
+
rule = commission.rules.first()
|
|
256
|
+
rule.timespan = DateRange(validity_date + timedelta(days=1), date.max) # type: ignore
|
|
257
|
+
rule.save()
|
|
258
|
+
assert not commission.is_valid(validity_date, product, min_aum)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pytest.mark.django_db
|
|
262
|
+
class TestCommissionType:
|
|
263
|
+
@pytest.mark.parametrize(
|
|
264
|
+
"commission_type__name,net_commission,compute_date,percent1,percent2",
|
|
265
|
+
[
|
|
266
|
+
(
|
|
267
|
+
"MANAGEMENT",
|
|
268
|
+
True,
|
|
269
|
+
fake.date_object(),
|
|
270
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
271
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
272
|
+
),
|
|
273
|
+
(
|
|
274
|
+
"MANAGEMENT",
|
|
275
|
+
False,
|
|
276
|
+
fake.date_object(),
|
|
277
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
278
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
279
|
+
),
|
|
280
|
+
(
|
|
281
|
+
"PERFORMANCE",
|
|
282
|
+
True,
|
|
283
|
+
fake.date_object(),
|
|
284
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
285
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
286
|
+
),
|
|
287
|
+
(
|
|
288
|
+
"PERFORMANCE",
|
|
289
|
+
False,
|
|
290
|
+
fake.date_object(),
|
|
291
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
292
|
+
fake.pydecimal(min_value=0, max_value=1, right_digits=2) / Decimal(2),
|
|
293
|
+
),
|
|
294
|
+
],
|
|
295
|
+
)
|
|
296
|
+
def test_get_valid_commissions(
|
|
297
|
+
self, account, product, commission_factory, commission_type, net_commission, compute_date, percent1, percent2
|
|
298
|
+
):
|
|
299
|
+
# Here we test the following:
|
|
300
|
+
# - computation of net/gross fees
|
|
301
|
+
# - invalid commission rules are not considered
|
|
302
|
+
invalid_aum_commission = commission_factory.create( # noqa
|
|
303
|
+
account=account,
|
|
304
|
+
commission_type=commission_type,
|
|
305
|
+
net_commission=net_commission,
|
|
306
|
+
order=0,
|
|
307
|
+
rule_aum=NumericRange(1e6 + 1, None), # type: ignore
|
|
308
|
+
) # invalid commission rule because the aum range is below the total aum
|
|
309
|
+
invalid_date_commission = commission_factory.create( # noqa
|
|
310
|
+
account=account,
|
|
311
|
+
commission_type=commission_type,
|
|
312
|
+
net_commission=net_commission,
|
|
313
|
+
order=1,
|
|
314
|
+
rule_timespan=DateRange(date.min, compute_date), # type: ignore
|
|
315
|
+
) # invalid commission rule because the rule is not valid for the given date
|
|
316
|
+
|
|
317
|
+
commission1 = commission_factory.create(
|
|
318
|
+
account=account,
|
|
319
|
+
commission_type=commission_type,
|
|
320
|
+
net_commission=net_commission,
|
|
321
|
+
order=2,
|
|
322
|
+
rule_percent=percent1,
|
|
323
|
+
)
|
|
324
|
+
commission2 = commission_factory.create(
|
|
325
|
+
account=account,
|
|
326
|
+
commission_type=commission_type,
|
|
327
|
+
net_commission=net_commission,
|
|
328
|
+
order=3,
|
|
329
|
+
rule_percent=percent2,
|
|
330
|
+
)
|
|
331
|
+
commission3 = commission_factory.create(
|
|
332
|
+
account=account,
|
|
333
|
+
commission_type=commission_type,
|
|
334
|
+
net_commission=False,
|
|
335
|
+
order=4,
|
|
336
|
+
rule_percent=Decimal(1) - percent1 - percent2 + Decimal(0.5),
|
|
337
|
+
) # We add to much percent so that the sum of gross percent is greater than 1
|
|
338
|
+
commission4 = commission_factory.create( # noqa
|
|
339
|
+
account=account, commission_type=commission_type, net_commission=False, order=5, rule_percent=percent1
|
|
340
|
+
) # there isn't any gross percent left, so the result is always zero
|
|
341
|
+
|
|
342
|
+
res = list(commission_type.get_valid_commissions(account, compute_date, product, Decimal(1e6)))
|
|
343
|
+
if net_commission:
|
|
344
|
+
assert res == [
|
|
345
|
+
(commission1, percent1),
|
|
346
|
+
(commission2, (Decimal(1.0) - percent1) * percent2),
|
|
347
|
+
(commission3, Decimal(1) - percent1 - (Decimal(1) - percent1) * percent2),
|
|
348
|
+
]
|
|
349
|
+
else:
|
|
350
|
+
if (
|
|
351
|
+
Decimal(1) - percent1 - percent2 == 0
|
|
352
|
+
): # in that case we don't exepct commission 3 to show up as a valid commission as the resulting percent will be zero (i.e. no percent left to assign)
|
|
353
|
+
assert res == [(commission1, percent1), (commission2, percent2)]
|
|
354
|
+
else:
|
|
355
|
+
assert res == [
|
|
356
|
+
(commission1, percent1),
|
|
357
|
+
(commission2, percent2),
|
|
358
|
+
(commission3, Decimal(1) - percent1 - percent2),
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
def test_get_valid_commissions_with_inheritance(
|
|
362
|
+
self, account_factory, commission_factory, commission_type, product
|
|
363
|
+
):
|
|
364
|
+
compute_date = fake.date_object()
|
|
365
|
+
parent_account = account_factory.create()
|
|
366
|
+
account = account_factory.create(parent=parent_account)
|
|
367
|
+
|
|
368
|
+
parent_commission_0 = commission_factory.create(
|
|
369
|
+
account=parent_account,
|
|
370
|
+
commission_type=commission_type,
|
|
371
|
+
net_commission=False,
|
|
372
|
+
order=0,
|
|
373
|
+
rule_percent=Decimal(0.1),
|
|
374
|
+
)
|
|
375
|
+
parent_commission_1 = commission_factory.create( # noqa
|
|
376
|
+
account=parent_account,
|
|
377
|
+
commission_type=commission_type,
|
|
378
|
+
net_commission=False,
|
|
379
|
+
order=1,
|
|
380
|
+
rule_percent=Decimal(0.2),
|
|
381
|
+
)
|
|
382
|
+
child_account_1 = commission_factory.create(
|
|
383
|
+
account=account, commission_type=commission_type, net_commission=False, order=1, rule_percent=Decimal(0.3)
|
|
384
|
+
)
|
|
385
|
+
child_account_2 = commission_factory.create(
|
|
386
|
+
account=account, commission_type=commission_type, net_commission=False, order=2, rule_percent=Decimal(0.4)
|
|
387
|
+
)
|
|
388
|
+
res = list(commission_type.get_valid_commissions(account, compute_date, product, Decimal(1e6)))
|
|
389
|
+
assert res == [
|
|
390
|
+
(parent_commission_0, parent_commission_0.rules.first().percent),
|
|
391
|
+
(child_account_1, child_account_1.rules.first().percent),
|
|
392
|
+
(child_account_2, child_account_2.rules.first().percent),
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
@pytest.mark.parametrize(
|
|
396
|
+
"val_date, percent1,percent2",
|
|
397
|
+
[
|
|
398
|
+
(
|
|
399
|
+
fake.date_object(),
|
|
400
|
+
fake.pydecimal(positive=True, max_value=1, right_digits=2) / Decimal(2),
|
|
401
|
+
fake.pydecimal(positive=True, max_value=1, right_digits=2) / Decimal(2),
|
|
402
|
+
)
|
|
403
|
+
],
|
|
404
|
+
)
|
|
405
|
+
def test_compute_rebates(
|
|
406
|
+
self,
|
|
407
|
+
val_date,
|
|
408
|
+
product,
|
|
409
|
+
account,
|
|
410
|
+
fees_factory,
|
|
411
|
+
customer_trade_factory,
|
|
412
|
+
commission_factory,
|
|
413
|
+
account_type_role_commission_factory,
|
|
414
|
+
account_role_factory,
|
|
415
|
+
instrument_price_factory,
|
|
416
|
+
claim_factory,
|
|
417
|
+
percent1,
|
|
418
|
+
percent2,
|
|
419
|
+
):
|
|
420
|
+
val_date = (val_date + BDay(0)).date()
|
|
421
|
+
val_date_1 = (val_date - BDay(1)).date()
|
|
422
|
+
fees_factory.create(linked_product=product, transaction_date=val_date_1, transaction_subtype="PERFORMANCE")
|
|
423
|
+
fees_factory.create(linked_product=product, transaction_date=val_date_1, transaction_subtype="MANAGEMENT")
|
|
424
|
+
perf_fees = fees_factory.create(
|
|
425
|
+
linked_product=product, transaction_date=val_date, transaction_subtype="PERFORMANCE"
|
|
426
|
+
)
|
|
427
|
+
mngt_fees = fees_factory.create(
|
|
428
|
+
linked_product=product, transaction_date=val_date, transaction_subtype="MANAGEMENT"
|
|
429
|
+
)
|
|
430
|
+
sub2 = customer_trade_factory.create(
|
|
431
|
+
underlying_instrument=product,
|
|
432
|
+
transaction_subtype=Trade.Type.SUBSCRIPTION,
|
|
433
|
+
value_date=val_date,
|
|
434
|
+
transaction_date=val_date_1, # we consider only trade in t-1
|
|
435
|
+
)
|
|
436
|
+
sub1 = customer_trade_factory.create(
|
|
437
|
+
underlying_instrument=product,
|
|
438
|
+
transaction_subtype=Trade.Type.SUBSCRIPTION,
|
|
439
|
+
value_date=val_date,
|
|
440
|
+
transaction_date=val_date_1,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
claim = claim_factory(
|
|
444
|
+
trade__value_date=val_date,
|
|
445
|
+
trade__transaction_date=val_date_1,
|
|
446
|
+
account=account,
|
|
447
|
+
trade=sub1,
|
|
448
|
+
product=sub1.underlying_instrument,
|
|
449
|
+
status=Claim.Status.APPROVED,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
product_shares = sub1.shares + sub2.shares
|
|
453
|
+
instrument_price_factory.create(
|
|
454
|
+
instrument=product, outstanding_shares=product_shares, date=val_date, calculated=True
|
|
455
|
+
)
|
|
456
|
+
instrument_price_factory.create(
|
|
457
|
+
instrument=product, outstanding_shares=product_shares, date=val_date, calculated=False
|
|
458
|
+
)
|
|
459
|
+
instrument_price_factory.create(
|
|
460
|
+
instrument=product, outstanding_shares=product_shares, date=val_date_1, calculated=True
|
|
461
|
+
)
|
|
462
|
+
instrument_price_factory.create(
|
|
463
|
+
instrument=product, outstanding_shares=product_shares, date=val_date_1, calculated=False
|
|
464
|
+
)
|
|
465
|
+
account_role1 = account_role_factory.create(account=account)
|
|
466
|
+
account_role2 = account_role_factory.create(account=account, role_type=account_role1.role_type)
|
|
467
|
+
|
|
468
|
+
commission_perf = commission_factory.create(
|
|
469
|
+
account=account, commission_type__name="PERFORMANCE", order=0, rule_percent=percent1
|
|
470
|
+
)
|
|
471
|
+
commission_perf_account_role = account_type_role_commission_factory.create( # noqa
|
|
472
|
+
account=account,
|
|
473
|
+
commission_type__name="PERFORMANCE",
|
|
474
|
+
order=1,
|
|
475
|
+
rule_percent=percent2,
|
|
476
|
+
account_role_type_recipient=account_role1.role_type,
|
|
477
|
+
)
|
|
478
|
+
commission_mngt = commission_factory.create(
|
|
479
|
+
account=account, commission_type__name="MANAGEMENT", order=0, rule_percent=percent1
|
|
480
|
+
)
|
|
481
|
+
res = dict()
|
|
482
|
+
for commission_type in CommissionType.objects.all():
|
|
483
|
+
for (
|
|
484
|
+
terminal_account,
|
|
485
|
+
compute_date,
|
|
486
|
+
commission,
|
|
487
|
+
product,
|
|
488
|
+
recipient,
|
|
489
|
+
recipient_fees,
|
|
490
|
+
_,
|
|
491
|
+
) in commission_type.compute_rebates(account):
|
|
492
|
+
res[recipient] = recipient_fees
|
|
493
|
+
assert res[commission_perf.crm_recipient] == pytest.approx(
|
|
494
|
+
perf_fees.total_value * (claim.shares / product_shares) * percent1, rel=Decimal(1e-4)
|
|
495
|
+
)
|
|
496
|
+
assert res[account_role1.entry] == pytest.approx(
|
|
497
|
+
perf_fees.total_value
|
|
498
|
+
* (claim.shares / product_shares)
|
|
499
|
+
* (Decimal(1.0) - percent1)
|
|
500
|
+
* percent2
|
|
501
|
+
/ Decimal(2),
|
|
502
|
+
rel=Decimal(1e-4),
|
|
503
|
+
)
|
|
504
|
+
assert res[account_role2.entry] == pytest.approx(
|
|
505
|
+
perf_fees.total_value
|
|
506
|
+
* (claim.shares / product_shares)
|
|
507
|
+
* (Decimal(1.0) - percent1)
|
|
508
|
+
* percent2
|
|
509
|
+
/ Decimal(2),
|
|
510
|
+
rel=Decimal(1e-4),
|
|
511
|
+
)
|
|
512
|
+
assert res[commission_mngt.crm_recipient] == pytest.approx(
|
|
513
|
+
mngt_fees.total_value * (claim.shares / product_shares) * percent1, rel=Decimal(1e-4)
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
def test_account_merging(self, account_factory, commission_factory, commission_role_factory, rebate_factory):
|
|
517
|
+
# TODO implemetns for commission
|
|
518
|
+
pivot_date = date(2023, 1, 1)
|
|
519
|
+
# for each type a
|
|
520
|
+
base_account = account_factory.create()
|
|
521
|
+
merged_account = account_factory.create()
|
|
522
|
+
|
|
523
|
+
base_mngt_commission = commission_factory.create(
|
|
524
|
+
account=base_account, rule_percent=0.2, rule_timespan=DateRange(date.min, pivot_date)
|
|
525
|
+
)
|
|
526
|
+
base_rule = base_mngt_commission.rules.first()
|
|
527
|
+
base_mngt_commission_role = commission_role_factory.create(commission=base_mngt_commission)
|
|
528
|
+
base_mngt_rebate = rebate_factory.create(commission=base_mngt_commission, account=base_account)
|
|
529
|
+
|
|
530
|
+
merged_mngt_commission = commission_factory.create(
|
|
531
|
+
crm_recipient=base_mngt_commission.crm_recipient,
|
|
532
|
+
account=merged_account,
|
|
533
|
+
rule_percent=0.5,
|
|
534
|
+
rule_timespan=DateRange(pivot_date + timedelta(days=1), date.max),
|
|
535
|
+
)
|
|
536
|
+
CommissionRule.objects.create( # we create a rule that overlaps the base rule but to be sure it does not change the base commission rule
|
|
537
|
+
commission=merged_mngt_commission,
|
|
538
|
+
timespan=DateRange(date.min, pivot_date), # type: ignore
|
|
539
|
+
assets_under_management_range=NumericRange(0, 1000000000000), # type: ignore
|
|
540
|
+
)
|
|
541
|
+
merged_mngt_commission_role = commission_role_factory.create(commission=merged_mngt_commission)
|
|
542
|
+
merged_mngt_rebate = rebate_factory.create(commission=merged_mngt_commission, account=merged_account)
|
|
543
|
+
merged_mngt_commission_rule = merged_mngt_commission.rules.first()
|
|
544
|
+
|
|
545
|
+
merged_perf_commission = commission_factory.create(
|
|
546
|
+
crm_recipient=base_mngt_commission.crm_recipient,
|
|
547
|
+
account=merged_account,
|
|
548
|
+
commission_type__name="Performance",
|
|
549
|
+
)
|
|
550
|
+
merged_perf_commission_rule = merged_perf_commission.rules.first()
|
|
551
|
+
merged_perf_commission_role = commission_role_factory.create(commission=merged_perf_commission)
|
|
552
|
+
merged_perf_rebate = rebate_factory.create(commission=merged_perf_commission, account=merged_account)
|
|
553
|
+
|
|
554
|
+
base_account.merge(merged_account)
|
|
555
|
+
|
|
556
|
+
base_mngt_commission.refresh_from_db()
|
|
557
|
+
|
|
558
|
+
# test that the non overlapping rule are forwarded to the existing commission
|
|
559
|
+
assert set(base_mngt_commission.rules.all()) == {base_rule, merged_mngt_commission_rule}
|
|
560
|
+
assert set(base_mngt_commission.roles.all()) == {base_mngt_commission_role, merged_mngt_commission_role}
|
|
561
|
+
|
|
562
|
+
# test that the base rebate didn't change
|
|
563
|
+
base_mngt_rebate.refresh_from_db()
|
|
564
|
+
assert base_mngt_rebate.commission == base_mngt_commission
|
|
565
|
+
assert base_mngt_rebate.account == base_account
|
|
566
|
+
|
|
567
|
+
# test that the overlapping commission are simply deleted
|
|
568
|
+
with pytest.raises(Commission.DoesNotExist):
|
|
569
|
+
merged_mngt_commission.refresh_from_db()
|
|
570
|
+
|
|
571
|
+
# checked that the rebate from the merged commission is reassigned to the base commission and account
|
|
572
|
+
merged_mngt_rebate.refresh_from_db()
|
|
573
|
+
assert merged_mngt_rebate.commission == base_mngt_commission
|
|
574
|
+
assert merged_mngt_rebate.account == base_account
|
|
575
|
+
|
|
576
|
+
base_perf_commission = Commission.objects.get(
|
|
577
|
+
commission_type__key="performance",
|
|
578
|
+
account=base_account,
|
|
579
|
+
crm_recipient=merged_perf_commission.crm_recipient,
|
|
580
|
+
)
|
|
581
|
+
assert set(base_perf_commission.rules.all()) == {merged_perf_commission_rule}
|
|
582
|
+
assert set(base_perf_commission.roles.all()) == {merged_perf_commission_role}
|
|
583
|
+
with pytest.raises(Commission.DoesNotExist):
|
|
584
|
+
merged_perf_commission.refresh_from_db()
|
|
585
|
+
merged_perf_rebate.refresh_from_db()
|
|
586
|
+
assert merged_perf_rebate.commission == base_perf_commission
|
|
587
|
+
assert merged_perf_rebate.account == base_account
|