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.

Files changed (76) hide show
  1. wbcommission/__init__.py +1 -0
  2. wbcommission/admin/__init__.py +4 -0
  3. wbcommission/admin/accounts.py +22 -0
  4. wbcommission/admin/commission.py +85 -0
  5. wbcommission/admin/rebate.py +7 -0
  6. wbcommission/analytics/__init__.py +0 -0
  7. wbcommission/analytics/marginality.py +181 -0
  8. wbcommission/apps.py +5 -0
  9. wbcommission/dynamic_preferences_registry.py +0 -0
  10. wbcommission/factories/__init__.py +9 -0
  11. wbcommission/factories/commission.py +100 -0
  12. wbcommission/factories/rebate.py +16 -0
  13. wbcommission/filters/__init__.py +7 -0
  14. wbcommission/filters/rebate.py +187 -0
  15. wbcommission/filters/signals.py +44 -0
  16. wbcommission/generators/__init__.py +2 -0
  17. wbcommission/generators/rebate_generator.py +93 -0
  18. wbcommission/migrations/0001_initial.py +299 -0
  19. wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
  20. wbcommission/migrations/0003_alter_commission_account.py +24 -0
  21. wbcommission/migrations/0004_rebate_audit_log.py +19 -0
  22. wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
  23. wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
  24. wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
  25. wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
  26. wbcommission/migrations/__init__.py +0 -0
  27. wbcommission/models/__init__.py +9 -0
  28. wbcommission/models/account_service.py +217 -0
  29. wbcommission/models/commission.py +679 -0
  30. wbcommission/models/rebate.py +319 -0
  31. wbcommission/models/signals.py +45 -0
  32. wbcommission/permissions.py +6 -0
  33. wbcommission/reports/__init__.py +0 -0
  34. wbcommission/reports/audit_report.py +51 -0
  35. wbcommission/reports/customer_report.py +299 -0
  36. wbcommission/reports/utils.py +30 -0
  37. wbcommission/serializers/__init__.py +3 -0
  38. wbcommission/serializers/commissions.py +26 -0
  39. wbcommission/serializers/rebate.py +87 -0
  40. wbcommission/serializers/signals.py +27 -0
  41. wbcommission/tests/__init__.py +0 -0
  42. wbcommission/tests/analytics/__init__.py +0 -0
  43. wbcommission/tests/analytics/test_marginality.py +253 -0
  44. wbcommission/tests/conftest.py +89 -0
  45. wbcommission/tests/models/__init__.py +0 -0
  46. wbcommission/tests/models/mixins.py +22 -0
  47. wbcommission/tests/models/test_account_service.py +293 -0
  48. wbcommission/tests/models/test_commission.py +587 -0
  49. wbcommission/tests/models/test_rebate.py +136 -0
  50. wbcommission/tests/signals.py +0 -0
  51. wbcommission/tests/test_permissions.py +66 -0
  52. wbcommission/tests/viewsets/__init__.py +0 -0
  53. wbcommission/tests/viewsets/test_rebate.py +76 -0
  54. wbcommission/urls.py +42 -0
  55. wbcommission/viewsets/__init__.py +7 -0
  56. wbcommission/viewsets/buttons/__init__.py +2 -0
  57. wbcommission/viewsets/buttons/rebate.py +46 -0
  58. wbcommission/viewsets/buttons/signals.py +53 -0
  59. wbcommission/viewsets/commissions.py +21 -0
  60. wbcommission/viewsets/display/__init__.py +5 -0
  61. wbcommission/viewsets/display/commissions.py +21 -0
  62. wbcommission/viewsets/display/rebate.py +117 -0
  63. wbcommission/viewsets/endpoints/__init__.py +4 -0
  64. wbcommission/viewsets/endpoints/commissions.py +0 -0
  65. wbcommission/viewsets/endpoints/rebate.py +21 -0
  66. wbcommission/viewsets/menu/__init__.py +1 -0
  67. wbcommission/viewsets/menu/commissions.py +0 -0
  68. wbcommission/viewsets/menu/rebate.py +13 -0
  69. wbcommission/viewsets/mixins.py +39 -0
  70. wbcommission/viewsets/rebate.py +481 -0
  71. wbcommission/viewsets/titles/__init__.py +1 -0
  72. wbcommission/viewsets/titles/commissions.py +0 -0
  73. wbcommission/viewsets/titles/rebate.py +11 -0
  74. wbcommission-2.2.1.dist-info/METADATA +11 -0
  75. wbcommission-2.2.1.dist-info/RECORD +76 -0
  76. 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