karrio-server-pricing 2026.1.1__py3-none-any.whl → 2026.1.4__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.
@@ -1,29 +1,44 @@
1
+ """
2
+ Tests for the Pricing module (Markup and Fee models).
3
+
4
+ These tests cover:
5
+ 1. Markup application to shipping rates (amount and percentage types)
6
+ 2. Fee capture after shipment label creation
7
+ 3. Various filter combinations (carrier_codes, service_codes, connection_ids)
8
+ """
9
+
1
10
  import json
2
11
  import logging
3
12
  from unittest.mock import patch, ANY
13
+ from django.test import TestCase
4
14
  from django.urls import reverse
5
15
  from rest_framework import status
6
16
  from karrio.core.models import RateDetails, ChargeDetails
7
17
  from karrio.server.core.tests import APITestCase
8
18
  import karrio.server.pricing.models as models
19
+ import karrio.server.pricing.signals as signals
9
20
 
10
21
  logging.disable(logging.CRITICAL)
11
22
 
12
23
 
13
- class TestPricing(APITestCase):
24
+ class TestMarkupApplication(APITestCase):
25
+ """Test markup application to shipping rates."""
26
+
14
27
  def setUp(self) -> None:
15
28
  super().setUp()
16
29
 
17
- self.charge: models.Surcharge = models.Surcharge.objects.create(
30
+ # Create a markup targeting specific carriers and services
31
+ self.markup: models.Markup = models.Markup.objects.create(
18
32
  **{
19
33
  "amount": 1.0,
20
34
  "name": "brokerage",
21
- "carriers": ["canadapost"],
22
- "services": ["canadapost_priority", "canadapost_regular_parcel"],
35
+ "carrier_codes": ["canadapost"],
36
+ "service_codes": ["canadapost_priority", "canadapost_regular_parcel"],
23
37
  }
24
38
  )
25
39
 
26
- def test_apply_surcharge_amount_to_shipment_rates(self):
40
+ def test_apply_markup_amount_to_shipment_rates(self):
41
+ """Test applying fixed amount markup to rates."""
27
42
  url = reverse("karrio.server.proxy:shipment-rates")
28
43
  data = RATING_DATA
29
44
 
@@ -35,10 +50,11 @@ class TestPricing(APITestCase):
35
50
  self.assertEqual(response.status_code, status.HTTP_200_OK)
36
51
  self.assertDictEqual(response_data, RATING_RESPONSE)
37
52
 
38
- def test_apply_surcharge_percentage_to_shipment_rates(self):
39
- self.charge.amount = 2.0
40
- self.charge.surcharge_type = "PERCENTAGE"
41
- self.charge.save()
53
+ def test_apply_markup_percentage_to_shipment_rates(self):
54
+ """Test applying percentage markup to rates."""
55
+ self.markup.amount = 2.0
56
+ self.markup.markup_type = "PERCENTAGE"
57
+ self.markup.save()
42
58
  url = reverse("karrio.server.proxy:shipment-rates")
43
59
  data = RATING_DATA
44
60
 
@@ -51,6 +67,378 @@ class TestPricing(APITestCase):
51
67
  self.assertDictEqual(response_data, RATING_WITH_PERCENTAGE_RESPONSE)
52
68
 
53
69
 
70
+ class TestMarkupFilters(TestCase):
71
+ """Test markup filter logic."""
72
+
73
+ def test_carrier_codes_filter(self):
74
+ """Test that markup only applies to specified carrier codes."""
75
+ markup = models.Markup.objects.create(
76
+ name="fedex_markup",
77
+ amount=5.0,
78
+ markup_type="AMOUNT",
79
+ carrier_codes=["fedex"],
80
+ )
81
+
82
+ # Create mock rate for FedEx
83
+ from karrio.server.core.datatypes import Rate, RateResponse
84
+
85
+ fedex_rate = Rate(
86
+ id="rate_1",
87
+ carrier_id="fedex",
88
+ carrier_name="fedex",
89
+ service="fedex_ground",
90
+ total_charge=10.0,
91
+ currency="USD",
92
+ extra_charges=[],
93
+ meta={"carrier_connection_id": "car_123"},
94
+ )
95
+
96
+ ups_rate = Rate(
97
+ id="rate_2",
98
+ carrier_id="ups",
99
+ carrier_name="ups",
100
+ service="ups_ground",
101
+ total_charge=12.0,
102
+ currency="USD",
103
+ extra_charges=[],
104
+ meta={"carrier_connection_id": "car_456"},
105
+ )
106
+
107
+ response = RateResponse(
108
+ rates=[fedex_rate, ups_rate],
109
+ messages=[],
110
+ )
111
+
112
+ result = markup.apply_charge(response)
113
+
114
+ # FedEx rate should have markup applied
115
+ fedex_result = next(r for r in result.rates if r.carrier_name == "fedex")
116
+ self.assertEqual(fedex_result.total_charge, 15.0) # 10 + 5
117
+
118
+ # UPS rate should NOT have markup applied
119
+ ups_result = next(r for r in result.rates if r.carrier_name == "ups")
120
+ self.assertEqual(ups_result.total_charge, 12.0) # unchanged
121
+
122
+ def test_service_codes_filter(self):
123
+ """Test that markup only applies to specified service codes."""
124
+ markup = models.Markup.objects.create(
125
+ name="express_markup",
126
+ amount=10.0,
127
+ markup_type="PERCENTAGE",
128
+ service_codes=["fedex_overnight"],
129
+ )
130
+
131
+ from karrio.server.core.datatypes import Rate, RateResponse
132
+
133
+ overnight_rate = Rate(
134
+ id="rate_1",
135
+ carrier_id="fedex",
136
+ carrier_name="fedex",
137
+ service="fedex_overnight",
138
+ total_charge=100.0,
139
+ currency="USD",
140
+ extra_charges=[],
141
+ meta={"carrier_connection_id": "car_123"},
142
+ )
143
+
144
+ ground_rate = Rate(
145
+ id="rate_2",
146
+ carrier_id="fedex",
147
+ carrier_name="fedex",
148
+ service="fedex_ground",
149
+ total_charge=50.0,
150
+ currency="USD",
151
+ extra_charges=[],
152
+ meta={"carrier_connection_id": "car_123"},
153
+ )
154
+
155
+ response = RateResponse(
156
+ rates=[overnight_rate, ground_rate],
157
+ messages=[],
158
+ )
159
+
160
+ result = markup.apply_charge(response)
161
+
162
+ # Overnight rate should have 10% markup applied
163
+ overnight_result = next(r for r in result.rates if r.service == "fedex_overnight")
164
+ self.assertEqual(overnight_result.total_charge, 110.0) # 100 + 10%
165
+
166
+ # Ground rate should NOT have markup applied
167
+ ground_result = next(r for r in result.rates if r.service == "fedex_ground")
168
+ self.assertEqual(ground_result.total_charge, 50.0) # unchanged
169
+
170
+ def test_connection_ids_filter(self):
171
+ """Test that markup only applies to specified connection IDs."""
172
+ markup = models.Markup.objects.create(
173
+ name="specific_connection_markup",
174
+ amount=3.0,
175
+ markup_type="AMOUNT",
176
+ connection_ids=["car_special_123"],
177
+ )
178
+
179
+ from karrio.server.core.datatypes import Rate, RateResponse
180
+
181
+ special_rate = Rate(
182
+ id="rate_1",
183
+ carrier_id="fedex",
184
+ carrier_name="fedex",
185
+ service="fedex_ground",
186
+ total_charge=25.0,
187
+ currency="USD",
188
+ extra_charges=[],
189
+ meta={"carrier_connection_id": "car_special_123"},
190
+ )
191
+
192
+ regular_rate = Rate(
193
+ id="rate_2",
194
+ carrier_id="fedex",
195
+ carrier_name="fedex",
196
+ service="fedex_ground",
197
+ total_charge=25.0,
198
+ currency="USD",
199
+ extra_charges=[],
200
+ meta={"carrier_connection_id": "car_regular_456"},
201
+ )
202
+
203
+ response = RateResponse(
204
+ rates=[special_rate, regular_rate],
205
+ messages=[],
206
+ )
207
+
208
+ result = markup.apply_charge(response)
209
+
210
+ # Rate with special connection should have markup
211
+ special_result = next(
212
+ r for r in result.rates
213
+ if r.meta.get("carrier_connection_id") == "car_special_123"
214
+ )
215
+ self.assertEqual(special_result.total_charge, 28.0) # 25 + 3
216
+
217
+ # Rate with regular connection should NOT have markup
218
+ regular_result = next(
219
+ r for r in result.rates
220
+ if r.meta.get("carrier_connection_id") == "car_regular_456"
221
+ )
222
+ self.assertEqual(regular_result.total_charge, 25.0) # unchanged
223
+
224
+ def test_empty_filters_apply_to_all(self):
225
+ """Test that markup with no filters applies to all rates."""
226
+ markup = models.Markup.objects.create(
227
+ name="global_markup",
228
+ amount=1.0,
229
+ markup_type="AMOUNT",
230
+ carrier_codes=[],
231
+ service_codes=[],
232
+ connection_ids=[],
233
+ )
234
+
235
+ from karrio.server.core.datatypes import Rate, RateResponse
236
+
237
+ rate1 = Rate(
238
+ id="rate_1",
239
+ carrier_id="fedex",
240
+ carrier_name="fedex",
241
+ service="fedex_ground",
242
+ total_charge=10.0,
243
+ currency="USD",
244
+ extra_charges=[],
245
+ meta={},
246
+ )
247
+
248
+ rate2 = Rate(
249
+ id="rate_2",
250
+ carrier_id="ups",
251
+ carrier_name="ups",
252
+ service="ups_ground",
253
+ total_charge=12.0,
254
+ currency="USD",
255
+ extra_charges=[],
256
+ meta={},
257
+ )
258
+
259
+ response = RateResponse(
260
+ rates=[rate1, rate2],
261
+ messages=[],
262
+ )
263
+
264
+ result = markup.apply_charge(response)
265
+
266
+ # Both rates should have markup applied
267
+ for rate in result.rates:
268
+ if rate.carrier_name == "fedex":
269
+ self.assertEqual(rate.total_charge, 11.0) # 10 + 1
270
+ elif rate.carrier_name == "ups":
271
+ self.assertEqual(rate.total_charge, 13.0) # 12 + 1
272
+
273
+
274
+ class TestFeeCapture(TestCase):
275
+ """Test fee capture after shipment creation."""
276
+
277
+ def setUp(self):
278
+ """Set up test data."""
279
+ # Create a markup
280
+ self.markup = models.Markup.objects.create(
281
+ name="test_markup",
282
+ amount=5.0,
283
+ markup_type="AMOUNT",
284
+ )
285
+
286
+ def test_capture_fees_from_shipment(self):
287
+ """Test that fees are captured from shipment's selected_rate via signal.
288
+
289
+ When a shipment is saved with status='purchased' and a selected_rate,
290
+ the fee capture signal should automatically capture fees.
291
+ """
292
+ from django.contrib.auth import get_user_model
293
+ import karrio.server.manager.models as manager
294
+
295
+ User = get_user_model()
296
+ user = User.objects.create_user(
297
+ email="test@example.com",
298
+ password="testpass123",
299
+ )
300
+
301
+ # Create a shipment with markup in extra_charges
302
+ # The signal should automatically capture fees on save
303
+ shipment = manager.Shipment.objects.create(
304
+ status="purchased",
305
+ test_mode=True,
306
+ shipper={"city": "Montreal"},
307
+ recipient={"city": "Toronto"},
308
+ parcels=[{"weight": 1}],
309
+ selected_rate={
310
+ "carrier_name": "fedex",
311
+ "carrier_id": "fedex",
312
+ "service": "fedex_ground",
313
+ "total_charge": 15.0,
314
+ "currency": "USD",
315
+ "extra_charges": [
316
+ {"id": self.markup.id, "name": "test_markup", "amount": 5.0, "currency": "USD"},
317
+ ],
318
+ "meta": {
319
+ "carrier_code": "fedex",
320
+ "carrier_connection_id": "car_123",
321
+ },
322
+ },
323
+ created_by=user,
324
+ )
325
+
326
+ # Verify fee was captured by the signal (don't call manually)
327
+ fees = models.Fee.objects.filter(shipment_id=shipment.id)
328
+ self.assertEqual(fees.count(), 1)
329
+
330
+ fee = fees.first()
331
+ self.assertEqual(fee.markup_id, self.markup.id)
332
+ self.assertEqual(fee.name, "test_markup")
333
+ self.assertEqual(fee.amount, 5.0)
334
+ self.assertEqual(fee.currency, "USD")
335
+ self.assertEqual(fee.carrier_code, "fedex")
336
+ self.assertEqual(fee.service_code, "fedex_ground")
337
+ self.assertEqual(fee.connection_id, "car_123")
338
+ self.assertEqual(fee.test_mode, True)
339
+
340
+ def test_capture_fees_function_directly(self):
341
+ """Test the capture_fees_for_shipment function in isolation.
342
+
343
+ Create shipment with status='created' (so signal won't fire),
344
+ then manually call capture function.
345
+ """
346
+ from django.contrib.auth import get_user_model
347
+ import karrio.server.manager.models as manager
348
+
349
+ User = get_user_model()
350
+ user = User.objects.create_user(
351
+ email="test_direct@example.com",
352
+ password="testpass123",
353
+ )
354
+
355
+ # Create shipment with status that won't trigger signal
356
+ shipment = manager.Shipment.objects.create(
357
+ status="created", # Signal won't fire for this status
358
+ test_mode=True,
359
+ shipper={"city": "Montreal"},
360
+ recipient={"city": "Toronto"},
361
+ parcels=[{"weight": 1}],
362
+ selected_rate={
363
+ "carrier_name": "fedex",
364
+ "carrier_id": "fedex",
365
+ "service": "fedex_ground",
366
+ "total_charge": 15.0,
367
+ "currency": "USD",
368
+ "extra_charges": [
369
+ {"id": self.markup.id, "name": "test_markup", "amount": 5.0, "currency": "USD"},
370
+ ],
371
+ "meta": {
372
+ "carrier_code": "fedex",
373
+ "carrier_connection_id": "car_123",
374
+ },
375
+ },
376
+ created_by=user,
377
+ )
378
+
379
+ # No fees should exist yet
380
+ self.assertEqual(models.Fee.objects.filter(shipment_id=shipment.id).count(), 0)
381
+
382
+ # Manually capture fees
383
+ signals.capture_fees_for_shipment(shipment)
384
+
385
+ # Verify fee was captured
386
+ fees = models.Fee.objects.filter(shipment_id=shipment.id)
387
+ self.assertEqual(fees.count(), 1)
388
+
389
+ fee = fees.first()
390
+ self.assertEqual(fee.markup_id, self.markup.id)
391
+ self.assertEqual(fee.amount, 5.0)
392
+
393
+ def test_no_duplicate_fee_capture(self):
394
+ """Test that fees are not captured twice for the same shipment.
395
+
396
+ Signal should check if fees exist before capturing.
397
+ """
398
+ from django.contrib.auth import get_user_model
399
+ import karrio.server.manager.models as manager
400
+
401
+ User = get_user_model()
402
+ user = User.objects.create_user(
403
+ email="test2@example.com",
404
+ password="testpass123",
405
+ )
406
+
407
+ # Create shipment - signal will capture fee
408
+ shipment = manager.Shipment.objects.create(
409
+ status="purchased",
410
+ test_mode=True,
411
+ shipper={"city": "Montreal"},
412
+ recipient={"city": "Toronto"},
413
+ parcels=[{"weight": 1}],
414
+ selected_rate={
415
+ "carrier_name": "fedex",
416
+ "carrier_id": "fedex",
417
+ "service": "fedex_ground",
418
+ "total_charge": 15.0,
419
+ "currency": "USD",
420
+ "extra_charges": [
421
+ {"id": self.markup.id, "name": "test_markup", "amount": 5.0, "currency": "USD"},
422
+ ],
423
+ "meta": {"carrier_connection_id": "car_123"},
424
+ },
425
+ created_by=user,
426
+ )
427
+
428
+ # Fee should already exist from signal
429
+ fees = models.Fee.objects.filter(shipment_id=shipment.id)
430
+ self.assertEqual(fees.count(), 1)
431
+
432
+ # Save again - signal should not create duplicate
433
+ shipment.save()
434
+
435
+ # Still only one fee
436
+ fees = models.Fee.objects.filter(shipment_id=shipment.id)
437
+ self.assertEqual(fees.count(), 1)
438
+
439
+
440
+ # Test data fixtures
441
+
54
442
  RATING_DATA = {
55
443
  "shipper": {
56
444
  "postal_code": "V6M2V9",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karrio_server_pricing
3
- Version: 2026.1.1
3
+ Version: 2026.1.4
4
4
  Summary: Multi-carrier shipping API Pricing panel
5
5
  Author-email: karrio <hello@karrio.io>
6
6
  License-Expression: LGPL-3.0
@@ -1,10 +1,10 @@
1
1
  karrio/server/pricing/__init__.py,sha256=zXpxAFuHsCdAaESYGLRvdqCs7CXtIn8cF688jV4ufQc,64
2
- karrio/server/pricing/admin.py,sha256=FPLSutMYkKF-fGWGeKQmfJLO5UAB8wRABhhVS9lAckE,1683
3
- karrio/server/pricing/apps.py,sha256=n9qkGVkmU1gTCLc7WR3iGaPWwW9NtaVFGR2xvBSMvA4,341
4
- karrio/server/pricing/models.py,sha256=r4-VCAd96AbAJMdqCbIcOnU3si3oH5oJkNfY-SpPU6U,5341
5
- karrio/server/pricing/serializers.py,sha256=VZnqJl4a1pi9L8frVrpphBHERxY5cl0EgVhJMmdVxQM,496
6
- karrio/server/pricing/signals.py,sha256=mdMjsNXIeu3QKtEBkHxiYJwsOAchWTzQ_RMsMxPIooo,984
7
- karrio/server/pricing/tests.py,sha256=rPTKlXn93buLLCyXxt5Y1OK2566aKY8cZVY4CUz2p6Q,11808
2
+ karrio/server/pricing/admin.py,sha256=vDYvoFk9M6bGK6NBoJCRkxL5dla4sibNfej69_RIOT8,5642
3
+ karrio/server/pricing/apps.py,sha256=LBpxtFvuUn36DjMiPlQL51V5WfVMDf7N4QS3IUs_Pcs,431
4
+ karrio/server/pricing/models.py,sha256=gRcCHNEQpQSP4T44RvyPInNYzXL3SHXq9Ar0f9il3YY,11697
5
+ karrio/server/pricing/serializers.py,sha256=48panQv9ZyLzkVa94XzOqbXxFm0daMK1_jOVJwQiNbI,574
6
+ karrio/server/pricing/signals.py,sha256=qIORi1NDw4kROAKOb6Dg1Vx3VOSduN28Z9bm1rHiNtU,6704
7
+ karrio/server/pricing/tests.py,sha256=iaTqsY2pSPR1YRy9TqjhkfvRN8776l0Zoh4xeT0awQc,24818
8
8
  karrio/server/pricing/views.py,sha256=xc1IQHrsij7j33TUbo-_oewy3vs03pw_etpBWaMYJl0,63
9
9
  karrio/server/pricing/migrations/0001_initial.py,sha256=KKEklxf8Q1-IQn-8ZchbCi3-zmIyjn80DaGqPgRRI5w,52327
10
10
  karrio/server/pricing/migrations/0002_auto_20201127_0721.py,sha256=zhiRIHjyUhwH8Mw2qljkWO1wkZPftqAJpwh6QH5oO2o,41778
@@ -81,8 +81,12 @@ karrio/server/pricing/migrations/0072_alter_surcharge_carriers_alter_surcharge_s
81
81
  karrio/server/pricing/migrations/0073_alter_surcharge_carriers_alter_surcharge_services.py,sha256=X0QX4UHAFI_Gu0zqPeL-AvZrz4HD2W7mW9LsvvBDpkE,239938
82
82
  karrio/server/pricing/migrations/0074_alter_surcharge_carriers_alter_surcharge_services.py,sha256=s-ic_8rAIdhUS7cmTwVdkeMQiemBMfpcfSwNnaTD410,11301
83
83
  karrio/server/pricing/migrations/0075_alter_surcharge_carriers_alter_surcharge_services.py,sha256=hfEWE4zBbvz4Iv58NIA5HdNxqgjnwQb0ACSgMMA_aMs,240966
84
+ karrio/server/pricing/migrations/0076_create_markup_and_fee_models.py,sha256=ZsfQoLv4shhia9p3tIZ_eotpIipVhuqhjFnzQ2Hc3GQ,9152
85
+ karrio/server/pricing/migrations/0077_migrate_surcharge_to_markup_data.py,sha256=K0R8ffn9h-xajThtfEgQR2nEj6sui0PAhT_ouxV6bqw,2192
86
+ karrio/server/pricing/migrations/0078_cleanup.py,sha256=aEO5ceQ6-35jz3mGblQ2RD_144Uu-gVAjaEckqHM1OM,4263
87
+ karrio/server/pricing/migrations/0079_fee_snapshot_model.py,sha256=n18S32UPAp_WMzVL4_Hf96drwGPE72pc45Ul2qPs-nk,9843
84
88
  karrio/server/pricing/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
- karrio_server_pricing-2026.1.1.dist-info/METADATA,sha256=fwcE-6DGyZv5FcZMww5glhTl-LMiTW7JJcAGKrwlxIU,684
86
- karrio_server_pricing-2026.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
- karrio_server_pricing-2026.1.1.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
88
- karrio_server_pricing-2026.1.1.dist-info/RECORD,,
89
+ karrio_server_pricing-2026.1.4.dist-info/METADATA,sha256=-6vjSrYm_khmzOs4sQIm143Q2nUEn5qtz0akNxAZQhw,684
90
+ karrio_server_pricing-2026.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
91
+ karrio_server_pricing-2026.1.4.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
92
+ karrio_server_pricing-2026.1.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5