diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e291644..8f0466ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx - Added `CLP` and `PEN` currency code support. See [related changelog](https://developer.paddle.com/changelog/2026/clp-pen-currencies?utm_source=dx&utm_medium=paddle-python-sdk) - Added `rotatable` to api_key events. See [related changelog](https://developer.paddle.com/changelog/2026/aws-secrets-manager-rotation?utm_source=dx&utm_medium=paddle-python-sdk) - Added `balance_movement_date` and `balance_movement_type` filters for payout reconciliation reports. See [related changelog](https://developer.paddle.com/changelog/2026/payout-reconciliation-report-filters?utm_source=dx&utm_medium=paddle-python-sdk) +- Added support for subscription history. See [related changelog](https://developer.paddle.com/changelog/2026/subscription-history?utm_source=dx&utm_medium=paddle-python-sdk) + - `Client.subscriptions.list_history` +- Added `type` to subscription `discount` +- Added `consent_requirements` to subscriptions +- Added `next_billed_at` filter to list subscriptions operation +- Added `checkout_domain.read`, `checkout_domain.write`, `client_token.read`, `client_token.write`, `metrics.read`, and `subscription_history.read` to `ApiKeyPermission` (permissions on `api_key.*` events) ### Changed diff --git a/paddle_billing/Entities/Collections/SubscriptionHistoryCollection.py b/paddle_billing/Entities/Collections/SubscriptionHistoryCollection.py new file mode 100644 index 00000000..682615d4 --- /dev/null +++ b/paddle_billing/Entities/Collections/SubscriptionHistoryCollection.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from typing import Any + +from paddle_billing.Entities.Collections.Collection import Collection +from paddle_billing.Entities.Collections.Paginator import Paginator +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistory import SubscriptionHistory + + +class SubscriptionHistoryCollection(Collection[SubscriptionHistory]): + @classmethod + def from_list( + cls, items_data: list[dict[str, Any]], paginator: Paginator | None = None + ) -> SubscriptionHistoryCollection: + items: list[SubscriptionHistory] = [SubscriptionHistory.from_dict(item) for item in items_data] + + return SubscriptionHistoryCollection(items, paginator) diff --git a/paddle_billing/Entities/Collections/__init__.py b/paddle_billing/Entities/Collections/__init__.py index 734577a3..38759f22 100644 --- a/paddle_billing/Entities/Collections/__init__.py +++ b/paddle_billing/Entities/Collections/__init__.py @@ -23,6 +23,7 @@ from paddle_billing.Entities.Collections.SimulationRunEventCollection import SimulationRunEventCollection from paddle_billing.Entities.Collections.SimulationTypeCollection import SimulationTypeCollection from paddle_billing.Entities.Collections.SubscriptionCollection import SubscriptionCollection +from paddle_billing.Entities.Collections.SubscriptionHistoryCollection import SubscriptionHistoryCollection from paddle_billing.Entities.Collections.SubscriptionPreviewCollection import SubscriptionPreviewCollection from paddle_billing.Entities.Collections.TransactionCollection import TransactionCollection from paddle_billing.Entities.Collections.TransactionPreviewCollection import TransactionPreviewCollection diff --git a/paddle_billing/Entities/Shared/ActionSource.py b/paddle_billing/Entities/Shared/ActionSource.py new file mode 100644 index 00000000..cee164b9 --- /dev/null +++ b/paddle_billing/Entities/Shared/ActionSource.py @@ -0,0 +1,14 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class ActionSource(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + System: "ActionSource" = "system" + Api: "ActionSource" = "api" + Dashboard: "ActionSource" = "dashboard" + CustomerPortal: "ActionSource" = "customer_portal" + SupportBot: "ActionSource" = "support_bot" + Retain: "ActionSource" = "retain" + Checkout: "ActionSource" = "checkout" + ExternalProvider: "ActionSource" = "external_provider" + PaddleClassic: "ActionSource" = "paddle_classic" + Unknown: "ActionSource" = "unknown" diff --git a/paddle_billing/Entities/Shared/Actor.py b/paddle_billing/Entities/Shared/Actor.py new file mode 100644 index 00000000..7beaf2d8 --- /dev/null +++ b/paddle_billing/Entities/Shared/Actor.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Shared.ActorType import ActorType + + +@dataclass +class Actor: + type: ActorType + id: str | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> Actor: + return Actor( + type=ActorType(data["type"]), + id=data.get("id"), + ) diff --git a/paddle_billing/Entities/Shared/ActorType.py b/paddle_billing/Entities/Shared/ActorType.py new file mode 100644 index 00000000..2298a935 --- /dev/null +++ b/paddle_billing/Entities/Shared/ActorType.py @@ -0,0 +1,10 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class ActorType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Customer: "ActorType" = "customer" + User: "ActorType" = "user" + ApiKey: "ActorType" = "api_key" + PaddleStaff: "ActorType" = "paddle_staff" + Publisher: "ActorType" = "publisher" + System: "ActorType" = "system" diff --git a/paddle_billing/Entities/Shared/__init__.py b/paddle_billing/Entities/Shared/__init__.py index ad31cf30..98645eba 100644 --- a/paddle_billing/Entities/Shared/__init__.py +++ b/paddle_billing/Entities/Shared/__init__.py @@ -1,4 +1,7 @@ from paddle_billing.Entities.Shared.Action import Action +from paddle_billing.Entities.Shared.ActionSource import ActionSource +from paddle_billing.Entities.Shared.Actor import Actor +from paddle_billing.Entities.Shared.ActorType import ActorType from paddle_billing.Entities.Shared.AddressPreview import AddressPreview from paddle_billing.Entities.Shared.AdjustmentActionType import AdjustmentActionType from paddle_billing.Entities.Shared.AdjustmentItemTotals import AdjustmentItemTotals diff --git a/paddle_billing/Entities/Subscription.py b/paddle_billing/Entities/Subscription.py index 41b0df83..cab554da 100644 --- a/paddle_billing/Entities/Subscription.py +++ b/paddle_billing/Entities/Subscription.py @@ -17,6 +17,7 @@ ) from paddle_billing.Entities.Subscriptions import ( + SubscriptionConsentRequirement, SubscriptionDiscount, SubscriptionItem, SubscriptionManagementUrls, @@ -51,6 +52,7 @@ class Subscription(Entity): items: list[SubscriptionItem] next_transaction: SubscriptionNextTransaction | None recurring_transaction_details: TransactionDetailsPreview | None + consent_requirements: list[SubscriptionConsentRequirement] import_meta: ImportMeta | None = None custom_data: CustomData | None = None @@ -98,4 +100,8 @@ def from_dict(data: dict[str, Any]) -> Subscription: if data.get("recurring_transaction_details") else None ), + consent_requirements=[ + SubscriptionConsentRequirement.from_dict(consent_requirement) + for consent_requirement in data.get("consent_requirements", []) + ], ) diff --git a/paddle_billing/Entities/SubscriptionPreview.py b/paddle_billing/Entities/SubscriptionPreview.py index 37951ee9..a897d21c 100644 --- a/paddle_billing/Entities/SubscriptionPreview.py +++ b/paddle_billing/Entities/SubscriptionPreview.py @@ -17,6 +17,7 @@ from paddle_billing.Entities.Shared.TransactionDetailsPreview import TransactionDetailsPreview from paddle_billing.Entities.Subscriptions import ( + SubscriptionConsentRequirement, SubscriptionDiscount, SubscriptionItem, SubscriptionManagementUrls, @@ -54,6 +55,7 @@ class SubscriptionPreview(Entity): next_transaction: SubscriptionNextTransaction | None recurring_transaction_details: TransactionDetailsPreview | None update_summary: SubscriptionPreviewSubscriptionUpdateSummary | None + consent_requirements: list[SubscriptionConsentRequirement] import_meta: ImportMeta | None @staticmethod @@ -108,5 +110,9 @@ def from_dict(data: dict[str, Any]) -> SubscriptionPreview: if data.get("update_summary") else None ), + consent_requirements=[ + SubscriptionConsentRequirement.from_dict(consent_requirement) + for consent_requirement in data.get("consent_requirements", []) + ], import_meta=ImportMeta.from_dict(data["import_meta"]) if data.get("import_meta") else None, ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistory.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistory.py new file mode 100644 index 00000000..5eb5d019 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistory.py @@ -0,0 +1,38 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Shared import ActionSource, Actor + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetail import ( + SubscriptionHistoryDetail, + SubscriptionHistoryDetailUnion, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryReason import SubscriptionHistoryReason + + +@dataclass +class SubscriptionHistory(Entity): + id: str + group_id: str + subscription_id: str + occurred_at: datetime + source: ActionSource + actor: Actor + reason: SubscriptionHistoryReason | None + detail: SubscriptionHistoryDetailUnion + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistory: + return SubscriptionHistory( + id=data["id"], + group_id=data["group_id"], + subscription_id=data["subscription_id"], + occurred_at=datetime.fromisoformat(data["occurred_at"]), + source=ActionSource(data["source"]), + actor=Actor.from_dict(data["actor"]), + reason=SubscriptionHistoryReason(data["reason"]) if data.get("reason") else None, + detail=SubscriptionHistoryDetail.from_dict(data["detail"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryAction.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryAction.py new file mode 100644 index 00000000..410b4d48 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryAction.py @@ -0,0 +1,37 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionHistoryAction(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + SubscriptionActivated: "SubscriptionHistoryAction" = "subscription_activated" + SubscriptionAddressUpdated: "SubscriptionHistoryAction" = "subscription_address_updated" + SubscriptionBillingCycleUpdated: "SubscriptionHistoryAction" = "subscription_billing_cycle_updated" + SubscriptionBillingDateUpdated: "SubscriptionHistoryAction" = "subscription_billing_date_updated" + SubscriptionBillingDetailsUpdated: "SubscriptionHistoryAction" = "subscription_billing_details_updated" + SubscriptionBusinessAdded: "SubscriptionHistoryAction" = "subscription_business_added" + SubscriptionBusinessRemoved: "SubscriptionHistoryAction" = "subscription_business_removed" + SubscriptionBusinessUpdated: "SubscriptionHistoryAction" = "subscription_business_updated" + SubscriptionCanceled: "SubscriptionHistoryAction" = "subscription_canceled" + SubscriptionCollectionModeUpdated: "SubscriptionHistoryAction" = "subscription_collection_mode_updated" + SubscriptionConsentRequirementGranted: "SubscriptionHistoryAction" = "subscription_consent_requirement_granted" + SubscriptionCreated: "SubscriptionHistoryAction" = "subscription_created" + SubscriptionCurrencyUpdated: "SubscriptionHistoryAction" = "subscription_currency_updated" + SubscriptionCustomDataUpdated: "SubscriptionHistoryAction" = "subscription_custom_data_updated" + SubscriptionCustomerUpdated: "SubscriptionHistoryAction" = "subscription_customer_updated" + SubscriptionDiscountAdded: "SubscriptionHistoryAction" = "subscription_discount_added" + SubscriptionDiscountExpired: "SubscriptionHistoryAction" = "subscription_discount_expired" + SubscriptionDiscountRemoved: "SubscriptionHistoryAction" = "subscription_discount_removed" + SubscriptionItemAdded: "SubscriptionHistoryAction" = "subscription_item_added" + SubscriptionItemQuantityUpdated: "SubscriptionHistoryAction" = "subscription_item_quantity_updated" + SubscriptionItemRemoved: "SubscriptionHistoryAction" = "subscription_item_removed" + SubscriptionOneOffChargeApplied: "SubscriptionHistoryAction" = "subscription_one_off_charge_applied" + SubscriptionPastDue: "SubscriptionHistoryAction" = "subscription_past_due" + SubscriptionPaused: "SubscriptionHistoryAction" = "subscription_paused" + SubscriptionPaymentAttempted: "SubscriptionHistoryAction" = "subscription_payment_attempted" + SubscriptionPaymentMethodAdded: "SubscriptionHistoryAction" = "subscription_payment_method_added" + SubscriptionPaymentMethodRemoved: "SubscriptionHistoryAction" = "subscription_payment_method_removed" + SubscriptionPaymentMethodUpdated: "SubscriptionHistoryAction" = "subscription_payment_method_updated" + SubscriptionRenewed: "SubscriptionHistoryAction" = "subscription_renewed" + SubscriptionResumed: "SubscriptionHistoryAction" = "subscription_resumed" + SubscriptionScheduledChangeAdded: "SubscriptionHistoryAction" = "subscription_scheduled_change_added" + SubscriptionScheduledChangeRemoved: "SubscriptionHistoryAction" = "subscription_scheduled_change_removed" + SubscriptionScheduledChangeUpdated: "SubscriptionHistoryAction" = "subscription_scheduled_change_updated" diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryCanceledEffectiveFrom.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryCanceledEffectiveFrom.py new file mode 100644 index 00000000..f41cc524 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryCanceledEffectiveFrom.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionHistoryCanceledEffectiveFrom(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Immediately: "SubscriptionHistoryCanceledEffectiveFrom" = "immediately" + NextBillingPeriod: "SubscriptionHistoryCanceledEffectiveFrom" = "next_billing_period" diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetail.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetail.py new file mode 100644 index 00000000..a974c40f --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetail.py @@ -0,0 +1,195 @@ +from __future__ import annotations +from typing import Any, Union + +from paddle_billing.Notifications.Entities.UndefinedEntity import UndefinedEntity + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionActivated import ( + SubscriptionHistoryDetailSubscriptionActivated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionAddressUpdated import ( + SubscriptionHistoryDetailSubscriptionAddressUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBillingCycleUpdated import ( + SubscriptionHistoryDetailSubscriptionBillingCycleUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBillingDateUpdated import ( + SubscriptionHistoryDetailSubscriptionBillingDateUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated import ( + SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBusinessAdded import ( + SubscriptionHistoryDetailSubscriptionBusinessAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBusinessRemoved import ( + SubscriptionHistoryDetailSubscriptionBusinessRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBusinessUpdated import ( + SubscriptionHistoryDetailSubscriptionBusinessUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCanceled import ( + SubscriptionHistoryDetailSubscriptionCanceled, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCollectionModeUpdated import ( + SubscriptionHistoryDetailSubscriptionCollectionModeUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionConsentRequirementGranted import ( # noqa: E501 + SubscriptionHistoryDetailSubscriptionConsentRequirementGranted, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCreated import ( + SubscriptionHistoryDetailSubscriptionCreated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCurrencyUpdated import ( + SubscriptionHistoryDetailSubscriptionCurrencyUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCustomDataUpdated import ( + SubscriptionHistoryDetailSubscriptionCustomDataUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCustomerUpdated import ( + SubscriptionHistoryDetailSubscriptionCustomerUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionDiscountAdded import ( + SubscriptionHistoryDetailSubscriptionDiscountAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionDiscountExpired import ( + SubscriptionHistoryDetailSubscriptionDiscountExpired, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionDiscountRemoved import ( + SubscriptionHistoryDetailSubscriptionDiscountRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionItemAdded import ( + SubscriptionHistoryDetailSubscriptionItemAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionItemQuantityUpdated import ( + SubscriptionHistoryDetailSubscriptionItemQuantityUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionItemRemoved import ( + SubscriptionHistoryDetailSubscriptionItemRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionOneOffChargeApplied import ( + SubscriptionHistoryDetailSubscriptionOneOffChargeApplied, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPastDue import ( + SubscriptionHistoryDetailSubscriptionPastDue, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaused import ( + SubscriptionHistoryDetailSubscriptionPaused, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentAttempted import ( + SubscriptionHistoryDetailSubscriptionPaymentAttempted, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentMethodAdded import ( + SubscriptionHistoryDetailSubscriptionPaymentMethodAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved import ( + SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated import ( + SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionRenewed import ( + SubscriptionHistoryDetailSubscriptionRenewed, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionResumed import ( + SubscriptionHistoryDetailSubscriptionResumed, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionScheduledChangeAdded import ( + SubscriptionHistoryDetailSubscriptionScheduledChangeAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved import ( + SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated import ( + SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated, +) + +SubscriptionHistoryDetailUnion = Union[ + SubscriptionHistoryDetailSubscriptionActivated, + SubscriptionHistoryDetailSubscriptionAddressUpdated, + SubscriptionHistoryDetailSubscriptionBillingCycleUpdated, + SubscriptionHistoryDetailSubscriptionBillingDateUpdated, + SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated, + SubscriptionHistoryDetailSubscriptionBusinessAdded, + SubscriptionHistoryDetailSubscriptionBusinessRemoved, + SubscriptionHistoryDetailSubscriptionBusinessUpdated, + SubscriptionHistoryDetailSubscriptionCanceled, + SubscriptionHistoryDetailSubscriptionCollectionModeUpdated, + SubscriptionHistoryDetailSubscriptionConsentRequirementGranted, + SubscriptionHistoryDetailSubscriptionCreated, + SubscriptionHistoryDetailSubscriptionCurrencyUpdated, + SubscriptionHistoryDetailSubscriptionCustomDataUpdated, + SubscriptionHistoryDetailSubscriptionCustomerUpdated, + SubscriptionHistoryDetailSubscriptionDiscountAdded, + SubscriptionHistoryDetailSubscriptionDiscountExpired, + SubscriptionHistoryDetailSubscriptionDiscountRemoved, + SubscriptionHistoryDetailSubscriptionItemAdded, + SubscriptionHistoryDetailSubscriptionItemQuantityUpdated, + SubscriptionHistoryDetailSubscriptionItemRemoved, + SubscriptionHistoryDetailSubscriptionOneOffChargeApplied, + SubscriptionHistoryDetailSubscriptionPastDue, + SubscriptionHistoryDetailSubscriptionPaused, + SubscriptionHistoryDetailSubscriptionPaymentAttempted, + SubscriptionHistoryDetailSubscriptionPaymentMethodAdded, + SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved, + SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated, + SubscriptionHistoryDetailSubscriptionRenewed, + SubscriptionHistoryDetailSubscriptionResumed, + SubscriptionHistoryDetailSubscriptionScheduledChangeAdded, + SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved, + SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated, + UndefinedEntity, +] + + +class SubscriptionHistoryDetail: + """ + Dispatches a subscription history `detail` payload to the dataclass matching its `action`. + + Unrecognised actions (for example, added by the API after this SDK version was released) + hydrate as an `UndefinedEntity` wrapping the raw dictionary. + """ + + _CLASSES: dict[str, type] = { + "subscription_activated": SubscriptionHistoryDetailSubscriptionActivated, + "subscription_address_updated": SubscriptionHistoryDetailSubscriptionAddressUpdated, + "subscription_billing_cycle_updated": SubscriptionHistoryDetailSubscriptionBillingCycleUpdated, + "subscription_billing_date_updated": SubscriptionHistoryDetailSubscriptionBillingDateUpdated, + "subscription_billing_details_updated": SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated, + "subscription_business_added": SubscriptionHistoryDetailSubscriptionBusinessAdded, + "subscription_business_removed": SubscriptionHistoryDetailSubscriptionBusinessRemoved, + "subscription_business_updated": SubscriptionHistoryDetailSubscriptionBusinessUpdated, + "subscription_canceled": SubscriptionHistoryDetailSubscriptionCanceled, + "subscription_collection_mode_updated": SubscriptionHistoryDetailSubscriptionCollectionModeUpdated, + "subscription_consent_requirement_granted": SubscriptionHistoryDetailSubscriptionConsentRequirementGranted, + "subscription_created": SubscriptionHistoryDetailSubscriptionCreated, + "subscription_currency_updated": SubscriptionHistoryDetailSubscriptionCurrencyUpdated, + "subscription_custom_data_updated": SubscriptionHistoryDetailSubscriptionCustomDataUpdated, + "subscription_customer_updated": SubscriptionHistoryDetailSubscriptionCustomerUpdated, + "subscription_discount_added": SubscriptionHistoryDetailSubscriptionDiscountAdded, + "subscription_discount_expired": SubscriptionHistoryDetailSubscriptionDiscountExpired, + "subscription_discount_removed": SubscriptionHistoryDetailSubscriptionDiscountRemoved, + "subscription_item_added": SubscriptionHistoryDetailSubscriptionItemAdded, + "subscription_item_quantity_updated": SubscriptionHistoryDetailSubscriptionItemQuantityUpdated, + "subscription_item_removed": SubscriptionHistoryDetailSubscriptionItemRemoved, + "subscription_one_off_charge_applied": SubscriptionHistoryDetailSubscriptionOneOffChargeApplied, + "subscription_past_due": SubscriptionHistoryDetailSubscriptionPastDue, + "subscription_paused": SubscriptionHistoryDetailSubscriptionPaused, + "subscription_payment_attempted": SubscriptionHistoryDetailSubscriptionPaymentAttempted, + "subscription_payment_method_added": SubscriptionHistoryDetailSubscriptionPaymentMethodAdded, + "subscription_payment_method_removed": SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved, + "subscription_payment_method_updated": SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated, + "subscription_renewed": SubscriptionHistoryDetailSubscriptionRenewed, + "subscription_resumed": SubscriptionHistoryDetailSubscriptionResumed, + "subscription_scheduled_change_added": SubscriptionHistoryDetailSubscriptionScheduledChangeAdded, + "subscription_scheduled_change_removed": SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved, + "subscription_scheduled_change_updated": SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated, + } + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailUnion: + detail_class = SubscriptionHistoryDetail._CLASSES.get(data.get("action")) + + if detail_class is None: + return UndefinedEntity(data) + + return detail_class.from_dict(data) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailItem.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailItem.py new file mode 100644 index 00000000..2496e3c8 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailItem.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Price import Price + + +@dataclass +class SubscriptionHistoryDetailItem: + price: Price + quantity: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailItem: + return SubscriptionHistoryDetailItem( + price=Price.from_dict(data["price"]), + quantity=data["quantity"], + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionActivated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionActivated.py new file mode 100644 index 00000000..7ff1b57c --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionActivated.py @@ -0,0 +1,30 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Shared.TimePeriod import TimePeriod + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionStatus import SubscriptionStatus + + +@dataclass +class SubscriptionHistoryDetailSubscriptionActivated: + action: SubscriptionHistoryAction + status: SubscriptionStatus + first_billed_at: datetime | None + next_billed_at: datetime | None + current_billing_period: TimePeriod + transaction_id: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionActivated: + return SubscriptionHistoryDetailSubscriptionActivated( + action=SubscriptionHistoryAction(data["action"]), + status=SubscriptionStatus(data["status"]), + first_billed_at=datetime.fromisoformat(data["first_billed_at"]) if data.get("first_billed_at") else None, + next_billed_at=datetime.fromisoformat(data["next_billed_at"]) if data.get("next_billed_at") else None, + current_billing_period=TimePeriod.from_dict(data["current_billing_period"]), + transaction_id=data["transaction_id"], + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionAddressUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionAddressUpdated.py new file mode 100644 index 00000000..d651949b --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionAddressUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Address import Address + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionAddressUpdated: + action: SubscriptionHistoryAction + address: Address + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionAddressUpdated: + return SubscriptionHistoryDetailSubscriptionAddressUpdated( + action=SubscriptionHistoryAction(data["action"]), + address=Address.from_dict(data["address"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingCycleUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingCycleUpdated.py new file mode 100644 index 00000000..26959028 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingCycleUpdated.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Shared import Duration, TimePeriod + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionBillingCycleUpdated: + action: SubscriptionHistoryAction + billing_cycle: Duration + current_billing_period: TimePeriod + next_billed_at: datetime | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionBillingCycleUpdated: + return SubscriptionHistoryDetailSubscriptionBillingCycleUpdated( + action=SubscriptionHistoryAction(data["action"]), + billing_cycle=Duration.from_dict(data["billing_cycle"]), + current_billing_period=TimePeriod.from_dict(data["current_billing_period"]), + next_billed_at=datetime.fromisoformat(data["next_billed_at"]) if data.get("next_billed_at") else None, + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingDateUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingDateUpdated.py new file mode 100644 index 00000000..1904e4c4 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingDateUpdated.py @@ -0,0 +1,28 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Shared import TimePeriod + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionProrationBillingMode import SubscriptionProrationBillingMode + + +@dataclass +class SubscriptionHistoryDetailSubscriptionBillingDateUpdated: + action: SubscriptionHistoryAction + next_billed_at: datetime | None + current_billing_period: TimePeriod + transaction_id: str | None + proration_billing_mode: SubscriptionProrationBillingMode + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionBillingDateUpdated: + return SubscriptionHistoryDetailSubscriptionBillingDateUpdated( + action=SubscriptionHistoryAction(data["action"]), + next_billed_at=datetime.fromisoformat(data["next_billed_at"]) if data.get("next_billed_at") else None, + current_billing_period=TimePeriod.from_dict(data["current_billing_period"]), + transaction_id=data.get("transaction_id"), + proration_billing_mode=SubscriptionProrationBillingMode(data["proration_billing_mode"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated.py new file mode 100644 index 00000000..27c81c3e --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Shared import BillingDetails + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated: + action: SubscriptionHistoryAction + billing_details: BillingDetails + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated: + return SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated( + action=SubscriptionHistoryAction(data["action"]), + billing_details=BillingDetails.from_dict(data["billing_details"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessAdded.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessAdded.py new file mode 100644 index 00000000..078068b0 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessAdded.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Business import Business + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionBusinessAdded: + action: SubscriptionHistoryAction + business: Business + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionBusinessAdded: + return SubscriptionHistoryDetailSubscriptionBusinessAdded( + action=SubscriptionHistoryAction(data["action"]), + business=Business.from_dict(data["business"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessRemoved.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessRemoved.py new file mode 100644 index 00000000..ccc45876 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessRemoved.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Business import Business + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionBusinessRemoved: + action: SubscriptionHistoryAction + business: Business + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionBusinessRemoved: + return SubscriptionHistoryDetailSubscriptionBusinessRemoved( + action=SubscriptionHistoryAction(data["action"]), + business=Business.from_dict(data["business"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessUpdated.py new file mode 100644 index 00000000..1615fdbc --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionBusinessUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Business import Business + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionBusinessUpdated: + action: SubscriptionHistoryAction + business: Business + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionBusinessUpdated: + return SubscriptionHistoryDetailSubscriptionBusinessUpdated( + action=SubscriptionHistoryAction(data["action"]), + business=Business.from_dict(data["business"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCanceled.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCanceled.py new file mode 100644 index 00000000..d29c8f17 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCanceled.py @@ -0,0 +1,23 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryCanceledEffectiveFrom import ( + SubscriptionHistoryCanceledEffectiveFrom, +) + + +@dataclass +class SubscriptionHistoryDetailSubscriptionCanceled: + action: SubscriptionHistoryAction + effective_from: SubscriptionHistoryCanceledEffectiveFrom | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionCanceled: + return SubscriptionHistoryDetailSubscriptionCanceled( + action=SubscriptionHistoryAction(data["action"]), + effective_from=( + SubscriptionHistoryCanceledEffectiveFrom(data["effective_from"]) if data.get("effective_from") else None + ), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCollectionModeUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCollectionModeUpdated.py new file mode 100644 index 00000000..a42f27b3 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCollectionModeUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Shared import CollectionMode + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionCollectionModeUpdated: + action: SubscriptionHistoryAction + collection_mode: CollectionMode + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionCollectionModeUpdated: + return SubscriptionHistoryDetailSubscriptionCollectionModeUpdated( + action=SubscriptionHistoryAction(data["action"]), + collection_mode=CollectionMode(data["collection_mode"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionConsentRequirementGranted.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionConsentRequirementGranted.py new file mode 100644 index 00000000..20718b4e --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionConsentRequirementGranted.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionConsentRequirement import SubscriptionConsentRequirement + + +@dataclass +class SubscriptionHistoryDetailSubscriptionConsentRequirementGranted: + action: SubscriptionHistoryAction + consent_requirement: SubscriptionConsentRequirement + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionConsentRequirementGranted: + return SubscriptionHistoryDetailSubscriptionConsentRequirementGranted( + action=SubscriptionHistoryAction(data["action"]), + consent_requirement=SubscriptionConsentRequirement.from_dict(data["consent_requirement"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCreated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCreated.py new file mode 100644 index 00000000..620b25ef --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCreated.py @@ -0,0 +1,64 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Address import Address +from paddle_billing.Entities.Business import Business +from paddle_billing.Entities.Customer import Customer + +from paddle_billing.Entities.Shared import ( + BillingDetails, + CollectionMode, + CurrencyCode, + CustomData, + Duration, + TimePeriod, +) + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailItem import SubscriptionHistoryDetailItem +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDiscount import SubscriptionHistoryDiscount +from paddle_billing.Entities.Subscriptions.SubscriptionStatus import SubscriptionStatus + + +@dataclass +class SubscriptionHistoryDetailSubscriptionCreated: + action: SubscriptionHistoryAction + status: SubscriptionStatus | None + collection_mode: CollectionMode | None + billing_details: BillingDetails | None + customer: Customer | None + address: Address | None + business: Business | None + currency_code: CurrencyCode | None + current_billing_period: TimePeriod | None + billing_cycle: Duration | None + items: list[SubscriptionHistoryDetailItem] | None + custom_data: CustomData | None + discount: SubscriptionHistoryDiscount | None + has_payment_method: bool | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionCreated: + return SubscriptionHistoryDetailSubscriptionCreated( + action=SubscriptionHistoryAction(data["action"]), + status=SubscriptionStatus(data["status"]) if data.get("status") else None, + collection_mode=CollectionMode(data["collection_mode"]) if data.get("collection_mode") else None, + billing_details=( + BillingDetails.from_dict(data["billing_details"]) if data.get("billing_details") else None + ), + customer=Customer.from_dict(data["customer"]) if data.get("customer") else None, + address=Address.from_dict(data["address"]) if data.get("address") else None, + business=Business.from_dict(data["business"]) if data.get("business") else None, + currency_code=CurrencyCode(data["currency_code"]) if data.get("currency_code") else None, + current_billing_period=( + TimePeriod.from_dict(data["current_billing_period"]) if data.get("current_billing_period") else None + ), + billing_cycle=Duration.from_dict(data["billing_cycle"]) if data.get("billing_cycle") else None, + items=( + [SubscriptionHistoryDetailItem.from_dict(item) for item in data["items"]] if data.get("items") else None + ), + custom_data=CustomData(data["custom_data"]) if data.get("custom_data") else None, + discount=SubscriptionHistoryDiscount.from_dict(data["discount"]) if data.get("discount") else None, + has_payment_method=data.get("has_payment_method"), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCurrencyUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCurrencyUpdated.py new file mode 100644 index 00000000..297637fd --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCurrencyUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Shared import CurrencyCode + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionCurrencyUpdated: + action: SubscriptionHistoryAction + currency_code: CurrencyCode + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionCurrencyUpdated: + return SubscriptionHistoryDetailSubscriptionCurrencyUpdated( + action=SubscriptionHistoryAction(data["action"]), + currency_code=CurrencyCode(data["currency_code"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCustomDataUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCustomDataUpdated.py new file mode 100644 index 00000000..034cd8f1 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCustomDataUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Shared import CustomData + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionCustomDataUpdated: + action: SubscriptionHistoryAction + custom_data: CustomData | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionCustomDataUpdated: + return SubscriptionHistoryDetailSubscriptionCustomDataUpdated( + action=SubscriptionHistoryAction(data["action"]), + custom_data=CustomData(data["custom_data"]) if data.get("custom_data") else None, + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCustomerUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCustomerUpdated.py new file mode 100644 index 00000000..27643bee --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionCustomerUpdated.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Customer import Customer + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionCustomerUpdated: + action: SubscriptionHistoryAction + customer: Customer + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionCustomerUpdated: + return SubscriptionHistoryDetailSubscriptionCustomerUpdated( + action=SubscriptionHistoryAction(data["action"]), + customer=Customer.from_dict(data["customer"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountAdded.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountAdded.py new file mode 100644 index 00000000..d038a2d3 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountAdded.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDiscount import SubscriptionHistoryDiscount + + +@dataclass +class SubscriptionHistoryDetailSubscriptionDiscountAdded: + action: SubscriptionHistoryAction + discount: SubscriptionHistoryDiscount + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionDiscountAdded: + return SubscriptionHistoryDetailSubscriptionDiscountAdded( + action=SubscriptionHistoryAction(data["action"]), + discount=SubscriptionHistoryDiscount.from_dict(data["discount"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountExpired.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountExpired.py new file mode 100644 index 00000000..9a46489e --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountExpired.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDiscount import SubscriptionHistoryDiscount + + +@dataclass +class SubscriptionHistoryDetailSubscriptionDiscountExpired: + action: SubscriptionHistoryAction + discount: SubscriptionHistoryDiscount + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionDiscountExpired: + return SubscriptionHistoryDetailSubscriptionDiscountExpired( + action=SubscriptionHistoryAction(data["action"]), + discount=SubscriptionHistoryDiscount.from_dict(data["discount"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountRemoved.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountRemoved.py new file mode 100644 index 00000000..e2ae6dc5 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionDiscountRemoved.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDiscount import SubscriptionHistoryDiscount + + +@dataclass +class SubscriptionHistoryDetailSubscriptionDiscountRemoved: + action: SubscriptionHistoryAction + discount: SubscriptionHistoryDiscount + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionDiscountRemoved: + return SubscriptionHistoryDetailSubscriptionDiscountRemoved( + action=SubscriptionHistoryAction(data["action"]), + discount=SubscriptionHistoryDiscount.from_dict(data["discount"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemAdded.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemAdded.py new file mode 100644 index 00000000..fd8ea4a1 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemAdded.py @@ -0,0 +1,35 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Price import Price + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryItemUpdateSummary import ( + SubscriptionHistoryItemUpdateSummary, +) +from paddle_billing.Entities.Subscriptions.SubscriptionOnPaymentFailure import SubscriptionOnPaymentFailure +from paddle_billing.Entities.Subscriptions.SubscriptionProrationBillingMode import SubscriptionProrationBillingMode + + +@dataclass +class SubscriptionHistoryDetailSubscriptionItemAdded: + action: SubscriptionHistoryAction + price: Price + quantity: int + update_summary: SubscriptionHistoryItemUpdateSummary + proration_billing_mode: SubscriptionProrationBillingMode + on_payment_failure: SubscriptionOnPaymentFailure + transaction_id: str | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionItemAdded: + return SubscriptionHistoryDetailSubscriptionItemAdded( + action=SubscriptionHistoryAction(data["action"]), + price=Price.from_dict(data["price"]), + quantity=data["quantity"], + update_summary=SubscriptionHistoryItemUpdateSummary.from_dict(data["update_summary"]), + proration_billing_mode=SubscriptionProrationBillingMode(data["proration_billing_mode"]), + on_payment_failure=SubscriptionOnPaymentFailure(data["on_payment_failure"]), + transaction_id=data.get("transaction_id"), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemQuantityUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemQuantityUpdated.py new file mode 100644 index 00000000..6b694607 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemQuantityUpdated.py @@ -0,0 +1,35 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Price import Price + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryItemUpdateSummary import ( + SubscriptionHistoryItemUpdateSummary, +) +from paddle_billing.Entities.Subscriptions.SubscriptionOnPaymentFailure import SubscriptionOnPaymentFailure +from paddle_billing.Entities.Subscriptions.SubscriptionProrationBillingMode import SubscriptionProrationBillingMode + + +@dataclass +class SubscriptionHistoryDetailSubscriptionItemQuantityUpdated: + action: SubscriptionHistoryAction + price: Price + quantity: int + update_summary: SubscriptionHistoryItemUpdateSummary + proration_billing_mode: SubscriptionProrationBillingMode + on_payment_failure: SubscriptionOnPaymentFailure + transaction_id: str | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionItemQuantityUpdated: + return SubscriptionHistoryDetailSubscriptionItemQuantityUpdated( + action=SubscriptionHistoryAction(data["action"]), + price=Price.from_dict(data["price"]), + quantity=data["quantity"], + update_summary=SubscriptionHistoryItemUpdateSummary.from_dict(data["update_summary"]), + proration_billing_mode=SubscriptionProrationBillingMode(data["proration_billing_mode"]), + on_payment_failure=SubscriptionOnPaymentFailure(data["on_payment_failure"]), + transaction_id=data.get("transaction_id"), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemRemoved.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemRemoved.py new file mode 100644 index 00000000..8775c49b --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionItemRemoved.py @@ -0,0 +1,35 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Price import Price + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryItemUpdateSummary import ( + SubscriptionHistoryItemUpdateSummary, +) +from paddle_billing.Entities.Subscriptions.SubscriptionOnPaymentFailure import SubscriptionOnPaymentFailure +from paddle_billing.Entities.Subscriptions.SubscriptionProrationBillingMode import SubscriptionProrationBillingMode + + +@dataclass +class SubscriptionHistoryDetailSubscriptionItemRemoved: + action: SubscriptionHistoryAction + price: Price + quantity: int + update_summary: SubscriptionHistoryItemUpdateSummary + proration_billing_mode: SubscriptionProrationBillingMode + on_payment_failure: SubscriptionOnPaymentFailure + transaction_id: str | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionItemRemoved: + return SubscriptionHistoryDetailSubscriptionItemRemoved( + action=SubscriptionHistoryAction(data["action"]), + price=Price.from_dict(data["price"]), + quantity=data["quantity"], + update_summary=SubscriptionHistoryItemUpdateSummary.from_dict(data["update_summary"]), + proration_billing_mode=SubscriptionProrationBillingMode(data["proration_billing_mode"]), + on_payment_failure=SubscriptionOnPaymentFailure(data["on_payment_failure"]), + transaction_id=data.get("transaction_id"), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionOneOffChargeApplied.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionOneOffChargeApplied.py new file mode 100644 index 00000000..2f460310 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionOneOffChargeApplied.py @@ -0,0 +1,29 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailItem import SubscriptionHistoryDetailItem +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryOneOffChargeAppliedEffectiveFrom import ( + SubscriptionHistoryOneOffChargeAppliedEffectiveFrom, +) +from paddle_billing.Entities.Subscriptions.SubscriptionOnPaymentFailure import SubscriptionOnPaymentFailure + + +@dataclass +class SubscriptionHistoryDetailSubscriptionOneOffChargeApplied: + action: SubscriptionHistoryAction + effective_from: SubscriptionHistoryOneOffChargeAppliedEffectiveFrom + items: list[SubscriptionHistoryDetailItem] + on_payment_failure: SubscriptionOnPaymentFailure + transaction_id: str | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionOneOffChargeApplied: + return SubscriptionHistoryDetailSubscriptionOneOffChargeApplied( + action=SubscriptionHistoryAction(data["action"]), + effective_from=SubscriptionHistoryOneOffChargeAppliedEffectiveFrom(data["effective_from"]), + items=[SubscriptionHistoryDetailItem.from_dict(item) for item in data["items"]], + on_payment_failure=SubscriptionOnPaymentFailure(data["on_payment_failure"]), + transaction_id=data.get("transaction_id"), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPastDue.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPastDue.py new file mode 100644 index 00000000..d0108d66 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPastDue.py @@ -0,0 +1,21 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionStatus import SubscriptionStatus + + +@dataclass +class SubscriptionHistoryDetailSubscriptionPastDue: + action: SubscriptionHistoryAction + status: SubscriptionStatus + transaction_id: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionPastDue: + return SubscriptionHistoryDetailSubscriptionPastDue( + action=SubscriptionHistoryAction(data["action"]), + status=SubscriptionStatus(data["status"]), + transaction_id=data["transaction_id"], + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaused.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaused.py new file mode 100644 index 00000000..e87cf70a --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaused.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryPausedEffectiveFrom import ( + SubscriptionHistoryPausedEffectiveFrom, +) +from paddle_billing.Entities.Subscriptions.SubscriptionStatus import SubscriptionStatus + + +@dataclass +class SubscriptionHistoryDetailSubscriptionPaused: + action: SubscriptionHistoryAction + effective_from: SubscriptionHistoryPausedEffectiveFrom + status: SubscriptionStatus + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionPaused: + return SubscriptionHistoryDetailSubscriptionPaused( + action=SubscriptionHistoryAction(data["action"]), + effective_from=SubscriptionHistoryPausedEffectiveFrom(data["effective_from"]), + status=SubscriptionStatus(data["status"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentAttempted.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentAttempted.py new file mode 100644 index 00000000..0b73bc08 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentAttempted.py @@ -0,0 +1,23 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryPaymentAttemptedOperation import ( + SubscriptionHistoryPaymentAttemptedOperation, +) + + +@dataclass +class SubscriptionHistoryDetailSubscriptionPaymentAttempted: + action: SubscriptionHistoryAction + operation: SubscriptionHistoryPaymentAttemptedOperation + transaction_id: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionPaymentAttempted: + return SubscriptionHistoryDetailSubscriptionPaymentAttempted( + action=SubscriptionHistoryAction(data["action"]), + operation=SubscriptionHistoryPaymentAttemptedOperation(data["operation"]), + transaction_id=data["transaction_id"], + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodAdded.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodAdded.py new file mode 100644 index 00000000..b908580b --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodAdded.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionPaymentMethodAdded: + action: SubscriptionHistoryAction + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionPaymentMethodAdded: + return SubscriptionHistoryDetailSubscriptionPaymentMethodAdded( + action=SubscriptionHistoryAction(data["action"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved.py new file mode 100644 index 00000000..92bd4c77 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved: + action: SubscriptionHistoryAction + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved: + return SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved( + action=SubscriptionHistoryAction(data["action"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated.py new file mode 100644 index 00000000..0bde6a47 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated.py @@ -0,0 +1,16 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated: + action: SubscriptionHistoryAction + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated: + return SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated( + action=SubscriptionHistoryAction(data["action"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionRenewed.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionRenewed.py new file mode 100644 index 00000000..2d8704db --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionRenewed.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Shared import TimePeriod + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction + + +@dataclass +class SubscriptionHistoryDetailSubscriptionRenewed: + action: SubscriptionHistoryAction + next_billed_at: datetime + current_billing_period: TimePeriod + transaction_id: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionRenewed: + return SubscriptionHistoryDetailSubscriptionRenewed( + action=SubscriptionHistoryAction(data["action"]), + next_billed_at=datetime.fromisoformat(data["next_billed_at"]), + current_billing_period=TimePeriod.from_dict(data["current_billing_period"]), + transaction_id=data["transaction_id"], + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionResumed.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionResumed.py new file mode 100644 index 00000000..9d4313b9 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionResumed.py @@ -0,0 +1,22 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionOnResume import SubscriptionOnResume +from paddle_billing.Entities.Subscriptions.SubscriptionStatus import SubscriptionStatus + + +@dataclass +class SubscriptionHistoryDetailSubscriptionResumed: + action: SubscriptionHistoryAction + status: SubscriptionStatus + on_resume: SubscriptionOnResume + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionResumed: + return SubscriptionHistoryDetailSubscriptionResumed( + action=SubscriptionHistoryAction(data["action"]), + status=SubscriptionStatus(data["status"]), + on_resume=SubscriptionOnResume(data["on_resume"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeAdded.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeAdded.py new file mode 100644 index 00000000..c293fb1f --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeAdded.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionScheduledChange import SubscriptionScheduledChange + + +@dataclass +class SubscriptionHistoryDetailSubscriptionScheduledChangeAdded: + action: SubscriptionHistoryAction + scheduled_change: SubscriptionScheduledChange + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionScheduledChangeAdded: + return SubscriptionHistoryDetailSubscriptionScheduledChangeAdded( + action=SubscriptionHistoryAction(data["action"]), + scheduled_change=SubscriptionScheduledChange.from_dict(data["scheduled_change"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved.py new file mode 100644 index 00000000..fc3ad7bf --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionScheduledChange import SubscriptionScheduledChange + + +@dataclass +class SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved: + action: SubscriptionHistoryAction + scheduled_change: SubscriptionScheduledChange + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved: + return SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved( + action=SubscriptionHistoryAction(data["action"]), + scheduled_change=SubscriptionScheduledChange.from_dict(data["scheduled_change"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated.py new file mode 100644 index 00000000..ecb45dfe --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated.py @@ -0,0 +1,19 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.SubscriptionScheduledChange import SubscriptionScheduledChange + + +@dataclass +class SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated: + action: SubscriptionHistoryAction + scheduled_change: SubscriptionScheduledChange + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated: + return SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated( + action=SubscriptionHistoryAction(data["action"]), + scheduled_change=SubscriptionScheduledChange.from_dict(data["scheduled_change"]), + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDiscount.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDiscount.py new file mode 100644 index 00000000..afaa3bb0 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryDiscount.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Discount import Discount +from paddle_billing.Entities.Subscriptions.SubscriptionDiscountType import SubscriptionDiscountType + + +@dataclass +class SubscriptionHistoryDiscount: + discount: Discount + type: SubscriptionDiscountType + starts_at: datetime + ends_at: datetime | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryDiscount: + return SubscriptionHistoryDiscount( + discount=Discount.from_dict(data["discount"]), + type=SubscriptionDiscountType(data["type"]), + starts_at=datetime.fromisoformat(data["starts_at"]), + ends_at=datetime.fromisoformat(data["ends_at"]) if data.get("ends_at") else None, + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryItemUpdateSummary.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryItemUpdateSummary.py new file mode 100644 index 00000000..37707cb8 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryItemUpdateSummary.py @@ -0,0 +1,14 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + + +@dataclass +class SubscriptionHistoryItemUpdateSummary: + quantity_delta: int + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionHistoryItemUpdateSummary: + return SubscriptionHistoryItemUpdateSummary( + quantity_delta=data["quantity_delta"], + ) diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryOneOffChargeAppliedEffectiveFrom.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryOneOffChargeAppliedEffectiveFrom.py new file mode 100644 index 00000000..9be81b5e --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryOneOffChargeAppliedEffectiveFrom.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionHistoryOneOffChargeAppliedEffectiveFrom(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Immediately: "SubscriptionHistoryOneOffChargeAppliedEffectiveFrom" = "immediately" + NextBillingPeriod: "SubscriptionHistoryOneOffChargeAppliedEffectiveFrom" = "next_billing_period" diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryPausedEffectiveFrom.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryPausedEffectiveFrom.py new file mode 100644 index 00000000..71a25bba --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryPausedEffectiveFrom.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionHistoryPausedEffectiveFrom(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Immediately: "SubscriptionHistoryPausedEffectiveFrom" = "immediately" + NextBillingPeriod: "SubscriptionHistoryPausedEffectiveFrom" = "next_billing_period" diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryPaymentAttemptedOperation.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryPaymentAttemptedOperation.py new file mode 100644 index 00000000..904297c6 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryPaymentAttemptedOperation.py @@ -0,0 +1,7 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionHistoryPaymentAttemptedOperation(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + SubscriptionUpdate: "SubscriptionHistoryPaymentAttemptedOperation" = "subscription_update" + SubscriptionOneOffCharge: "SubscriptionHistoryPaymentAttemptedOperation" = "subscription_one_off_charge" + SubscriptionActivate: "SubscriptionHistoryPaymentAttemptedOperation" = "subscription_activate" diff --git a/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryReason.py b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryReason.py new file mode 100644 index 00000000..aaf85360 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/SubscriptionHistoryReason.py @@ -0,0 +1,10 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionHistoryReason(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + CardlessTrialEnded: "SubscriptionHistoryReason" = "cardless_trial_ended" + ImportIssue: "SubscriptionHistoryReason" = "import_issue" + MissingConsent: "SubscriptionHistoryReason" = "missing_consent" + SellerRequest: "SubscriptionHistoryReason" = "seller_request" + CustomerRequest: "SubscriptionHistoryReason" = "customer_request" + Chargeback: "SubscriptionHistoryReason" = "chargeback" diff --git a/paddle_billing/Entities/Subscriptions/History/__init__.py b/paddle_billing/Entities/Subscriptions/History/__init__.py new file mode 100644 index 00000000..819dbaba --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/History/__init__.py @@ -0,0 +1,123 @@ +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistory import SubscriptionHistory +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryAction import SubscriptionHistoryAction +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryCanceledEffectiveFrom import ( + SubscriptionHistoryCanceledEffectiveFrom, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetail import ( + SubscriptionHistoryDetail, + SubscriptionHistoryDetailUnion, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailItem import SubscriptionHistoryDetailItem +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionActivated import ( + SubscriptionHistoryDetailSubscriptionActivated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionAddressUpdated import ( + SubscriptionHistoryDetailSubscriptionAddressUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBillingCycleUpdated import ( + SubscriptionHistoryDetailSubscriptionBillingCycleUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBillingDateUpdated import ( + SubscriptionHistoryDetailSubscriptionBillingDateUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated import ( + SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBusinessAdded import ( + SubscriptionHistoryDetailSubscriptionBusinessAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBusinessRemoved import ( + SubscriptionHistoryDetailSubscriptionBusinessRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionBusinessUpdated import ( + SubscriptionHistoryDetailSubscriptionBusinessUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCanceled import ( + SubscriptionHistoryDetailSubscriptionCanceled, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCollectionModeUpdated import ( + SubscriptionHistoryDetailSubscriptionCollectionModeUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionConsentRequirementGranted import ( # noqa: E501 + SubscriptionHistoryDetailSubscriptionConsentRequirementGranted, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCreated import ( + SubscriptionHistoryDetailSubscriptionCreated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCurrencyUpdated import ( + SubscriptionHistoryDetailSubscriptionCurrencyUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCustomDataUpdated import ( + SubscriptionHistoryDetailSubscriptionCustomDataUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionCustomerUpdated import ( + SubscriptionHistoryDetailSubscriptionCustomerUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionDiscountAdded import ( + SubscriptionHistoryDetailSubscriptionDiscountAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionDiscountExpired import ( + SubscriptionHistoryDetailSubscriptionDiscountExpired, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionDiscountRemoved import ( + SubscriptionHistoryDetailSubscriptionDiscountRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionItemAdded import ( + SubscriptionHistoryDetailSubscriptionItemAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionItemQuantityUpdated import ( + SubscriptionHistoryDetailSubscriptionItemQuantityUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionItemRemoved import ( + SubscriptionHistoryDetailSubscriptionItemRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionOneOffChargeApplied import ( + SubscriptionHistoryDetailSubscriptionOneOffChargeApplied, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPastDue import ( + SubscriptionHistoryDetailSubscriptionPastDue, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaused import ( + SubscriptionHistoryDetailSubscriptionPaused, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentAttempted import ( + SubscriptionHistoryDetailSubscriptionPaymentAttempted, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentMethodAdded import ( + SubscriptionHistoryDetailSubscriptionPaymentMethodAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved import ( + SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated import ( + SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionRenewed import ( + SubscriptionHistoryDetailSubscriptionRenewed, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionResumed import ( + SubscriptionHistoryDetailSubscriptionResumed, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionScheduledChangeAdded import ( + SubscriptionHistoryDetailSubscriptionScheduledChangeAdded, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved import ( + SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated import ( + SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryDiscount import SubscriptionHistoryDiscount +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryItemUpdateSummary import ( + SubscriptionHistoryItemUpdateSummary, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryOneOffChargeAppliedEffectiveFrom import ( + SubscriptionHistoryOneOffChargeAppliedEffectiveFrom, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryPausedEffectiveFrom import ( + SubscriptionHistoryPausedEffectiveFrom, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryPaymentAttemptedOperation import ( + SubscriptionHistoryPaymentAttemptedOperation, +) +from paddle_billing.Entities.Subscriptions.History.SubscriptionHistoryReason import SubscriptionHistoryReason diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirement.py b/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirement.py new file mode 100644 index 00000000..f1b10599 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirement.py @@ -0,0 +1,34 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Entities.Shared.TimePeriod import TimePeriod + +from paddle_billing.Entities.Subscriptions.SubscriptionConsentRequirementType import SubscriptionConsentRequirementType +from paddle_billing.Entities.Subscriptions.SubscriptionConsentRequirementStatus import ( + SubscriptionConsentRequirementStatus, +) + + +@dataclass +class SubscriptionConsentRequirement: + id: str + requirement: SubscriptionConsentRequirementType + status: SubscriptionConsentRequirementStatus + created_at: datetime + consent_period: TimePeriod | None + granted_at: datetime | None + voided_at: datetime | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionConsentRequirement: + return SubscriptionConsentRequirement( + id=data["id"], + requirement=SubscriptionConsentRequirementType(data["requirement"]), + status=SubscriptionConsentRequirementStatus(data["status"]), + created_at=datetime.fromisoformat(data["created_at"]), + consent_period=TimePeriod.from_dict(data["consent_period"]) if data.get("consent_period") else None, + granted_at=datetime.fromisoformat(data["granted_at"]) if data.get("granted_at") else None, + voided_at=datetime.fromisoformat(data["voided_at"]) if data.get("voided_at") else None, + ) diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirementStatus.py b/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirementStatus.py new file mode 100644 index 00000000..b244d1a5 --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirementStatus.py @@ -0,0 +1,7 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionConsentRequirementStatus(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Pending: "SubscriptionConsentRequirementStatus" = "pending" + Granted: "SubscriptionConsentRequirementStatus" = "granted" + Voided: "SubscriptionConsentRequirementStatus" = "voided" diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirementType.py b/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirementType.py new file mode 100644 index 00000000..4c621d6e --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/SubscriptionConsentRequirementType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionConsentRequirementType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + TrialEnding: "SubscriptionConsentRequirementType" = "trial_ending" + IntroductoryDiscountEnding: "SubscriptionConsentRequirementType" = "introductory_discount_ending" diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionDiscount.py b/paddle_billing/Entities/Subscriptions/SubscriptionDiscount.py index 1e62a619..e5bc3ee1 100644 --- a/paddle_billing/Entities/Subscriptions/SubscriptionDiscount.py +++ b/paddle_billing/Entities/Subscriptions/SubscriptionDiscount.py @@ -3,10 +3,13 @@ from datetime import datetime from typing import Any +from paddle_billing.Entities.Subscriptions.SubscriptionDiscountType import SubscriptionDiscountType + @dataclass class SubscriptionDiscount: id: str + type: SubscriptionDiscountType starts_at: datetime | None ends_at: datetime | None @@ -14,6 +17,7 @@ class SubscriptionDiscount: def from_dict(data: dict[str, Any]) -> SubscriptionDiscount: return SubscriptionDiscount( id=data["id"], + type=SubscriptionDiscountType(data["type"]), starts_at=datetime.fromisoformat(data["starts_at"]) if data.get("starts_at") else None, ends_at=datetime.fromisoformat(data["ends_at"]) if data.get("ends_at") else None, ) diff --git a/paddle_billing/Entities/Subscriptions/SubscriptionDiscountType.py b/paddle_billing/Entities/Subscriptions/SubscriptionDiscountType.py new file mode 100644 index 00000000..96b16dab --- /dev/null +++ b/paddle_billing/Entities/Subscriptions/SubscriptionDiscountType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionDiscountType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Recurring: "SubscriptionDiscountType" = "recurring" + OneOff: "SubscriptionDiscountType" = "one-off" diff --git a/paddle_billing/Entities/Subscriptions/__init__.py b/paddle_billing/Entities/Subscriptions/__init__.py index 106ace8a..c61ce2b9 100644 --- a/paddle_billing/Entities/Subscriptions/__init__.py +++ b/paddle_billing/Entities/Subscriptions/__init__.py @@ -1,8 +1,16 @@ from paddle_billing.Entities.Subscriptions.SubscriptionAdjustmentItem import SubscriptionAdjustmentItem from paddle_billing.Entities.Subscriptions.SubscriptionAdjustmentPreview import SubscriptionAdjustmentPreview from paddle_billing.Entities.Subscriptions.SubscriptionCharge import SubscriptionCharge +from paddle_billing.Entities.Subscriptions.SubscriptionConsentRequirement import SubscriptionConsentRequirement +from paddle_billing.Entities.Subscriptions.SubscriptionConsentRequirementStatus import ( + SubscriptionConsentRequirementStatus, +) +from paddle_billing.Entities.Subscriptions.SubscriptionConsentRequirementType import ( + SubscriptionConsentRequirementType, +) from paddle_billing.Entities.Subscriptions.SubscriptionCredit import SubscriptionCredit from paddle_billing.Entities.Subscriptions.SubscriptionDiscount import SubscriptionDiscount +from paddle_billing.Entities.Subscriptions.SubscriptionDiscountType import SubscriptionDiscountType from paddle_billing.Entities.Subscriptions.SubscriptionEffectiveFrom import SubscriptionEffectiveFrom from paddle_billing.Entities.Subscriptions.SubscriptionItem import SubscriptionItem from paddle_billing.Entities.Subscriptions.SubscriptionItemStatus import SubscriptionItemStatus diff --git a/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py b/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py index 30f6757e..fe83a91b 100644 --- a/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py +++ b/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py @@ -8,12 +8,17 @@ class ApiKeyPermission(PaddleStrEnum, metaclass=PaddleStrEnumMeta): AdjustmentWrite: "ApiKeyPermission" = "adjustment.write" BusinessRead: "ApiKeyPermission" = "business.read" BusinessWrite: "ApiKeyPermission" = "business.write" + CheckoutDomainRead: "ApiKeyPermission" = "checkout_domain.read" + CheckoutDomainWrite: "ApiKeyPermission" = "checkout_domain.write" + ClientTokenRead: "ApiKeyPermission" = "client_token.read" + ClientTokenWrite: "ApiKeyPermission" = "client_token.write" CustomerRead: "ApiKeyPermission" = "customer.read" CustomerWrite: "ApiKeyPermission" = "customer.write" CustomerAuthTokenWrite: "ApiKeyPermission" = "customer_auth_token.write" CustomerPortalSessionWrite: "ApiKeyPermission" = "customer_portal_session.write" DiscountRead: "ApiKeyPermission" = "discount.read" DiscountWrite: "ApiKeyPermission" = "discount.write" + MetricsRead: "ApiKeyPermission" = "metrics.read" NotificationRead: "ApiKeyPermission" = "notification.read" NotificationWrite: "ApiKeyPermission" = "notification.write" NotificationSettingRead: "ApiKeyPermission" = "notification_setting.read" @@ -30,5 +35,6 @@ class ApiKeyPermission(PaddleStrEnum, metaclass=PaddleStrEnumMeta): ReportWrite: "ApiKeyPermission" = "report.write" SubscriptionRead: "ApiKeyPermission" = "subscription.read" SubscriptionWrite: "ApiKeyPermission" = "subscription.write" + SubscriptionHistoryRead: "ApiKeyPermission" = "subscription_history.read" TransactionRead: "ApiKeyPermission" = "transaction.read" TransactionWrite: "ApiKeyPermission" = "transaction.write" diff --git a/paddle_billing/Notifications/Entities/Simulations/Subscription.py b/paddle_billing/Notifications/Entities/Simulations/Subscription.py index 64a7476a..0bec64fb 100644 --- a/paddle_billing/Notifications/Entities/Simulations/Subscription.py +++ b/paddle_billing/Notifications/Entities/Simulations/Subscription.py @@ -14,6 +14,7 @@ TimePeriod, ) from paddle_billing.Notifications.Entities.Subscriptions import ( + SubscriptionConsentRequirement, SubscriptionDiscount, SubscriptionItem, SubscriptionScheduledChange, @@ -37,6 +38,7 @@ class Subscription(SimulationEntity): billing_details: BillingDetails | None | Undefined = Undefined() business_id: str | None | Undefined = Undefined() canceled_at: datetime | None | Undefined = Undefined() + consent_requirements: list[SubscriptionConsentRequirement] | None | Undefined = Undefined() current_billing_period: TimePeriod | None | Undefined = Undefined() custom_data: CustomData | None | Undefined = Undefined() discount: SubscriptionDiscount | None | Undefined = Undefined() @@ -75,6 +77,14 @@ def from_dict(data: dict[str, Any]) -> Subscription: if data.get("canceled_at") else data.get("canceled_at", Undefined()) ), + consent_requirements=( + [ + SubscriptionConsentRequirement.from_dict(consent_requirement) + for consent_requirement in data["consent_requirements"] + ] + if isinstance(data.get("consent_requirements"), list) + else data.get("consent_requirements", Undefined()) + ), custom_data=( CustomData(data["custom_data"]) if isinstance(data.get("custom_data"), dict) diff --git a/paddle_billing/Notifications/Entities/Simulations/SubscriptionCreated.py b/paddle_billing/Notifications/Entities/Simulations/SubscriptionCreated.py index 70b9f56c..54670819 100644 --- a/paddle_billing/Notifications/Entities/Simulations/SubscriptionCreated.py +++ b/paddle_billing/Notifications/Entities/Simulations/SubscriptionCreated.py @@ -14,6 +14,7 @@ TimePeriod, ) from paddle_billing.Notifications.Entities.Subscriptions import ( + SubscriptionConsentRequirement, SubscriptionDiscount, SubscriptionItem, SubscriptionScheduledChange, @@ -37,6 +38,7 @@ class SubscriptionCreated(SimulationEntity): billing_details: BillingDetails | None | Undefined = Undefined() business_id: str | None | Undefined = Undefined() canceled_at: datetime | None | Undefined = Undefined() + consent_requirements: list[SubscriptionConsentRequirement] | None | Undefined = Undefined() current_billing_period: TimePeriod | None | Undefined = Undefined() custom_data: CustomData | None | Undefined = Undefined() discount: SubscriptionDiscount | None | Undefined = Undefined() @@ -77,6 +79,14 @@ def from_dict(data: dict[str, Any]) -> SubscriptionCreated: if data.get("canceled_at") else data.get("canceled_at", Undefined()) ), + consent_requirements=( + [ + SubscriptionConsentRequirement.from_dict(consent_requirement) + for consent_requirement in data["consent_requirements"] + ] + if isinstance(data.get("consent_requirements"), list) + else data.get("consent_requirements", Undefined()) + ), custom_data=( CustomData(data["custom_data"]) if isinstance(data.get("custom_data"), dict) diff --git a/paddle_billing/Notifications/Entities/Subscription.py b/paddle_billing/Notifications/Entities/Subscription.py index 5a18c43a..b75cf339 100644 --- a/paddle_billing/Notifications/Entities/Subscription.py +++ b/paddle_billing/Notifications/Entities/Subscription.py @@ -14,6 +14,7 @@ TimePeriod, ) from paddle_billing.Notifications.Entities.Subscriptions import ( + SubscriptionConsentRequirement, SubscriptionDiscount, SubscriptionItem, SubscriptionScheduledChange, @@ -36,6 +37,7 @@ class Subscription(Entity): billing_details: BillingDetails | None = None business_id: str | None = None canceled_at: datetime | None = None + consent_requirements: list[SubscriptionConsentRequirement] | None = None current_billing_period: TimePeriod | None = None custom_data: CustomData | None = None discount: SubscriptionDiscount | None = None @@ -62,6 +64,14 @@ def from_dict(data: dict[str, Any]) -> Subscription: items=[SubscriptionItem.from_dict(item) for item in data["items"]], billing_details=BillingDetails.from_dict(data["billing_details"]) if data.get("billing_details") else None, canceled_at=datetime.fromisoformat(data["canceled_at"]) if data.get("canceled_at") else None, + consent_requirements=( + [ + SubscriptionConsentRequirement.from_dict(consent_requirement) + for consent_requirement in data["consent_requirements"] + ] + if data.get("consent_requirements") is not None + else None + ), custom_data=CustomData(data["custom_data"]) if data.get("custom_data") else None, discount=SubscriptionDiscount.from_dict(data["discount"]) if data.get("discount") else None, first_billed_at=datetime.fromisoformat(data["first_billed_at"]) if data.get("first_billed_at") else None, diff --git a/paddle_billing/Notifications/Entities/SubscriptionCreated.py b/paddle_billing/Notifications/Entities/SubscriptionCreated.py index d74aef62..a460b41e 100644 --- a/paddle_billing/Notifications/Entities/SubscriptionCreated.py +++ b/paddle_billing/Notifications/Entities/SubscriptionCreated.py @@ -14,6 +14,7 @@ TimePeriod, ) from paddle_billing.Notifications.Entities.Subscriptions import ( + SubscriptionConsentRequirement, SubscriptionDiscount, SubscriptionItem, SubscriptionScheduledChange, @@ -36,6 +37,7 @@ class SubscriptionCreated(Entity): billing_details: BillingDetails | None = None business_id: str | None = None canceled_at: datetime | None = None + consent_requirements: list[SubscriptionConsentRequirement] | None = None current_billing_period: TimePeriod | None = None custom_data: CustomData | None = None discount: SubscriptionDiscount | None = None @@ -64,6 +66,14 @@ def from_dict(data: dict[str, Any]) -> SubscriptionCreated: items=[SubscriptionItem.from_dict(item) for item in data["items"]], billing_details=BillingDetails.from_dict(data["billing_details"]) if data.get("billing_details") else None, canceled_at=datetime.fromisoformat(data["canceled_at"]) if data.get("canceled_at") else None, + consent_requirements=( + [ + SubscriptionConsentRequirement.from_dict(consent_requirement) + for consent_requirement in data["consent_requirements"] + ] + if data.get("consent_requirements") is not None + else None + ), custom_data=CustomData(data["custom_data"]) if data.get("custom_data") else None, discount=SubscriptionDiscount.from_dict(data["discount"]) if data.get("discount") else None, first_billed_at=datetime.fromisoformat(data["first_billed_at"]) if data.get("first_billed_at") else None, diff --git a/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirement.py b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirement.py new file mode 100644 index 00000000..bc964055 --- /dev/null +++ b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirement.py @@ -0,0 +1,36 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Notifications.Entities.Shared.TimePeriod import TimePeriod + +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionConsentRequirementType import ( + SubscriptionConsentRequirementType, +) +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionConsentRequirementStatus import ( + SubscriptionConsentRequirementStatus, +) + + +@dataclass +class SubscriptionConsentRequirement: + id: str + requirement: SubscriptionConsentRequirementType + status: SubscriptionConsentRequirementStatus + created_at: datetime + consent_period: TimePeriod | None + granted_at: datetime | None + voided_at: datetime | None + + @staticmethod + def from_dict(data: dict[str, Any]) -> SubscriptionConsentRequirement: + return SubscriptionConsentRequirement( + id=data["id"], + requirement=SubscriptionConsentRequirementType(data["requirement"]), + status=SubscriptionConsentRequirementStatus(data["status"]), + created_at=datetime.fromisoformat(data["created_at"]), + consent_period=TimePeriod.from_dict(data["consent_period"]) if data.get("consent_period") else None, + granted_at=datetime.fromisoformat(data["granted_at"]) if data.get("granted_at") else None, + voided_at=datetime.fromisoformat(data["voided_at"]) if data.get("voided_at") else None, + ) diff --git a/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirementStatus.py b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirementStatus.py new file mode 100644 index 00000000..b244d1a5 --- /dev/null +++ b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirementStatus.py @@ -0,0 +1,7 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionConsentRequirementStatus(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Pending: "SubscriptionConsentRequirementStatus" = "pending" + Granted: "SubscriptionConsentRequirementStatus" = "granted" + Voided: "SubscriptionConsentRequirementStatus" = "voided" diff --git a/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirementType.py b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirementType.py new file mode 100644 index 00000000..4c621d6e --- /dev/null +++ b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionConsentRequirementType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionConsentRequirementType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + TrialEnding: "SubscriptionConsentRequirementType" = "trial_ending" + IntroductoryDiscountEnding: "SubscriptionConsentRequirementType" = "introductory_discount_ending" diff --git a/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscount.py b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscount.py index 1e62a619..107edc95 100644 --- a/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscount.py +++ b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscount.py @@ -3,12 +3,15 @@ from datetime import datetime from typing import Any +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionDiscountType import SubscriptionDiscountType + @dataclass class SubscriptionDiscount: id: str starts_at: datetime | None ends_at: datetime | None + type: SubscriptionDiscountType | None = None @staticmethod def from_dict(data: dict[str, Any]) -> SubscriptionDiscount: @@ -16,4 +19,5 @@ def from_dict(data: dict[str, Any]) -> SubscriptionDiscount: id=data["id"], starts_at=datetime.fromisoformat(data["starts_at"]) if data.get("starts_at") else None, ends_at=datetime.fromisoformat(data["ends_at"]) if data.get("ends_at") else None, + type=SubscriptionDiscountType(data["type"]) if data.get("type") else None, ) diff --git a/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscountType.py b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscountType.py new file mode 100644 index 00000000..96b16dab --- /dev/null +++ b/paddle_billing/Notifications/Entities/Subscriptions/SubscriptionDiscountType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SubscriptionDiscountType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Recurring: "SubscriptionDiscountType" = "recurring" + OneOff: "SubscriptionDiscountType" = "one-off" diff --git a/paddle_billing/Notifications/Entities/Subscriptions/__init__.py b/paddle_billing/Notifications/Entities/Subscriptions/__init__.py index e21247b4..02d3474a 100644 --- a/paddle_billing/Notifications/Entities/Subscriptions/__init__.py +++ b/paddle_billing/Notifications/Entities/Subscriptions/__init__.py @@ -1,7 +1,17 @@ from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionAdjustmentItem import SubscriptionAdjustmentItem from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionCharge import SubscriptionCharge +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionConsentRequirement import ( + SubscriptionConsentRequirement, +) +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionConsentRequirementStatus import ( + SubscriptionConsentRequirementStatus, +) +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionConsentRequirementType import ( + SubscriptionConsentRequirementType, +) from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionCredit import SubscriptionCredit from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionDiscount import SubscriptionDiscount +from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionDiscountType import SubscriptionDiscountType from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionEffectiveFrom import SubscriptionEffectiveFrom from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionItem import SubscriptionItem from paddle_billing.Notifications.Entities.Subscriptions.SubscriptionItemStatus import SubscriptionItemStatus diff --git a/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptionHistory.py b/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptionHistory.py new file mode 100644 index 00000000..d8bc2bd5 --- /dev/null +++ b/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptionHistory.py @@ -0,0 +1,61 @@ +from paddle_billing.EnumStringify import enum_stringify +from paddle_billing.HasParameters import HasParameters + +from paddle_billing.Entities.Shared import ActionSource, ActorType +from paddle_billing.Entities.Subscriptions.History import SubscriptionHistoryAction, SubscriptionHistoryReason + +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + +from paddle_billing.Resources.Shared.Operations import DateComparison, Pager + + +class ListSubscriptionHistory(HasParameters): + def __init__( + self, + pager: Pager | None = None, + actions: list[SubscriptionHistoryAction] | None = None, + sources: list[ActionSource] | None = None, + actor_types: list[ActorType] | None = None, + actor_ids: list[str] | None = None, + reasons: list[SubscriptionHistoryReason] | None = None, + occurred_at: DateComparison | None = None, + ): + self.pager = pager + self.occurred_at = occurred_at + self.actions = actions if actions is not None else [] + self.sources = sources if sources is not None else [] + self.actor_types = actor_types if actor_types is not None else [] + self.actor_ids = actor_ids if actor_ids is not None else [] + self.reasons = reasons if reasons is not None else [] + + # Validation + for field_name, field_value, field_type in [ + ("actions", self.actions, SubscriptionHistoryAction), + ("sources", self.sources, ActionSource), + ("actor_types", self.actor_types, ActorType), + ("actor_ids", self.actor_ids, str), + ("reasons", self.reasons, SubscriptionHistoryReason), + ]: + invalid_items = [item for item in field_value if not isinstance(item, field_type)] + if invalid_items: + raise InvalidArgumentException.array_contains_invalid_types( + field_name, field_type.__name__, invalid_items + ) + + def get_parameters(self) -> dict[str, str]: + parameters: dict[str, str | None] = self.pager.get_parameters() if self.pager else {} + + if self.occurred_at is not None: + parameters[f"occurred_at{self.occurred_at.comparator}"] = self.occurred_at.formatted() + + parameters.update( + { + "action": ",".join(map(enum_stringify, self.actions)), + "source": ",".join(map(enum_stringify, self.sources)), + "actor_type": ",".join(map(enum_stringify, self.actor_types)), + "actor_id": ",".join(self.actor_ids), + "reason": ",".join(map(enum_stringify, self.reasons)), + } + ) + + return {key: value for key, value in parameters.items() if value} diff --git a/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptions.py b/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptions.py index 7f803d0e..ebbdaccd 100644 --- a/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptions.py +++ b/paddle_billing/Resources/Subscriptions/Operations/ListSubscriptions.py @@ -6,7 +6,7 @@ from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException -from paddle_billing.Resources.Shared.Operations import Pager +from paddle_billing.Resources.Shared.Operations import DateComparison, Pager class ListSubscriptions(HasParameters): @@ -17,12 +17,14 @@ def __init__( collection_mode: CollectionMode | None = None, customer_ids: list[str] | None = None, ids: list[str] | None = None, + next_billed_at: DateComparison | None = None, price_ids: list[str] | None = None, scheduled_change_actions: list[SubscriptionScheduledChangeAction] | None = None, statuses: list[SubscriptionStatus] | None = None, ): self.pager = pager self.collection_mode = collection_mode + self.next_billed_at = next_billed_at self.address_ids = address_ids if address_ids is not None else [] self.customer_ids = customer_ids if customer_ids is not None else [] self.ids = ids if ids is not None else [] @@ -47,6 +49,10 @@ def __init__( def get_parameters(self) -> dict[str, str]: parameters: dict[str, str | None] = self.pager.get_parameters() if self.pager else {} + + if self.next_billed_at is not None: + parameters[f"next_billed_at{self.next_billed_at.comparator}"] = self.next_billed_at.formatted() + parameters.update( { "address_id": ",".join(self.address_ids), diff --git a/paddle_billing/Resources/Subscriptions/Operations/__init__.py b/paddle_billing/Resources/Subscriptions/Operations/__init__.py index 8fb601fb..afc841fc 100644 --- a/paddle_billing/Resources/Subscriptions/Operations/__init__.py +++ b/paddle_billing/Resources/Subscriptions/Operations/__init__.py @@ -1,6 +1,7 @@ from paddle_billing.Resources.Subscriptions.Operations.CancelSubscription import CancelSubscription from paddle_billing.Resources.Subscriptions.Operations.CreateOneTimeCharge import CreateOneTimeCharge from paddle_billing.Resources.Subscriptions.Operations.Get.Includes import Includes as SubscriptionIncludes +from paddle_billing.Resources.Subscriptions.Operations.ListSubscriptionHistory import ListSubscriptionHistory from paddle_billing.Resources.Subscriptions.Operations.ListSubscriptions import ListSubscriptions from paddle_billing.Resources.Subscriptions.Operations.PauseSubscription import PauseSubscription from paddle_billing.Resources.Subscriptions.Operations.PreviewOneTimeCharge import PreviewOneTimeCharge diff --git a/paddle_billing/Resources/Subscriptions/SubscriptionsClient.py b/paddle_billing/Resources/Subscriptions/SubscriptionsClient.py index 867a47b1..1d3af622 100644 --- a/paddle_billing/Resources/Subscriptions/SubscriptionsClient.py +++ b/paddle_billing/Resources/Subscriptions/SubscriptionsClient.py @@ -3,7 +3,7 @@ from paddle_billing.Entities.Subscription import Subscription from paddle_billing.Entities.SubscriptionPreview import SubscriptionPreview from paddle_billing.Entities.Transaction import Transaction -from paddle_billing.Entities.Collections import Paginator, SubscriptionCollection +from paddle_billing.Entities.Collections import Paginator, SubscriptionCollection, SubscriptionHistoryCollection from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException @@ -11,6 +11,7 @@ CancelSubscription, CreateOneTimeCharge, SubscriptionIncludes, + ListSubscriptionHistory, ListSubscriptions, PauseSubscription, PreviewOneTimeCharge, @@ -41,6 +42,19 @@ def list(self, operation: ListSubscriptions | None = None) -> SubscriptionCollec parser.get_list(), Paginator(self.client, parser.get_pagination(), SubscriptionCollection) ) + def list_history( + self, subscription_id: str, operation: ListSubscriptionHistory | None = None + ) -> SubscriptionHistoryCollection: + if operation is None: + operation = ListSubscriptionHistory() + + self.response = self.client.get_raw(f"/subscriptions/{subscription_id}/history", operation.get_parameters()) + parser = ResponseParser(self.response) + + return SubscriptionHistoryCollection.from_list( + parser.get_list(), Paginator(self.client, parser.get_pagination(), SubscriptionHistoryCollection) + ) + def get(self, subscription_id: str, includes=None) -> Subscription: if includes is None: includes = [] diff --git a/tests/Functional/Resources/Events/_fixtures/response/list_default.json b/tests/Functional/Resources/Events/_fixtures/response/list_default.json index 118ab1db..f5f3fc4e 100644 --- a/tests/Functional/Resources/Events/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Events/_fixtures/response/list_default.json @@ -18,6 +18,7 @@ "next_billed_at": "2023-12-24T14:11:11.447004Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "discount": null, "collection_mode": "automatic", "billing_details": null, @@ -104,6 +105,7 @@ "next_billed_at": "2024-05-12T13:16:08.821891Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "discount": null, "collection_mode": "automatic", "billing_details": null, @@ -1864,6 +1866,7 @@ "next_billed_at": "2023-12-24T14:11:11.447004Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "discount": null, "collection_mode": "automatic", "billing_details": null, diff --git a/tests/Functional/Resources/Events/_fixtures/response/list_default.json.single b/tests/Functional/Resources/Events/_fixtures/response/list_default.json.single index f5b95e46..b7583a7e 100644 --- a/tests/Functional/Resources/Events/_fixtures/response/list_default.json.single +++ b/tests/Functional/Resources/Events/_fixtures/response/list_default.json.single @@ -18,6 +18,7 @@ "next_billed_at": "2023-12-24T14:11:11.447004Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "discount": null, "collection_mode": "automatic", "billing_details": null, diff --git a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json index 18a57baa..7ff04561 100644 --- a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json @@ -433,6 +433,7 @@ "updated_at": "2023-08-18T13:15:46.864163Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01h84cjfwmdph1k8kgsyjt3k7g", "billing_cycle": { diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/api_key.created.json b/tests/Functional/Resources/Simulations/_fixtures/payload/api_key.created.json index c87ee7c9..99dd97c2 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/api_key.created.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/api_key.created.json @@ -7,8 +7,12 @@ "permissions": [ "address.read", "business.read", + "checkout_domain.read", + "client_token.read", "customer.read", + "metrics.read", "subscription.read", + "subscription_history.read", "transaction.read" ], "expires_at": "2025-06-26T06:58:38.517522Z", diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.activated.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.activated.json index 228b8d26..a78b497d 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.activated.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.activated.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T10:18:48.831000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.canceled.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.canceled.json index ec328aa1..58285fa7 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.canceled.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.canceled.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -149,7 +149,7 @@ "type": "standard", "tax_category": "standard", "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your AeroEdit subscription.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/qgyipKJwRtq98YNboipo_vip-support.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/qgyipKJwRtq98YNboipo_vip-support.png", "custom_data": null, "status": "active", "import_meta": null, @@ -175,6 +175,7 @@ "updated_at": "2024-04-12T11:24:54.873000Z", "business_id": null, "canceled_at": "2024-04-12T11:24:54.868000Z", + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.created.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.created.json index 3a57ffbd..9005f26e 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.created.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.created.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T10:18:48.831000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.imported.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.imported.json index 8887f114..23097e71 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.imported.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.imported.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T11:27:08.658000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": { diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.past_due.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.past_due.json index b566fed9..568f5df1 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.past_due.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.past_due.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-05-12T10:19:26.014628Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.paused.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.paused.json index 4fe494d7..b5b8a3ce 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.paused.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.paused.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T12:43:43.219000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.resumed.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.resumed.json index cac6ba93..6cc6b778 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.resumed.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.resumed.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T12:44:51.309000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.trialing.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.trialing.json index 5bc3f7f2..58c04be2 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.trialing.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.trialing.json @@ -62,7 +62,7 @@ "type": "standard", "tax_category": "standard", "description": "Essential tools for student pilots to manage flight logs, analyze performance, and plan routes, and ensure compliance. Valid student pilot certificate from the FAA required.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -105,6 +105,7 @@ "updated_at": "2024-04-12T11:30:29.648000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.updated.json b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.updated.json index 7bf3b826..8860babf 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.updated.json +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/subscription.updated.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -149,7 +149,7 @@ "type": "standard", "tax_category": "standard", "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your AeroEdit subscription.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/qgyipKJwRtq98YNboipo_vip-support.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/qgyipKJwRtq98YNboipo_vip-support.png", "custom_data": null, "status": "active", "import_meta": null, @@ -175,6 +175,7 @@ "updated_at": "2024-04-12T10:49:38.771000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json index cdb1f949..5c16c2f3 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json @@ -13,6 +13,20 @@ "next_billed_at": "2024-03-08T11:02:03.946454Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [ + { + "id": "subcr_01j3g2y5g0f5q9k2h1n8x7e6d5", + "requirement": "trial_ending", + "status": "pending", + "created_at": "2026-06-01T00:00:00Z", + "consent_period": { + "starts_at": "2026-06-01T00:00:00Z", + "ends_at": "2026-06-15T00:00:00Z" + }, + "granted_at": null, + "voided_at": null + } + ], "collection_mode": "automatic", "billing_details": null, "current_billing_period": { diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json index e0fb475a..a3978710 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json @@ -13,6 +13,7 @@ "next_billed_at": "2024-03-08T11:02:03.946454Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "collection_mode": "automatic", "billing_details": null, "current_billing_period": { diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json index 4c001315..e3760958 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json @@ -14,6 +14,7 @@ "next_billed_at": "2024-02-22T11:40:06.048Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "collection_mode": "automatic", "billing_details": null, "current_billing_period": { @@ -116,6 +117,7 @@ "next_billed_at": null, "paused_at": null, "canceled_at": "2024-03-08T11:21:37.227055Z", + "consent_requirements": [], "collection_mode": "automatic", "billing_details": null, "current_billing_period": null, @@ -253,6 +255,7 @@ "next_billed_at": "2025-02-08T11:15:15.565Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "collection_mode": "manual", "billing_details": { "enable_checkout": false, @@ -328,6 +331,7 @@ }, "discount": { "id": "dsc_01hv6scyf7qdnzcdq01t2y8dx4", + "type": "recurring", "starts_at": null, "ends_at": null }, @@ -347,6 +351,7 @@ "next_billed_at": null, "paused_at": null, "canceled_at": null, + "consent_requirements": [], "collection_mode": "automatic", "billing_details": null, "current_billing_period": { @@ -476,6 +481,7 @@ }, "discount": { "id": "dsc_01hv6scyf7qdnzcdq01t2y8dx4", + "type": "recurring", "starts_at": "2024-04-12T10:18:47.635628Z", "ends_at": "2024-05-12T10:18:47.635628Z" }, diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_all_actions.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_all_actions.json new file mode 100644 index 00000000..c6a170d0 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_all_actions.json @@ -0,0 +1,827 @@ +{ + "data": [ + { + "id": "subhis_01k0a0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0a0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T12:42:27.000000Z", + "source": "checkout", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_activated", + "status": "active", + "first_billed_at": "2026-06-29T12:42:28.000000Z", + "next_billed_at": "2026-07-29T12:42:28.000000Z", + "current_billing_period": { + "starts_at": "2026-06-29T12:42:28.000000Z", + "ends_at": "2026-07-29T12:42:28.000000Z" + }, + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9v" + } + }, + { + "id": "subhis_01k0b0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0b0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T13:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_address_updated", + "address": { + "id": "add_01hv8gq3318ktkfengj2r75gfx", + "city": "San Jose", + "region": "CA", + "status": "active", + "created_at": "2024-04-12T06:45:53.267000Z", + "first_line": "5400 E Washington Drive, Floor 2", + "updated_at": "2024-04-12T06:47:01.533006Z", + "custom_data": { + "crm_id": "08bcfb7a-41d8-4747-9ade-3d885c287d0f" + }, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "description": "California Office", + "import_meta": null, + "postal_code": "95314", + "second_line": null, + "country_code": "US" + } + } + }, + { + "id": "subhis_01k0c0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0c0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T14:00:00.000000Z", + "source": "api", + "actor": { "type": "api_key", "id": "apikey_01h8441jn5pcwrfhwh78jqt8hl" }, + "reason": null, + "detail": { + "action": "subscription_billing_cycle_updated", + "billing_cycle": { "frequency": 3, "interval": "month" }, + "current_billing_period": { + "starts_at": "2026-06-29T14:00:00.000000Z", + "ends_at": "2026-09-29T14:00:00.000000Z" + }, + "next_billed_at": "2026-09-29T14:00:00.000000Z" + } + }, + { + "id": "subhis_01k0d0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0d0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T15:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_billing_date_updated", + "next_billed_at": "2026-08-01T00:00:00.000000Z", + "current_billing_period": { + "starts_at": "2026-07-01T00:00:00.000000Z", + "ends_at": "2026-08-01T00:00:00.000000Z" + }, + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9z", + "proration_billing_mode": "prorated_immediately" + } + }, + { + "id": "subhis_01k0e0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0e0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T16:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_billing_details_updated", + "billing_details": { + "enable_checkout": true, + "payment_terms": { + "interval": "month", + "frequency": 1 + }, + "purchase_order_number": "10009", + "additional_information": null + } + } + }, + { + "id": "subhis_01k0f0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0f0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T17:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_business_added", + "business": { + "id": "biz_01hv8hkr641vmpwytx38znv56k", + "name": "Uplift Inc.", + "status": "active", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + } + ], + "created_at": "2024-04-12T06:58:37.892000Z", + "updated_at": "2024-04-12T07:01:03.510528Z", + "custom_data": { + "crm_id": "eb9b8d9b-7dd6-48e6-8c39-8557bba5eaa9" + }, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "import_meta": null, + "company_number": "555775291485", + "tax_identifier": "555952383" + } + } + }, + { + "id": "subhis_01k0g0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0g0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T18:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_business_removed", + "business": { + "id": "biz_01hv8hkr641vmpwytx38znv56k", + "name": "Uplift Inc.", + "status": "active", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + } + ], + "created_at": "2024-04-12T06:58:37.892000Z", + "updated_at": "2024-04-12T07:01:03.510528Z", + "custom_data": { + "crm_id": "eb9b8d9b-7dd6-48e6-8c39-8557bba5eaa9" + }, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "import_meta": null, + "company_number": "555775291485", + "tax_identifier": "555952383" + } + } + }, + { + "id": "subhis_01k0h0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0h0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T19:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_business_updated", + "business": { + "id": "biz_01hv8hkr641vmpwytx38znv56k", + "name": "Uplift Inc.", + "status": "active", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + } + ], + "created_at": "2024-04-12T06:58:37.892000Z", + "updated_at": "2024-04-12T07:01:03.510528Z", + "custom_data": { + "crm_id": "eb9b8d9b-7dd6-48e6-8c39-8557bba5eaa9" + }, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "import_meta": null, + "company_number": "555775291485", + "tax_identifier": "555952383" + } + } + }, + { + "id": "subhis_01k0i0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0i0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-04T12:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": "customer_request", + "detail": { + "action": "subscription_canceled", + "effective_from": "next_billing_period" + } + }, + { + "id": "subhis_01k0j0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0j0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T20:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_collection_mode_updated", + "collection_mode": "manual" + } + }, + { + "id": "subhis_01k0k0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0k0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-01T00:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_consent_requirement_granted", + "consent_requirement": { + "id": "subcr_01j3g2y5g0f5q9k2h1n8x7e6d5", + "requirement": "trial_ending", + "status": "granted", + "created_at": "2026-06-01T00:00:00.000000Z", + "consent_period": { + "starts_at": "2026-06-01T00:00:00.000000Z", + "ends_at": "2026-06-15T00:00:00.000000Z" + }, + "granted_at": "2026-06-10T00:00:00.000000Z", + "voided_at": null + } + } + }, + { + "id": "subhis_01k0l0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0l0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T12:42:27.000000Z", + "source": "checkout", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_created", + "status": "active", + "collection_mode": "automatic", + "billing_details": null, + "customer": null, + "address": null, + "business": null, + "currency_code": "USD", + "current_billing_period": { + "starts_at": "2026-06-29T12:42:27.000000Z", + "ends_at": "2026-07-29T12:42:27.000000Z" + }, + "billing_cycle": { "frequency": 1, "interval": "month" }, + "items": null, + "custom_data": null, + "discount": null, + "has_payment_method": true + } + }, + { + "id": "subhis_01k0m0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0m0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T21:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_currency_updated", + "currency_code": "EUR" + } + }, + { + "id": "subhis_01k0n0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0n0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T22:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_custom_data_updated", + "custom_data": { "key": "value" } + } + }, + { + "id": "subhis_01k0o0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0o0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T23:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_customer_updated", + "customer": { + "id": "ctm_01h844p3h41s12zs5mn4axja51", + "status": "active", + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "name": "Alex Wilson", + "email": "test2@example.com", + "marketing_consent": false, + "locale": "en", + "created_at": "2023-08-18T10:57:31.172000Z", + "updated_at": "2023-08-18T10:57:31.172000Z", + "import_meta": null + } + } + }, + { + "id": "subhis_01k0p0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0p0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-02T10:00:00.000000Z", + "source": "api", + "actor": { "type": "api_key", "id": "apikey_01h8441jn5pcwrfhwh78jqt8hl" }, + "reason": null, + "detail": { + "action": "subscription_discount_added", + "discount": { + "discount": { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "active", + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "code": "ABCDE12345", + "type": "percentage", + "mode": "standard", + "amount": "10", + "currency_code": "USD", + "recur": true, + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": ["pro_01gsz4t5hdjse780zja8vvr7jg", "pro_01gsz4s0w61y0pp88528f1wvvb"], + "expires_at": null, + "times_used": 0, + "created_at": "2023-08-18T08:51:07.596000Z", + "updated_at": "2023-08-18T08:51:07.596000Z", + "import_meta": null, + "custom_data": { "key": "value" }, + "discount_group_id": null, + "discount_group": null + }, + "type": "recurring", + "starts_at": "2026-07-02T10:00:00.000000Z", + "ends_at": null + } + } + }, + { + "id": "subhis_01k0q0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0q0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-15T10:00:00.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_discount_expired", + "discount": { + "discount": { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "expired", + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "code": "ABCDE12345", + "type": "percentage", + "mode": "standard", + "amount": "10", + "currency_code": "USD", + "recur": true, + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": ["pro_01gsz4t5hdjse780zja8vvr7jg", "pro_01gsz4s0w61y0pp88528f1wvvb"], + "expires_at": "2026-07-15T10:00:00.000000Z", + "times_used": 1, + "created_at": "2023-08-18T08:51:07.596000Z", + "updated_at": "2023-08-18T08:51:07.596000Z", + "import_meta": null, + "custom_data": { "key": "value" }, + "discount_group_id": null, + "discount_group": null + }, + "type": "recurring", + "starts_at": "2026-07-02T10:00:00.000000Z", + "ends_at": "2026-07-15T10:00:00.000000Z" + } + } + }, + { + "id": "subhis_01k0r0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0r0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-16T10:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_discount_removed", + "discount": { + "discount": { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "active", + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "code": "ABCDE12345", + "type": "percentage", + "mode": "standard", + "amount": "10", + "currency_code": "USD", + "recur": true, + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": ["pro_01gsz4t5hdjse780zja8vvr7jg", "pro_01gsz4s0w61y0pp88528f1wvvb"], + "expires_at": null, + "times_used": 0, + "created_at": "2023-08-18T08:51:07.596000Z", + "updated_at": "2023-08-18T08:51:07.596000Z", + "import_meta": null, + "custom_data": { "key": "value" }, + "discount_group_id": null, + "discount_group": null + }, + "type": "recurring", + "starts_at": "2026-07-02T10:00:00.000000Z", + "ends_at": null + } + } + }, + { + "id": "subhis_01k0s0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0s0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-01T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_item_added", + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "type": "standard", + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { "frequency": 1, "interval": "month" }, + "trial_period": null, + "unit_price": { "amount": "3000", "currency_code": "USD" }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { "minimum": 1, "maximum": 999 }, + "import_meta": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null, + "import_meta": null, + "prices": [], + "type": null, + "created_at": "2023-08-16T14:38:08.300000Z", + "updated_at": "2023-08-16T14:38:08.300000Z" + }, + "created_at": "2023-02-23T13:55:22.538367Z", + "updated_at": "2023-11-09T14:07:16.051528Z" + }, + "quantity": 5, + "update_summary": { "quantity_delta": 5 }, + "proration_billing_mode": "prorated_immediately", + "on_payment_failure": "prevent_change", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9x" + } + }, + { + "id": "subhis_01k0t0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0t0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-08T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_item_quantity_updated", + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "type": "standard", + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { "frequency": 1, "interval": "month" }, + "trial_period": null, + "unit_price": { "amount": "3000", "currency_code": "USD" }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { "minimum": 1, "maximum": 999 }, + "import_meta": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null, + "import_meta": null, + "prices": [], + "type": null, + "created_at": "2023-08-16T14:38:08.300000Z", + "updated_at": "2023-08-16T14:38:08.300000Z" + }, + "created_at": "2023-02-23T13:55:22.538367Z", + "updated_at": "2023-11-09T14:07:16.051528Z" + }, + "quantity": 8, + "update_summary": { "quantity_delta": 3 }, + "proration_billing_mode": "prorated_next_billing_period", + "on_payment_failure": "apply_change", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s8a0a" + } + }, + { + "id": "subhis_01k0u0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0u0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-09T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_item_removed", + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "type": "standard", + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { "frequency": 1, "interval": "month" }, + "trial_period": null, + "unit_price": { "amount": "3000", "currency_code": "USD" }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { "minimum": 1, "maximum": 999 }, + "import_meta": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null, + "import_meta": null, + "prices": [], + "type": null, + "created_at": "2023-08-16T14:38:08.300000Z", + "updated_at": "2023-08-16T14:38:08.300000Z" + }, + "created_at": "2023-02-23T13:55:22.538367Z", + "updated_at": "2023-11-09T14:07:16.051528Z" + }, + "quantity": 0, + "update_summary": { "quantity_delta": -8 }, + "proration_billing_mode": "full_immediately", + "on_payment_failure": "prevent_change", + "transaction_id": null + } + }, + { + "id": "subhis_01k0v0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0v0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-10T09:00:00.000000Z", + "source": "api", + "actor": { "type": "api_key", "id": "apikey_01h8441jn5pcwrfhwh78jqt8hl" }, + "reason": null, + "detail": { + "action": "subscription_one_off_charge_applied", + "effective_from": "immediately", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "type": "standard", + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { "frequency": 1, "interval": "month" }, + "trial_period": null, + "unit_price": { "amount": "3000", "currency_code": "USD" }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { "minimum": 1, "maximum": 999 }, + "import_meta": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null, + "import_meta": null, + "prices": [], + "type": null, + "created_at": "2023-08-16T14:38:08.300000Z", + "updated_at": "2023-08-16T14:38:08.300000Z" + }, + "created_at": "2023-02-23T13:55:22.538367Z", + "updated_at": "2023-11-09T14:07:16.051528Z" + }, + "quantity": 1 + } + ], + "on_payment_failure": "prevent_change", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s8b1b" + } + }, + { + "id": "subhis_01k0w0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0w0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-11T09:00:00.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_past_due", + "status": "past_due", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s8c2c" + } + }, + { + "id": "subhis_01k0x0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0x0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-12T09:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": "customer_request", + "detail": { + "action": "subscription_paused", + "effective_from": "next_billing_period", + "status": "paused" + } + }, + { + "id": "subhis_01k8w1f5n7w8x9y0z1a2b3c4e3", + "group_id": "subhisgrp_01k8w1f5n7w8x9y0z1a2b3c4e3", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-06T14:00:00.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_payment_attempted", + "operation": "subscription_update", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9y" + } + }, + { + "id": "subhis_01k6w1f5n7w8x9y0z1a2b3c4e1", + "group_id": "subhisgrp_01k6w1f5n7w8x9y0z1a2b3c4e1", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-05T13:00:00.000000Z", + "source": "checkout", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_payment_method_added" + } + }, + { + "id": "subhis_01k0y0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0y0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-13T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_payment_method_removed" + } + }, + { + "id": "subhis_01k0z0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0z0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-14T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_payment_method_updated" + } + }, + { + "id": "subhis_01k1w1f5n7w8x9y0z1a2b3c4d6", + "group_id": "subhisgrp_01k1w1f5n7w8x9y0z1a2b3c4d6", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-29T12:42:28.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_renewed", + "next_billed_at": "2026-08-29T12:42:28.000000Z", + "current_billing_period": { + "starts_at": "2026-07-29T12:42:28.000000Z", + "ends_at": "2026-08-29T12:42:28.000000Z" + }, + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9w" + } + }, + { + "id": "subhis_01k1a0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k1a0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-20T09:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_resumed", + "status": "active", + "on_resume": "start_new_billing_period" + } + }, + { + "id": "subhis_01k4w1f5n7w8x9y0z1a2b3c4d9", + "group_id": "subhisgrp_01k4w1f5n7w8x9y0z1a2b3c4d9", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-03T11:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_scheduled_change_added", + "scheduled_change": { + "action": "cancel", + "effective_at": "2026-08-01T00:00:00.000000Z", + "resume_at": null + } + } + }, + { + "id": "subhis_01k1b0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k1b0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-21T09:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_scheduled_change_removed", + "scheduled_change": { + "action": "cancel", + "effective_at": "2026-08-01T00:00:00.000000Z", + "resume_at": null + } + } + }, + { + "id": "subhis_01k1c0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k1c0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-22T09:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_scheduled_change_updated", + "scheduled_change": { + "action": "pause", + "effective_at": "2026-08-15T00:00:00.000000Z", + "resume_at": "2026-09-15T00:00:00.000000Z" + } + } + } + ], + "meta": { + "request_id": "170e71a2-ed13-4f45-b002-45693f5361b4", + "pagination": { + "per_page": 50, + "next": "https://sandbox-api.paddle.com/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history?after=subhis_01k1c0a4m6v7w8x9y0z1a2b3c4", + "has_more": false, + "estimated_total": 33 + } + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_default.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_default.json new file mode 100644 index 00000000..02cea9cb --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_default.json @@ -0,0 +1,261 @@ +{ + "data": [ + { + "id": "subhis_01k0w0a4m6v7w8x9y0z1a2b3c4", + "group_id": "subhisgrp_01k0w0a4m6v7w8x9y0z1a2b3c4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T12:42:27.000000Z", + "source": "checkout", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_created", + "status": "active", + "collection_mode": "automatic", + "billing_details": null, + "customer": null, + "address": null, + "business": null, + "currency_code": "USD", + "current_billing_period": { + "starts_at": "2026-06-29T12:42:27.000000Z", + "ends_at": "2026-07-29T12:42:27.000000Z" + }, + "billing_cycle": { "frequency": 1, "interval": "month" }, + "items": null, + "custom_data": null, + "discount": null, + "has_payment_method": true + } + }, + { + "id": "subhis_01k0w1f5n7w8x9y0z1a2b3c4d5", + "group_id": "subhisgrp_01k0w1f5n7w8x9y0z1a2b3c4d5", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-29T12:42:28.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_activated", + "status": "active", + "first_billed_at": "2026-06-29T12:42:28.000000Z", + "next_billed_at": "2026-07-29T12:42:28.000000Z", + "current_billing_period": { + "starts_at": "2026-06-29T12:42:28.000000Z", + "ends_at": "2026-07-29T12:42:28.000000Z" + }, + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9v" + } + }, + { + "id": "subhis_01k1w1f5n7w8x9y0z1a2b3c4d6", + "group_id": "subhisgrp_01k1w1f5n7w8x9y0z1a2b3c4d6", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-29T12:42:28.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_renewed", + "next_billed_at": "2026-08-29T12:42:28.000000Z", + "current_billing_period": { + "starts_at": "2026-07-29T12:42:28.000000Z", + "ends_at": "2026-08-29T12:42:28.000000Z" + }, + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9w" + } + }, + { + "id": "subhis_01k2w1f5n7w8x9y0z1a2b3c4d7", + "group_id": "subhisgrp_01k2w1f5n7w8x9y0z1a2b3c4d7", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-01T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_item_added", + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "type": "standard", + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { "frequency": 1, "interval": "month" }, + "trial_period": null, + "unit_price": { "amount": "3000", "currency_code": "USD" }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { "minimum": 1, "maximum": 999 }, + "import_meta": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null, + "import_meta": null, + "prices": [], + "type": null, + "created_at": "2023-08-16T14:38:08.300000Z", + "updated_at": "2023-08-16T14:38:08.300000Z" + }, + "created_at": "2023-02-23T13:55:22.538367Z", + "updated_at": "2023-11-09T14:07:16.051528Z" + }, + "quantity": 5, + "update_summary": { "quantity_delta": 5 }, + "proration_billing_mode": "prorated_immediately", + "on_payment_failure": "prevent_change", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9x" + } + }, + { + "id": "subhis_01k3w1f5n7w8x9y0z1a2b3c4d8", + "group_id": "subhisgrp_01k3w1f5n7w8x9y0z1a2b3c4d8", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-02T10:00:00.000000Z", + "source": "api", + "actor": { "type": "api_key", "id": "apikey_01h8441jn5pcwrfhwh78jqt8hl" }, + "reason": null, + "detail": { + "action": "subscription_discount_added", + "discount": { + "discount": { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "active", + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "code": "ABCDE12345", + "type": "percentage", + "mode": "standard", + "amount": "10", + "currency_code": "USD", + "recur": true, + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": ["pro_01gsz4t5hdjse780zja8vvr7jg", "pro_01gsz4s0w61y0pp88528f1wvvb"], + "expires_at": null, + "times_used": 0, + "created_at": "2023-08-18T08:51:07.596000Z", + "updated_at": "2023-08-18T08:51:07.596000Z", + "import_meta": null, + "custom_data": { "key": "value" }, + "discount_group_id": null, + "discount_group": null + }, + "type": "recurring", + "starts_at": "2026-07-02T10:00:00.000000Z", + "ends_at": null + } + } + }, + { + "id": "subhis_01k4w1f5n7w8x9y0z1a2b3c4d9", + "group_id": "subhisgrp_01k4w1f5n7w8x9y0z1a2b3c4d9", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-03T11:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_scheduled_change_added", + "scheduled_change": { + "action": "cancel", + "effective_at": "2026-08-01T00:00:00.000000Z", + "resume_at": null + } + } + }, + { + "id": "subhis_01k5w1f5n7w8x9y0z1a2b3c4e0", + "group_id": "subhisgrp_01k5w1f5n7w8x9y0z1a2b3c4e0", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-04T12:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": "customer_request", + "detail": { + "action": "subscription_canceled", + "effective_from": "next_billing_period" + } + }, + { + "id": "subhis_01k6w1f5n7w8x9y0z1a2b3c4e1", + "group_id": "subhisgrp_01k6w1f5n7w8x9y0z1a2b3c4e1", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-05T13:00:00.000000Z", + "source": "checkout", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_payment_method_added" + } + }, + { + "id": "subhis_01k7w1f5n7w8x9y0z1a2b3c4e2", + "group_id": "subhisgrp_01k7w1f5n7w8x9y0z1a2b3c4e2", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-06-01T00:00:00.000000Z", + "source": "customer_portal", + "actor": { "type": "customer", "id": "ctm_01hv8wt8nffez4p2t6typn4a5j" }, + "reason": null, + "detail": { + "action": "subscription_consent_requirement_granted", + "consent_requirement": { + "id": "subcr_01j3g2y5g0f5q9k2h1n8x7e6d5", + "requirement": "trial_ending", + "status": "granted", + "created_at": "2026-06-01T00:00:00.000000Z", + "consent_period": { + "starts_at": "2026-06-01T00:00:00.000000Z", + "ends_at": "2026-06-15T00:00:00.000000Z" + }, + "granted_at": "2026-06-10T00:00:00.000000Z", + "voided_at": null + } + } + }, + { + "id": "subhis_01k8w1f5n7w8x9y0z1a2b3c4e3", + "group_id": "subhisgrp_01k8w1f5n7w8x9y0z1a2b3c4e3", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-06T14:00:00.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_payment_attempted", + "operation": "subscription_update", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9y" + } + }, + { + "id": "subhis_01k9w1f5n7w8x9y0z1a2b3c4e4", + "group_id": "subhisgrp_01k9w1f5n7w8x9y0z1a2b3c4e4", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-07T15:00:00.000000Z", + "source": "unknown", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_something_new", + "some_field": "some_value" + } + } + ], + "meta": { + "request_id": "170e71a2-ed13-4f45-b002-45693f5361b4", + "pagination": { + "per_page": 50, + "next": "https://sandbox-api.paddle.com/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history?after=subhis_01k9w1f5n7w8x9y0z1a2b3c4e4", + "has_more": false, + "estimated_total": 11 + } + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_paginated_page_one.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_paginated_page_one.json new file mode 100644 index 00000000..ac736bd5 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_paginated_page_one.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "id": "subhis_01paga0a4m6v7w8x9y0z1a2b3c", + "group_id": "subhisgrp_01paga0a4m6v7w8x9y0z1a2b3c", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-10T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_paused", + "effective_from": "immediately", + "status": "paused" + } + }, + { + "id": "subhis_02paga0a4m6v7w8x9y0z1a2b3c", + "group_id": "subhisgrp_02paga0a4m6v7w8x9y0z1a2b3c", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-11T09:00:00.000000Z", + "source": "dashboard", + "actor": { "type": "user", "id": "usr_01h8441jn5pcwrfhwh78jqt8hk" }, + "reason": null, + "detail": { + "action": "subscription_resumed", + "status": "active", + "on_resume": "start_new_billing_period" + } + } + ], + "meta": { + "request_id": "270e71a2-ed13-4f45-b002-45693f5361b5", + "pagination": { + "per_page": 2, + "next": "https://sandbox-api.paddle.com/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history?after=subhis_02paga0a4m6v7w8x9y0z1a2b3c", + "has_more": true, + "estimated_total": 4 + } + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_paginated_page_two.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_paginated_page_two.json new file mode 100644 index 00000000..de745a0c --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_history_paginated_page_two.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "subhis_03paga0a4m6v7w8x9y0z1a2b3c", + "group_id": "subhisgrp_03paga0a4m6v7w8x9y0z1a2b3c", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-12T09:00:00.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_past_due", + "status": "past_due", + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s7t9z" + } + }, + { + "id": "subhis_04paga0a4m6v7w8x9y0z1a2b3c", + "group_id": "subhisgrp_04paga0a4m6v7w8x9y0z1a2b3c", + "subscription_id": "sub_01hv959anj4zrw503h2acawb3p", + "occurred_at": "2026-07-13T09:00:00.000000Z", + "source": "system", + "actor": { "type": "system", "id": null }, + "reason": null, + "detail": { + "action": "subscription_activated", + "status": "active", + "first_billed_at": null, + "next_billed_at": "2026-08-13T09:00:00.000000Z", + "current_billing_period": { + "starts_at": "2026-07-13T09:00:00.000000Z", + "ends_at": "2026-08-13T09:00:00.000000Z" + }, + "transaction_id": "txn_01h89231k3a1q8mn6p4r5s8a0a" + } + } + ], + "meta": { + "request_id": "270e71a2-ed13-4f45-b002-45693f5361b6", + "pagination": { + "per_page": 2, + "next": "https://sandbox-api.paddle.com/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history?after=subhis_04paga0a4m6v7w8x9y0z1a2b3c", + "has_more": false, + "estimated_total": 4 + } + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json index 1748be57..4743da9a 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json @@ -12,6 +12,7 @@ "next_billed_at": "2024-03-08T11:17:08.807055Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "collection_mode": "automatic", "billing_details": null, "current_billing_period": { diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json index f5392ce0..a238b889 100644 --- a/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json @@ -12,6 +12,7 @@ "next_billed_at": "2024-03-08T11:02:03.946454Z", "paused_at": null, "canceled_at": null, + "consent_requirements": [], "collection_mode": "automatic", "billing_details": null, "current_billing_period": { diff --git a/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py b/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py index 899695f7..a20c40ff 100644 --- a/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py +++ b/tests/Functional/Resources/Subscriptions/test_SubscriptionsClient.py @@ -1,15 +1,21 @@ -from json import loads +from json import dumps, loads from pytest import mark from urllib.parse import unquote -from paddle_billing.Entities.Collections import SubscriptionCollection +from paddle_billing.Json import PayloadEncoder + +from paddle_billing.Entities.Collections import SubscriptionCollection, SubscriptionHistoryCollection from paddle_billing.Entities.DateTime import DateTime from paddle_billing.Entities.Subscription import Subscription from paddle_billing.Entities.SubscriptionPreview import SubscriptionPreview from paddle_billing.Entities.Transaction import Transaction from paddle_billing.Entities.Discount import Discount, DiscountStatus +from paddle_billing.Notifications.Entities.UndefinedEntity import UndefinedEntity + from paddle_billing.Entities.Shared import ( + ActionSource, + ActorType, CollectionMode, CurrencyCode, CustomData, @@ -24,6 +30,10 @@ ) from paddle_billing.Entities.Subscriptions import ( + SubscriptionConsentRequirement, + SubscriptionConsentRequirementStatus, + SubscriptionConsentRequirementType, + SubscriptionDiscountType, SubscriptionEffectiveFrom, SubscriptionOnPaymentFailure, SubscriptionOnResume, @@ -33,10 +43,49 @@ SubscriptionStatus, ) -from paddle_billing.Resources.Shared.Operations import Pager +from paddle_billing.Entities.Subscriptions.History import ( + SubscriptionHistoryAction, + SubscriptionHistoryDetailSubscriptionActivated, + SubscriptionHistoryDetailSubscriptionAddressUpdated, + SubscriptionHistoryDetailSubscriptionBillingCycleUpdated, + SubscriptionHistoryDetailSubscriptionBillingDateUpdated, + SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated, + SubscriptionHistoryDetailSubscriptionBusinessAdded, + SubscriptionHistoryDetailSubscriptionBusinessRemoved, + SubscriptionHistoryDetailSubscriptionBusinessUpdated, + SubscriptionHistoryDetailSubscriptionCanceled, + SubscriptionHistoryDetailSubscriptionCollectionModeUpdated, + SubscriptionHistoryDetailSubscriptionConsentRequirementGranted, + SubscriptionHistoryDetailSubscriptionCreated, + SubscriptionHistoryDetailSubscriptionCurrencyUpdated, + SubscriptionHistoryDetailSubscriptionCustomDataUpdated, + SubscriptionHistoryDetailSubscriptionCustomerUpdated, + SubscriptionHistoryDetailSubscriptionDiscountAdded, + SubscriptionHistoryDetailSubscriptionDiscountExpired, + SubscriptionHistoryDetailSubscriptionDiscountRemoved, + SubscriptionHistoryDetailSubscriptionItemAdded, + SubscriptionHistoryDetailSubscriptionItemQuantityUpdated, + SubscriptionHistoryDetailSubscriptionItemRemoved, + SubscriptionHistoryDetailSubscriptionOneOffChargeApplied, + SubscriptionHistoryDetailSubscriptionPastDue, + SubscriptionHistoryDetailSubscriptionPaused, + SubscriptionHistoryDetailSubscriptionPaymentAttempted, + SubscriptionHistoryDetailSubscriptionPaymentMethodAdded, + SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved, + SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated, + SubscriptionHistoryDetailSubscriptionRenewed, + SubscriptionHistoryDetailSubscriptionResumed, + SubscriptionHistoryDetailSubscriptionScheduledChangeAdded, + SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved, + SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated, + SubscriptionHistoryReason, +) + +from paddle_billing.Resources.Shared.Operations import Comparator, DateComparison, Pager from paddle_billing.Resources.Subscriptions.Operations import ( CancelSubscription, CreateOneTimeCharge, + ListSubscriptionHistory, ListSubscriptions, PauseSubscription, PreviewOneTimeCharge, @@ -68,6 +117,42 @@ from tests.Utils.ReadsFixture import ReadsFixtures +SUBSCRIPTION_HISTORY_DETAIL_TYPES = { + "subscription_activated": SubscriptionHistoryDetailSubscriptionActivated, + "subscription_address_updated": SubscriptionHistoryDetailSubscriptionAddressUpdated, + "subscription_billing_cycle_updated": SubscriptionHistoryDetailSubscriptionBillingCycleUpdated, + "subscription_billing_date_updated": SubscriptionHistoryDetailSubscriptionBillingDateUpdated, + "subscription_billing_details_updated": SubscriptionHistoryDetailSubscriptionBillingDetailsUpdated, + "subscription_business_added": SubscriptionHistoryDetailSubscriptionBusinessAdded, + "subscription_business_removed": SubscriptionHistoryDetailSubscriptionBusinessRemoved, + "subscription_business_updated": SubscriptionHistoryDetailSubscriptionBusinessUpdated, + "subscription_canceled": SubscriptionHistoryDetailSubscriptionCanceled, + "subscription_collection_mode_updated": SubscriptionHistoryDetailSubscriptionCollectionModeUpdated, + "subscription_consent_requirement_granted": SubscriptionHistoryDetailSubscriptionConsentRequirementGranted, + "subscription_created": SubscriptionHistoryDetailSubscriptionCreated, + "subscription_currency_updated": SubscriptionHistoryDetailSubscriptionCurrencyUpdated, + "subscription_custom_data_updated": SubscriptionHistoryDetailSubscriptionCustomDataUpdated, + "subscription_customer_updated": SubscriptionHistoryDetailSubscriptionCustomerUpdated, + "subscription_discount_added": SubscriptionHistoryDetailSubscriptionDiscountAdded, + "subscription_discount_expired": SubscriptionHistoryDetailSubscriptionDiscountExpired, + "subscription_discount_removed": SubscriptionHistoryDetailSubscriptionDiscountRemoved, + "subscription_item_added": SubscriptionHistoryDetailSubscriptionItemAdded, + "subscription_item_quantity_updated": SubscriptionHistoryDetailSubscriptionItemQuantityUpdated, + "subscription_item_removed": SubscriptionHistoryDetailSubscriptionItemRemoved, + "subscription_one_off_charge_applied": SubscriptionHistoryDetailSubscriptionOneOffChargeApplied, + "subscription_past_due": SubscriptionHistoryDetailSubscriptionPastDue, + "subscription_paused": SubscriptionHistoryDetailSubscriptionPaused, + "subscription_payment_attempted": SubscriptionHistoryDetailSubscriptionPaymentAttempted, + "subscription_payment_method_added": SubscriptionHistoryDetailSubscriptionPaymentMethodAdded, + "subscription_payment_method_removed": SubscriptionHistoryDetailSubscriptionPaymentMethodRemoved, + "subscription_payment_method_updated": SubscriptionHistoryDetailSubscriptionPaymentMethodUpdated, + "subscription_renewed": SubscriptionHistoryDetailSubscriptionRenewed, + "subscription_resumed": SubscriptionHistoryDetailSubscriptionResumed, + "subscription_scheduled_change_added": SubscriptionHistoryDetailSubscriptionScheduledChangeAdded, + "subscription_scheduled_change_removed": SubscriptionHistoryDetailSubscriptionScheduledChangeRemoved, + "subscription_scheduled_change_updated": SubscriptionHistoryDetailSubscriptionScheduledChangeUpdated, +} + class TestSubscriptionsClient: @mark.parametrize( @@ -328,6 +413,20 @@ def test_update_subscription_uses_expected_payload( ReadsFixtures.read_raw_json_fixture("response/list_default"), "/subscriptions?scheduled_change_action=pause,cancel", ), + ( + ListSubscriptions(next_billed_at=DateComparison(DateTime("2023-11-06 14:00:00").as_datetime)), + 200, + ReadsFixtures.read_raw_json_fixture("response/list_default"), + "/subscriptions?next_billed_at=2023-11-06T14:00:00.000000Z", + ), + ( + ListSubscriptions( + next_billed_at=DateComparison(DateTime("2023-11-06 14:00:00").as_datetime, Comparator.GT) + ), + 200, + ReadsFixtures.read_raw_json_fixture("response/list_default"), + "/subscriptions?next_billed_at[GT]=2023-11-06T14:00:00.000000Z", + ), ], ids=[ "List subscriptions without pagination", @@ -343,6 +442,8 @@ def test_update_subscription_uses_expected_payload( "List subscriptions filtered by multiple price_ids", "List subscriptions with scheduled_change_actions", "List subscriptions with multiple scheduled_change_actions", + "List subscriptions filtered by next_billed_at without a comparator", + "List subscriptions filtered by next_billed_at with a comparator", ], ) def test_list_subscriptions_returns_expected_response( @@ -390,8 +491,232 @@ def test_list_subscriptions_supports_null_discount_starts_at( assert response.items[1].discount is None assert response.items[2].discount.starts_at is None assert response.items[2].discount.ends_at is None + assert response.items[2].discount.type == SubscriptionDiscountType.Recurring assert response.items[3].discount.starts_at.isoformat() == "2024-04-12T10:18:47.635628+00:00" assert response.items[3].discount.ends_at.isoformat() == "2024-05-12T10:18:47.635628+00:00" + assert response.items[3].discount.type == SubscriptionDiscountType.Recurring + + @mark.parametrize( + "subscription_id, operation, expected_response_body, expected_url", + [ + ( + "sub_01hv959anj4zrw503h2acawb3p", + None, + ReadsFixtures.read_raw_json_fixture("response/list_history_default"), + "/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + ), + ( + "sub_01hv959anj4zrw503h2acawb3p", + ListSubscriptionHistory(), + ReadsFixtures.read_raw_json_fixture("response/list_history_default"), + "/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + ), + ( + "sub_01hv959anj4zrw503h2acawb3p", + ListSubscriptionHistory(pager=Pager()), + ReadsFixtures.read_raw_json_fixture("response/list_history_default"), + "/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history?order_by=id[asc]&per_page=50", + ), + ( + "sub_01hv959anj4zrw503h2acawb3p", + ListSubscriptionHistory( + actions=[ + SubscriptionHistoryAction.SubscriptionRenewed, + SubscriptionHistoryAction.SubscriptionPaused, + ], + sources=[ActionSource.Api, ActionSource.Dashboard], + actor_types=[ActorType.ApiKey], + actor_ids=["apikey_01h8441jn5pcwrfhwh78jqt8hl"], + reasons=[SubscriptionHistoryReason.CustomerRequest], + occurred_at=DateComparison(DateTime("2026-06-01 00:00:00"), Comparator.GTE), + ), + ReadsFixtures.read_raw_json_fixture("response/list_history_default"), + "/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history" + "?occurred_at[GTE]=2026-06-01T00:00:00.000000Z" + "&action=subscription_renewed,subscription_paused" + "&source=api,dashboard" + "&actor_type=api_key" + "&actor_id=apikey_01h8441jn5pcwrfhwh78jqt8hl" + "&reason=customer_request", + ), + ], + ids=[ + "List subscription history without an operation", + "List subscription history without filters", + "List subscription history with default pagination", + "List subscription history with filters", + ], + ) + def test_list_subscription_history_returns_expected_response( + self, + test_client, + mock_requests, + subscription_id, + operation, + expected_response_body, + expected_url, + ): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.get(expected_url, status_code=200, text=expected_response_body) + + response = test_client.client.subscriptions.list_history(subscription_id, operation) + response_json = test_client.client.subscriptions.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, SubscriptionHistoryCollection) + assert last_request is not None + assert last_request.method == "GET" + assert test_client.client.status_code == 200 + assert ( + unquote(last_request.url) == expected_url + ), "The URL does not match the expected URL, verify the query string is correct" + assert response_json == loads( + str(expected_response_body) + ), "The response JSON generated by ResponseParser() doesn't match the expected fixture JSON" + + def test_list_subscription_history_round_trips_through_payload_encoder( + self, + test_client, + mock_requests, + ): + expected_response_body = ReadsFixtures.read_raw_json_fixture("response/list_history_default") + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=expected_response_body, + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + assert isinstance(response, SubscriptionHistoryCollection) + assert loads(dumps(response.items, cls=PayloadEncoder)) == loads(expected_response_body)["data"] + + def test_list_subscription_history_hydrates_unknown_action_as_undefined_entity( + self, + test_client, + mock_requests, + ): + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_history_default"), + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + unrecognised_entry = response.items[-1] + assert isinstance(unrecognised_entry.detail, UndefinedEntity) + assert unrecognised_entry.detail.to_dict() == { + "action": "subscription_something_new", + "some_field": "some_value", + } + + def test_list_subscription_history_hydrates_known_action_as_typed_detail( + self, + test_client, + mock_requests, + ): + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_history_default"), + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + activated_entry = response.items[1] + assert isinstance(activated_entry.detail, SubscriptionHistoryDetailSubscriptionActivated) + assert activated_entry.detail.action == SubscriptionHistoryAction.SubscriptionActivated + assert activated_entry.detail.transaction_id == "txn_01h89231k3a1q8mn6p4r5s7t9v" + + @mark.parametrize( + "action, expected_detail_class", + list(SUBSCRIPTION_HISTORY_DETAIL_TYPES.items()), + ids=list(SUBSCRIPTION_HISTORY_DETAIL_TYPES.keys()), + ) + def test_list_subscription_history_hydrates_action_as_typed_detail( + self, + test_client, + mock_requests, + action, + expected_detail_class, + ): + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_history_all_actions"), + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + entry = next((item for item in response.items if item.detail.action.value == action), None) + + assert entry is not None, f"Fixture list_history_all_actions is missing an entry for {action}" + assert isinstance(entry.detail, expected_detail_class) + assert entry.detail.action == SubscriptionHistoryAction(action) + + def test_list_subscription_history_covers_every_known_action( + self, + test_client, + mock_requests, + ): + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_history_all_actions"), + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + assert set(SUBSCRIPTION_HISTORY_DETAIL_TYPES) == set( + SubscriptionHistoryAction.members().values() + ), "Every action defined on SubscriptionHistoryAction must be covered by the fixture and this test" + assert {item.detail.action.value for item in response.items} == set(SUBSCRIPTION_HISTORY_DETAIL_TYPES) + + def test_list_subscription_history_all_actions_round_trip_through_payload_encoder( + self, + test_client, + mock_requests, + ): + expected_response_body = ReadsFixtures.read_raw_json_fixture("response/list_history_all_actions") + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=expected_response_body, + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + assert isinstance(response, SubscriptionHistoryCollection) + assert loads(dumps(response.items, cls=PayloadEncoder)) == loads(expected_response_body)["data"] + + def test_list_subscription_history_can_paginate( + self, + test_client, + mock_requests, + ): + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_history_paginated_page_one"), + ) + + mock_requests.get( + f"{test_client.base_url}/subscriptions/sub_01hv959anj4zrw503h2acawb3p/history" + "?after=subhis_02paga0a4m6v7w8x9y0z1a2b3c", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_history_paginated_page_two"), + ) + + response = test_client.client.subscriptions.list_history("sub_01hv959anj4zrw503h2acawb3p") + + assert isinstance(response, SubscriptionHistoryCollection) + + all_entries = [] + for entry in response: + all_entries.append(entry) + + assert len(all_entries) == 4 @mark.parametrize( "subscription_id, includes, expected_response_status, expected_response_body, expected_url", @@ -476,6 +801,32 @@ def test_get_subscription_returns_transaction_line_item_proration( assert isinstance(response, Subscription) + def test_get_subscription_returns_consent_requirements( + self, + test_client, + mock_requests, + ): + expected_url = f"{test_client.base_url}/subscriptions/sub_01h7zcgmdc6tmwtjehp3sh7azf" + mock_requests.get( + expected_url, status_code=200, text=ReadsFixtures.read_raw_json_fixture("response/full_entity") + ) + + response = test_client.client.subscriptions.get("sub_01h7zcgmdc6tmwtjehp3sh7azf") + + assert isinstance(response, Subscription) + assert len(response.consent_requirements) == 1 + + consent_requirement = response.consent_requirements[0] + assert isinstance(consent_requirement, SubscriptionConsentRequirement) + assert consent_requirement.id == "subcr_01j3g2y5g0f5q9k2h1n8x7e6d5" + assert consent_requirement.requirement == SubscriptionConsentRequirementType.TrialEnding + assert consent_requirement.status == SubscriptionConsentRequirementStatus.Pending + assert consent_requirement.created_at.isoformat() == "2026-06-01T00:00:00+00:00" + assert consent_requirement.consent_period.starts_at.isoformat() == "2026-06-01T00:00:00+00:00" + assert consent_requirement.consent_period.ends_at.isoformat() == "2026-06-15T00:00:00+00:00" + assert consent_requirement.granted_at is None + assert consent_requirement.voided_at is None + recurring_transaction_proration = response.recurring_transaction_details.line_items[0].proration assert recurring_transaction_proration is not None assert ( @@ -1378,3 +1729,19 @@ def test_preview_create_subscription_one_time_charge_handles_null_ids( assert response.next_transaction.details.line_items[2].product.id is None assert response.recurring_transaction_details.line_items[2].price_id is None assert response.recurring_transaction_details.line_items[2].product.id is None + + def test_subscription_preview_tolerates_missing_consent_requirements(self): + data = loads(ReadsFixtures.read_raw_json_fixture("response/preview_update_full_entity"))["data"] + data.pop("consent_requirements", None) + + preview = SubscriptionPreview.from_dict(data) + + assert preview.consent_requirements == [] + + def test_subscription_tolerates_missing_consent_requirements(self): + data = loads(ReadsFixtures.read_raw_json_fixture("response/full_entity"))["data"] + data.pop("consent_requirements", None) + + subscription = Subscription.from_dict(data) + + assert subscription.consent_requirements == [] diff --git a/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py b/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py new file mode 100644 index 00000000..418cc8e4 --- /dev/null +++ b/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py @@ -0,0 +1,39 @@ +from json import loads + +from pytest import mark + +from paddle_billing.Notifications.Entities.ApiKey import ApiKey +from paddle_billing.Notifications.Entities.ApiKeys import ApiKeyPermission + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestApiKeyPermission: + @mark.parametrize( + "value, expected_name", + [ + ("checkout_domain.read", "CheckoutDomainRead"), + ("checkout_domain.write", "CheckoutDomainWrite"), + ("client_token.read", "ClientTokenRead"), + ("client_token.write", "ClientTokenWrite"), + ("metrics.read", "MetricsRead"), + ("subscription_history.read", "SubscriptionHistoryRead"), + ], + ) + def test_permission_is_known(self, value, expected_name): + permission = ApiKeyPermission(value) + + assert permission.is_known() + assert permission.name == expected_name + assert permission == getattr(ApiKeyPermission, expected_name) + + def test_api_key_entity_hydrates_new_permissions(self): + entity = ApiKey.from_dict(loads(ReadsFixtures.read_raw_json_fixture("notification/entity/api_key.created"))) + + for permission in [ + ApiKeyPermission.CheckoutDomainRead, + ApiKeyPermission.ClientTokenRead, + ApiKeyPermission.MetricsRead, + ApiKeyPermission.SubscriptionHistoryRead, + ]: + assert permission in entity.permissions diff --git a/tests/Unit/Entities/_fixtures/notification/entity/api_key.created.json b/tests/Unit/Entities/_fixtures/notification/entity/api_key.created.json index c87ee7c9..99dd97c2 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/api_key.created.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/api_key.created.json @@ -7,8 +7,12 @@ "permissions": [ "address.read", "business.read", + "checkout_domain.read", + "client_token.read", "customer.read", + "metrics.read", "subscription.read", + "subscription_history.read", "transaction.read" ], "expires_at": "2025-06-26T06:58:38.517522Z", diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.activated.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.activated.json index 228b8d26..a78b497d 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.activated.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.activated.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T10:18:48.831000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.canceled.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.canceled.json index ec328aa1..58285fa7 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.canceled.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.canceled.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -149,7 +149,7 @@ "type": "standard", "tax_category": "standard", "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your AeroEdit subscription.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/qgyipKJwRtq98YNboipo_vip-support.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/qgyipKJwRtq98YNboipo_vip-support.png", "custom_data": null, "status": "active", "import_meta": null, @@ -175,6 +175,7 @@ "updated_at": "2024-04-12T11:24:54.873000Z", "business_id": null, "canceled_at": "2024-04-12T11:24:54.868000Z", + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.created.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.created.json index a015b4e5..15870bb4 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.created.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.created.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -119,6 +119,7 @@ "status": "active", "discount": { "id": "dsc_01hv6scyf7qdnzcdq01t2y8dx4", + "type": "recurring", "starts_at": "2024-04-12T10:18:47.635628Z", "ends_at": "2024-05-12T10:18:47.635628Z" }, @@ -129,6 +130,7 @@ "updated_at": "2024-04-12T10:18:48.831000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.imported.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.imported.json index 8887f114..23097e71 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.imported.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.imported.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T11:27:08.658000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": { diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.past_due.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.past_due.json index b566fed9..568f5df1 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.past_due.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.past_due.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-05-12T10:19:26.014628Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.paused.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.paused.json index dbfee9d0..fc1a6ad4 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.paused.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.paused.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -119,6 +119,7 @@ "status": "paused", "discount": { "id": "dsc_01hv6scyf7qdnzcdq01t2y8dx4", + "type": "recurring", "starts_at": null, "ends_at": null }, @@ -129,6 +130,7 @@ "updated_at": "2024-04-12T12:43:43.219000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.resumed.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.resumed.json index cac6ba93..6cc6b778 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.resumed.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.resumed.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -125,6 +125,7 @@ "updated_at": "2024-04-12T12:44:51.309000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.trialing.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.trialing.json index 5bc3f7f2..58c04be2 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.trialing.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.trialing.json @@ -62,7 +62,7 @@ "type": "standard", "tax_category": "standard", "description": "Essential tools for student pilots to manage flight logs, analyze performance, and plan routes, and ensure compliance. Valid student pilot certificate from the FAA required.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -105,6 +105,7 @@ "updated_at": "2024-04-12T11:30:29.648000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Entities/_fixtures/notification/entity/subscription.updated.json b/tests/Unit/Entities/_fixtures/notification/entity/subscription.updated.json index 7bf3b826..8860babf 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/subscription.updated.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/subscription.updated.json @@ -35,7 +35,7 @@ "type": "standard", "tax_category": "standard", "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/bT1XUOJAQhOUxGs83cbk_pro.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", "custom_data": { "features": { "aircraft_performance": true, @@ -99,7 +99,7 @@ "type": "standard", "tax_category": "standard", "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/97dRpA6SXzcE6ekK9CAr_analytics.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", "custom_data": null, "status": "active", "import_meta": null, @@ -149,7 +149,7 @@ "type": "standard", "tax_category": "standard", "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your AeroEdit subscription.", - "image_url": "https:\/\/paddle.s3.amazonaws.com\/user\/165798\/qgyipKJwRtq98YNboipo_vip-support.png", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/qgyipKJwRtq98YNboipo_vip-support.png", "custom_data": null, "status": "active", "import_meta": null, @@ -175,6 +175,7 @@ "updated_at": "2024-04-12T10:49:38.771000Z", "business_id": null, "canceled_at": null, + "consent_requirements": [], "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "import_meta": null, diff --git a/tests/Unit/Resources/Subscriptions/Operations/__init__.py b/tests/Unit/Resources/Subscriptions/Operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Unit/Resources/Subscriptions/Operations/test_ListSubscriptionHistory.py b/tests/Unit/Resources/Subscriptions/Operations/test_ListSubscriptionHistory.py new file mode 100644 index 00000000..a6d3efb1 --- /dev/null +++ b/tests/Unit/Resources/Subscriptions/Operations/test_ListSubscriptionHistory.py @@ -0,0 +1,105 @@ +from datetime import datetime + +from pytest import raises + +from paddle_billing.Entities.Shared import ActionSource, ActorType + +from paddle_billing.Entities.Subscriptions.History import SubscriptionHistoryAction, SubscriptionHistoryReason + +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + +from paddle_billing.Resources.Shared.Operations import Comparator, DateComparison, Pager +from paddle_billing.Resources.Subscriptions.Operations import ListSubscriptionHistory + + +class TestListSubscriptionHistory: + def test_returns_no_parameters_by_default(self): + operation = ListSubscriptionHistory() + + assert operation.get_parameters() == {} + + def test_serializes_pager_parameters(self): + operation = ListSubscriptionHistory(pager=Pager()) + + assert operation.get_parameters() == {"order_by": "id[asc]", "per_page": 50} + + def test_serializes_a_single_action(self): + operation = ListSubscriptionHistory(actions=[SubscriptionHistoryAction.SubscriptionRenewed]) + + assert operation.get_parameters() == {"action": "subscription_renewed"} + + def test_serializes_multiple_actions_as_a_comma_separated_list(self): + operation = ListSubscriptionHistory( + actions=[ + SubscriptionHistoryAction.SubscriptionRenewed, + SubscriptionHistoryAction.SubscriptionPaused, + ] + ) + + assert operation.get_parameters() == {"action": "subscription_renewed,subscription_paused"} + + def test_serializes_sources(self): + operation = ListSubscriptionHistory(sources=[ActionSource.Api, ActionSource.Dashboard]) + + assert operation.get_parameters() == {"source": "api,dashboard"} + + def test_serializes_actor_types(self): + operation = ListSubscriptionHistory(actor_types=[ActorType.ApiKey]) + + assert operation.get_parameters() == {"actor_type": "api_key"} + + def test_serializes_actor_ids(self): + operation = ListSubscriptionHistory(actor_ids=["apikey_01h8441jn5pcwrfhwh78jqt8hl", "usr_01h8441jn5"]) + + assert operation.get_parameters() == {"actor_id": "apikey_01h8441jn5pcwrfhwh78jqt8hl,usr_01h8441jn5"} + + def test_serializes_reasons(self): + operation = ListSubscriptionHistory(reasons=[SubscriptionHistoryReason.CustomerRequest]) + + assert operation.get_parameters() == {"reason": "customer_request"} + + def test_serializes_occurred_at_with_a_comparator(self): + operation = ListSubscriptionHistory(occurred_at=DateComparison(datetime(2026, 6, 1, 0, 0, 0), Comparator.GTE)) + + assert operation.get_parameters() == {"occurred_at[GTE]": "2026-06-01T00:00:00.000000Z"} + + def test_serializes_occurred_at_without_a_comparator(self): + operation = ListSubscriptionHistory(occurred_at=DateComparison(datetime(2026, 6, 1, 0, 0, 0))) + + assert operation.get_parameters() == {"occurred_at": "2026-06-01T00:00:00.000000Z"} + + def test_raises_invalid_argument_exception_for_invalid_actions(self): + with raises(InvalidArgumentException) as exception_info: + ListSubscriptionHistory(actions=["subscription_renewed"]) + + assert ( + str(exception_info.value) + == "Expected 'actions' to only contain type 'SubscriptionHistoryAction' ('str' given)" + ) + + def test_raises_invalid_argument_exception_for_invalid_sources(self): + with raises(InvalidArgumentException) as exception_info: + ListSubscriptionHistory(sources=[1]) + + assert str(exception_info.value) == "Expected 'sources' to only contain type 'ActionSource' ('int' given)" + + def test_raises_invalid_argument_exception_for_invalid_actor_types(self): + with raises(InvalidArgumentException) as exception_info: + ListSubscriptionHistory(actor_types=[1]) + + assert str(exception_info.value) == "Expected 'actor_types' to only contain type 'ActorType' ('int' given)" + + def test_raises_invalid_argument_exception_for_invalid_actor_ids(self): + with raises(InvalidArgumentException) as exception_info: + ListSubscriptionHistory(actor_ids=[1, datetime.now()]) + + assert str(exception_info.value) == "Expected 'actor_ids' to only contain type 'str' ('int', 'datetime' given)" + + def test_raises_invalid_argument_exception_for_invalid_reasons(self): + with raises(InvalidArgumentException) as exception_info: + ListSubscriptionHistory(reasons=[1]) + + assert ( + str(exception_info.value) + == "Expected 'reasons' to only contain type 'SubscriptionHistoryReason' ('int' given)" + ) diff --git a/tests/Unit/Resources/Subscriptions/__init__.py b/tests/Unit/Resources/Subscriptions/__init__.py new file mode 100644 index 00000000..e69de29b