diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0d71ba..f111526a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ 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 + +- Added support for verifications + - `Client.verifications.list` + - `Client.verifications.get` +- Added support for countries + - `Client.countries.list` +- Added support for currencies + - `Client.currencies.list` +- Added `verification.read` to `ApiKeyPermission` (permissions on `api_key.*` events) + ## 1.15.0 - 2026-07-28 ### Added diff --git a/paddle_billing/Client.py b/paddle_billing/Client.py index bab21533..fb0f0eec 100644 --- a/paddle_billing/Client.py +++ b/paddle_billing/Client.py @@ -20,6 +20,8 @@ from paddle_billing.Resources.Businesses.BusinessesClient import BusinessesClient from paddle_billing.Resources.CheckoutDomains.CheckoutDomainsClient import CheckoutDomainsClient from paddle_billing.Resources.ClientTokens.ClientTokensClient import ClientTokensClient +from paddle_billing.Resources.Countries.CountriesClient import CountriesClient +from paddle_billing.Resources.Currencies.CurrenciesClient import CurrenciesClient from paddle_billing.Resources.Customers.CustomersClient import CustomersClient from paddle_billing.Resources.CustomerPortalSessions.CustomerPortalSessionsClient import CustomerPortalSessionsClient from paddle_billing.Resources.DiscountGroups.DiscountGroupsClient import DiscountGroupsClient @@ -42,6 +44,7 @@ from paddle_billing.Resources.Subscriptions.SubscriptionsClient import SubscriptionsClient from paddle_billing.Resources.Metrics.MetricsClient import MetricsClient from paddle_billing.Resources.Transactions.TransactionsClient import TransactionsClient +from paddle_billing.Resources.Verifications.VerificationsClient import VerificationsClient class Client: @@ -76,6 +79,8 @@ def __init__( self.businesses = BusinessesClient(self) self.checkout_domains = CheckoutDomainsClient(self) self.client_tokens = ClientTokensClient(self) + self.countries = CountriesClient(self) + self.currencies = CurrenciesClient(self) self.customers = CustomersClient(self) self.customer_portal_sessions = CustomerPortalSessionsClient(self) self.discounts = DiscountsClient(self) @@ -96,6 +101,7 @@ def __init__( self.simulation_types = SimulationTypesClient(self) self.subscriptions = SubscriptionsClient(self) self.transactions = TransactionsClient(self) + self.verifications = VerificationsClient(self) self.ip_addresses = IPAddressesClient(self) self.metrics = MetricsClient(self) diff --git a/paddle_billing/Entities/Collections/CountryDefinitionCollection.py b/paddle_billing/Entities/Collections/CountryDefinitionCollection.py new file mode 100644 index 00000000..4da33139 --- /dev/null +++ b/paddle_billing/Entities/Collections/CountryDefinitionCollection.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.CountryDefinition import CountryDefinition + + +class CountryDefinitionCollection(Collection[CountryDefinition]): + @classmethod + def from_list( + cls, items_data: list[dict[str, Any]], paginator: Paginator | None = None + ) -> CountryDefinitionCollection: + items: list[CountryDefinition] = [CountryDefinition.from_dict(item) for item in items_data] + + return CountryDefinitionCollection(items, paginator) diff --git a/paddle_billing/Entities/Collections/CurrencyDefinitionCollection.py b/paddle_billing/Entities/Collections/CurrencyDefinitionCollection.py new file mode 100644 index 00000000..791e41d0 --- /dev/null +++ b/paddle_billing/Entities/Collections/CurrencyDefinitionCollection.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.CurrencyDefinition import CurrencyDefinition + + +class CurrencyDefinitionCollection(Collection[CurrencyDefinition]): + @classmethod + def from_list( + cls, items_data: list[dict[str, Any]], paginator: Paginator | None = None + ) -> CurrencyDefinitionCollection: + items: list[CurrencyDefinition] = [CurrencyDefinition.from_dict(item) for item in items_data] + + return CurrencyDefinitionCollection(items, paginator) diff --git a/paddle_billing/Entities/Collections/VerificationCollection.py b/paddle_billing/Entities/Collections/VerificationCollection.py new file mode 100644 index 00000000..59536d48 --- /dev/null +++ b/paddle_billing/Entities/Collections/VerificationCollection.py @@ -0,0 +1,14 @@ +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.Verification import Verification + + +class VerificationCollection(Collection[Verification]): + @classmethod + def from_list(cls, items_data: list[dict[str, Any]], paginator: Paginator | None = None) -> VerificationCollection: + items: list[Verification] = [Verification.from_dict(item) for item in items_data] + + return VerificationCollection(items, paginator) diff --git a/paddle_billing/Entities/Collections/__init__.py b/paddle_billing/Entities/Collections/__init__.py index 38759f22..454dafb6 100644 --- a/paddle_billing/Entities/Collections/__init__.py +++ b/paddle_billing/Entities/Collections/__init__.py @@ -4,7 +4,9 @@ from paddle_billing.Entities.Collections.CheckoutDomainCollection import CheckoutDomainCollection from paddle_billing.Entities.Collections.ClientTokenCollection import ClientTokenCollection from paddle_billing.Entities.Collections.Collection import Collection +from paddle_billing.Entities.Collections.CountryDefinitionCollection import CountryDefinitionCollection from paddle_billing.Entities.Collections.CreditBalanceCollection import CreditBalanceCollection +from paddle_billing.Entities.Collections.CurrencyDefinitionCollection import CurrencyDefinitionCollection from paddle_billing.Entities.Collections.CustomerCollection import CustomerCollection from paddle_billing.Entities.Collections.DiscountCollection import DiscountCollection from paddle_billing.Entities.Collections.DiscountGroupCollection import DiscountGroupCollection @@ -28,3 +30,4 @@ from paddle_billing.Entities.Collections.TransactionCollection import TransactionCollection from paddle_billing.Entities.Collections.TransactionPreviewCollection import TransactionPreviewCollection from paddle_billing.Entities.Collections.TransactionsDataCollection import TransactionsDataCollection +from paddle_billing.Entities.Collections.VerificationCollection import VerificationCollection diff --git a/paddle_billing/Entities/Countries/CountryDefinitionTaxMode.py b/paddle_billing/Entities/Countries/CountryDefinitionTaxMode.py new file mode 100644 index 00000000..e0d88496 --- /dev/null +++ b/paddle_billing/Entities/Countries/CountryDefinitionTaxMode.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class CountryDefinitionTaxMode(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Internal: "CountryDefinitionTaxMode" = "internal" + External: "CountryDefinitionTaxMode" = "external" diff --git a/paddle_billing/Entities/Countries/__init__.py b/paddle_billing/Entities/Countries/__init__.py new file mode 100644 index 00000000..a67f3898 --- /dev/null +++ b/paddle_billing/Entities/Countries/__init__.py @@ -0,0 +1 @@ +from paddle_billing.Entities.Countries.CountryDefinitionTaxMode import CountryDefinitionTaxMode diff --git a/paddle_billing/Entities/CountryDefinition.py b/paddle_billing/Entities/CountryDefinition.py new file mode 100644 index 00000000..3410856d --- /dev/null +++ b/paddle_billing/Entities/CountryDefinition.py @@ -0,0 +1,36 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Countries import CountryDefinitionTaxMode +from paddle_billing.Entities.Shared import CountryCode, CountryCodeIso3, CurrencyCode, CurrencyCodePayouts + + +@dataclass +class CountryDefinition(Entity): + iso: CountryCode + iso3: CountryCodeIso3 + name: str + printable_name: str + uses_postal_code: bool + transaction_currency: CurrencyCode + payout_currency: CurrencyCodePayouts + allow_sales: bool + has_regions: bool + tax_mode: CountryDefinitionTaxMode + + @staticmethod + def from_dict(data: dict[str, Any]) -> CountryDefinition: + return CountryDefinition( + iso=CountryCode(data["iso"]), + iso3=CountryCodeIso3(data["iso3"]), + name=data["name"], + printable_name=data["printable_name"], + uses_postal_code=data["uses_postal_code"], + transaction_currency=CurrencyCode(data["transaction_currency"]), + payout_currency=CurrencyCodePayouts(data["payout_currency"]), + allow_sales=data["allow_sales"], + has_regions=data["has_regions"], + tax_mode=CountryDefinitionTaxMode(data["tax_mode"]), + ) diff --git a/paddle_billing/Entities/CurrencyDefinition.py b/paddle_billing/Entities/CurrencyDefinition.py new file mode 100644 index 00000000..d5ff32e2 --- /dev/null +++ b/paddle_billing/Entities/CurrencyDefinition.py @@ -0,0 +1,27 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Any + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.Shared import CountryCode, CurrencyCode + + +@dataclass +class CurrencyDefinition(Entity): + code: CurrencyCode + name: str + decimal_places: int + can_be_balance: bool + applicable_countries: list[CountryCode] + minimum_payment_amount: str + + @staticmethod + def from_dict(data: dict[str, Any]) -> CurrencyDefinition: + return CurrencyDefinition( + code=CurrencyCode(data["code"]), + name=data["name"], + decimal_places=data["decimal_places"], + can_be_balance=data["can_be_balance"], + applicable_countries=[CountryCode(country) for country in data["applicable_countries"]], + minimum_payment_amount=data["minimum_payment_amount"], + ) diff --git a/paddle_billing/Entities/Shared/CountryCodeIso3.py b/paddle_billing/Entities/Shared/CountryCodeIso3.py new file mode 100644 index 00000000..7e657c50 --- /dev/null +++ b/paddle_billing/Entities/Shared/CountryCodeIso3.py @@ -0,0 +1,256 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class CountryCodeIso3(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + ABW: "CountryCodeIso3" = "ABW" + AGO: "CountryCodeIso3" = "AGO" + AIA: "CountryCodeIso3" = "AIA" + ALA: "CountryCodeIso3" = "ALA" + ALB: "CountryCodeIso3" = "ALB" + AND: "CountryCodeIso3" = "AND" + ARE: "CountryCodeIso3" = "ARE" + ARG: "CountryCodeIso3" = "ARG" + ARM: "CountryCodeIso3" = "ARM" + ASM: "CountryCodeIso3" = "ASM" + ATF: "CountryCodeIso3" = "ATF" + ATG: "CountryCodeIso3" = "ATG" + AUS: "CountryCodeIso3" = "AUS" + AUT: "CountryCodeIso3" = "AUT" + AZE: "CountryCodeIso3" = "AZE" + BDI: "CountryCodeIso3" = "BDI" + BEL: "CountryCodeIso3" = "BEL" + BEN: "CountryCodeIso3" = "BEN" + BES: "CountryCodeIso3" = "BES" + BFA: "CountryCodeIso3" = "BFA" + BGD: "CountryCodeIso3" = "BGD" + BGR: "CountryCodeIso3" = "BGR" + BHR: "CountryCodeIso3" = "BHR" + BHS: "CountryCodeIso3" = "BHS" + BIH: "CountryCodeIso3" = "BIH" + BLM: "CountryCodeIso3" = "BLM" + BLZ: "CountryCodeIso3" = "BLZ" + BMU: "CountryCodeIso3" = "BMU" + BOL: "CountryCodeIso3" = "BOL" + BRA: "CountryCodeIso3" = "BRA" + BRB: "CountryCodeIso3" = "BRB" + BRN: "CountryCodeIso3" = "BRN" + BTN: "CountryCodeIso3" = "BTN" + BVT: "CountryCodeIso3" = "BVT" + BWA: "CountryCodeIso3" = "BWA" + CAN: "CountryCodeIso3" = "CAN" + CCK: "CountryCodeIso3" = "CCK" + CHE: "CountryCodeIso3" = "CHE" + CHL: "CountryCodeIso3" = "CHL" + CHN: "CountryCodeIso3" = "CHN" + CIV: "CountryCodeIso3" = "CIV" + CMR: "CountryCodeIso3" = "CMR" + COG: "CountryCodeIso3" = "COG" + COK: "CountryCodeIso3" = "COK" + COL: "CountryCodeIso3" = "COL" + COM: "CountryCodeIso3" = "COM" + CPV: "CountryCodeIso3" = "CPV" + CRI: "CountryCodeIso3" = "CRI" + CUW: "CountryCodeIso3" = "CUW" + CXR: "CountryCodeIso3" = "CXR" + CYM: "CountryCodeIso3" = "CYM" + CYP: "CountryCodeIso3" = "CYP" + CZE: "CountryCodeIso3" = "CZE" + DEU: "CountryCodeIso3" = "DEU" + DJI: "CountryCodeIso3" = "DJI" + DMA: "CountryCodeIso3" = "DMA" + DNK: "CountryCodeIso3" = "DNK" + DOM: "CountryCodeIso3" = "DOM" + DZA: "CountryCodeIso3" = "DZA" + ECU: "CountryCodeIso3" = "ECU" + EGY: "CountryCodeIso3" = "EGY" + ERI: "CountryCodeIso3" = "ERI" + ESH: "CountryCodeIso3" = "ESH" + ESP: "CountryCodeIso3" = "ESP" + EST: "CountryCodeIso3" = "EST" + ETH: "CountryCodeIso3" = "ETH" + FIN: "CountryCodeIso3" = "FIN" + FJI: "CountryCodeIso3" = "FJI" + FLK: "CountryCodeIso3" = "FLK" + FRA: "CountryCodeIso3" = "FRA" + FRO: "CountryCodeIso3" = "FRO" + FSM: "CountryCodeIso3" = "FSM" + GAB: "CountryCodeIso3" = "GAB" + GBR: "CountryCodeIso3" = "GBR" + GEO: "CountryCodeIso3" = "GEO" + GGY: "CountryCodeIso3" = "GGY" + GHA: "CountryCodeIso3" = "GHA" + GIB: "CountryCodeIso3" = "GIB" + GIN: "CountryCodeIso3" = "GIN" + GLP: "CountryCodeIso3" = "GLP" + GMB: "CountryCodeIso3" = "GMB" + GNB: "CountryCodeIso3" = "GNB" + GNQ: "CountryCodeIso3" = "GNQ" + GRC: "CountryCodeIso3" = "GRC" + GRD: "CountryCodeIso3" = "GRD" + GRL: "CountryCodeIso3" = "GRL" + GTM: "CountryCodeIso3" = "GTM" + GUF: "CountryCodeIso3" = "GUF" + GUM: "CountryCodeIso3" = "GUM" + GUY: "CountryCodeIso3" = "GUY" + HKG: "CountryCodeIso3" = "HKG" + HMD: "CountryCodeIso3" = "HMD" + HND: "CountryCodeIso3" = "HND" + HRV: "CountryCodeIso3" = "HRV" + HUN: "CountryCodeIso3" = "HUN" + IDN: "CountryCodeIso3" = "IDN" + IMN: "CountryCodeIso3" = "IMN" + IND: "CountryCodeIso3" = "IND" + IOT: "CountryCodeIso3" = "IOT" + IRL: "CountryCodeIso3" = "IRL" + IRQ: "CountryCodeIso3" = "IRQ" + ISL: "CountryCodeIso3" = "ISL" + ISR: "CountryCodeIso3" = "ISR" + ITA: "CountryCodeIso3" = "ITA" + JAM: "CountryCodeIso3" = "JAM" + JEY: "CountryCodeIso3" = "JEY" + JOR: "CountryCodeIso3" = "JOR" + JPN: "CountryCodeIso3" = "JPN" + KAZ: "CountryCodeIso3" = "KAZ" + KEN: "CountryCodeIso3" = "KEN" + KGZ: "CountryCodeIso3" = "KGZ" + KHM: "CountryCodeIso3" = "KHM" + KIR: "CountryCodeIso3" = "KIR" + KNA: "CountryCodeIso3" = "KNA" + KOR: "CountryCodeIso3" = "KOR" + KWT: "CountryCodeIso3" = "KWT" + LAO: "CountryCodeIso3" = "LAO" + LBN: "CountryCodeIso3" = "LBN" + LBR: "CountryCodeIso3" = "LBR" + LCA: "CountryCodeIso3" = "LCA" + LIE: "CountryCodeIso3" = "LIE" + LKA: "CountryCodeIso3" = "LKA" + LSO: "CountryCodeIso3" = "LSO" + LTU: "CountryCodeIso3" = "LTU" + LUX: "CountryCodeIso3" = "LUX" + LVA: "CountryCodeIso3" = "LVA" + MAC: "CountryCodeIso3" = "MAC" + MAF: "CountryCodeIso3" = "MAF" + MAR: "CountryCodeIso3" = "MAR" + MCO: "CountryCodeIso3" = "MCO" + MDA: "CountryCodeIso3" = "MDA" + MDG: "CountryCodeIso3" = "MDG" + MDV: "CountryCodeIso3" = "MDV" + MEX: "CountryCodeIso3" = "MEX" + MHL: "CountryCodeIso3" = "MHL" + MKD: "CountryCodeIso3" = "MKD" + MLT: "CountryCodeIso3" = "MLT" + MNE: "CountryCodeIso3" = "MNE" + MNG: "CountryCodeIso3" = "MNG" + MNP: "CountryCodeIso3" = "MNP" + MOZ: "CountryCodeIso3" = "MOZ" + MRT: "CountryCodeIso3" = "MRT" + MSR: "CountryCodeIso3" = "MSR" + MTQ: "CountryCodeIso3" = "MTQ" + MUS: "CountryCodeIso3" = "MUS" + MWI: "CountryCodeIso3" = "MWI" + MYS: "CountryCodeIso3" = "MYS" + MYT: "CountryCodeIso3" = "MYT" + NAM: "CountryCodeIso3" = "NAM" + NCL: "CountryCodeIso3" = "NCL" + NER: "CountryCodeIso3" = "NER" + NFK: "CountryCodeIso3" = "NFK" + NGA: "CountryCodeIso3" = "NGA" + NIU: "CountryCodeIso3" = "NIU" + NLD: "CountryCodeIso3" = "NLD" + NOR: "CountryCodeIso3" = "NOR" + NPL: "CountryCodeIso3" = "NPL" + NRU: "CountryCodeIso3" = "NRU" + NZL: "CountryCodeIso3" = "NZL" + OMN: "CountryCodeIso3" = "OMN" + PAK: "CountryCodeIso3" = "PAK" + PAN: "CountryCodeIso3" = "PAN" + PCN: "CountryCodeIso3" = "PCN" + PER: "CountryCodeIso3" = "PER" + PHL: "CountryCodeIso3" = "PHL" + PLW: "CountryCodeIso3" = "PLW" + PNG: "CountryCodeIso3" = "PNG" + POL: "CountryCodeIso3" = "POL" + PRI: "CountryCodeIso3" = "PRI" + PRT: "CountryCodeIso3" = "PRT" + PRY: "CountryCodeIso3" = "PRY" + PSE: "CountryCodeIso3" = "PSE" + PYF: "CountryCodeIso3" = "PYF" + QAT: "CountryCodeIso3" = "QAT" + REU: "CountryCodeIso3" = "REU" + ROM: "CountryCodeIso3" = "ROM" + RWA: "CountryCodeIso3" = "RWA" + SAU: "CountryCodeIso3" = "SAU" + SEN: "CountryCodeIso3" = "SEN" + SGP: "CountryCodeIso3" = "SGP" + SGS: "CountryCodeIso3" = "SGS" + SHN: "CountryCodeIso3" = "SHN" + SJM: "CountryCodeIso3" = "SJM" + SLB: "CountryCodeIso3" = "SLB" + SLE: "CountryCodeIso3" = "SLE" + SLV: "CountryCodeIso3" = "SLV" + SMR: "CountryCodeIso3" = "SMR" + SPM: "CountryCodeIso3" = "SPM" + SRB: "CountryCodeIso3" = "SRB" + STP: "CountryCodeIso3" = "STP" + SUR: "CountryCodeIso3" = "SUR" + SVK: "CountryCodeIso3" = "SVK" + SVN: "CountryCodeIso3" = "SVN" + SWE: "CountryCodeIso3" = "SWE" + SWZ: "CountryCodeIso3" = "SWZ" + SXM: "CountryCodeIso3" = "SXM" + SYC: "CountryCodeIso3" = "SYC" + TCA: "CountryCodeIso3" = "TCA" + TCD: "CountryCodeIso3" = "TCD" + TGO: "CountryCodeIso3" = "TGO" + THA: "CountryCodeIso3" = "THA" + TJK: "CountryCodeIso3" = "TJK" + TKL: "CountryCodeIso3" = "TKL" + TKM: "CountryCodeIso3" = "TKM" + TLS: "CountryCodeIso3" = "TLS" + TON: "CountryCodeIso3" = "TON" + TTO: "CountryCodeIso3" = "TTO" + TUN: "CountryCodeIso3" = "TUN" + TUR: "CountryCodeIso3" = "TUR" + TUV: "CountryCodeIso3" = "TUV" + TWN: "CountryCodeIso3" = "TWN" + TZA: "CountryCodeIso3" = "TZA" + UGA: "CountryCodeIso3" = "UGA" + UKR: "CountryCodeIso3" = "UKR" + UMI: "CountryCodeIso3" = "UMI" + URY: "CountryCodeIso3" = "URY" + USA: "CountryCodeIso3" = "USA" + UZB: "CountryCodeIso3" = "UZB" + VAT: "CountryCodeIso3" = "VAT" + VCT: "CountryCodeIso3" = "VCT" + VGB: "CountryCodeIso3" = "VGB" + VIR: "CountryCodeIso3" = "VIR" + VNM: "CountryCodeIso3" = "VNM" + VUT: "CountryCodeIso3" = "VUT" + WLF: "CountryCodeIso3" = "WLF" + WSM: "CountryCodeIso3" = "WSM" + XXK: "CountryCodeIso3" = "XXK" + ZAF: "CountryCodeIso3" = "ZAF" + ZMB: "CountryCodeIso3" = "ZMB" + AFG: "CountryCodeIso3" = "AFG" + ANT: "CountryCodeIso3" = "ANT" + ATA: "CountryCodeIso3" = "ATA" + BLR: "CountryCodeIso3" = "BLR" + CAF: "CountryCodeIso3" = "CAF" + COD: "CountryCodeIso3" = "COD" + CUB: "CountryCodeIso3" = "CUB" + HTI: "CountryCodeIso3" = "HTI" + IRN: "CountryCodeIso3" = "IRN" + LBY: "CountryCodeIso3" = "LBY" + MLI: "CountryCodeIso3" = "MLI" + MMR: "CountryCodeIso3" = "MMR" + NIC: "CountryCodeIso3" = "NIC" + PRK: "CountryCodeIso3" = "PRK" + RUS: "CountryCodeIso3" = "RUS" + SCG: "CountryCodeIso3" = "SCG" + SDN: "CountryCodeIso3" = "SDN" + SOM: "CountryCodeIso3" = "SOM" + SSD: "CountryCodeIso3" = "SSD" + SYR: "CountryCodeIso3" = "SYR" + VEN: "CountryCodeIso3" = "VEN" + YEM: "CountryCodeIso3" = "YEM" + ZWE: "CountryCodeIso3" = "ZWE" diff --git a/paddle_billing/Entities/Shared/__init__.py b/paddle_billing/Entities/Shared/__init__.py index 98645eba..88699f88 100644 --- a/paddle_billing/Entities/Shared/__init__.py +++ b/paddle_billing/Entities/Shared/__init__.py @@ -17,6 +17,7 @@ from paddle_billing.Entities.Shared.CollectionMode import CollectionMode from paddle_billing.Entities.Shared.Contacts import Contacts from paddle_billing.Entities.Shared.CountryCode import CountryCode +from paddle_billing.Entities.Shared.CountryCodeIso3 import CountryCodeIso3 from paddle_billing.Entities.Shared.CurrencyCode import CurrencyCode from paddle_billing.Entities.Shared.CurrencyCodeAdjustments import CurrencyCodeAdjustments from paddle_billing.Entities.Shared.CurrencyCodePayouts import CurrencyCodePayouts diff --git a/paddle_billing/Entities/Verification.py b/paddle_billing/Entities/Verification.py new file mode 100644 index 00000000..aeaabaf5 --- /dev/null +++ b/paddle_billing/Entities/Verification.py @@ -0,0 +1,29 @@ +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.VerificationStep import VerificationStep +from paddle_billing.Entities.Verifications import VerificationStatus, VerificationType + + +@dataclass +class Verification(Entity): + id: str + status: VerificationStatus + verification_type: VerificationType + steps: list[VerificationStep] + created_at: datetime + updated_at: datetime + + @staticmethod + def from_dict(data: dict[str, Any]) -> Verification: + return Verification( + id=data["id"], + status=VerificationStatus(data["status"]), + verification_type=VerificationType(data["verification_type"]), + steps=[VerificationStep.from_dict(step) for step in data["steps"]], + created_at=datetime.fromisoformat(data["created_at"]), + updated_at=datetime.fromisoformat(data["updated_at"]), + ) diff --git a/paddle_billing/Entities/VerificationStep.py b/paddle_billing/Entities/VerificationStep.py new file mode 100644 index 00000000..d3f457e9 --- /dev/null +++ b/paddle_billing/Entities/VerificationStep.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.Entity import Entity +from paddle_billing.Entities.Verifications import VerificationStepKey, VerificationStepStatus + + +@dataclass +class VerificationStep(Entity): + id: str + key: VerificationStepKey + status: VerificationStepStatus + expires_at: datetime | None + created_at: datetime + updated_at: datetime + + @staticmethod + def from_dict(data: dict[str, Any]) -> VerificationStep: + return VerificationStep( + id=data["id"], + key=VerificationStepKey(data["key"]), + status=VerificationStepStatus(data["status"]), + expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None, + created_at=datetime.fromisoformat(data["created_at"]), + updated_at=datetime.fromisoformat(data["updated_at"]), + ) diff --git a/paddle_billing/Entities/Verifications/VerificationStatus.py b/paddle_billing/Entities/Verifications/VerificationStatus.py new file mode 100644 index 00000000..47a6eab9 --- /dev/null +++ b/paddle_billing/Entities/Verifications/VerificationStatus.py @@ -0,0 +1,11 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class VerificationStatus(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Pending: "VerificationStatus" = "pending" + InProgress: "VerificationStatus" = "in_progress" + InReview: "VerificationStatus" = "in_review" + ActionRequired: "VerificationStatus" = "action_required" + Completed: "VerificationStatus" = "completed" + Failed: "VerificationStatus" = "failed" + Canceled: "VerificationStatus" = "canceled" diff --git a/paddle_billing/Entities/Verifications/VerificationStepKey.py b/paddle_billing/Entities/Verifications/VerificationStepKey.py new file mode 100644 index 00000000..9b1a7232 --- /dev/null +++ b/paddle_billing/Entities/Verifications/VerificationStepKey.py @@ -0,0 +1,10 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class VerificationStepKey(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + AccountSetup: "VerificationStepKey" = "account_setup" + DomainReview: "VerificationStepKey" = "domain_review" + BusinessIdentification: "VerificationStepKey" = "business_identification" + IdentityVerification: "VerificationStepKey" = "identity_verification" + FinalReview: "VerificationStepKey" = "final_review" + AutomatedReview: "VerificationStepKey" = "automated_review" diff --git a/paddle_billing/Entities/Verifications/VerificationStepStatus.py b/paddle_billing/Entities/Verifications/VerificationStepStatus.py new file mode 100644 index 00000000..e3d3a410 --- /dev/null +++ b/paddle_billing/Entities/Verifications/VerificationStepStatus.py @@ -0,0 +1,11 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class VerificationStepStatus(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Pending: "VerificationStepStatus" = "pending" + InProgress: "VerificationStepStatus" = "in_progress" + InReview: "VerificationStepStatus" = "in_review" + ActionRequired: "VerificationStepStatus" = "action_required" + Completed: "VerificationStepStatus" = "completed" + Failed: "VerificationStepStatus" = "failed" + Canceled: "VerificationStepStatus" = "canceled" diff --git a/paddle_billing/Entities/Verifications/VerificationType.py b/paddle_billing/Entities/Verifications/VerificationType.py new file mode 100644 index 00000000..8ac5f534 --- /dev/null +++ b/paddle_billing/Entities/Verifications/VerificationType.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class VerificationType(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Onboarding: "VerificationType" = "onboarding" + Domain: "VerificationType" = "domain" diff --git a/paddle_billing/Entities/Verifications/__init__.py b/paddle_billing/Entities/Verifications/__init__.py new file mode 100644 index 00000000..a0e44325 --- /dev/null +++ b/paddle_billing/Entities/Verifications/__init__.py @@ -0,0 +1,4 @@ +from paddle_billing.Entities.Verifications.VerificationStatus import VerificationStatus +from paddle_billing.Entities.Verifications.VerificationStepKey import VerificationStepKey +from paddle_billing.Entities.Verifications.VerificationStepStatus import VerificationStepStatus +from paddle_billing.Entities.Verifications.VerificationType import VerificationType diff --git a/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py b/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py index fe83a91b..738c08c7 100644 --- a/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py +++ b/paddle_billing/Notifications/Entities/ApiKeys/ApiKeyPermission.py @@ -38,3 +38,4 @@ class ApiKeyPermission(PaddleStrEnum, metaclass=PaddleStrEnumMeta): SubscriptionHistoryRead: "ApiKeyPermission" = "subscription_history.read" TransactionRead: "ApiKeyPermission" = "transaction.read" TransactionWrite: "ApiKeyPermission" = "transaction.write" + VerificationRead: "ApiKeyPermission" = "verification.read" diff --git a/paddle_billing/Resources/Countries/CountriesClient.py b/paddle_billing/Resources/Countries/CountriesClient.py new file mode 100644 index 00000000..984b4273 --- /dev/null +++ b/paddle_billing/Resources/Countries/CountriesClient.py @@ -0,0 +1,21 @@ +from paddle_billing.ResponseParser import ResponseParser +from paddle_billing.Entities.Collections import Paginator, CountryDefinitionCollection + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from paddle_billing.Client import Client + + +class CountriesClient: + def __init__(self, client: "Client"): + self.client = client + self.response = None + + def list(self) -> CountryDefinitionCollection: + self.response = self.client.get_raw("/countries") + parser = ResponseParser(self.response) + + return CountryDefinitionCollection.from_list( + parser.get_list(), Paginator(self.client, parser.get_pagination(), CountryDefinitionCollection) + ) diff --git a/paddle_billing/Resources/Countries/__init__.py b/paddle_billing/Resources/Countries/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/paddle_billing/Resources/Currencies/CurrenciesClient.py b/paddle_billing/Resources/Currencies/CurrenciesClient.py new file mode 100644 index 00000000..f90b7c14 --- /dev/null +++ b/paddle_billing/Resources/Currencies/CurrenciesClient.py @@ -0,0 +1,21 @@ +from paddle_billing.ResponseParser import ResponseParser +from paddle_billing.Entities.Collections import Paginator, CurrencyDefinitionCollection + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from paddle_billing.Client import Client + + +class CurrenciesClient: + def __init__(self, client: "Client"): + self.client = client + self.response = None + + def list(self) -> CurrencyDefinitionCollection: + self.response = self.client.get_raw("/currencies") + parser = ResponseParser(self.response) + + return CurrencyDefinitionCollection.from_list( + parser.get_list(), Paginator(self.client, parser.get_pagination(), CurrencyDefinitionCollection) + ) diff --git a/paddle_billing/Resources/Currencies/__init__.py b/paddle_billing/Resources/Currencies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/paddle_billing/Resources/Verifications/Operations/ListVerifications.py b/paddle_billing/Resources/Verifications/Operations/ListVerifications.py new file mode 100644 index 00000000..556f90a6 --- /dev/null +++ b/paddle_billing/Resources/Verifications/Operations/ListVerifications.py @@ -0,0 +1,40 @@ +from paddle_billing.HasParameters import HasParameters + +from paddle_billing.Entities.Verifications import VerificationType + +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + +from paddle_billing.Resources.Shared.Operations import Pager + + +class ListVerifications(HasParameters): + def __init__( + self, + pager: Pager | None = None, + ids: list[str] | None = None, + verification_type: VerificationType | None = None, + ): + self.pager = pager + self.ids = ids if ids is not None else [] + self.verification_type = verification_type + + # Validation + for field_name, field_value, field_type in [ + ("ids", self.ids, str), + ]: + 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 = {} + if self.pager: + parameters.update(self.pager.get_parameters()) + if self.ids: + parameters["id"] = ",".join(self.ids) + if self.verification_type: + parameters["verification_type"] = self.verification_type.value + + return parameters diff --git a/paddle_billing/Resources/Verifications/Operations/__init__.py b/paddle_billing/Resources/Verifications/Operations/__init__.py new file mode 100644 index 00000000..76dbb69a --- /dev/null +++ b/paddle_billing/Resources/Verifications/Operations/__init__.py @@ -0,0 +1 @@ +from paddle_billing.Resources.Verifications.Operations.ListVerifications import ListVerifications diff --git a/paddle_billing/Resources/Verifications/VerificationsClient.py b/paddle_billing/Resources/Verifications/VerificationsClient.py new file mode 100644 index 00000000..669a44ac --- /dev/null +++ b/paddle_billing/Resources/Verifications/VerificationsClient.py @@ -0,0 +1,34 @@ +from paddle_billing.ResponseParser import ResponseParser + +from paddle_billing.Entities.Collections import VerificationCollection, Paginator +from paddle_billing.Entities.Verification import Verification + +from paddle_billing.Resources.Verifications.Operations import ListVerifications + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from paddle_billing.Client import Client + + +class VerificationsClient: + def __init__(self, client: "Client"): + self.client = client + self.response = None + + def list(self, operation: ListVerifications | None = None) -> VerificationCollection: + if operation is None: + operation = ListVerifications() + + self.response = self.client.get_raw("/verifications", operation.get_parameters()) + parser = ResponseParser(self.response) + + return VerificationCollection.from_list( + parser.get_list(), Paginator(self.client, parser.get_pagination(), VerificationCollection) + ) + + def get(self, verification_id: str) -> Verification: + self.response = self.client.get_raw(f"/verifications/{verification_id}") + parser = ResponseParser(self.response) + + return Verification.from_dict(parser.get_dict()) diff --git a/paddle_billing/Resources/Verifications/__init__.py b/paddle_billing/Resources/Verifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Functional/Resources/Countries/__init__.py b/tests/Functional/Resources/Countries/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Functional/Resources/Countries/_fixtures/response/list_default.json b/tests/Functional/Resources/Countries/_fixtures/response/list_default.json new file mode 100644 index 00000000..8cd6f43d --- /dev/null +++ b/tests/Functional/Resources/Countries/_fixtures/response/list_default.json @@ -0,0 +1,43 @@ +{ + "data": [ + { + "iso": "US", + "iso3": "USA", + "name": "UNITED STATES", + "printable_name": "United States", + "uses_postal_code": true, + "transaction_currency": "USD", + "payout_currency": "USD", + "allow_sales": true, + "has_regions": false, + "tax_mode": "external" + }, + { + "iso": "GB", + "iso3": "GBR", + "name": "UNITED KINGDOM", + "printable_name": "United Kingdom", + "uses_postal_code": true, + "transaction_currency": "GBP", + "payout_currency": "GBP", + "allow_sales": true, + "has_regions": false, + "tax_mode": "internal" + }, + { + "iso": "RU", + "iso3": "RUS", + "name": "RUSSIAN FEDERATION", + "printable_name": "Russian Federation", + "uses_postal_code": true, + "transaction_currency": "USD", + "payout_currency": "USD", + "allow_sales": false, + "has_regions": true, + "tax_mode": "internal" + } + ], + "meta": { + "request_id": "0968f849-046a-4de6-a9c8-34d22aecd514" + } +} diff --git a/tests/Functional/Resources/Countries/test_CountriesClient.py b/tests/Functional/Resources/Countries/test_CountriesClient.py new file mode 100644 index 00000000..1301c7da --- /dev/null +++ b/tests/Functional/Resources/Countries/test_CountriesClient.py @@ -0,0 +1,56 @@ +from json import loads +from urllib.parse import unquote + +from paddle_billing.Entities.Collections import CountryDefinitionCollection +from paddle_billing.Entities.Countries import CountryDefinitionTaxMode +from paddle_billing.Entities.Shared import CountryCode, CountryCodeIso3, CurrencyCode, CurrencyCodePayouts + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestCountriesClient: + def test_list_countries_returns_expected_response(self, test_client, mock_requests): + expected_body = ReadsFixtures.read_raw_json_fixture("response/list_default") + expected_url = f"{test_client.base_url}/countries" + mock_requests.get(expected_url, status_code=200, text=expected_body) + + countries = test_client.client.countries.list() + response_json = test_client.client.countries.response.json() + last_request = mock_requests.last_request + + assert isinstance(countries, CountryDefinitionCollection) + 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 + assert response_json == loads(str(expected_body)) + + countries = list(countries) + assert len(countries) == 3 + + us = countries[0] + assert us.iso == CountryCode.US + assert us.iso3 == CountryCodeIso3.USA + assert us.name == "UNITED STATES" + assert us.printable_name == "United States" + assert us.uses_postal_code is True + assert us.transaction_currency == CurrencyCode.USD + assert us.payout_currency == CurrencyCodePayouts.USD + assert us.allow_sales is True + assert us.has_regions is False + assert us.tax_mode == CountryDefinitionTaxMode.External + + assert countries[1].tax_mode == CountryDefinitionTaxMode.Internal + + def test_list_countries_parses_non_supported_country(self, test_client, mock_requests): + mock_requests.get( + f"{test_client.base_url}/countries", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_default"), + ) + + russia = list(test_client.client.countries.list())[2] + + assert russia.iso3 == CountryCodeIso3.RUS + assert russia.allow_sales is False + assert russia.has_regions is True diff --git a/tests/Functional/Resources/Currencies/__init__.py b/tests/Functional/Resources/Currencies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Functional/Resources/Currencies/_fixtures/response/list_default.json b/tests/Functional/Resources/Currencies/_fixtures/response/list_default.json new file mode 100644 index 00000000..50f96a61 --- /dev/null +++ b/tests/Functional/Resources/Currencies/_fixtures/response/list_default.json @@ -0,0 +1,35 @@ +{ + "data": [ + { + "code": "GBP", + "name": "Pound Sterling", + "decimal_places": 2, + "can_be_balance": true, + "applicable_countries": [ + "FK", + "GB", + "GG", + "GI", + "GS", + "IM", + "IO", + "JE", + "SH" + ], + "minimum_payment_amount": "55" + }, + { + "code": "JPY", + "name": "Japanese Yen", + "decimal_places": 0, + "can_be_balance": false, + "applicable_countries": [ + "JP" + ], + "minimum_payment_amount": "75" + } + ], + "meta": { + "request_id": "1e6d2e6b-3b8b-4f1e-9b0a-8e5c8a0f5c2a" + } +} diff --git a/tests/Functional/Resources/Currencies/test_CurrenciesClient.py b/tests/Functional/Resources/Currencies/test_CurrenciesClient.py new file mode 100644 index 00000000..b12b1bea --- /dev/null +++ b/tests/Functional/Resources/Currencies/test_CurrenciesClient.py @@ -0,0 +1,39 @@ +from json import loads +from urllib.parse import unquote + +from paddle_billing.Entities.Collections import CurrencyDefinitionCollection +from paddle_billing.Entities.Shared import CountryCode, CurrencyCode + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestCurrenciesClient: + def test_list_currencies_returns_expected_response(self, test_client, mock_requests): + expected_body = ReadsFixtures.read_raw_json_fixture("response/list_default") + expected_url = f"{test_client.base_url}/currencies" + mock_requests.get(expected_url, status_code=200, text=expected_body) + + currencies = test_client.client.currencies.list() + response_json = test_client.client.currencies.response.json() + last_request = mock_requests.last_request + + assert isinstance(currencies, CurrencyDefinitionCollection) + 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 + assert response_json == loads(str(expected_body)) + + currencies = list(currencies) + assert len(currencies) == 2 + + gbp = currencies[0] + assert gbp.code == CurrencyCode.GBP + assert gbp.name == "Pound Sterling" + assert gbp.decimal_places == 2 + assert gbp.can_be_balance is True + assert gbp.applicable_countries[:2] == [CountryCode.FK, CountryCode.GB] + assert len(gbp.applicable_countries) == 9 + assert gbp.minimum_payment_amount == "55" + + assert currencies[1].can_be_balance is False diff --git a/tests/Functional/Resources/Verifications/__init__.py b/tests/Functional/Resources/Verifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Functional/Resources/Verifications/_fixtures/response/full_entity.json b/tests/Functional/Resources/Verifications/_fixtures/response/full_entity.json new file mode 100644 index 00000000..7354d17c --- /dev/null +++ b/tests/Functional/Resources/Verifications/_fixtures/response/full_entity.json @@ -0,0 +1,46 @@ +{ + "data": { + "id": "slrvrf_01jjr5d4c14h4y3lqlk9bedkw7", + "status": "action_required", + "verification_type": "onboarding", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z", + "steps": [ + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw8", + "key": "account_setup", + "status": "completed", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw9", + "key": "domain_review", + "status": "completed", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw10", + "key": "business_identification", + "status": "action_required", + "expires_at": "2025-04-10T10:30:00.00Z", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw11", + "key": "identity_verification", + "status": "canceled", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + } + ] + }, + "meta": { + "request_id": "f2c1a7e6-9b34-4d05-8a71-3e5c0d9b64af" + } +} diff --git a/tests/Functional/Resources/Verifications/_fixtures/response/list_default.json b/tests/Functional/Resources/Verifications/_fixtures/response/list_default.json new file mode 100644 index 00000000..7984b8cd --- /dev/null +++ b/tests/Functional/Resources/Verifications/_fixtures/response/list_default.json @@ -0,0 +1,71 @@ +{ + "data": [ + { + "id": "slrvrf_01jjr5d4c14h4y3lqlk9bedkw7", + "status": "action_required", + "verification_type": "onboarding", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z", + "steps": [ + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw8", + "key": "account_setup", + "status": "completed", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw9", + "key": "domain_review", + "status": "completed", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw10", + "key": "business_identification", + "status": "action_required", + "expires_at": "2025-04-10T10:30:00.00Z", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw11", + "key": "identity_verification", + "status": "canceled", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + } + ] + }, + { + "id": "slrvrf_01jjr5d4c14h4y3lqlk9bedkw6", + "status": "canceled", + "verification_type": "domain", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z", + "steps": [ + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw12", + "key": "domain_review", + "status": "canceled", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + } + ] + } + ], + "meta": { + "request_id": "f2c1a7e6-9b34-4d05-8a71-3e5c0d9b64af", + "pagination": { + "per_page": 50, + "next": "https://sandbox-api.paddle.com/verifications?after=slrvrf_01jjr5d4c14h4y3lqlk9bedkw6", + "has_more": false, + "estimated_total": 2 + } + } +} diff --git a/tests/Functional/Resources/Verifications/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/Verifications/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 00000000..5d35d396 --- /dev/null +++ b/tests/Functional/Resources/Verifications/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,54 @@ +{ + "data": [ + { + "id": "slrvrf_01jjr5d4c14h4y3lqlk9bedkw7", + "status": "action_required", + "verification_type": "onboarding", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z", + "steps": [ + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw8", + "key": "account_setup", + "status": "completed", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw9", + "key": "domain_review", + "status": "completed", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw10", + "key": "business_identification", + "status": "action_required", + "expires_at": "2025-04-10T10:30:00.00Z", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + }, + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw11", + "key": "identity_verification", + "status": "canceled", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + } + ] + } + ], + "meta": { + "request_id": "f2c1a7e6-9b34-4d05-8a71-3e5c0d9b64af", + "pagination": { + "per_page": 1, + "next": "https://sandbox-api.paddle.com/verifications?after=slrvrf_01jjr5d4c14h4y3lqlk9bedkw6", + "has_more": true, + "estimated_total": 2 + } + } +} diff --git a/tests/Functional/Resources/Verifications/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/Verifications/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 00000000..f9faaa11 --- /dev/null +++ b/tests/Functional/Resources/Verifications/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,30 @@ +{ + "data": [ + { + "id": "slrvrf_01jjr5d4c14h4y3lqlk9bedkw6", + "status": "canceled", + "verification_type": "domain", + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z", + "steps": [ + { + "id": "slrvrfstp_01jjr5d4c14h4y3lqlk9bedkw12", + "key": "domain_review", + "status": "canceled", + "expires_at": null, + "created_at": "2025-03-10T10:30:00.00Z", + "updated_at": "2025-03-10T10:30:00.00Z" + } + ] + } + ], + "meta": { + "request_id": "f2c1a7e6-9b34-4d05-8a71-3e5c0d9b64af", + "pagination": { + "per_page": 1, + "next": "https://sandbox-api.paddle.com/verifications", + "has_more": false, + "estimated_total": 2 + } + } +} diff --git a/tests/Functional/Resources/Verifications/test_VerificationsClient.py b/tests/Functional/Resources/Verifications/test_VerificationsClient.py new file mode 100644 index 00000000..fca8b8a7 --- /dev/null +++ b/tests/Functional/Resources/Verifications/test_VerificationsClient.py @@ -0,0 +1,113 @@ +from json import loads +from pytest import mark +from urllib.parse import unquote + +from paddle_billing.Entities.Collections import VerificationCollection +from paddle_billing.Entities.Verification import Verification +from paddle_billing.Entities.Verifications import ( + VerificationStatus, + VerificationStepKey, + VerificationStepStatus, + VerificationType, +) + +from paddle_billing.Resources.Shared.Operations import Pager +from paddle_billing.Resources.Verifications.Operations import ListVerifications + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestVerificationsClient: + @mark.parametrize( + "operation, expected_url", + [ + (ListVerifications(), "/verifications"), + (ListVerifications(Pager()), "/verifications?order_by=id[asc]&per_page=50"), + ( + ListVerifications(Pager(after="slrvrf_01jjr5d4c14h4y3lqlk9bedkw7")), + "/verifications?after=slrvrf_01jjr5d4c14h4y3lqlk9bedkw7&order_by=id[asc]&per_page=50", + ), + ( + ListVerifications(ids=["slrvrf_01jjr5d4c14h4y3lqlk9bedkw7", "slrvrf_01jjr5d4c14h4y3lqlk9bedkw6"]), + "/verifications?id=slrvrf_01jjr5d4c14h4y3lqlk9bedkw7,slrvrf_01jjr5d4c14h4y3lqlk9bedkw6", + ), + ( + ListVerifications(verification_type=VerificationType.Onboarding), + "/verifications?verification_type=onboarding", + ), + ], + ids=[ + "List verifications without pagination", + "List verifications with default pagination", + "List paginated verifications after specified id", + "List verifications filtered by ids", + "List verifications filtered by type", + ], + ) + def test_list_verifications_returns_expected_response(self, test_client, mock_requests, operation, expected_url): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.get( + expected_url, status_code=200, text=ReadsFixtures.read_raw_json_fixture("response/list_default") + ) + + response = test_client.client.verifications.list(operation) + last_request = mock_requests.last_request + + assert isinstance(response, VerificationCollection) + 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" + + verifications = list(response) + assert len(verifications) == 2 + assert verifications[1].status == VerificationStatus.Canceled + assert verifications[1].verification_type == VerificationType.Domain + assert verifications[1].steps[0].status == VerificationStepStatus.Canceled + + def test_list_verifications_can_paginate(self, test_client, mock_requests): + mock_requests.get( + f"{test_client.base_url}/verifications", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_paginated_page_one"), + ) + mock_requests.get( + f"{test_client.base_url}/verifications?after=slrvrf_01jjr5d4c14h4y3lqlk9bedkw6", + status_code=200, + text=ReadsFixtures.read_raw_json_fixture("response/list_paginated_page_two"), + ) + + response = test_client.client.verifications.list() + + assert isinstance(response, VerificationCollection) + assert len(list(response)) == 2 + + def test_get_verification_returns_expected_response(self, test_client, mock_requests): + expected_body = ReadsFixtures.read_raw_json_fixture("response/full_entity") + expected_url = f"{test_client.base_url}/verifications/slrvrf_01jjr5d4c14h4y3lqlk9bedkw7" + mock_requests.get(expected_url, status_code=200, text=expected_body) + + response = test_client.client.verifications.get("slrvrf_01jjr5d4c14h4y3lqlk9bedkw7") + response_json = test_client.client.verifications.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, Verification) + 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 + assert response_json == loads(str(expected_body)) + + assert response.id == "slrvrf_01jjr5d4c14h4y3lqlk9bedkw7" + assert response.status == VerificationStatus.ActionRequired + assert response.verification_type == VerificationType.Onboarding + assert len(response.steps) == 4 + assert response.steps[0].key == VerificationStepKey.AccountSetup + assert response.steps[0].expires_at is None + assert response.steps[2].key == VerificationStepKey.BusinessIdentification + assert response.steps[2].status == VerificationStepStatus.ActionRequired + assert response.steps[2].expires_at is not None + assert response.steps[2].expires_at.year == 2025 + assert response.steps[3].status == VerificationStepStatus.Canceled diff --git a/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py b/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py index 418cc8e4..4ca92d63 100644 --- a/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py +++ b/tests/Unit/Entities/Notifications/test_ApiKeyPermission.py @@ -18,6 +18,7 @@ class TestApiKeyPermission: ("client_token.write", "ClientTokenWrite"), ("metrics.read", "MetricsRead"), ("subscription_history.read", "SubscriptionHistoryRead"), + ("verification.read", "VerificationRead"), ], ) def test_permission_is_known(self, value, expected_name): diff --git a/tests/Unit/Resources/Verifications/Operations/__init__.py b/tests/Unit/Resources/Verifications/Operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/Unit/Resources/Verifications/Operations/test_ListVerifications.py b/tests/Unit/Resources/Verifications/Operations/test_ListVerifications.py new file mode 100644 index 00000000..a94f76ca --- /dev/null +++ b/tests/Unit/Resources/Verifications/Operations/test_ListVerifications.py @@ -0,0 +1,27 @@ +from pytest import raises + +from paddle_billing.Entities.Verifications import VerificationType + +from paddle_billing.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException + +from paddle_billing.Resources.Shared.Operations import Pager +from paddle_billing.Resources.Verifications.Operations import ListVerifications + + +class TestListVerifications: + def test_returns_no_parameters_by_default(self): + assert ListVerifications().get_parameters() == {} + + def test_returns_pager_parameters(self): + assert ListVerifications(Pager()).get_parameters() == {"order_by": "id[asc]", "per_page": 50} + + def test_returns_id_and_type_parameters(self): + operation = ListVerifications(ids=["slrvrf_1", "slrvrf_2"], verification_type=VerificationType.Domain) + + assert operation.get_parameters() == {"id": "slrvrf_1,slrvrf_2", "verification_type": "domain"} + + def test_raises_invalid_argument_exception_for_invalid_ids(self): + with raises(InvalidArgumentException) as exception_info: + ListVerifications(ids=[1]) + + assert str(exception_info.value) == "Expected 'ids' to only contain type 'str' ('int' given)" diff --git a/tests/Unit/Resources/Verifications/__init__.py b/tests/Unit/Resources/Verifications/__init__.py new file mode 100644 index 00000000..e69de29b