diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a354f37..d36b21fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-python-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools. +## 1.3.0 - 2024-12-17 + +### Added + +- Support for adjustment type, see [related changelog](https://developer.paddle.com/changelog/2024/refund-credit-full-total?utm_source=dx&utm_medium=paddle-python-sdk) +- Added Vietnamese Dong (`VND`) as a supported currency for payments [related changelog](https://developer.paddle.com/changelog/2024/vietnamese-dong-vnd-supported-currency?utm_source=dx&utm_medium=paddle-python-sdk) + ## 1.2.1 - 2024-12-04 ### Fixed diff --git a/paddle_billing/Client.py b/paddle_billing/Client.py index b073d9cd..45059bf7 100644 --- a/paddle_billing/Client.py +++ b/paddle_billing/Client.py @@ -204,7 +204,7 @@ def build_request_session(self) -> Session: "Authorization": f"Bearer {self.__api_key}", "Content-Type": "application/json", "Paddle-Version": str(self.use_api_version), - "User-Agent": "PaddleSDK/python 1.2.1", + "User-Agent": "PaddleSDK/python 1.3.0", } ) diff --git a/paddle_billing/Entities/Adjustment.py b/paddle_billing/Entities/Adjustment.py index 31ffce1b..f8b904ba 100644 --- a/paddle_billing/Entities/Adjustment.py +++ b/paddle_billing/Entities/Adjustment.py @@ -6,6 +6,7 @@ from paddle_billing.Entities.Adjustments import AdjustmentItem, AdjustmentTaxRatesUsed from paddle_billing.Entities.Shared import ( Action, + AdjustmentActionType, AdjustmentStatus, CurrencyCode, PayoutTotalsAdjustment, @@ -30,6 +31,7 @@ class Adjustment(Entity): created_at: datetime updated_at: datetime | None tax_rates_used: list[AdjustmentTaxRatesUsed] + type: AdjustmentActionType @staticmethod def from_dict(data: dict) -> Adjustment: @@ -51,4 +53,5 @@ def from_dict(data: dict) -> Adjustment: ), updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else None, tax_rates_used=[AdjustmentTaxRatesUsed.from_dict(item) for item in data["tax_rates_used"]], + type=AdjustmentStatus(data["type"]), ) diff --git a/paddle_billing/Entities/Shared/AdjustmentActionType.py b/paddle_billing/Entities/Shared/AdjustmentActionType.py new file mode 100644 index 00000000..4fc6e9a9 --- /dev/null +++ b/paddle_billing/Entities/Shared/AdjustmentActionType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class AdjustmentActionType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Full: "AdjustmentActionType" = "full" + Partial: "AdjustmentActionType" = "partial" diff --git a/paddle_billing/Entities/Shared/CurrencyCode.py b/paddle_billing/Entities/Shared/CurrencyCode.py index 0dffec82..6756106c 100644 --- a/paddle_billing/Entities/Shared/CurrencyCode.py +++ b/paddle_billing/Entities/Shared/CurrencyCode.py @@ -31,4 +31,5 @@ class CurrencyCode(PaddleStrEnum, metaclass=PaddleStrEnumMeta): TRY: "CurrencyCode" = "TRY" TWD: "CurrencyCode" = "TWD" UAH: "CurrencyCode" = "UAH" + VND: "CurrencyCode" = "VND" ZAR: "CurrencyCode" = "ZAR" diff --git a/paddle_billing/Entities/Shared/__init__.py b/paddle_billing/Entities/Shared/__init__.py index bc277b28..c7320bb0 100644 --- a/paddle_billing/Entities/Shared/__init__.py +++ b/paddle_billing/Entities/Shared/__init__.py @@ -1,5 +1,6 @@ from paddle_billing.Entities.Shared.Action import Action from paddle_billing.Entities.Shared.AddressPreview import AddressPreview +from paddle_billing.Entities.Shared.AdjustmentActionType import AdjustmentActionType from paddle_billing.Entities.Shared.AdjustmentItemTotals import AdjustmentItemTotals from paddle_billing.Entities.Shared.AdjustmentStatus import AdjustmentStatus from paddle_billing.Entities.Shared.AdjustmentTotals import AdjustmentTotals diff --git a/paddle_billing/Notifications/Entities/Adjustment.py b/paddle_billing/Notifications/Entities/Adjustment.py index 4b67f1bd..a78b8e30 100644 --- a/paddle_billing/Notifications/Entities/Adjustment.py +++ b/paddle_billing/Notifications/Entities/Adjustment.py @@ -6,6 +6,7 @@ from paddle_billing.Notifications.Entities.Entity import Entity from paddle_billing.Notifications.Entities.Shared import ( Action, + AdjustmentActionType, AdjustmentStatus, CurrencyCode, PayoutTotalsAdjustment, @@ -30,6 +31,7 @@ class Adjustment(Entity): created_at: datetime updated_at: datetime | None tax_rates_used: list[AdjustmentTaxRatesUsed] | None + type: AdjustmentActionType | None @staticmethod def from_dict(data: dict) -> Adjustment: @@ -55,4 +57,5 @@ def from_dict(data: dict) -> Adjustment: if data.get("tax_rates_used") else None ), + type=AdjustmentActionType(data["type"]) if data.get("type") else None, ) diff --git a/paddle_billing/Notifications/Entities/Shared/AdjustmentActionType.py b/paddle_billing/Notifications/Entities/Shared/AdjustmentActionType.py new file mode 100644 index 00000000..4fc6e9a9 --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/AdjustmentActionType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class AdjustmentActionType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Full: "AdjustmentActionType" = "full" + Partial: "AdjustmentActionType" = "partial" diff --git a/paddle_billing/Notifications/Entities/Shared/CurrencyCode.py b/paddle_billing/Notifications/Entities/Shared/CurrencyCode.py index 0dffec82..6756106c 100644 --- a/paddle_billing/Notifications/Entities/Shared/CurrencyCode.py +++ b/paddle_billing/Notifications/Entities/Shared/CurrencyCode.py @@ -31,4 +31,5 @@ class CurrencyCode(PaddleStrEnum, metaclass=PaddleStrEnumMeta): TRY: "CurrencyCode" = "TRY" TWD: "CurrencyCode" = "TWD" UAH: "CurrencyCode" = "UAH" + VND: "CurrencyCode" = "VND" ZAR: "CurrencyCode" = "ZAR" diff --git a/paddle_billing/Notifications/Entities/Shared/__init__.py b/paddle_billing/Notifications/Entities/Shared/__init__.py index 9c0d11fa..df9c5ba2 100644 --- a/paddle_billing/Notifications/Entities/Shared/__init__.py +++ b/paddle_billing/Notifications/Entities/Shared/__init__.py @@ -1,5 +1,6 @@ from paddle_billing.Notifications.Entities.Shared.Action import Action from paddle_billing.Notifications.Entities.Shared.AddressPreview import AddressPreview +from paddle_billing.Notifications.Entities.Shared.AdjustmentActionType import AdjustmentActionType from paddle_billing.Notifications.Entities.Shared.AdjustmentItemTotals import AdjustmentItemTotals from paddle_billing.Notifications.Entities.Shared.AdjustmentStatus import AdjustmentStatus from paddle_billing.Notifications.Entities.Shared.AdjustmentTotals import AdjustmentTotals diff --git a/paddle_billing/Resources/Adjustments/Operations/CreateAdjustment.py b/paddle_billing/Resources/Adjustments/Operations/CreateAdjustment.py index 48a9dba4..2530c11e 100644 --- a/paddle_billing/Resources/Adjustments/Operations/CreateAdjustment.py +++ b/paddle_billing/Resources/Adjustments/Operations/CreateAdjustment.py @@ -1,15 +1,58 @@ from dataclasses import dataclass +from paddle_billing.Undefined import Undefined + from paddle_billing.Operation import Operation -from paddle_billing.Entities.Shared import Action +from paddle_billing.Entities.Shared import Action, AdjustmentActionType from paddle_billing.Resources.Adjustments.Operations import CreateAdjustmentItem +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + @dataclass class CreateAdjustment(Operation): action: Action - items: list[CreateAdjustmentItem] + items: list[CreateAdjustmentItem] | None | Undefined reason: str transaction_id: str + type: AdjustmentActionType | Undefined = Undefined() + + def __post_init__(self): + if self.type != AdjustmentActionType.Full and ( + self.items is None or isinstance(self.items, Undefined) or len(self.items) == 0 + ): + raise InvalidArgumentException.array_is_empty("items") + + if self.type == AdjustmentActionType.Full and isinstance(self.items, list): + raise InvalidArgumentException("items are not allowed when the adjustment type is full") + + @staticmethod + def full( + action: Action, + reason: str, + transaction_id: str, + ) -> "CreateAdjustment": + return CreateAdjustment( + action=action, + reason=reason, + transaction_id=transaction_id, + items=Undefined(), + type=AdjustmentActionType.Full, + ) + + @staticmethod + def partial( + action: Action, + items: list[CreateAdjustmentItem], + reason: str, + transaction_id: str, + ) -> "CreateAdjustment": + return CreateAdjustment( + action=action, + reason=reason, + transaction_id=transaction_id, + items=items, + type=AdjustmentActionType.Partial, + ) diff --git a/setup.py b/setup.py index 5a88d8c4..45f690a5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( - version="1.2.1", + version="1.3.0", author="Paddle and contributors", author_email="team-dx@paddle.com", description="Paddle's Python SDK for Paddle Billing", diff --git a/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_full_with_no_items.json b/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_full_with_no_items.json new file mode 100644 index 00000000..2c6d2049 --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_full_with_no_items.json @@ -0,0 +1,6 @@ +{ + "action": "refund", + "type": "full", + "reason": "error", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5" +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_full_with_null_items.json b/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_full_with_null_items.json new file mode 100644 index 00000000..09698c8e --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_full_with_null_items.json @@ -0,0 +1,7 @@ +{ + "action": "refund", + "type": "full", + "items": null, + "reason": "error", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5" +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_partial_with_items.json b/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_partial_with_items.json new file mode 100644 index 00000000..78000855 --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/request/create_type_partial_with_items.json @@ -0,0 +1,13 @@ +{ + "action": "refund", + "type": "partial", + "items": [ + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "partial", + "amount": "100" + } + ], + "reason": "error", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5" +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json b/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json index 77ad8635..6058bb27 100644 --- a/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json +++ b/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json @@ -2,6 +2,7 @@ "data": { "id": "adj_01h8c65c2ggq5nxswnnwv78e75", "action": "refund", + "type": "partial", "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", diff --git a/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json b/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json index bce36439..6909dbb8 100644 --- a/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json @@ -3,6 +3,7 @@ { "id": "adj_01h8c65c2ggq5nxswnnwv78e75", "action": "refund", + "type": "partial", "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", @@ -55,6 +56,7 @@ { "id": "adj_01h8bxezh16gm6t8rx21dx271b", "action": "credit", + "type": "partial", "credit_applied_to_balance": true, "transaction_id": "txn_01h8bx69629a16wwm9z8rjmak3", "subscription_id": "sub_01h8bx8fmywym11t6swgzba704", @@ -114,6 +116,7 @@ { "id": "adj_01h7jgzjqt2s8sab70e03ptkhv", "action": "credit", + "type": "partial", "credit_applied_to_balance": true, "transaction_id": "txn_01h7jgd9bkwjscj3ae15g5d3vs", "subscription_id": "sub_01h7ht5z5wdg9pz18jx1fagp8k", @@ -173,6 +176,7 @@ { "id": "adj_01h7jf6ptkfsc93hzc20fgf8wy", "action": "credit", + "type": "partial", "credit_applied_to_balance": true, "transaction_id": "txn_01h7je77vc1qmzxntem45ebb5q", "subscription_id": "sub_01h7ht5z5wdg9pz18jx1fagp8k", @@ -232,6 +236,7 @@ { "id": "adj_01h468w41ttb2j2bh8av74gwt1", "action": "credit", + "type": "partial", "credit_applied_to_balance": true, "transaction_id": "txn_01h468crc3b3fe98a5ft53recb", "subscription_id": "sub_01h468kv3jhs5jk330gszncsgt", diff --git a/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json index 9e0984f0..d6fea647 100644 --- a/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json +++ b/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json @@ -2,6 +2,7 @@ "data": { "id": "adj_01h8c65c2ggq5nxswnnwv78e75", "action": "refund", + "type": "partial", "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", diff --git a/tests/Functional/Resources/Adjustments/test_AdjustmentsClient.py b/tests/Functional/Resources/Adjustments/test_AdjustmentsClient.py index 2bc79b40..9e82b517 100644 --- a/tests/Functional/Resources/Adjustments/test_AdjustmentsClient.py +++ b/tests/Functional/Resources/Adjustments/test_AdjustmentsClient.py @@ -5,7 +5,7 @@ from paddle_billing.Entities.Adjustment import Adjustment, AdjustmentTaxRatesUsed from paddle_billing.Entities.Collections import AdjustmentCollection from paddle_billing.Entities.AdjustmentCreditNote import AdjustmentCreditNote -from paddle_billing.Entities.Shared import Action, AdjustmentStatus, AdjustmentType, Disposition +from paddle_billing.Entities.Shared import Action, AdjustmentActionType, AdjustmentStatus, AdjustmentType, Disposition from paddle_billing.Resources.Adjustments.Operations import ( CreateAdjustment, @@ -15,6 +15,8 @@ ) from paddle_billing.Resources.Shared.Operations import Pager +from paddle_billing.Undefined import Undefined + from tests.Utils.ReadsFixture import ReadsFixtures @@ -49,10 +51,77 @@ class TestAdjustmentsClient: ReadsFixtures.read_raw_json_fixture("response/full_entity"), "/adjustments", ), + ( + CreateAdjustment( + Action.Refund, + [CreateAdjustmentItem("txnitm_01h8bxryv3065dyh6103p3yg28", AdjustmentType.Partial, "100")], + "error", + "txn_01h8bxpvx398a7zbawb77y0kp5", + AdjustmentActionType.Partial, + ), + ReadsFixtures.read_raw_json_fixture("request/create_type_partial_with_items"), + 200, + ReadsFixtures.read_raw_json_fixture("response/minimal_entity"), + "/adjustments", + ), + ( + CreateAdjustment.partial( + Action.Refund, + [CreateAdjustmentItem("txnitm_01h8bxryv3065dyh6103p3yg28", AdjustmentType.Partial, "100")], + "error", + "txn_01h8bxpvx398a7zbawb77y0kp5", + ), + ReadsFixtures.read_raw_json_fixture("request/create_type_partial_with_items"), + 200, + ReadsFixtures.read_raw_json_fixture("response/minimal_entity"), + "/adjustments", + ), + ( + CreateAdjustment.full( + Action.Refund, + "error", + "txn_01h8bxpvx398a7zbawb77y0kp5", + ), + ReadsFixtures.read_raw_json_fixture("request/create_type_full_with_no_items"), + 200, + ReadsFixtures.read_raw_json_fixture("response/minimal_entity"), + "/adjustments", + ), + ( + CreateAdjustment( + Action.Refund, + None, + "error", + "txn_01h8bxpvx398a7zbawb77y0kp5", + AdjustmentActionType.Full, + ), + ReadsFixtures.read_raw_json_fixture("request/create_type_full_with_null_items"), + 200, + ReadsFixtures.read_raw_json_fixture("response/minimal_entity"), + "/adjustments", + ), + ( + CreateAdjustment( + Action.Refund, + Undefined(), + "error", + "txn_01h8bxpvx398a7zbawb77y0kp5", + AdjustmentActionType.Full, + ), + ReadsFixtures.read_raw_json_fixture("request/create_type_full_with_no_items"), + 200, + ReadsFixtures.read_raw_json_fixture("response/minimal_entity"), + "/adjustments", + ), ], ids=[ "Create adjustment with basic data", "Create adjustment with full data", + "Create adjustment with basic data with type", + "Create partial adjustment with items", + "Create full adjustment with no items", + "Create full adjustment with None items", + "Create full adjustment with Undefined items", ], ) def test_create_adjustment_uses_expected_payload( diff --git a/tests/Unit/Resources/Adjustments/Operations/test_CreateAdjustment.py b/tests/Unit/Resources/Adjustments/Operations/test_CreateAdjustment.py new file mode 100644 index 00000000..09a33d37 --- /dev/null +++ b/tests/Unit/Resources/Adjustments/Operations/test_CreateAdjustment.py @@ -0,0 +1,48 @@ +from pytest import mark, raises + +from paddle_billing.Undefined import Undefined +from paddle_billing.Entities.Shared import Action, AdjustmentActionType +from paddle_billing.Resources.Adjustments.Operations import CreateAdjustment +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + + +class TestCreateAdjustment: + @mark.parametrize( + "items", + [ + (None), + (Undefined()), + ], + ids=[ + "Full type with None items", + "Full type with undefined items", + ], + ) + def test_does_not_raise_invalid_argument_exception_for_full_type_with_empty_items( + self, + items, + ): + operation = CreateAdjustment( + Action.Refund, items, "error", "txn_01h8bxpvx398a7zbawb77y0kp5", AdjustmentActionType.Full + ) + + assert operation.items == items + + @mark.parametrize( + "items, type, expected_message", + [ + ([], AdjustmentActionType.Full, "items are not allowed when the adjustment type is full"), + ([], AdjustmentActionType.Partial, "'items' cannot be empty"), + ([], Undefined(), "'items' cannot be empty"), + ], + ids=[ + "Full type with items", + "Partial type with empty items", + "Undefined type with empty items", + ], + ) + def test_raises_invalid_argument_exception(self, items, type, expected_message): + with raises(InvalidArgumentException) as exception_info: + CreateAdjustment(Action.Refund, items, "error", "txn_01h8bxpvx398a7zbawb77y0kp5", type) + + assert str(exception_info.value) == expected_message