From a3913ec4f3a634cff7e1457ae0674836434a3f25 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 13:11:41 +0000 Subject: [PATCH 1/7] feat: Add Payment Method entities and endpoints --- .../Collections/PaymentMethodCollection.py | 13 +++++++ .../Entities/Collections/__init__.py | 1 + paddle_billing/Entities/CustomerAuthToken.py | 18 +++++++++ paddle_billing/Entities/PaymentMethod.py | 38 +++++++++++++++++++ .../Shared/CustomerPaymentMethodOrigin.py | 6 +++ .../Shared/CustomerPaymentMethodType.py | 9 +++++ paddle_billing/Entities/Shared/Paypal.py | 15 ++++++++ paddle_billing/Entities/Shared/__init__.py | 3 ++ .../Notifications/Entities/PaymentMethod.py | 38 +++++++++++++++++++ .../Shared/CustomerPaymentMethodOrigin.py | 6 +++ .../Shared/CustomerPaymentMethodType.py | 9 +++++ .../Notifications/Entities/Shared/Paypal.py | 15 ++++++++ .../Notifications/Entities/Shared/__init__.py | 3 ++ .../Events/PaymentMethodDeleted.py | 17 +++++++++ .../Events/PaymentMethodSaved.py | 17 +++++++++ .../Resources/Customers/CustomersClient.py | 36 +++++++++++++++++- .../Operations/ListPaymentMethods.py | 31 +++++++++++++++ .../Customers/Operations/__init__.py | 1 + 18 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 paddle_billing/Entities/Collections/PaymentMethodCollection.py create mode 100644 paddle_billing/Entities/CustomerAuthToken.py create mode 100644 paddle_billing/Entities/PaymentMethod.py create mode 100644 paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py create mode 100644 paddle_billing/Entities/Shared/CustomerPaymentMethodType.py create mode 100644 paddle_billing/Entities/Shared/Paypal.py create mode 100644 paddle_billing/Notifications/Entities/PaymentMethod.py create mode 100644 paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py create mode 100644 paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py create mode 100644 paddle_billing/Notifications/Entities/Shared/Paypal.py create mode 100644 paddle_billing/Notifications/Events/PaymentMethodDeleted.py create mode 100644 paddle_billing/Notifications/Events/PaymentMethodSaved.py create mode 100644 paddle_billing/Resources/Customers/Operations/ListPaymentMethods.py diff --git a/paddle_billing/Entities/Collections/PaymentMethodCollection.py b/paddle_billing/Entities/Collections/PaymentMethodCollection.py new file mode 100644 index 00000000..1c739d64 --- /dev/null +++ b/paddle_billing/Entities/Collections/PaymentMethodCollection.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from paddle_billing.Entities.Collections.Collection import Collection +from paddle_billing.Entities.Collections.Paginator import Paginator +from paddle_billing.Entities.PaymentMethod import PaymentMethod + + +class PaymentMethodCollection(Collection[PaymentMethod]): + @classmethod + def from_list(cls, items_data: list, paginator: Paginator | None = None) -> PaymentMethodCollection: + items: list[PaymentMethod] = [PaymentMethod.from_dict(item) for item in items_data] + + return PaymentMethodCollection(items, paginator) diff --git a/paddle_billing/Entities/Collections/__init__.py b/paddle_billing/Entities/Collections/__init__.py index 17de49e0..c5773a59 100644 --- a/paddle_billing/Entities/Collections/__init__.py +++ b/paddle_billing/Entities/Collections/__init__.py @@ -11,6 +11,7 @@ from paddle_billing.Entities.Collections.NotificationLogCollection import NotificationLogCollection from paddle_billing.Entities.Collections.NotificationSettingCollection import NotificationSettingCollection from paddle_billing.Entities.Collections.Paginator import Paginator +from paddle_billing.Entities.Collections.PaymentMethodCollection import PaymentMethodCollection from paddle_billing.Entities.Collections.PriceCollection import PriceCollection from paddle_billing.Entities.Collections.ProductCollection import ProductCollection from paddle_billing.Entities.Collections.ReportCollection import ReportCollection diff --git a/paddle_billing/Entities/CustomerAuthToken.py b/paddle_billing/Entities/CustomerAuthToken.py new file mode 100644 index 00000000..91170262 --- /dev/null +++ b/paddle_billing/Entities/CustomerAuthToken.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime + +from paddle_billing.Entities.Entity import Entity + + +@dataclass +class CustomerAuthToken(Entity): + customer_auth_token: str + expires_at: datetime + + @staticmethod + def from_dict(data: dict) -> CustomerAuthToken: + return CustomerAuthToken( + customer_auth_token=data["customer_auth_token"], + expires_at=datetime.fromisoformat(data["expires_at"]), + ) diff --git a/paddle_billing/Entities/PaymentMethod.py b/paddle_billing/Entities/PaymentMethod.py new file mode 100644 index 00000000..7767c450 --- /dev/null +++ b/paddle_billing/Entities/PaymentMethod.py @@ -0,0 +1,38 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Shared import ( + Card, + Paypal, + CustomerPaymentMethodOrigin, + CustomerPaymentMethodType, +) + + +@dataclass +class PaymentMethod(Entity): + id: str + customer_id: str + address_id: str + type: CustomerPaymentMethodType | None + card: Card | None + paypal: Paypal | None + origin: CustomerPaymentMethodOrigin + saved_at: datetime + updated_at: datetime + + @staticmethod + def from_dict(data: dict) -> PaymentMethod: + return PaymentMethod( + id=data["id"], + customer_id=data["customer_id"], + address_id=data["address_id"], + type=CustomerPaymentMethodType(data["type"]), + card=Card.from_dict(data["card"]) if data.get("card") else None, + paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, + origin=CustomerPaymentMethodOrigin(data["origin"]), + saved_at=datetime.fromisoformat(data["saved_at"]), + updated_at=datetime.fromisoformat(data["updated_at"]), + ) diff --git a/paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py b/paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py new file mode 100644 index 00000000..f0161c81 --- /dev/null +++ b/paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class CustomerPaymentMethodOrigin(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + SavedDuringPurchase: "CustomerPaymentMethodOrigin" = "saved_during_purchase" + Subscription: "CustomerPaymentMethodOrigin" = "subscription" diff --git a/paddle_billing/Entities/Shared/CustomerPaymentMethodType.py b/paddle_billing/Entities/Shared/CustomerPaymentMethodType.py new file mode 100644 index 00000000..dfec0b0c --- /dev/null +++ b/paddle_billing/Entities/Shared/CustomerPaymentMethodType.py @@ -0,0 +1,9 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class CustomerPaymentMethodType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Alipay: "CustomerPaymentMethodType" = "alipay" + ApplePay: "CustomerPaymentMethodType" = "apple_pay" + Card: "CustomerPaymentMethodType" = "card" + GooglePay: "CustomerPaymentMethodType" = "google_pay" + Paypal: "CustomerPaymentMethodType" = "paypal" diff --git a/paddle_billing/Entities/Shared/Paypal.py b/paddle_billing/Entities/Shared/Paypal.py new file mode 100644 index 00000000..bc672c86 --- /dev/null +++ b/paddle_billing/Entities/Shared/Paypal.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass +class Paypal: + email: str + reference: str + + @staticmethod + def from_dict(data: dict) -> Paypal: + return Paypal( + email=data["email"], + reference=data["reference"], + ) diff --git a/paddle_billing/Entities/Shared/__init__.py b/paddle_billing/Entities/Shared/__init__.py index 6e2bd6e6..c3e4bb40 100644 --- a/paddle_billing/Entities/Shared/__init__.py +++ b/paddle_billing/Entities/Shared/__init__.py @@ -16,6 +16,8 @@ from paddle_billing.Entities.Shared.CurrencyCodeAdjustments import CurrencyCodeAdjustments from paddle_billing.Entities.Shared.CurrencyCodePayouts import CurrencyCodePayouts from paddle_billing.Entities.Shared.CustomData import CustomData +from paddle_billing.Entities.Shared.CustomerPaymentMethodOrigin import CustomerPaymentMethodOrigin +from paddle_billing.Entities.Shared.CustomerPaymentMethodType import CustomerPaymentMethodType from paddle_billing.Entities.Shared.Data import Data from paddle_billing.Entities.Shared.Disposition import Disposition from paddle_billing.Entities.Shared.Duration import Duration @@ -31,6 +33,7 @@ from paddle_billing.Entities.Shared.PaymentAttemptStatus import PaymentAttemptStatus from paddle_billing.Entities.Shared.PaymentMethodType import PaymentMethodType from paddle_billing.Entities.Shared.PayoutTotalsAdjustment import PayoutTotalsAdjustment +from paddle_billing.Entities.Shared.Paypal import Paypal from paddle_billing.Entities.Shared.PriceQuantity import PriceQuantity from paddle_billing.Entities.Shared.Proration import Proration from paddle_billing.Entities.Shared.Status import Status diff --git a/paddle_billing/Notifications/Entities/PaymentMethod.py b/paddle_billing/Notifications/Entities/PaymentMethod.py new file mode 100644 index 00000000..f05a0f60 --- /dev/null +++ b/paddle_billing/Notifications/Entities/PaymentMethod.py @@ -0,0 +1,38 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime + +from paddle_billing.Notifications.Entities.Entity import Entity +from paddle_billing.Notifications.Entities.Shared import ( + Card, + Paypal, + CustomerPaymentMethodOrigin, + CustomerPaymentMethodType, +) + + +@dataclass +class PaymentMethod(Entity): + id: str + customer_id: str + address_id: str + type: CustomerPaymentMethodType | None + card: Card | None + paypal: Paypal | None + origin: CustomerPaymentMethodOrigin + saved_at: datetime + updated_at: datetime + + @staticmethod + def from_dict(data: dict) -> PaymentMethod: + return PaymentMethod( + id=data["id"], + customer_id=data["customer_id"], + address_id=data["address_id"], + type=CustomerPaymentMethodType(data["type"]), + card=Card.from_dict(data["card"]) if data.get("card") else None, + paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, + origin=CustomerPaymentMethodOrigin(data["origin"]), + saved_at=datetime.fromisoformat(data["saved_at"]), + updated_at=datetime.fromisoformat(data["updated_at"]), + ) diff --git a/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py b/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py new file mode 100644 index 00000000..f0161c81 --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class CustomerPaymentMethodOrigin(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + SavedDuringPurchase: "CustomerPaymentMethodOrigin" = "saved_during_purchase" + Subscription: "CustomerPaymentMethodOrigin" = "subscription" diff --git a/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py b/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py new file mode 100644 index 00000000..dfec0b0c --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py @@ -0,0 +1,9 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class CustomerPaymentMethodType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Alipay: "CustomerPaymentMethodType" = "alipay" + ApplePay: "CustomerPaymentMethodType" = "apple_pay" + Card: "CustomerPaymentMethodType" = "card" + GooglePay: "CustomerPaymentMethodType" = "google_pay" + Paypal: "CustomerPaymentMethodType" = "paypal" diff --git a/paddle_billing/Notifications/Entities/Shared/Paypal.py b/paddle_billing/Notifications/Entities/Shared/Paypal.py new file mode 100644 index 00000000..bc672c86 --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/Paypal.py @@ -0,0 +1,15 @@ +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass +class Paypal: + email: str + reference: str + + @staticmethod + def from_dict(data: dict) -> Paypal: + return Paypal( + email=data["email"], + reference=data["reference"], + ) diff --git a/paddle_billing/Notifications/Entities/Shared/__init__.py b/paddle_billing/Notifications/Entities/Shared/__init__.py index 0de8d283..27ed398d 100644 --- a/paddle_billing/Notifications/Entities/Shared/__init__.py +++ b/paddle_billing/Notifications/Entities/Shared/__init__.py @@ -15,6 +15,8 @@ from paddle_billing.Notifications.Entities.Shared.CurrencyCodeAdjustments import CurrencyCodeAdjustments from paddle_billing.Notifications.Entities.Shared.CurrencyCodePayouts import CurrencyCodePayouts from paddle_billing.Notifications.Entities.Shared.CustomData import CustomData +from paddle_billing.Notifications.Entities.Shared.CustomerPaymentMethodOrigin import CustomerPaymentMethodOrigin +from paddle_billing.Notifications.Entities.Shared.CustomerPaymentMethodType import CustomerPaymentMethodType from paddle_billing.Notifications.Entities.Shared.Data import Data from paddle_billing.Notifications.Entities.Shared.Duration import Duration from paddle_billing.Notifications.Entities.Shared.ErrorCode import ErrorCode @@ -26,6 +28,7 @@ from paddle_billing.Notifications.Entities.Shared.PaymentAttemptStatus import PaymentAttemptStatus from paddle_billing.Notifications.Entities.Shared.PaymentMethodType import PaymentMethodType from paddle_billing.Notifications.Entities.Shared.PayoutTotalsAdjustment import PayoutTotalsAdjustment +from paddle_billing.Notifications.Entities.Shared.Paypal import Paypal from paddle_billing.Notifications.Entities.Shared.PriceQuantity import PriceQuantity from paddle_billing.Notifications.Entities.Shared.Proration import Proration from paddle_billing.Notifications.Entities.Shared.Status import Status diff --git a/paddle_billing/Notifications/Events/PaymentMethodDeleted.py b/paddle_billing/Notifications/Events/PaymentMethodDeleted.py new file mode 100644 index 00000000..03c0d7e4 --- /dev/null +++ b/paddle_billing/Notifications/Events/PaymentMethodDeleted.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from paddle_billing.Entities.Event import Event +from paddle_billing.Entities.Events import EventTypeName + +from paddle_billing.Notifications.Entities.Customer import PaymentMethod + + +class PaymentMethodDeleted(Event): + def __init__( + self, + event_id: str, + event_type: EventTypeName, + occurred_at: datetime, + data: PaymentMethod, + ): + super().__init__(event_id, event_type, occurred_at, data) diff --git a/paddle_billing/Notifications/Events/PaymentMethodSaved.py b/paddle_billing/Notifications/Events/PaymentMethodSaved.py new file mode 100644 index 00000000..892ace40 --- /dev/null +++ b/paddle_billing/Notifications/Events/PaymentMethodSaved.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from paddle_billing.Entities.Event import Event +from paddle_billing.Entities.Events import EventTypeName + +from paddle_billing.Notifications.Entities.PaymentMethod import PaymentMethod + + +class PaymentMethodSaved(Event): + def __init__( + self, + event_id: str, + event_type: EventTypeName, + occurred_at: datetime, + data: PaymentMethod, + ): + super().__init__(event_id, event_type, occurred_at, data) diff --git a/paddle_billing/Resources/Customers/CustomersClient.py b/paddle_billing/Resources/Customers/CustomersClient.py index a3a5d918..a2879c9f 100644 --- a/paddle_billing/Resources/Customers/CustomersClient.py +++ b/paddle_billing/Resources/Customers/CustomersClient.py @@ -1,13 +1,21 @@ from paddle_billing.ResponseParser import ResponseParser -from paddle_billing.Entities.Collections import Paginator, CreditBalanceCollection, CustomerCollection +from paddle_billing.Entities.Collections import ( + Paginator, + CreditBalanceCollection, + CustomerCollection, + PaymentMethodCollection, +) from paddle_billing.Entities.Customer import Customer +from paddle_billing.Entities.CustomerAuthToken import CustomerAuthToken from paddle_billing.Entities.Shared import Status +from paddle_billing.Entities.PaymentMethod import PaymentMethod from paddle_billing.Resources.Customers.Operations import ( CreateCustomer, ListCreditBalances, ListCustomers, + ListPaymentMethods, UpdateCustomer, ) @@ -62,3 +70,29 @@ def credit_balances(self, customer_id: str, operation: ListCreditBalances = None parser = ResponseParser(self.response) return CreditBalanceCollection.from_list(parser.get_data()) + + def create_auth_token(self, customer_id: str) -> CustomerAuthToken: + self.response = self.client.post_raw(f"/customers/{customer_id}/auth-token") + parser = ResponseParser(self.response) + + return CustomerAuthToken.from_dict(parser.get_data()) + + def list_payment_methods(self, customer_id: str, operation: ListPaymentMethods = None) -> PaymentMethodCollection: + if operation is None: + operation = ListPaymentMethods() + + self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods", operation) + parser = ResponseParser(self.response) + + return PaymentMethodCollection.from_list(parser.get_data()) + + def get_payment_method(self, customer_id: str, payment_method_id: str) -> PaymentMethod: + self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") + parser = ResponseParser(self.response) + + return PaymentMethod.from_dict(parser.get_data()) + + def delete_payment_method(self, customer_id: str, payment_method_id: str) -> None: + self.response = self.client.delete_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") + + return None diff --git a/paddle_billing/Resources/Customers/Operations/ListPaymentMethods.py b/paddle_billing/Resources/Customers/Operations/ListPaymentMethods.py new file mode 100644 index 00000000..9e58b6a7 --- /dev/null +++ b/paddle_billing/Resources/Customers/Operations/ListPaymentMethods.py @@ -0,0 +1,31 @@ +from paddle_billing.HasParameters import HasParameters +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException +from paddle_billing.Resources.Shared.Operations import Pager + + +class ListPaymentMethods(HasParameters): + def __init__( + self, + pager: Pager | None = None, + address_ids: list[str] = None, + supports_checkout: bool = None, + ): + self.pager = pager + self.address_ids = address_ids + self.supports_checkout = supports_checkout + + # Validation + invalid_items = [id for id in address_ids if not isinstance(id, str)] + if invalid_items: + raise InvalidArgumentException.array_contains_invalid_types("ids", str.__name__, invalid_items) + + def get_parameters(self) -> dict: + parameters = {} + if self.pager: + parameters.update(self.pager.get_parameters()) + if self.address_ids: + parameters["address_id"] = ",".join(self.address_ids) + if self.supports_checkout is not None: + parameters["supports_checkout"] = "true" if self.supports_checkout else "false" + + return parameters diff --git a/paddle_billing/Resources/Customers/Operations/__init__.py b/paddle_billing/Resources/Customers/Operations/__init__.py index 935970be..1694cfea 100644 --- a/paddle_billing/Resources/Customers/Operations/__init__.py +++ b/paddle_billing/Resources/Customers/Operations/__init__.py @@ -1,4 +1,5 @@ from paddle_billing.Resources.Customers.Operations.CreateCustomer import CreateCustomer from paddle_billing.Resources.Customers.Operations.ListCreditBalances import ListCreditBalances from paddle_billing.Resources.Customers.Operations.ListCustomers import ListCustomers +from paddle_billing.Resources.Customers.Operations.ListPaymentMethods import ListPaymentMethods from paddle_billing.Resources.Customers.Operations.UpdateCustomer import UpdateCustomer From 739e9057f42ef5e504e2b0001f259d24dd2d1640 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 13:40:58 +0000 Subject: [PATCH 2/7] feat: Move payment methods endpoints into separate client --- paddle_billing/Client.py | 2 + .../Resources/Customers/CustomersClient.py | 23 ----------- .../Customers/Operations/__init__.py | 1 - .../Operations/ListPaymentMethods.py | 0 .../PaymentMethods/Operations/__init__.py | 1 + .../PaymentMethods/PaymentMethodsClient.py | 41 +++++++++++++++++++ .../Resources/PaymentMethods/__init__.py | 0 7 files changed, 44 insertions(+), 24 deletions(-) rename paddle_billing/Resources/{Customers => PaymentMethods}/Operations/ListPaymentMethods.py (100%) create mode 100644 paddle_billing/Resources/PaymentMethods/Operations/__init__.py create mode 100644 paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py create mode 100644 paddle_billing/Resources/PaymentMethods/__init__.py diff --git a/paddle_billing/Client.py b/paddle_billing/Client.py index 2f49743f..51b203b2 100644 --- a/paddle_billing/Client.py +++ b/paddle_billing/Client.py @@ -27,6 +27,7 @@ from paddle_billing.Resources.Notifications.NotificationsClient import NotificationsClient from paddle_billing.Resources.NotificationLogs.NotificationLogsClient import NotificationLogsClient from paddle_billing.Resources.NotificationSettings.NotificationSettingsClient import NotificationSettingsClient +from paddle_billing.Resources.PaymentMethods.PaymentMethodsClient import PaymentMethodsClient from paddle_billing.Resources.Prices.PricesClient import PricesClient from paddle_billing.Resources.PricingPreviews.PricingPreviewsClient import PricingPreviewsClient from paddle_billing.Resources.Products.ProductsClient import ProductsClient @@ -90,6 +91,7 @@ def __init__( self.notifications = NotificationsClient(self) self.notification_logs = NotificationLogsClient(self) self.notification_settings = NotificationSettingsClient(self) + self.payment_methods = PaymentMethodsClient(self) self.prices = PricesClient(self) self.pricing_previews = PricingPreviewsClient(self) self.products = ProductsClient(self) diff --git a/paddle_billing/Resources/Customers/CustomersClient.py b/paddle_billing/Resources/Customers/CustomersClient.py index a2879c9f..0b7c4561 100644 --- a/paddle_billing/Resources/Customers/CustomersClient.py +++ b/paddle_billing/Resources/Customers/CustomersClient.py @@ -4,18 +4,15 @@ Paginator, CreditBalanceCollection, CustomerCollection, - PaymentMethodCollection, ) from paddle_billing.Entities.Customer import Customer from paddle_billing.Entities.CustomerAuthToken import CustomerAuthToken from paddle_billing.Entities.Shared import Status -from paddle_billing.Entities.PaymentMethod import PaymentMethod from paddle_billing.Resources.Customers.Operations import ( CreateCustomer, ListCreditBalances, ListCustomers, - ListPaymentMethods, UpdateCustomer, ) @@ -76,23 +73,3 @@ def create_auth_token(self, customer_id: str) -> CustomerAuthToken: parser = ResponseParser(self.response) return CustomerAuthToken.from_dict(parser.get_data()) - - def list_payment_methods(self, customer_id: str, operation: ListPaymentMethods = None) -> PaymentMethodCollection: - if operation is None: - operation = ListPaymentMethods() - - self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods", operation) - parser = ResponseParser(self.response) - - return PaymentMethodCollection.from_list(parser.get_data()) - - def get_payment_method(self, customer_id: str, payment_method_id: str) -> PaymentMethod: - self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") - parser = ResponseParser(self.response) - - return PaymentMethod.from_dict(parser.get_data()) - - def delete_payment_method(self, customer_id: str, payment_method_id: str) -> None: - self.response = self.client.delete_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") - - return None diff --git a/paddle_billing/Resources/Customers/Operations/__init__.py b/paddle_billing/Resources/Customers/Operations/__init__.py index 1694cfea..935970be 100644 --- a/paddle_billing/Resources/Customers/Operations/__init__.py +++ b/paddle_billing/Resources/Customers/Operations/__init__.py @@ -1,5 +1,4 @@ from paddle_billing.Resources.Customers.Operations.CreateCustomer import CreateCustomer from paddle_billing.Resources.Customers.Operations.ListCreditBalances import ListCreditBalances from paddle_billing.Resources.Customers.Operations.ListCustomers import ListCustomers -from paddle_billing.Resources.Customers.Operations.ListPaymentMethods import ListPaymentMethods from paddle_billing.Resources.Customers.Operations.UpdateCustomer import UpdateCustomer diff --git a/paddle_billing/Resources/Customers/Operations/ListPaymentMethods.py b/paddle_billing/Resources/PaymentMethods/Operations/ListPaymentMethods.py similarity index 100% rename from paddle_billing/Resources/Customers/Operations/ListPaymentMethods.py rename to paddle_billing/Resources/PaymentMethods/Operations/ListPaymentMethods.py diff --git a/paddle_billing/Resources/PaymentMethods/Operations/__init__.py b/paddle_billing/Resources/PaymentMethods/Operations/__init__.py new file mode 100644 index 00000000..875bd229 --- /dev/null +++ b/paddle_billing/Resources/PaymentMethods/Operations/__init__.py @@ -0,0 +1 @@ +from paddle_billing.Resources.PaymentMethods.Operations.ListPaymentMethods import ListPaymentMethods diff --git a/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py b/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py new file mode 100644 index 00000000..a732fc18 --- /dev/null +++ b/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py @@ -0,0 +1,41 @@ +from paddle_billing.ResponseParser import ResponseParser + +from paddle_billing.Entities.Collections import ( + PaymentMethodCollection, +) +from paddle_billing.Entities.PaymentMethod import PaymentMethod + +from paddle_billing.Resources.PaymentMethods.Operations import ( + ListPaymentMethods, +) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from paddle_billing.Client import Client + + +class PaymentMethodsClient: + def __init__(self, client: "Client"): + self.client = client + self.response = None + + def list(self, customer_id: str, operation: ListPaymentMethods = None) -> PaymentMethodCollection: + if operation is None: + operation = ListPaymentMethods() + + self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods", operation) + parser = ResponseParser(self.response) + + return PaymentMethodCollection.from_list(parser.get_data()) + + def get(self, customer_id: str, payment_method_id: str) -> PaymentMethod: + self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") + parser = ResponseParser(self.response) + + return PaymentMethod.from_dict(parser.get_data()) + + def delete(self, customer_id: str, payment_method_id: str) -> None: + self.response = self.client.delete_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") + + return None diff --git a/paddle_billing/Resources/PaymentMethods/__init__.py b/paddle_billing/Resources/PaymentMethods/__init__.py new file mode 100644 index 00000000..e69de29b From 091d97fcc62a55dd2895138ea76b307a01196a7d Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 15:04:00 +0000 Subject: [PATCH 3/7] feat: Change saved payment method type prefixes --- paddle_billing/Entities/PaymentMethod.py | 12 ++++++------ .../Entities/Shared/CustomerPaymentMethodOrigin.py | 6 ------ .../Entities/Shared/CustomerPaymentMethodType.py | 9 --------- .../Entities/Shared/SavedPaymentMethodOrigin.py | 6 ++++++ .../Entities/Shared/SavedPaymentMethodType.py | 9 +++++++++ paddle_billing/Entities/Shared/__init__.py | 4 ++-- .../Notifications/Entities/PaymentMethod.py | 12 ++++++------ .../Entities/Shared/CustomerPaymentMethodOrigin.py | 6 ------ .../Entities/Shared/CustomerPaymentMethodType.py | 9 --------- .../Entities/Shared/SavedPaymentMethodOrigin.py | 6 ++++++ .../Entities/Shared/SavedPaymentMethodType.py | 9 +++++++++ .../Notifications/Entities/Shared/__init__.py | 4 ++-- 12 files changed, 46 insertions(+), 46 deletions(-) delete mode 100644 paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py delete mode 100644 paddle_billing/Entities/Shared/CustomerPaymentMethodType.py create mode 100644 paddle_billing/Entities/Shared/SavedPaymentMethodOrigin.py create mode 100644 paddle_billing/Entities/Shared/SavedPaymentMethodType.py delete mode 100644 paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py delete mode 100644 paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py create mode 100644 paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodOrigin.py create mode 100644 paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodType.py diff --git a/paddle_billing/Entities/PaymentMethod.py b/paddle_billing/Entities/PaymentMethod.py index 7767c450..f8552e8e 100644 --- a/paddle_billing/Entities/PaymentMethod.py +++ b/paddle_billing/Entities/PaymentMethod.py @@ -6,8 +6,8 @@ from paddle_billing.Entities.Shared import ( Card, Paypal, - CustomerPaymentMethodOrigin, - CustomerPaymentMethodType, + SavedPaymentMethodOrigin, + SavedPaymentMethodType, ) @@ -16,10 +16,10 @@ class PaymentMethod(Entity): id: str customer_id: str address_id: str - type: CustomerPaymentMethodType | None + type: SavedPaymentMethodType | None card: Card | None paypal: Paypal | None - origin: CustomerPaymentMethodOrigin + origin: SavedPaymentMethodOrigin saved_at: datetime updated_at: datetime @@ -29,10 +29,10 @@ def from_dict(data: dict) -> PaymentMethod: id=data["id"], customer_id=data["customer_id"], address_id=data["address_id"], - type=CustomerPaymentMethodType(data["type"]), + type=SavedPaymentMethodType(data["type"]), card=Card.from_dict(data["card"]) if data.get("card") else None, paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, - origin=CustomerPaymentMethodOrigin(data["origin"]), + origin=SavedPaymentMethodOrigin(data["origin"]), saved_at=datetime.fromisoformat(data["saved_at"]), updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py b/paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py deleted file mode 100644 index f0161c81..00000000 --- a/paddle_billing/Entities/Shared/CustomerPaymentMethodOrigin.py +++ /dev/null @@ -1,6 +0,0 @@ -from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta - - -class CustomerPaymentMethodOrigin(PaddleStrEnum, metaclass=PaddleStrEnumMeta): - SavedDuringPurchase: "CustomerPaymentMethodOrigin" = "saved_during_purchase" - Subscription: "CustomerPaymentMethodOrigin" = "subscription" diff --git a/paddle_billing/Entities/Shared/CustomerPaymentMethodType.py b/paddle_billing/Entities/Shared/CustomerPaymentMethodType.py deleted file mode 100644 index dfec0b0c..00000000 --- a/paddle_billing/Entities/Shared/CustomerPaymentMethodType.py +++ /dev/null @@ -1,9 +0,0 @@ -from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta - - -class CustomerPaymentMethodType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): - Alipay: "CustomerPaymentMethodType" = "alipay" - ApplePay: "CustomerPaymentMethodType" = "apple_pay" - Card: "CustomerPaymentMethodType" = "card" - GooglePay: "CustomerPaymentMethodType" = "google_pay" - Paypal: "CustomerPaymentMethodType" = "paypal" diff --git a/paddle_billing/Entities/Shared/SavedPaymentMethodOrigin.py b/paddle_billing/Entities/Shared/SavedPaymentMethodOrigin.py new file mode 100644 index 00000000..69ed7731 --- /dev/null +++ b/paddle_billing/Entities/Shared/SavedPaymentMethodOrigin.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SavedPaymentMethodOrigin(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + SavedDuringPurchase: "SavedPaymentMethodOrigin" = "saved_during_purchase" + Subscription: "SavedPaymentMethodOrigin" = "subscription" diff --git a/paddle_billing/Entities/Shared/SavedPaymentMethodType.py b/paddle_billing/Entities/Shared/SavedPaymentMethodType.py new file mode 100644 index 00000000..8687f6de --- /dev/null +++ b/paddle_billing/Entities/Shared/SavedPaymentMethodType.py @@ -0,0 +1,9 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SavedPaymentMethodType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Alipay: "SavedPaymentMethodType" = "alipay" + ApplePay: "SavedPaymentMethodType" = "apple_pay" + Card: "SavedPaymentMethodType" = "card" + GooglePay: "SavedPaymentMethodType" = "google_pay" + Paypal: "SavedPaymentMethodType" = "paypal" diff --git a/paddle_billing/Entities/Shared/__init__.py b/paddle_billing/Entities/Shared/__init__.py index c3e4bb40..bc277b28 100644 --- a/paddle_billing/Entities/Shared/__init__.py +++ b/paddle_billing/Entities/Shared/__init__.py @@ -16,8 +16,6 @@ from paddle_billing.Entities.Shared.CurrencyCodeAdjustments import CurrencyCodeAdjustments from paddle_billing.Entities.Shared.CurrencyCodePayouts import CurrencyCodePayouts from paddle_billing.Entities.Shared.CustomData import CustomData -from paddle_billing.Entities.Shared.CustomerPaymentMethodOrigin import CustomerPaymentMethodOrigin -from paddle_billing.Entities.Shared.CustomerPaymentMethodType import CustomerPaymentMethodType from paddle_billing.Entities.Shared.Data import Data from paddle_billing.Entities.Shared.Disposition import Disposition from paddle_billing.Entities.Shared.Duration import Duration @@ -36,6 +34,8 @@ from paddle_billing.Entities.Shared.Paypal import Paypal from paddle_billing.Entities.Shared.PriceQuantity import PriceQuantity from paddle_billing.Entities.Shared.Proration import Proration +from paddle_billing.Entities.Shared.SavedPaymentMethodOrigin import SavedPaymentMethodOrigin +from paddle_billing.Entities.Shared.SavedPaymentMethodType import SavedPaymentMethodType from paddle_billing.Entities.Shared.Status import Status from paddle_billing.Entities.Shared.TransactionStatus import TransactionStatus from paddle_billing.Entities.Shared.TaxCategory import TaxCategory diff --git a/paddle_billing/Notifications/Entities/PaymentMethod.py b/paddle_billing/Notifications/Entities/PaymentMethod.py index f05a0f60..5bf6d117 100644 --- a/paddle_billing/Notifications/Entities/PaymentMethod.py +++ b/paddle_billing/Notifications/Entities/PaymentMethod.py @@ -6,8 +6,8 @@ from paddle_billing.Notifications.Entities.Shared import ( Card, Paypal, - CustomerPaymentMethodOrigin, - CustomerPaymentMethodType, + SavedPaymentMethodOrigin, + SavedPaymentMethodType, ) @@ -16,10 +16,10 @@ class PaymentMethod(Entity): id: str customer_id: str address_id: str - type: CustomerPaymentMethodType | None + type: SavedPaymentMethodType | None card: Card | None paypal: Paypal | None - origin: CustomerPaymentMethodOrigin + origin: SavedPaymentMethodOrigin saved_at: datetime updated_at: datetime @@ -29,10 +29,10 @@ def from_dict(data: dict) -> PaymentMethod: id=data["id"], customer_id=data["customer_id"], address_id=data["address_id"], - type=CustomerPaymentMethodType(data["type"]), + type=SavedPaymentMethodType(data["type"]), card=Card.from_dict(data["card"]) if data.get("card") else None, paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, - origin=CustomerPaymentMethodOrigin(data["origin"]), + origin=SavedPaymentMethodOrigin(data["origin"]), saved_at=datetime.fromisoformat(data["saved_at"]), updated_at=datetime.fromisoformat(data["updated_at"]), ) diff --git a/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py b/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py deleted file mode 100644 index f0161c81..00000000 --- a/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodOrigin.py +++ /dev/null @@ -1,6 +0,0 @@ -from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta - - -class CustomerPaymentMethodOrigin(PaddleStrEnum, metaclass=PaddleStrEnumMeta): - SavedDuringPurchase: "CustomerPaymentMethodOrigin" = "saved_during_purchase" - Subscription: "CustomerPaymentMethodOrigin" = "subscription" diff --git a/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py b/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py deleted file mode 100644 index dfec0b0c..00000000 --- a/paddle_billing/Notifications/Entities/Shared/CustomerPaymentMethodType.py +++ /dev/null @@ -1,9 +0,0 @@ -from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta - - -class CustomerPaymentMethodType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): - Alipay: "CustomerPaymentMethodType" = "alipay" - ApplePay: "CustomerPaymentMethodType" = "apple_pay" - Card: "CustomerPaymentMethodType" = "card" - GooglePay: "CustomerPaymentMethodType" = "google_pay" - Paypal: "CustomerPaymentMethodType" = "paypal" diff --git a/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodOrigin.py b/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodOrigin.py new file mode 100644 index 00000000..69ed7731 --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodOrigin.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SavedPaymentMethodOrigin(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + SavedDuringPurchase: "SavedPaymentMethodOrigin" = "saved_during_purchase" + Subscription: "SavedPaymentMethodOrigin" = "subscription" diff --git a/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodType.py b/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodType.py new file mode 100644 index 00000000..8687f6de --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodType.py @@ -0,0 +1,9 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SavedPaymentMethodType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Alipay: "SavedPaymentMethodType" = "alipay" + ApplePay: "SavedPaymentMethodType" = "apple_pay" + Card: "SavedPaymentMethodType" = "card" + GooglePay: "SavedPaymentMethodType" = "google_pay" + Paypal: "SavedPaymentMethodType" = "paypal" diff --git a/paddle_billing/Notifications/Entities/Shared/__init__.py b/paddle_billing/Notifications/Entities/Shared/__init__.py index 27ed398d..92a7fff6 100644 --- a/paddle_billing/Notifications/Entities/Shared/__init__.py +++ b/paddle_billing/Notifications/Entities/Shared/__init__.py @@ -15,8 +15,6 @@ from paddle_billing.Notifications.Entities.Shared.CurrencyCodeAdjustments import CurrencyCodeAdjustments from paddle_billing.Notifications.Entities.Shared.CurrencyCodePayouts import CurrencyCodePayouts from paddle_billing.Notifications.Entities.Shared.CustomData import CustomData -from paddle_billing.Notifications.Entities.Shared.CustomerPaymentMethodOrigin import CustomerPaymentMethodOrigin -from paddle_billing.Notifications.Entities.Shared.CustomerPaymentMethodType import CustomerPaymentMethodType from paddle_billing.Notifications.Entities.Shared.Data import Data from paddle_billing.Notifications.Entities.Shared.Duration import Duration from paddle_billing.Notifications.Entities.Shared.ErrorCode import ErrorCode @@ -31,6 +29,8 @@ from paddle_billing.Notifications.Entities.Shared.Paypal import Paypal from paddle_billing.Notifications.Entities.Shared.PriceQuantity import PriceQuantity from paddle_billing.Notifications.Entities.Shared.Proration import Proration +from paddle_billing.Notifications.Entities.Shared.SavedPaymentMethodOrigin import SavedPaymentMethodOrigin +from paddle_billing.Notifications.Entities.Shared.SavedPaymentMethodType import SavedPaymentMethodType from paddle_billing.Notifications.Entities.Shared.Status import Status from paddle_billing.Notifications.Entities.Shared.TransactionStatus import TransactionStatus from paddle_billing.Notifications.Entities.Shared.TaxCategory import TaxCategory From 0efb7ecb798d1a8c7039d31bffb90b60cc9abd95 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 21:28:19 +0000 Subject: [PATCH 4/7] feat: Add test coverage for payment method endpoints --- .../Operations/ListPaymentMethods.py | 7 +- .../PaymentMethods/PaymentMethodsClient.py | 6 +- .../_fixtures/response/auth_token.json | 9 + .../Customers/test_CustomersClient.py | 34 +++ .../Resources/PaymentMethods/__init__.py | 0 .../_fixtures/response/full_entity_card.json | 22 ++ .../response/full_entity_paypal.json | 19 ++ .../_fixtures/response/list_default.json | 47 +++ .../response/list_paginated_page_one.json | 47 +++ .../response/list_paginated_page_two.json | 47 +++ .../test_PaymentMethodsClient.py | 287 ++++++++++++++++++ 11 files changed, 521 insertions(+), 4 deletions(-) create mode 100644 tests/Functional/Resources/Customers/_fixtures/response/auth_token.json create mode 100644 tests/Functional/Resources/PaymentMethods/__init__.py create mode 100644 tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json create mode 100644 tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json create mode 100644 tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json create mode 100644 tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json create mode 100644 tests/Functional/Resources/PaymentMethods/test_PaymentMethodsClient.py diff --git a/paddle_billing/Resources/PaymentMethods/Operations/ListPaymentMethods.py b/paddle_billing/Resources/PaymentMethods/Operations/ListPaymentMethods.py index 9e58b6a7..b205052d 100644 --- a/paddle_billing/Resources/PaymentMethods/Operations/ListPaymentMethods.py +++ b/paddle_billing/Resources/PaymentMethods/Operations/ListPaymentMethods.py @@ -15,9 +15,10 @@ def __init__( self.supports_checkout = supports_checkout # Validation - invalid_items = [id for id in address_ids if not isinstance(id, str)] - if invalid_items: - raise InvalidArgumentException.array_contains_invalid_types("ids", str.__name__, invalid_items) + if address_ids is not None: + invalid_items = [id for id in address_ids if not isinstance(id, str)] + if invalid_items: + raise InvalidArgumentException.array_contains_invalid_types("ids", str.__name__, invalid_items) def get_parameters(self) -> dict: parameters = {} diff --git a/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py b/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py index a732fc18..445320b3 100644 --- a/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py +++ b/paddle_billing/Resources/PaymentMethods/PaymentMethodsClient.py @@ -2,6 +2,7 @@ from paddle_billing.Entities.Collections import ( PaymentMethodCollection, + Paginator, ) from paddle_billing.Entities.PaymentMethod import PaymentMethod @@ -27,7 +28,10 @@ def list(self, customer_id: str, operation: ListPaymentMethods = None) -> Paymen self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods", operation) parser = ResponseParser(self.response) - return PaymentMethodCollection.from_list(parser.get_data()) + return PaymentMethodCollection.from_list( + parser.get_data(), + Paginator(self.client, parser.get_pagination(), PaymentMethodCollection), + ) def get(self, customer_id: str, payment_method_id: str) -> PaymentMethod: self.response = self.client.get_raw(f"/customers/{customer_id}/payment-methods/{payment_method_id}") diff --git a/tests/Functional/Resources/Customers/_fixtures/response/auth_token.json b/tests/Functional/Resources/Customers/_fixtures/response/auth_token.json new file mode 100644 index 00000000..18fd0ab6 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/response/auth_token.json @@ -0,0 +1,9 @@ +{ + "data": { + "customer_auth_token": "pca_01hwyzq8hmdwed5p4jc4hnv6bh_01hwwggymjn0yhhb2gr4p91276_6xaav4lydudt6bgmuefeaf2xnu3umegx", + "expires_at": "2024-05-03T10:34:12.345Z" + }, + "meta": { + "request_id": "fa176777-4bca-49ec-aa1e-f53885333cb7" + } + } diff --git a/tests/Functional/Resources/Customers/test_CustomersClient.py b/tests/Functional/Resources/Customers/test_CustomersClient.py index 146f4af7..502d9739 100644 --- a/tests/Functional/Resources/Customers/test_CustomersClient.py +++ b/tests/Functional/Resources/Customers/test_CustomersClient.py @@ -4,6 +4,7 @@ from paddle_billing.Entities.Collections import CreditBalanceCollection, CustomerCollection from paddle_billing.Entities.Customer import Customer +from paddle_billing.Entities.CustomerAuthToken import CustomerAuthToken from paddle_billing.Entities.Shared import CustomData, Status, CurrencyCode from paddle_billing.Resources.Customers.Operations import ( @@ -323,3 +324,36 @@ def test_list_credit_balance_customers_returns_expected_response( assert response_json == loads( str(expected_response_body) ), "The response JSON generated by ResponseParser() doesn't match the expected fixture JSON" + + def test_create_customer_auth_token_returns_expected_response( + self, + test_client, + mock_requests, + ): + customer_id = "ctm_01h8441jn5pcwrfhwh78jqt8hk" + expected_path = "/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/auth-token" + expected_response_body = ReadsFixtures.read_raw_json_fixture("response/auth_token") + + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.post(expected_url, status_code=200, text=expected_response_body) + + response = test_client.client.customers.create_auth_token(customer_id) + response_json = test_client.client.customers.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, CustomerAuthToken) + assert last_request is not None + assert last_request.method == "POST" + 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 doesn't match the expected fixture JSON" + + assert ( + response.customer_auth_token + == "pca_01hwyzq8hmdwed5p4jc4hnv6bh_01hwwggymjn0yhhb2gr4p91276_6xaav4lydudt6bgmuefeaf2xnu3umegx" + ) + assert response.expires_at.isoformat() == "2024-05-03T10:34:12.345000+00:00" diff --git a/tests/Functional/Resources/PaymentMethods/__init__.py b/tests/Functional/Resources/PaymentMethods/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json new file mode 100644 index 00000000..abc94eec --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json @@ -0,0 +1,22 @@ +{ + "data": { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025, + "cardholder_name": "Sam Miller" + }, + "paypal": null, + "origin": "subscription", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-04T11:50:23.422Z" + }, + "meta": { + "request_id": "03dae283-b7e9-47dc-b8c0-229576d90139" + } + } diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json new file mode 100644 index 00000000..261be4fb --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "paypal", + "card": null, + "paypal": { + "email": "sam@example.com", + "reference": "some-reference" + }, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-04T11:50:23.422Z" + }, + "meta": { + "request_id": "03dae283-b7e9-47dc-b8c0-229576d90139" + } + } diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json new file mode 100644 index 00000000..00224be9 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + }, + { + "id": "paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + } + ], + "meta": { + "request_id": "f831dd0b-150d-41c8-a952-5f8f84dbdcee", + "pagination": { + "per_page": 2, + "next": "https://api.paddle.com/customers/ctm_01h282ye3v2d9cmcm8dzpawrd0/payment-methods?after=paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "has_more": false, + "estimated_total": 1 + } + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 00000000..75097959 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + }, + { + "id": "paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + } + ], + "meta": { + "request_id": "f831dd0b-150d-41c8-a952-5f8f84dbdcee", + "pagination": { + "per_page": 2, + "next": "https://sandbox-api.paddle.com/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "has_more": true, + "estimated_total": 4 + } + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 00000000..916b8ef1 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "paymtd_03hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + }, + { + "id": "paymtd_04hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": 2, + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + } + ], + "meta": { + "request_id": "f831dd0b-150d-41c8-a952-5f8f84dbdcee", + "pagination": { + "per_page": 2, + "next": "https://sandbox-api.paddle.com/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_04hs8zx6x377xfsfrt2bqsevbw", + "has_more": false, + "estimated_total": 4 + } + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/PaymentMethods/test_PaymentMethodsClient.py b/tests/Functional/Resources/PaymentMethods/test_PaymentMethodsClient.py new file mode 100644 index 00000000..bfbfed61 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/test_PaymentMethodsClient.py @@ -0,0 +1,287 @@ +from json import loads +from pytest import mark +from urllib.parse import unquote + +from paddle_billing.Entities.Collections import PaymentMethodCollection +from paddle_billing.Entities.PaymentMethod import PaymentMethod + +from paddle_billing.Resources.Shared.Operations import Pager + +from paddle_billing.Resources.PaymentMethods.Operations import ( + ListPaymentMethods, +) + +from paddle_billing.Entities.Shared import Card, Paypal, SavedPaymentMethodOrigin, SavedPaymentMethodType + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestPaymentMethodsClient: + @mark.parametrize( + "customer_id, expected_response_status, expected_response_body, expected_path", + [ + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + 200, + ReadsFixtures.read_raw_json_fixture("response/list_default"), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods", + ) + ], + ids=["List payment-methods"], + ) + def test_list_payment_methods_returns_expected_response( + self, + test_client, + mock_requests, + customer_id, + expected_response_status, + expected_response_body, + expected_path, + ): + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.get(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.payment_methods.list(customer_id) + last_request = mock_requests.last_request + + assert isinstance(response, PaymentMethodCollection) + assert all(isinstance(item, PaymentMethod) for item in response.items), "Not all items are PaymentMethod" + assert last_request is not None + + assert last_request.method == "GET" + assert test_client.client.status_code == expected_response_status + assert ( + unquote(last_request.url) == expected_url + ), "The URL does not match the expected URL, verify the query string is correct" + + @mark.parametrize( + "customer_id, operation, expected_path", + [ + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + None, + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods", + ), + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + ListPaymentMethods( + pager=None, + address_ids=["add_01hv8h6jj90jjz0d71m6hj4r9z", "add_02hv8h6jj90jjz0d71m6hj4r9z"], + ), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?address_id=add_01hv8h6jj90jjz0d71m6hj4r9z,add_02hv8h6jj90jjz0d71m6hj4r9z", + ), + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + ListPaymentMethods( + pager=None, + supports_checkout=False, + ), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?supports_checkout=false", + ), + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + ListPaymentMethods( + pager=None, + supports_checkout=True, + ), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?supports_checkout=true", + ), + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + ListPaymentMethods( + pager=None, + supports_checkout=True, + address_ids=["add_01hv8h6jj90jjz0d71m6hj4r9z", "add_02hv8h6jj90jjz0d71m6hj4r9z"], + ), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?address_id=add_01hv8h6jj90jjz0d71m6hj4r9z,add_02hv8h6jj90jjz0d71m6hj4r9z&supports_checkout=true", + ), + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + ListPaymentMethods( + pager=Pager(), + ), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?order_by=id[asc]&per_page=50", + ), + ( + "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + ListPaymentMethods( + pager=Pager( + after="paymtd_01hs8zx6x377xfsfrt2bqsevbw", + order_by="id[desc]", + per_page=100, + ), + ), + "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_01hs8zx6x377xfsfrt2bqsevbw&order_by=id[desc]&per_page=100", + ), + ], + ids=[ + "List all payment-methods", + "List by address IDs", + "List supports_checkout false", + "List supports_checkout true", + "List by address IDs and supports_checkout", + "List payment-methods with pagination", + "List payment-methods with pagination after", + ], + ) + def test_list_payment_methods_hits_expected_url( + self, + test_client, + mock_requests, + customer_id, + operation, + expected_path, + ): + expected_url = f"{test_client.base_url}{expected_path}" + + mock_requests.get( + expected_url, + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_default"), + ) + + response = test_client.client.payment_methods.list(customer_id, operation) + last_request = mock_requests.last_request + + assert isinstance(response, PaymentMethodCollection) + assert ( + unquote(last_request.url) == expected_url + ), "The URL does not match the expected URL, verify the query string is correct" + + def test_list_payment_methods_can_paginate( + self, + test_client, + mock_requests, + ): + mock_requests.get( + f"{test_client.base_url}/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_paginated_page_one"), + ) + + mock_requests.get( + f"{test_client.base_url}/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_02hs8zx6x377xfsfrt2bqsevbw", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_paginated_page_two"), + ) + + response = test_client.client.payment_methods.list("ctm_01hv6y1jedq4p1n0yqn5ba3ky4") + + assert isinstance(response, PaymentMethodCollection) + + allPaymentMethods = [] + for paymentMethod in response: + allPaymentMethods.append(paymentMethod) + + assert len(allPaymentMethods) == 4 + + def test_get_payment_methods_returns_expected_card_response( + self, + test_client, + mock_requests, + ): + customer_id = "ctm_01hv6y1jedq4p1n0yqn5ba3ky4" + payment_method_id = "paymtd_01hs8zx6x377xfsfrt2bqsevbw" + expected_response_status = 200 + expected_response_body = ReadsFixtures.read_raw_json_fixture("response/full_entity_card") + expected_path = "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods/paymtd_01hs8zx6x377xfsfrt2bqsevbw" + + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.get(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.payment_methods.get(customer_id, payment_method_id) + response_json = test_client.client.payment_methods.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, PaymentMethod) + assert last_request is not None + assert last_request.method == "GET" + assert test_client.client.status_code == expected_response_status + 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" + + assert response.customer_id == "ctm_01hv6y1jedq4p1n0yqn5ba3ky4" + assert response.address_id == "add_01hv8h6jj90jjz0d71m6hj4r9z" + assert response.paypal is None + assert response.type == SavedPaymentMethodType.Card + assert response.origin == SavedPaymentMethodOrigin.Subscription + assert response.saved_at.isoformat() == "2024-05-03T11:50:23.422000+00:00" + assert response.updated_at.isoformat() == "2024-05-04T11:50:23.422000+00:00" + + card = response.card + assert isinstance(card, Card) + assert card.type == "visa" + assert card.last4 == 2 + assert card.expiry_month == 1 + assert card.expiry_year == 2025 + assert card.cardholder_name == "Sam Miller" + + def test_get_payment_methods_returns_expected_paypal_response( + self, + test_client, + mock_requests, + ): + customer_id = "ctm_01hv6y1jedq4p1n0yqn5ba3ky4" + payment_method_id = "paymtd_01hs8zx6x377xfsfrt2bqsevbw" + expected_response_status = 200 + expected_response_body = ReadsFixtures.read_raw_json_fixture("response/full_entity_paypal") + expected_path = "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods/paymtd_01hs8zx6x377xfsfrt2bqsevbw" + + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.get(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.payment_methods.get(customer_id, payment_method_id) + response_json = test_client.client.payment_methods.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, PaymentMethod) + assert last_request is not None + assert last_request.method == "GET" + assert test_client.client.status_code == expected_response_status + 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" + + assert response.customer_id == "ctm_01hv6y1jedq4p1n0yqn5ba3ky4" + assert response.address_id == "add_01hv8h6jj90jjz0d71m6hj4r9z" + assert response.card is None + assert response.type == SavedPaymentMethodType.Paypal + assert response.origin == SavedPaymentMethodOrigin.SavedDuringPurchase + assert response.saved_at.isoformat() == "2024-05-03T11:50:23.422000+00:00" + assert response.updated_at.isoformat() == "2024-05-04T11:50:23.422000+00:00" + + paypal = response.paypal + assert isinstance(paypal, Paypal) + assert paypal.email == "sam@example.com" + assert paypal.reference == "some-reference" + + def test_delete_payment_method_returns_expected_response( + self, + test_client, + mock_requests, + ): + customer_id = "ctm_01hv6y1jedq4p1n0yqn5ba3ky4" + payment_method_id = "paymtd_01hs8zx6x377xfsfrt2bqsevbw" + expected_response_status = 200 + expected_path = "/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods/paymtd_01hs8zx6x377xfsfrt2bqsevbw" + + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.delete(expected_url, status_code=expected_response_status) + + response = test_client.client.payment_methods.delete(customer_id, payment_method_id) + last_request = mock_requests.last_request + + assert response is None + assert last_request is not None + assert last_request.method == "DELETE" + assert test_client.client.status_code == expected_response_status + assert ( + unquote(last_request.url) == expected_url + ), "The URL does not match the expected URL, verify the query string is correct" From c76c0fc0e1b6408fa9afbee1026450ebd78f7bdb Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 22:07:00 +0000 Subject: [PATCH 5/7] feat: Add test coverage for payment method events --- .../Notifications/Entities/Entity.py | 4 +- .../Entities/PaymentMethodDeleted.py | 41 +++++++++++++++++++ .../SavedPaymentMethodDeletionReason.py | 6 +++ .../Notifications/Entities/Shared/__init__.py | 3 ++ .../Notifications/test_NotificationEvent.py | 4 ++ .../entity/payment_method.deleted.json | 10 +++++ .../entity/payment_method.saved.json | 9 ++++ tests/Unit/Entities/test_Event.py | 34 +++++++++++++++ 8 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 paddle_billing/Notifications/Entities/PaymentMethodDeleted.py create mode 100644 paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodDeletionReason.py create mode 100644 tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json create mode 100644 tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json diff --git a/paddle_billing/Notifications/Entities/Entity.py b/paddle_billing/Notifications/Entities/Entity.py index c061bfbc..d89134d4 100644 --- a/paddle_billing/Notifications/Entities/Entity.py +++ b/paddle_billing/Notifications/Entities/Entity.py @@ -44,10 +44,12 @@ def from_dict_for_event_type(data: dict, event_type: str) -> Entity | UndefinedE def _resolve_event_class_name(event_type) -> str: if event_type == "subscription.created": return "SubscriptionCreated" + if event_type == "payment_method.deleted": + return "PaymentMethodDeleted" event_entity = event_type.split(".")[0] or "" - return event_entity.lower().title() + return event_entity.lower().title().replace("_", "") def to_dict(self) -> dict: return asdict(self) diff --git a/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py b/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py new file mode 100644 index 00000000..3f12b11f --- /dev/null +++ b/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py @@ -0,0 +1,41 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime + +from paddle_billing.Notifications.Entities.Entity import Entity +from paddle_billing.Notifications.Entities.Shared import ( + Card, + Paypal, + SavedPaymentMethodDeletionReason, + SavedPaymentMethodOrigin, + SavedPaymentMethodType, +) + + +@dataclass +class PaymentMethodDeleted(Entity): + id: str + customer_id: str + address_id: str + type: SavedPaymentMethodType | None + card: Card | None + paypal: Paypal | None + origin: SavedPaymentMethodOrigin + saved_at: datetime + updated_at: datetime + deletion_reason: SavedPaymentMethodDeletionReason + + @staticmethod + def from_dict(data: dict) -> PaymentMethodDeleted: + return PaymentMethodDeleted( + id=data["id"], + customer_id=data["customer_id"], + address_id=data["address_id"], + type=SavedPaymentMethodType(data["type"]), + card=Card.from_dict(data["card"]) if data.get("card") else None, + paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, + origin=SavedPaymentMethodOrigin(data["origin"]), + saved_at=datetime.fromisoformat(data["saved_at"]), + updated_at=datetime.fromisoformat(data["updated_at"]), + deletion_reason=SavedPaymentMethodDeletionReason(data["deletion_reason"]), + ) diff --git a/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodDeletionReason.py b/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodDeletionReason.py new file mode 100644 index 00000000..3267a82a --- /dev/null +++ b/paddle_billing/Notifications/Entities/Shared/SavedPaymentMethodDeletionReason.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class SavedPaymentMethodDeletionReason(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + ReplacedByNewerVersion: "SavedPaymentMethodDeletionReason" = "replaced_by_newer_version" + Api: "SavedPaymentMethodDeletionReason" = "api" diff --git a/paddle_billing/Notifications/Entities/Shared/__init__.py b/paddle_billing/Notifications/Entities/Shared/__init__.py index 92a7fff6..9c0d11fa 100644 --- a/paddle_billing/Notifications/Entities/Shared/__init__.py +++ b/paddle_billing/Notifications/Entities/Shared/__init__.py @@ -29,6 +29,9 @@ from paddle_billing.Notifications.Entities.Shared.Paypal import Paypal from paddle_billing.Notifications.Entities.Shared.PriceQuantity import PriceQuantity from paddle_billing.Notifications.Entities.Shared.Proration import Proration +from paddle_billing.Notifications.Entities.Shared.SavedPaymentMethodDeletionReason import ( + SavedPaymentMethodDeletionReason, +) from paddle_billing.Notifications.Entities.Shared.SavedPaymentMethodOrigin import SavedPaymentMethodOrigin from paddle_billing.Notifications.Entities.Shared.SavedPaymentMethodType import SavedPaymentMethodType from paddle_billing.Notifications.Entities.Shared.Status import Status diff --git a/tests/Unit/Entities/Notifications/test_NotificationEvent.py b/tests/Unit/Entities/Notifications/test_NotificationEvent.py index b3a26953..c3c1d9ce 100644 --- a/tests/Unit/Entities/Notifications/test_NotificationEvent.py +++ b/tests/Unit/Entities/Notifications/test_NotificationEvent.py @@ -29,6 +29,8 @@ class TestNotificationEvent: ("discount.created", "Discount"), ("discount.imported", "Discount"), ("discount.updated", "Discount"), + ("payment_method.deleted", "PaymentMethodDeleted"), + ("payment_method.saved", "PaymentMethod"), ("payout.created", "Payout"), ("payout.paid", "Payout"), ("price.created", "Price"), @@ -73,6 +75,8 @@ class TestNotificationEvent: "discount.created", "discount.imported", "discount.updated", + "payment_method.deleted", + "payment_method.saved", "payout.created", "payout.paid", "price.created", diff --git a/tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json new file mode 100644 index 00000000..bca973a3 --- /dev/null +++ b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json @@ -0,0 +1,10 @@ +{ + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8gq3318ktkfengj2r75gfx", + "deletion_reason": "replaced_by_newer_version", + "type": "card", + "origin": "saved_during_purchase", + "saved_at": "2024-05-02T02:55:25.198953Z", + "updated_at": "2024-05-03T12:24:18.826338Z" +} diff --git a/tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json new file mode 100644 index 00000000..498939c9 --- /dev/null +++ b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json @@ -0,0 +1,9 @@ +{ + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8gq3318ktkfengj2r75gfx", + "type": "card", + "origin": "saved_during_purchase", + "saved_at": "2024-05-02T02:55:25.198953Z", + "updated_at": "2024-05-02T02:55:25.198953Z" +} diff --git a/tests/Unit/Entities/test_Event.py b/tests/Unit/Entities/test_Event.py index d78a6cd6..5a30e370 100644 --- a/tests/Unit/Entities/test_Event.py +++ b/tests/Unit/Entities/test_Event.py @@ -6,8 +6,15 @@ from paddle_billing.Notifications.Entities.Subscription import Subscription from paddle_billing.Notifications.Entities.SubscriptionCreated import SubscriptionCreated +from paddle_billing.Notifications.Entities.PaymentMethodDeleted import PaymentMethodDeleted from paddle_billing.Notifications.Entities.UndefinedEntity import UndefinedEntity +from paddle_billing.Notifications.Entities.Shared import ( + SavedPaymentMethodDeletionReason, + SavedPaymentMethodType, + SavedPaymentMethodOrigin, +) + from tests.Utils.ReadsFixture import ReadsFixtures @@ -29,6 +36,8 @@ class TestEvent: ("discount.created", "Discount"), ("discount.imported", "Discount"), ("discount.updated", "Discount"), + ("payment_method.deleted", "PaymentMethodDeleted"), + ("payment_method.saved", "PaymentMethod"), ("payout.created", "Payout"), ("payout.paid", "Payout"), ("price.created", "Price"), @@ -73,6 +82,8 @@ class TestEvent: "discount.created", "discount.imported", "discount.updated", + "payment_method.deleted", + "payment_method.saved", "payout.created", "payout.paid", "price.created", @@ -199,3 +210,26 @@ def test_unknown_event_type_is_handled(self): assert event.occurred_at.isoformat() == "2023-08-21T11:57:47.390028+00:00" assert isinstance(event.data, UndefinedEntity) assert event.data.to_dict()["id"] == "add_01hv8gq3318ktkfengj2r75gfx" + + def test_payment_method_deleted(self): + event = Event.from_dict( + { + "data": loads(ReadsFixtures.read_raw_json_fixture("notification/entity/payment_method.deleted")), + "event_type": "payment_method.deleted", + "event_id": "evt_01h8bzakzx3hm2fmen703n5q45", + "occurred_at": "2023-08-21T11:57:47.390028Z", + } + ) + + assert event.event_id == "evt_01h8bzakzx3hm2fmen703n5q45" + assert event.event_type == "payment_method.deleted" + assert event.occurred_at.isoformat() == "2023-08-21T11:57:47.390028+00:00" + assert isinstance(event.data, PaymentMethodDeleted) + assert event.data.id == "paymtd_01hs8zx6x377xfsfrt2bqsevbw" + assert event.data.customer_id == "ctm_01hv6y1jedq4p1n0yqn5ba3ky4" + assert event.data.address_id == "add_01hv8gq3318ktkfengj2r75gfx" + assert event.data.type == SavedPaymentMethodType.Card + assert event.data.origin == SavedPaymentMethodOrigin.SavedDuringPurchase + assert event.data.saved_at.isoformat() == "2024-05-02T02:55:25.198953+00:00" + assert event.data.updated_at.isoformat() == "2024-05-03T12:24:18.826338+00:00" + assert event.data.deletion_reason == SavedPaymentMethodDeletionReason.ReplacedByNewerVersion From 4b63b271db419b26287ed9b3a1d8e69565f1b5fb Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 22:07:36 +0000 Subject: [PATCH 6/7] docs: Update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b9d0bf..1ca6c751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-python-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools. +## [Unreleased] + +### Added + +- Support for saved payment methods, see [related changelog](https://developer.paddle.com/changelog/2024/saved-payment-methods?utm_source=dx&utm_medium=paddle-python-sdk) + - `Client.payment_methods.list` + - `Client.payment_methods.get` + - `Client.payment_methods.delete` + - `Client.customers.create_auth_token` + ## 1.0.0 - 2024-11-11 ### Changed From e123a27607abdd6bd7ada98d71112df77aaca6d8 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 12 Nov 2024 22:22:45 +0000 Subject: [PATCH 7/7] feat: Remove card and paypal from payment method notification entities --- paddle_billing/Notifications/Entities/PaymentMethod.py | 6 ------ .../Notifications/Entities/PaymentMethodDeleted.py | 6 ------ 2 files changed, 12 deletions(-) diff --git a/paddle_billing/Notifications/Entities/PaymentMethod.py b/paddle_billing/Notifications/Entities/PaymentMethod.py index 5bf6d117..4339987a 100644 --- a/paddle_billing/Notifications/Entities/PaymentMethod.py +++ b/paddle_billing/Notifications/Entities/PaymentMethod.py @@ -4,8 +4,6 @@ from paddle_billing.Notifications.Entities.Entity import Entity from paddle_billing.Notifications.Entities.Shared import ( - Card, - Paypal, SavedPaymentMethodOrigin, SavedPaymentMethodType, ) @@ -17,8 +15,6 @@ class PaymentMethod(Entity): customer_id: str address_id: str type: SavedPaymentMethodType | None - card: Card | None - paypal: Paypal | None origin: SavedPaymentMethodOrigin saved_at: datetime updated_at: datetime @@ -30,8 +26,6 @@ def from_dict(data: dict) -> PaymentMethod: customer_id=data["customer_id"], address_id=data["address_id"], type=SavedPaymentMethodType(data["type"]), - card=Card.from_dict(data["card"]) if data.get("card") else None, - paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, origin=SavedPaymentMethodOrigin(data["origin"]), saved_at=datetime.fromisoformat(data["saved_at"]), updated_at=datetime.fromisoformat(data["updated_at"]), diff --git a/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py b/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py index 3f12b11f..978cf522 100644 --- a/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py +++ b/paddle_billing/Notifications/Entities/PaymentMethodDeleted.py @@ -4,8 +4,6 @@ from paddle_billing.Notifications.Entities.Entity import Entity from paddle_billing.Notifications.Entities.Shared import ( - Card, - Paypal, SavedPaymentMethodDeletionReason, SavedPaymentMethodOrigin, SavedPaymentMethodType, @@ -18,8 +16,6 @@ class PaymentMethodDeleted(Entity): customer_id: str address_id: str type: SavedPaymentMethodType | None - card: Card | None - paypal: Paypal | None origin: SavedPaymentMethodOrigin saved_at: datetime updated_at: datetime @@ -32,8 +28,6 @@ def from_dict(data: dict) -> PaymentMethodDeleted: customer_id=data["customer_id"], address_id=data["address_id"], type=SavedPaymentMethodType(data["type"]), - card=Card.from_dict(data["card"]) if data.get("card") else None, - paypal=Paypal.from_dict(data["paypal"]) if data.get("paypal") else None, origin=SavedPaymentMethodOrigin(data["origin"]), saved_at=datetime.fromisoformat(data["saved_at"]), updated_at=datetime.fromisoformat(data["updated_at"]),