diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6fd3e..1a1dd57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx ### Added - Added `product` to `subscription.items[]`, see [related changelog](https://developer.paddle.com/changelog/2024/subscription-items-product?utm_source=dx&utm_medium=paddle-python-sdk) +- Support custom prices when updating and previewing subscriptions, see [related changelog](https://developer.paddle.com/changelog/2024/add-custom-items-subscription) + +### Changed + +- `paddle_billing.Entities.Shared.CustomData` is no longer a `dataclass` ## 0.2.2 - 2024-09-03 diff --git a/paddle_billing/Client.py b/paddle_billing/Client.py index 86517dd..aaabe9b 100644 --- a/paddle_billing/Client.py +++ b/paddle_billing/Client.py @@ -105,10 +105,6 @@ def logging_hook(self, response, *args, **kwargs): @staticmethod def serialize_json_payload(payload: dict) -> str: - # Removes unneeded level of nested CustomData data - if payload.get('custom_data') and 'data' in payload['custom_data']: - payload['custom_data'] = payload['custom_data']['data'] - json_payload = json_dumps(payload, cls=PayloadEncoder) final_json = json_payload if json_payload != '[]' else '{}' diff --git a/paddle_billing/Entities/Shared/CustomData.py b/paddle_billing/Entities/Shared/CustomData.py index a66b2cc..1f0c17e 100644 --- a/paddle_billing/Entities/Shared/CustomData.py +++ b/paddle_billing/Entities/Shared/CustomData.py @@ -1,10 +1,14 @@ -from dataclasses import dataclass - - -@dataclass class CustomData: data: dict | list # JSON serializable Python types + def __init__(self, data: dict | list): + self.data = data + + def get_parameters(self): return self.data + + + def to_json(self): + return self.data diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionItemsWithPrice.py b/paddle_billing/Entities/Subscriptions/SubscriptionItemsWithPrice.py index 55128a7..183e294 100644 --- a/paddle_billing/Entities/Subscriptions/SubscriptionItemsWithPrice.py +++ b/paddle_billing/Entities/Subscriptions/SubscriptionItemsWithPrice.py @@ -1,5 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import asdict, dataclass from paddle_billing.Entities.Subscriptions.SubscriptionNonCatalogPrice import SubscriptionNonCatalogPrice from paddle_billing.Entities.Subscriptions.SubscriptionNonCatalogPriceWithProduct import SubscriptionNonCatalogPriceWithProduct @@ -17,3 +17,7 @@ def from_dict(data: dict) -> SubscriptionItemsWithPrice: price = data['price'], quantity = data['quantity'], ) + + + def get_parameters(self) -> dict: + return asdict(self) diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPrice.py b/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPrice.py index f6d81a1..f509fb6 100644 --- a/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPrice.py +++ b/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPrice.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from paddle_billing.Entities.Shared import CustomData, Money, PriceQuantity, TaxMode, UnitPriceOverride +from paddle_billing.Entities.Shared import CustomData, Money, PriceQuantity, TaxMode, UnitPriceOverride, TimePeriod @dataclass @@ -14,6 +14,8 @@ class SubscriptionNonCatalogPrice: unit_price_overrides: list[UnitPriceOverride] quantity: PriceQuantity custom_data: CustomData | None + billing_cycle: TimePeriod | None + trial_period: TimePeriod | None @staticmethod @@ -27,4 +29,6 @@ def from_dict(data: dict) -> SubscriptionNonCatalogPrice: unit_price_overrides = [UnitPriceOverride.from_dict(override) for override in data['unit_price_overrides']], quantity = PriceQuantity.from_dict(data['quantity']), custom_data = CustomData(data['custom_data']) if data.get('custom_data') else None, + billing_cycle = TimePeriod.from_dict(data['billing_cycle']) if data.get('billing_cycle') else None, + trial_period = TimePeriod.from_dict(data['trial_period']) if data.get('trial_period') else None, ) diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPriceWithProduct.py b/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPriceWithProduct.py index 9eb2595..f6c57fb 100644 --- a/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPriceWithProduct.py +++ b/paddle_billing/Entities/Subscriptions/SubscriptionNonCatalogPriceWithProduct.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from paddle_billing.Entities.Shared import CustomData, Money, PriceQuantity, TaxMode, UnitPriceOverride +from paddle_billing.Entities.Shared import CustomData, Money, PriceQuantity, TaxMode, UnitPriceOverride, TimePeriod from paddle_billing.Entities.Subscriptions.SubscriptionNonCatalogProduct import SubscriptionNonCatalogProduct @@ -16,6 +16,8 @@ class SubscriptionNonCatalogPriceWithProduct: unit_price_overrides: list[UnitPriceOverride] quantity: PriceQuantity custom_data: CustomData | None + billing_cycle: TimePeriod | None + trial_period: TimePeriod | None @staticmethod @@ -29,4 +31,6 @@ def from_dict(data: dict) -> SubscriptionNonCatalogPriceWithProduct: unit_price_overrides = [UnitPriceOverride.from_dict(override) for override in data['unit_price_overrides']], quantity = PriceQuantity.from_dict(data['quantity']), custom_data = CustomData(data['custom_data']) if data.get('custom_data') else None, + billing_cycle = TimePeriod.from_dict(data['billing_cycle']) if data.get('billing_cycle') else None, + trial_period = TimePeriod.from_dict(data['trial_period']) if data.get('trial_period') else None, ) diff --git a/paddle_billing/Resources/Subscriptions/Operations/PreviewUpdateSubscription.py b/paddle_billing/Resources/Subscriptions/Operations/PreviewUpdateSubscription.py index 0add00b..6cf45cd 100644 --- a/paddle_billing/Resources/Subscriptions/Operations/PreviewUpdateSubscription.py +++ b/paddle_billing/Resources/Subscriptions/Operations/PreviewUpdateSubscription.py @@ -4,26 +4,26 @@ from paddle_billing.Entities.DateTime import DateTime from paddle_billing.Entities.Shared import BillingDetails, CollectionMode, CurrencyCode, CustomData -from paddle_billing.Entities.Subscriptions import SubscriptionItems, SubscriptionOnPaymentFailure, SubscriptionProrationBillingMode +from paddle_billing.Entities.Subscriptions import SubscriptionItems, SubscriptionItemsWithPrice, SubscriptionOnPaymentFailure, SubscriptionProrationBillingMode, SubscriptionScheduledChange from paddle_billing.Resources.Subscriptions.Operations.Update.SubscriptionDiscount import SubscriptionDiscount @dataclass class PreviewUpdateSubscription: - customer_id: str | Undefined = Undefined() - address_id: str | Undefined = Undefined() - business_id: str | None | Undefined = Undefined() - currency_code: CurrencyCode | Undefined = Undefined() - next_billed_at: DateTime | Undefined = Undefined() - discount: SubscriptionDiscount | None | Undefined = Undefined() - collection_mode: CollectionMode | Undefined = Undefined() - billing_details: BillingDetails | None | Undefined = Undefined() - scheduled_change: None | Undefined = Undefined() - items: list[SubscriptionItems] | Undefined = Undefined() - custom_data: CustomData | None | Undefined = Undefined() - proration_billing_mode: SubscriptionProrationBillingMode | Undefined = Undefined() - on_payment_failure: SubscriptionOnPaymentFailure | Undefined = Undefined() + customer_id: str | Undefined = Undefined() + address_id: str | Undefined = Undefined() + business_id: str | None | Undefined = Undefined() + currency_code: CurrencyCode | Undefined = Undefined() + next_billed_at: DateTime | Undefined = Undefined() + discount: SubscriptionDiscount | None | Undefined = Undefined() + collection_mode: CollectionMode | Undefined = Undefined() + billing_details: BillingDetails | None | Undefined = Undefined() + scheduled_change: None | Undefined = Undefined() + items: list[SubscriptionItems | SubscriptionItemsWithPrice] | Undefined = Undefined() + custom_data: CustomData | None | Undefined = Undefined() + proration_billing_mode: SubscriptionProrationBillingMode | Undefined = Undefined() + on_payment_failure: SubscriptionOnPaymentFailure | Undefined = Undefined() def get_parameters(self) -> dict: @@ -35,3 +35,4 @@ def get_parameters(self) -> dict: parameters['items'] = [item.get_parameters() for item in self.items] return parameters + diff --git a/paddle_billing/Resources/Subscriptions/Operations/UpdateSubscription.py b/paddle_billing/Resources/Subscriptions/Operations/UpdateSubscription.py index 3206d0a..537be37 100644 --- a/paddle_billing/Resources/Subscriptions/Operations/UpdateSubscription.py +++ b/paddle_billing/Resources/Subscriptions/Operations/UpdateSubscription.py @@ -4,26 +4,26 @@ from paddle_billing.Entities.DateTime import DateTime from paddle_billing.Entities.Shared import BillingDetails, CollectionMode, CurrencyCode, CustomData -from paddle_billing.Entities.Subscriptions import SubscriptionItems, SubscriptionOnPaymentFailure, SubscriptionProrationBillingMode +from paddle_billing.Entities.Subscriptions import SubscriptionItems, SubscriptionItemsWithPrice, SubscriptionOnPaymentFailure, SubscriptionProrationBillingMode, SubscriptionScheduledChange from paddle_billing.Resources.Subscriptions.Operations.Update.SubscriptionDiscount import SubscriptionDiscount @dataclass class UpdateSubscription: - customer_id: str | Undefined = Undefined() - address_id: str | Undefined = Undefined() - business_id: str | None | Undefined = Undefined() - currency_code: CurrencyCode | Undefined = Undefined() - next_billed_at: DateTime | Undefined = Undefined() - discount: SubscriptionDiscount | None | Undefined = Undefined() - collection_mode: CollectionMode | Undefined = Undefined() - billing_details: BillingDetails | None | Undefined = Undefined() - scheduled_change: None | Undefined = Undefined() - items: list[SubscriptionItems] | Undefined = Undefined() - custom_data: CustomData | None | Undefined = Undefined() - proration_billing_mode: SubscriptionProrationBillingMode | Undefined = Undefined() - on_payment_failure: SubscriptionOnPaymentFailure | Undefined = Undefined() + customer_id: str | Undefined = Undefined() + address_id: str | Undefined = Undefined() + business_id: str | None | Undefined = Undefined() + currency_code: CurrencyCode | Undefined = Undefined() + next_billed_at: DateTime | Undefined = Undefined() + discount: SubscriptionDiscount | None | Undefined = Undefined() + collection_mode: CollectionMode | Undefined = Undefined() + billing_details: BillingDetails | None | Undefined = Undefined() + scheduled_change: None | Undefined = Undefined() + items: list[SubscriptionItems | SubscriptionItemsWithPrice] | Undefined = Undefined() + custom_data: CustomData | None | Undefined = Undefined() + proration_billing_mode: SubscriptionProrationBillingMode | Undefined = Undefined() + on_payment_failure: SubscriptionOnPaymentFailure | Undefined = Undefined() def get_parameters(self) -> dict: diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json index 0201ca7..a1929b8 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json @@ -13,7 +13,74 @@ "scheduled_change": null, "items": [ { "price_id": "pri_01gsz91wy9k1yn7kx82aafwvea", "quantity": 1 }, - { "price_id": "pri_01gsz91wy9k1yn7kx82bafwvea", "quantity": 5 } + { "price_id": "pri_01gsz91wy9k1yn7kx82bafwvea", "quantity": 5 }, + { + "price": { + "custom_data": { + "key": "value" + }, + "description": "some description", + "name": "some name", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "quantity": { + "maximum": 3, + "minimum": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1", + "currency_code": "GBP" + }, + "unit_price_overrides": [], + "billing_cycle": { + "frequency": 1, + "interval": "day" + }, + "trial_period": { + "frequency": 2, + "interval": "day" + } + }, + "quantity": 2 + }, + { + "price": { + "custom_data": { + "key": "value" + }, + "description": "some description", + "name": "some name", + "product": { + "custom_data": { + "key": "value" + }, + "description": "some description", + "image_url": "https://www.example.com/image.jpg", + "name": "some name", + "tax_category": "digital-goods", + "type": "custom" + }, + "quantity": { + "maximum": 3, + "minimum": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1", + "currency_code": "GBP" + }, + "unit_price_overrides": [], + "billing_cycle": { + "frequency": 1, + "interval": "day" + }, + "trial_period": { + "frequency": 2, + "interval": "day" + } + }, + "quantity": 2 + } ], "proration_billing_mode": "full_immediately", "custom_data": { diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json index 0201ca7..a1929b8 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json @@ -13,7 +13,74 @@ "scheduled_change": null, "items": [ { "price_id": "pri_01gsz91wy9k1yn7kx82aafwvea", "quantity": 1 }, - { "price_id": "pri_01gsz91wy9k1yn7kx82bafwvea", "quantity": 5 } + { "price_id": "pri_01gsz91wy9k1yn7kx82bafwvea", "quantity": 5 }, + { + "price": { + "custom_data": { + "key": "value" + }, + "description": "some description", + "name": "some name", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "quantity": { + "maximum": 3, + "minimum": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1", + "currency_code": "GBP" + }, + "unit_price_overrides": [], + "billing_cycle": { + "frequency": 1, + "interval": "day" + }, + "trial_period": { + "frequency": 2, + "interval": "day" + } + }, + "quantity": 2 + }, + { + "price": { + "custom_data": { + "key": "value" + }, + "description": "some description", + "name": "some name", + "product": { + "custom_data": { + "key": "value" + }, + "description": "some description", + "image_url": "https://www.example.com/image.jpg", + "name": "some name", + "tax_category": "digital-goods", + "type": "custom" + }, + "quantity": { + "maximum": 3, + "minimum": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1", + "currency_code": "GBP" + }, + "unit_price_overrides": [], + "billing_cycle": { + "frequency": 1, + "interval": "day" + }, + "trial_period": { + "frequency": 2, + "interval": "day" + } + }, + "quantity": 2 + } ], "proration_billing_mode": "full_immediately", "custom_data": { diff --git a/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py b/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py index 5220a97..336564e 100644 --- a/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py +++ b/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py @@ -8,10 +8,27 @@ from paddle_billing.Entities.SubscriptionPreview import SubscriptionPreview from paddle_billing.Entities.Transaction import Transaction -from paddle_billing.Entities.Shared import CollectionMode, CurrencyCode, CustomData +from paddle_billing.Entities.Shared import ( + CollectionMode, + CurrencyCode, + CustomData, + TaxMode, + Money, + PriceQuantity, + CustomData, + TimePeriod, + Interval, + CatalogType, + TaxCategory, +) + from paddle_billing.Entities.Subscriptions import ( SubscriptionEffectiveFrom, SubscriptionItems, + SubscriptionItemsWithPrice, + SubscriptionNonCatalogPrice, + SubscriptionNonCatalogPriceWithProduct, + SubscriptionNonCatalogProduct, SubscriptionOnPaymentFailure, SubscriptionProrationBillingMode, SubscriptionResumeEffectiveFrom, @@ -58,7 +75,8 @@ class TestSubscriptionsClient: 200, ReadsFixtures.read_raw_json_fixture('response/full_entity'), '/subscriptions/sub_01h8bx8fmywym11t6swgzba704', - ), ( + ), + ( 'sub_01h8bx8fmywym11t6swgzba704', UpdateSubscription( customer_id = 'ctm_01h8441jn5pcwrfhwh78jqt8hk', @@ -78,6 +96,43 @@ class TestSubscriptionsClient: items = [ SubscriptionItems('pri_01gsz91wy9k1yn7kx82aafwvea', 1), SubscriptionItems('pri_01gsz91wy9k1yn7kx82bafwvea', 5), + SubscriptionItemsWithPrice( + SubscriptionNonCatalogPrice( + 'some description', + 'some name', + 'pro_01gsz4t5hdjse780zja8vvr7jg', + TaxMode.AccountSetting, + Money('1', CurrencyCode.GBP), + list(), + PriceQuantity(1, 3), + CustomData({'key': 'value'}), + TimePeriod(Interval.Day, 1), + TimePeriod(Interval.Day, 2), + ), + 2, + ), + SubscriptionItemsWithPrice( + SubscriptionNonCatalogPriceWithProduct( + 'some description', + 'some name', + SubscriptionNonCatalogProduct( + 'some name', + 'some description', + CatalogType.Custom, + TaxCategory.DigitalGoods, + 'https://www.example.com/image.jpg', + CustomData({'key': 'value'}), + ), + TaxMode.AccountSetting, + Money('1', CurrencyCode.GBP), + list(), + PriceQuantity(1, 3), + CustomData({'key': 'value'}), + TimePeriod(Interval.Day, 1), + TimePeriod(Interval.Day, 2), + ), + 2, + ), ], ), ReadsFixtures.read_raw_json_fixture('request/update_full'), @@ -665,6 +720,43 @@ def test_create_subscription_one_time_charge_uses_expected_payload( items = [ SubscriptionItems('pri_01gsz91wy9k1yn7kx82aafwvea', 1), SubscriptionItems('pri_01gsz91wy9k1yn7kx82bafwvea', 5), + SubscriptionItemsWithPrice( + SubscriptionNonCatalogPrice( + 'some description', + 'some name', + 'pro_01gsz4t5hdjse780zja8vvr7jg', + TaxMode.AccountSetting, + Money('1', CurrencyCode.GBP), + list(), + PriceQuantity(1, 3), + CustomData({'key': 'value'}), + TimePeriod(Interval.Day, 1), + TimePeriod(Interval.Day, 2), + ), + 2, + ), + SubscriptionItemsWithPrice( + SubscriptionNonCatalogPriceWithProduct( + 'some description', + 'some name', + SubscriptionNonCatalogProduct( + 'some name', + 'some description', + CatalogType.Custom, + TaxCategory.DigitalGoods, + 'https://www.example.com/image.jpg', + CustomData({'key': 'value'}), + ), + TaxMode.AccountSetting, + Money('1', CurrencyCode.GBP), + list(), + PriceQuantity(1, 3), + CustomData({'key': 'value'}), + TimePeriod(Interval.Day, 1), + TimePeriod(Interval.Day, 2), + ), + 2, + ), ], ), ReadsFixtures.read_raw_json_fixture('request/preview_update_full'),