diff --git a/CHANGELOG.md b/CHANGELOG.md index f0808f71..39c85b69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-python-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools. +## [Unreleased] + +### Added + +- Added `transactions.revise` operation to revise a transaction and added `revised_at` to `Transaction` entity, see [related changelog](https://developer.paddle.com/changelog/2024/revise-transaction-customer-information?utm_source=dx&utm_medium=paddle-python-sdk). +- Added support for `transaction.revised` notification, see [related changelog](https://developer.paddle.com/changelog/2024/revise-transaction-customer-information?utm_source=dx&utm_medium=paddle-python-sdk). + ## 1.4.0 - 2024-12-19 ### Added diff --git a/paddle_billing/Entities/Events/EventTypeName.py b/paddle_billing/Entities/Events/EventTypeName.py index c400913b..f3ba380f 100644 --- a/paddle_billing/Entities/Events/EventTypeName.py +++ b/paddle_billing/Entities/Events/EventTypeName.py @@ -47,6 +47,7 @@ class EventTypeName(PaddleStrEnum, metaclass=PaddleStrEnumMeta): TransactionPastDue: "EventTypeName" = "transaction.past_due" TransactionPaymentFailed: "EventTypeName" = "transaction.payment_failed" TransactionReady: "EventTypeName" = "transaction.ready" + TransactionRevised: "EventTypeName" = "transaction.revised" TransactionUpdated: "EventTypeName" = "transaction.updated" ReportCreated: "EventTypeName" = "report.created" ReportUpdated: "EventTypeName" = "report.updated" diff --git a/paddle_billing/Entities/Transaction.py b/paddle_billing/Entities/Transaction.py index ad77117b..7e924630 100644 --- a/paddle_billing/Entities/Transaction.py +++ b/paddle_billing/Entities/Transaction.py @@ -58,6 +58,7 @@ class Transaction(Entity): customer: Customer | None = None discount: Discount | None = None available_payment_methods: list[PaymentMethodType] | None = None + revised_at: datetime | None = None @staticmethod def from_dict(data: dict) -> Transaction: @@ -95,4 +96,5 @@ def from_dict(data: dict) -> Transaction: if data.get("adjustment_totals") else None ), + revised_at=datetime.fromisoformat(data["revised_at"]) if data.get("revised_at") else None, ) diff --git a/paddle_billing/Notifications/Entities/Transaction.py b/paddle_billing/Notifications/Entities/Transaction.py index bb6472b6..5b14738c 100644 --- a/paddle_billing/Notifications/Entities/Transaction.py +++ b/paddle_billing/Notifications/Entities/Transaction.py @@ -43,6 +43,7 @@ class Transaction(Entity): created_at: datetime updated_at: datetime billed_at: datetime | None + revised_at: datetime | None = None @staticmethod def from_dict(data: dict) -> Transaction: @@ -69,4 +70,5 @@ def from_dict(data: dict) -> Transaction: billing_period=TimePeriod.from_dict(data["billing_period"]) if data.get("billing_period") else None, custom_data=CustomData(data["custom_data"]) if data.get("custom_data") else None, checkout=Checkout.from_dict(data["checkout"]) if data.get("checkout") else None, + revised_at=datetime.fromisoformat(data["revised_at"]) if data.get("revised_at") else None, ) diff --git a/paddle_billing/Notifications/Events/TransactionRevised.py b/paddle_billing/Notifications/Events/TransactionRevised.py new file mode 100644 index 00000000..ccc9aed8 --- /dev/null +++ b/paddle_billing/Notifications/Events/TransactionRevised.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.Transaction import Transaction + + +class TransactionRevised(Event): + def __init__( + self, + event_id: str, + event_type: EventTypeName, + occurred_at: datetime, + data: Transaction, + ): + super().__init__(event_id, event_type, occurred_at, data) diff --git a/paddle_billing/Resources/Transactions/Operations/Revise/ReviseAddress.py b/paddle_billing/Resources/Transactions/Operations/Revise/ReviseAddress.py new file mode 100644 index 00000000..17389715 --- /dev/null +++ b/paddle_billing/Resources/Transactions/Operations/Revise/ReviseAddress.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from paddle_billing.Operation import Operation +from paddle_billing.Undefined import Undefined + + +@dataclass +class ReviseAddress(Operation): + first_line: str | Undefined = Undefined() + second_line: str | None | Undefined = Undefined() + city: str | Undefined = Undefined() + region: str | Undefined = Undefined() diff --git a/paddle_billing/Resources/Transactions/Operations/Revise/ReviseBusiness.py b/paddle_billing/Resources/Transactions/Operations/Revise/ReviseBusiness.py new file mode 100644 index 00000000..c89002a5 --- /dev/null +++ b/paddle_billing/Resources/Transactions/Operations/Revise/ReviseBusiness.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from paddle_billing.Operation import Operation +from paddle_billing.Undefined import Undefined + + +@dataclass +class ReviseBusiness(Operation): + name: str | Undefined = Undefined() + tax_identifier: str | Undefined = Undefined() diff --git a/paddle_billing/Resources/Transactions/Operations/Revise/ReviseCustomer.py b/paddle_billing/Resources/Transactions/Operations/Revise/ReviseCustomer.py new file mode 100644 index 00000000..61c769c2 --- /dev/null +++ b/paddle_billing/Resources/Transactions/Operations/Revise/ReviseCustomer.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +from paddle_billing.Operation import Operation +from paddle_billing.Undefined import Undefined + + +@dataclass +class ReviseCustomer(Operation): + name: str | Undefined = Undefined() diff --git a/paddle_billing/Resources/Transactions/Operations/Revise/__init__.py b/paddle_billing/Resources/Transactions/Operations/Revise/__init__.py new file mode 100644 index 00000000..e59e6088 --- /dev/null +++ b/paddle_billing/Resources/Transactions/Operations/Revise/__init__.py @@ -0,0 +1,3 @@ +from paddle_billing.Resources.Transactions.Operations.Revise.ReviseAddress import ReviseAddress +from paddle_billing.Resources.Transactions.Operations.Revise.ReviseBusiness import ReviseBusiness +from paddle_billing.Resources.Transactions.Operations.Revise.ReviseCustomer import ReviseCustomer diff --git a/paddle_billing/Resources/Transactions/Operations/ReviseTransaction.py b/paddle_billing/Resources/Transactions/Operations/ReviseTransaction.py new file mode 100644 index 00000000..7023b52e --- /dev/null +++ b/paddle_billing/Resources/Transactions/Operations/ReviseTransaction.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + +from paddle_billing.Operation import Operation +from paddle_billing.Undefined import Undefined +from paddle_billing.Resources.Transactions.Operations.Revise import ( + ReviseAddress, + ReviseBusiness, + ReviseCustomer, +) + + +@dataclass +class ReviseTransaction(Operation): + address: ReviseAddress | Undefined = Undefined() + business: ReviseBusiness | Undefined = Undefined() + customer: ReviseCustomer | Undefined = Undefined() diff --git a/paddle_billing/Resources/Transactions/Operations/__init__.py b/paddle_billing/Resources/Transactions/Operations/__init__.py index c73d9668..f4b79a26 100644 --- a/paddle_billing/Resources/Transactions/Operations/__init__.py +++ b/paddle_billing/Resources/Transactions/Operations/__init__.py @@ -7,3 +7,4 @@ from paddle_billing.Resources.Transactions.Operations.PreviewTransactionByIP import PreviewTransactionByIP from paddle_billing.Resources.Transactions.Operations.UpdateTransaction import UpdateTransaction from paddle_billing.Resources.Transactions.Operations.GetTransactionInvoice import GetTransactionInvoice +from paddle_billing.Resources.Transactions.Operations.ReviseTransaction import ReviseTransaction diff --git a/paddle_billing/Resources/Transactions/TransactionsClient.py b/paddle_billing/Resources/Transactions/TransactionsClient.py index a93b4147..62da05a2 100644 --- a/paddle_billing/Resources/Transactions/TransactionsClient.py +++ b/paddle_billing/Resources/Transactions/TransactionsClient.py @@ -16,6 +16,7 @@ PreviewTransactionByIP, TransactionIncludes, GetTransactionInvoice, + ReviseTransaction, ) from typing import TYPE_CHECKING @@ -91,3 +92,9 @@ def get_invoice_pdf(self, transaction_id: str, operation: GetTransactionInvoice parser = ResponseParser(self.response) return TransactionData.from_dict(parser.get_data()) + + def revise(self, transaction_id: str, operation: ReviseTransaction) -> Transaction: + self.response = self.client.post_raw(f"/transactions/{transaction_id}/revise", operation) + parser = ResponseParser(self.response) + + return Transaction.from_dict(parser.get_data()) diff --git a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json index c6daf429..9e7b9858 100644 --- a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json @@ -407,14 +407,14 @@ "next_billed_at": "2023-08-28T13:15:46.864158Z", "previously_billed_at": null, "product": { - "id": "pro_01h84cd36f900f3wmpdfamgv8w", - "name": "ChatApp Pro", - "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", - "tax_category": "standard", - "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", - "status": "active", - "created_at": "2023-08-16T14:38:08.3Z", - "updated_at": "2023-08-16T14:38:08.3Z" + "id": "pro_01h84cd36f900f3wmpdfamgv8w", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "created_at": "2023-08-16T14:38:08.3Z", + "updated_at": "2023-08-16T14:38:08.3Z" } } ], @@ -620,6 +620,343 @@ "retry_at": null, "times_attempted": 1, "notification_setting_id": "ntfset_01h7zcdzf04a7wvyja9k9p1n3p" + }, + { + "id": "ntf_01hv8x2azy7scaan4s0eb0273x", + "type": "transaction.revised", + "status": "delivered", + "payload": { + "data": { + "id": "txn_01hv8wptq8987qeep44cyrewp9", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [], + "import_meta": null + }, + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:00.907834Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [], + "import_meta": null + }, + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [], + "import_meta": null + }, + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "completed", + "details": { + "totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hv8wt98jahpbm1t1tzr06z6n", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard", + "import_meta": null + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hv8wt98jahpbm1t1v1sd067y", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard", + "import_meta": null + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01hv8wt98jahpbm1t1v67vqnb6", + "totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard", + "import_meta": null + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "tax_rates_used": [ + { + "totals": { + "tax": "5315", + "total": "65215", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://aeroedit.com/pay?_ptxn=txn_01hv8wptq8987qeep44cyrewp9" + }, + "payments": [ + { + "amount": "65215", + "status": "captured", + "created_at": "2024-04-12T10:18:33.579142Z", + "error_code": null, + "captured_at": "2024-04-12T10:18:47.635628Z", + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2025, + "expiry_month": 1, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01hv8x1tpjfnttxddw73xnqx6s", + "payment_attempt_id": "937640dd-e3dc-40df-a16c-bb75aafd8f71", + "stored_payment_method_id": "281ff2ca-8550-42b9-bf39-15948e7de62d" + }, + { + "amount": "65215", + "status": "error", + "created_at": "2024-04-12T10:15:57.888183Z", + "error_code": "declined", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "0002", + "expiry_year": 2025, + "expiry_month": 1, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01hv8wx2mka7dfsqjjsxh1ne7z", + "payment_attempt_id": "8f72cfa6-26b4-4a57-91dc-8f2708f7822d", + "stored_payment_method_id": "a78ece50-356f-4e0c-b72d-ad5368b0a0d9" + } + ], + "billed_at": "2024-04-12T10:18:48.294633Z", + "address_id": "add_01hv8gq3318ktkfengj2r75gfx", + "created_at": "2024-04-12T10:12:33.2014Z", + "invoice_id": "inv_01hv8x29nsh54c2pgt0hnq0zkx", + "updated_at": "2024-04-12T10:18:49.738971238Z", + "revised_at": "2024-04-12T10:18:49.738972238Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "discount_id": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-05-12T10:18:47.635628Z", + "starts_at": "2024-04-12T10:18:47.635628Z" + }, + "invoice_number": "325-10566", + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01hv8x29kz0t586xy6zn1a62ny" + }, + "event_id": "evt_01hv8x2axb33yr5y238zfwcn5p", + "event_type": "transaction.revised", + "occurred_at": "2024-04-12T10:18:50.155553Z", + "notification_id": "ntf_01hv8x2azy7scaan4s0eb0273x" + }, + "occurred_at": "2023-08-18T10:46:18.792661Z", + "delivered_at": "2023-08-18T10:46:19.396422Z", + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-18T10:46:18.887423Z", + "retry_at": null, + "times_attempted": 1, + "notification_setting_id": "ntfset_01h7zcdzf04a7wvyja9k9p1n3p" } ], "meta": { diff --git a/tests/Functional/Resources/Notifications/test_NotificationsClient.py b/tests/Functional/Resources/Notifications/test_NotificationsClient.py index 4146a5fe..a044ccb3 100644 --- a/tests/Functional/Resources/Notifications/test_NotificationsClient.py +++ b/tests/Functional/Resources/Notifications/test_NotificationsClient.py @@ -12,6 +12,7 @@ from paddle_billing.Notifications.Entities.Adjustment import Adjustment from paddle_billing.Notifications.Entities.Adjustments.AdjustmentTaxRatesUsed import AdjustmentTaxRatesUsed from paddle_billing.Notifications.Entities.Business import Business +from paddle_billing.Notifications.Entities.Transaction import Transaction from paddle_billing.Resources.Notifications.Operations import ListNotifications from paddle_billing.Resources.Shared.Operations import Pager @@ -142,7 +143,7 @@ def test_list_notifications_returns_expected_response( unquote(last_request.url) == expected_url ), "The URL does not match the expected URL, verify the query string is correct" - assert len(response.items) == 7 + assert len(response.items) == 8 for notification in response.items: assert isinstance(notification, Notification) assert isinstance(notification.payload, NotificationEvent) @@ -196,6 +197,26 @@ def test_list_adjustment_notification_with_and_without_tax_rates_used( assert isinstance(adjustmentWithoutTaxRatesUsed, Adjustment) assert adjustmentWithoutTaxRatesUsed.tax_rates_used is None + def test_list_transaction_notification_with_revised_at( + self, + test_client, + mock_requests, + ): + expected_url = f"{test_client.base_url}/notifications" + mock_requests.get( + expected_url, status_code=200, text=ReadsFixtures.read_raw_json_fixture("response/list_default") + ) + + response = test_client.client.notifications.list(ListNotifications()) + + transactionRevised = response.items[7].payload + assert isinstance(transactionRevised, NotificationEvent) + + transaction = transactionRevised.data + assert isinstance(transaction, Transaction) + + assert transaction.revised_at.isoformat() == "2024-04-12T10:18:49.738972+00:00" + @mark.parametrize( "notification_id, expected_response_status, expected_response_body, expected_url", [ diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/revise_basic.json b/tests/Functional/Resources/Transactions/_fixtures/request/revise_basic.json new file mode 100644 index 00000000..85046c28 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/revise_basic.json @@ -0,0 +1,11 @@ +{ + "customer": { + "name": "Sam Miller" + }, + "business": { + "name": "Some Business" + }, + "address": { + "first_line": "3811 Ditmars Blvd" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/revise_customer.json b/tests/Functional/Resources/Transactions/_fixtures/request/revise_customer.json new file mode 100644 index 00000000..72fa7106 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/revise_customer.json @@ -0,0 +1,5 @@ +{ + "customer": { + "name": "Sam Miller" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/revise_full.json b/tests/Functional/Resources/Transactions/_fixtures/request/revise_full.json new file mode 100644 index 00000000..880e39e8 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/revise_full.json @@ -0,0 +1,15 @@ +{ + "customer": { + "name": "Sam Miller" + }, + "business": { + "name": "Some Business", + "tax_identifier": "AB0123456789" + }, + "address": { + "first_line": "3811 Ditmars Blvd", + "second_line": null, + "city": "Some City", + "region": "Some Region" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json index 16d7f547..98f32745 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json @@ -29,6 +29,7 @@ "created_at": "2023-11-07T15:45:39.297512Z", "updated_at": "2023-11-07T15:45:45.086499Z", "billed_at": "2023-11-07T15:45:39.201442Z", + "revised_at": null, "items": [ { "price": { diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json index 3ef5eec1..6a7923fa 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json @@ -29,6 +29,7 @@ "created_at": "2023-11-07T15:45:39.297512Z", "updated_at": "2023-11-07T15:45:45.086499Z", "billed_at": "2023-11-07T15:45:39.201442Z", + "revised_at": null, "items": [ { "price": { diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json b/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json index c4fa453d..6509fb02 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json @@ -30,6 +30,7 @@ "created_at": "2023-08-21T08:40:00.766226Z", "updated_at": "2023-08-21T08:46:37.414122Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -264,6 +265,7 @@ "created_at": "2023-08-21T07:49:16.634436Z", "updated_at": "2023-08-21T07:49:16.634436Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -518,6 +520,7 @@ "created_at": "2023-08-21T07:48:01.560677Z", "updated_at": "2023-08-21T08:54:11.273239Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -775,6 +778,7 @@ "created_at": "2023-08-18T21:13:07.033262Z", "updated_at": "2023-08-18T21:13:14.83528Z", "billed_at": "2023-08-18T21:13:06.616004Z", + "revised_at": null, "items": [ { "price": { @@ -1023,6 +1027,7 @@ "created_at": "2023-08-16T14:46:05.86701Z", "updated_at": "2023-08-19T14:47:05.97537Z", "billed_at": "2023-08-16T14:46:05.489912Z", + "revised_at": null, "items": [ { "price": { @@ -1153,6 +1158,7 @@ "created_at": "2023-07-26T15:35:06.134251Z", "updated_at": "2023-07-26T15:35:11.182344Z", "billed_at": "2023-07-26T15:35:05.739403Z", + "revised_at": "2023-07-26T15:35:05.739403Z", "items": [ { "price": { diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json index 2731fd6c..50813561 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json @@ -30,6 +30,7 @@ "created_at": "2023-08-21T08:40:00.766226Z", "updated_at": "2023-08-21T08:46:37.414122Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -262,6 +263,7 @@ "created_at": "2023-08-21T07:49:16.634436Z", "updated_at": "2023-08-21T07:49:16.634436Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -510,6 +512,7 @@ "created_at": "2023-08-21T07:48:01.560677Z", "updated_at": "2023-08-21T08:54:11.273239Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -761,6 +764,7 @@ "created_at": "2023-08-18T21:13:07.033262Z", "updated_at": "2023-08-18T21:13:14.83528Z", "billed_at": "2023-08-18T21:13:06.616004Z", + "revised_at": null, "items": [ { "price": { @@ -989,6 +993,7 @@ "created_at": "2023-08-16T14:46:05.86701Z", "updated_at": "2023-08-19T14:47:05.97537Z", "billed_at": "2023-08-16T14:46:05.489912Z", + "revised_at": null, "items": [ { "price": { @@ -1119,6 +1124,7 @@ "created_at": "2023-07-26T15:35:06.134251Z", "updated_at": "2023-07-26T15:35:11.182344Z", "billed_at": "2023-07-26T15:35:05.739403Z", + "revised_at": null, "items": [ { "price": { diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json index 7d664119..fa84db19 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json @@ -30,6 +30,7 @@ "created_at": "2023-08-21T08:40:00.766226Z", "updated_at": "2023-08-21T08:46:37.414122Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -262,6 +263,7 @@ "created_at": "2023-08-21T07:49:16.634436Z", "updated_at": "2023-08-21T07:49:16.634436Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -510,6 +512,7 @@ "created_at": "2023-08-21T07:48:01.560677Z", "updated_at": "2023-08-21T08:54:11.273239Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { @@ -761,6 +764,7 @@ "created_at": "2023-08-18T21:13:07.033262Z", "updated_at": "2023-08-18T21:13:14.83528Z", "billed_at": "2023-08-18T21:13:06.616004Z", + "revised_at": null, "items": [ { "price": { @@ -989,6 +993,7 @@ "created_at": "2023-08-16T14:46:05.86701Z", "updated_at": "2023-08-19T14:47:05.97537Z", "billed_at": "2023-08-16T14:46:05.489912Z", + "revised_at": null, "items": [ { "price": { @@ -1119,6 +1124,7 @@ "created_at": "2023-07-26T15:35:06.134251Z", "updated_at": "2023-07-26T15:35:11.182344Z", "billed_at": "2023-07-26T15:35:05.739403Z", + "revised_at": null, "items": [ { "price": { diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json index 4f01f322..9f5578d4 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json @@ -18,6 +18,7 @@ "created_at": "2023-11-07T10:51:19.601401071Z", "updated_at": "2023-11-07T10:51:19.601401071Z", "billed_at": null, + "revised_at": null, "items": [ { "price": { diff --git a/tests/Functional/Resources/Transactions/test_TransactionsClient.py b/tests/Functional/Resources/Transactions/test_TransactionsClient.py index 9d66f475..8fd999e3 100644 --- a/tests/Functional/Resources/Transactions/test_TransactionsClient.py +++ b/tests/Functional/Resources/Transactions/test_TransactionsClient.py @@ -37,6 +37,7 @@ PreviewTransactionByIP, UpdateTransaction, GetTransactionInvoice, + ReviseTransaction, ) from paddle_billing.Resources.Transactions.Operations.Preview import ( @@ -61,6 +62,12 @@ TransactionUpdateItemWithPrice, ) +from paddle_billing.Resources.Transactions.Operations.Revise import ( + ReviseAddress, + ReviseBusiness, + ReviseCustomer, +) + from tests.Utils.ReadsFixture import ReadsFixtures @@ -670,6 +677,26 @@ def test_list_transactions_returns_expected_response( unquote(last_request.url) == expected_url ), "The URL does not match the expected URL, verify the query string is correct" + def test_list_transactions_with_and_without_revised_at( + self, + test_client, + mock_requests, + ): + expected_url = f"{test_client.base_url}/transactions" + mock_requests.get( + expected_url, status_code=200, text=ReadsFixtures.read_raw_json_fixture("response/list_default") + ) + + response = test_client.client.transactions.list() + + assert isinstance(response, TransactionCollection) + + transaction_without_revised_at = response.items[0] + assert transaction_without_revised_at.revised_at is None + + transaction_with_revised_at = response.items[5] + assert transaction_with_revised_at.revised_at.isoformat() == "2023-07-26T15:35:05.739403+00:00" + @mark.parametrize( "transaction_id, includes, expected_response_status, expected_response_body, expected_url", [ @@ -1242,3 +1269,98 @@ def test_get_transaction_invoice_pdf_hits_expected_url( assert ( unquote(last_request.url) == expected_url ), "The URL does not match the expected URL, verify the query string is correct" + + @mark.parametrize( + "transaction_id, operation, expected_request_body, expected_response_status, expected_response_body, expected_path", + [ + ( + "txn_01h7zcgmdc6tmwtjehp3sh7azf", + ReviseTransaction( + customer=ReviseCustomer( + name="Sam Miller", + ), + ), + ReadsFixtures.read_raw_json_fixture("request/revise_customer"), + 200, + ReadsFixtures.read_raw_json_fixture("response/full_entity"), + "/transactions/txn_01h7zcgmdc6tmwtjehp3sh7azf/revise", + ), + ( + "txn_01h7zcgmdc6tmwtjehp3sh7azf", + ReviseTransaction( + address=ReviseAddress( + first_line="3811 Ditmars Blvd", + ), + customer=ReviseCustomer( + name="Sam Miller", + ), + business=ReviseBusiness( + name="Some Business", + ), + ), + ReadsFixtures.read_raw_json_fixture("request/revise_basic"), + 200, + ReadsFixtures.read_raw_json_fixture("response/full_entity"), + "/transactions/txn_01h7zcgmdc6tmwtjehp3sh7azf/revise", + ), + ( + "txn_01h7zcgmdc6tmwtjehp3sh7azf", + ReviseTransaction( + address=ReviseAddress( + first_line="3811 Ditmars Blvd", + second_line=None, + city="Some City", + region="Some Region", + ), + customer=ReviseCustomer( + name="Sam Miller", + ), + business=ReviseBusiness( + name="Some Business", + tax_identifier="AB0123456789", + ), + ), + ReadsFixtures.read_raw_json_fixture("request/revise_full"), + 200, + ReadsFixtures.read_raw_json_fixture("response/full_entity"), + "/transactions/txn_01h7zcgmdc6tmwtjehp3sh7azf/revise", + ), + ], + ids=[ + "Customer revision", + "Basic revision", + "Full revision", + ], + ) + def test_revise_transaction_uses_expected_payload( + self, + test_client, + mock_requests, + transaction_id, + operation, + expected_request_body, + expected_response_status, + expected_response_body, + expected_path, + ): + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.post(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.transactions.revise(transaction_id, operation) + response_json = test_client.client.transactions.response.json() + request_json = test_client.client.payload + last_request = mock_requests.last_request + + assert isinstance(response, Transaction) + assert last_request is not None + assert last_request.method == "POST" + 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 loads(request_json) == loads( + expected_request_body + ), "The request JSON doesn't match the expected fixture JSON" + assert response_json == loads( + str(expected_response_body) + ), "The response JSON doesn't match the expected fixture JSON" diff --git a/tests/Unit/Entities/Notifications/test_NotificationEvent.py b/tests/Unit/Entities/Notifications/test_NotificationEvent.py index 13c99c51..c6c17870 100644 --- a/tests/Unit/Entities/Notifications/test_NotificationEvent.py +++ b/tests/Unit/Entities/Notifications/test_NotificationEvent.py @@ -56,6 +56,7 @@ class TestNotificationEvent: ("transaction.past_due", "Transaction"), ("transaction.payment_failed", "Transaction"), ("transaction.ready", "Transaction"), + ("transaction.revised", "Transaction"), ("transaction.updated", "Transaction"), ("report.created", "Report"), ("report.updated", "Report"), @@ -102,6 +103,7 @@ class TestNotificationEvent: "transaction.past_due", "transaction.payment_failed", "transaction.ready", + "transaction.revised", "transaction.updated", "report.created", "report.updated", diff --git a/tests/Unit/Entities/_fixtures/notification/entity/transaction.revised.json b/tests/Unit/Entities/_fixtures/notification/entity/transaction.revised.json new file mode 100644 index 00000000..2597be87 --- /dev/null +++ b/tests/Unit/Entities/_fixtures/notification/entity/transaction.revised.json @@ -0,0 +1,318 @@ +{ + "id": "txn_01hv8wptq8987qeep44cyrewp9", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [], + "import_meta": null + }, + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:00.907834Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [], + "import_meta": null + }, + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [], + "import_meta": null + }, + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "completed", + "details": { + "totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hv8wt98jahpbm1t1tzr06z6n", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard", + "import_meta": null + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hv8wt98jahpbm1t1v1sd067y", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard", + "import_meta": null + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01hv8wt98jahpbm1t1v67vqnb6", + "totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard", + "import_meta": null + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "tax_rates_used": [ + { + "totals": { + "tax": "5315", + "total": "65215", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://aeroedit.com/pay?_ptxn=txn_01hv8wptq8987qeep44cyrewp9" + }, + "payments": [ + { + "amount": "65215", + "status": "captured", + "created_at": "2024-04-12T10:18:33.579142Z", + "error_code": null, + "captured_at": "2024-04-12T10:18:47.635628Z", + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2025, + "expiry_month": 1, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01hv8x1tpjfnttxddw73xnqx6s", + "payment_attempt_id": "937640dd-e3dc-40df-a16c-bb75aafd8f71", + "stored_payment_method_id": "281ff2ca-8550-42b9-bf39-15948e7de62d" + }, + { + "amount": "65215", + "status": "error", + "created_at": "2024-04-12T10:15:57.888183Z", + "error_code": "declined", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "0002", + "expiry_year": 2025, + "expiry_month": 1, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01hv8wx2mka7dfsqjjsxh1ne7z", + "payment_attempt_id": "8f72cfa6-26b4-4a57-91dc-8f2708f7822d", + "stored_payment_method_id": "a78ece50-356f-4e0c-b72d-ad5368b0a0d9" + } + ], + "billed_at": "2024-04-12T10:18:48.294633Z", + "address_id": "add_01hv8gq3318ktkfengj2r75gfx", + "created_at": "2024-04-12T10:12:33.2014Z", + "invoice_id": "inv_01hv8x29nsh54c2pgt0hnq0zkx", + "updated_at": "2024-04-12T10:18:49.738971238Z", + "revised_at": null, + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "discount_id": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-05-12T10:18:47.635628Z", + "starts_at": "2024-04-12T10:18:47.635628Z" + }, + "invoice_number": "325-10566", + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01hv8x29kz0t586xy6zn1a62ny" +} \ No newline at end of file