From c4549fcf123fbc3a889966b5545da09b7905cf11 Mon Sep 17 00:00:00 2001 From: davidgrayston-paddle Date: Fri, 11 Oct 2024 17:37:37 +0100 Subject: [PATCH] fix: Allow null product IDs in transaction preview responses (#90) --- CHANGELOG.md | 8 ++- .../Shared/TransactionLineItemPreview.php | 9 ++- .../TransactionItemPreviewWithPrice.php | 4 +- .../Transaction/TransactionPreviewPrice.php | 9 +-- .../Transaction/TransactionPreviewProduct.php | 55 +++++++++++++++++++ .../Transactions/TransactionsClientTest.php | 17 ++++++ .../_fixtures/response/preview_entity.json | 37 ++++++++++++- 7 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 src/Entities/Transaction/TransactionPreviewProduct.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e027ca8..3194ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,11 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx ### Fixed - Dropped `receipt_data` on create and preview of a one-time charge for Subscriptions and Transactions -- `TransactionsClient::preview()` `TransactionPreview` response now allows null price ID for non-catalog prices: - - `TransactionPreview` `items[]->price` can now return `Price` (with `id`) or `TransactionPreviewPrice` (with nullable `id`) - - `TransactionPreview` `details->lineItems[]->priceId` is now nullable +- `TransactionsClient::preview()` `TransactionPreview` response now allows null IDs for non-catalog prices and products: + - `items[]->price` can now return `Price` (with `id`) or `TransactionPreviewPrice` (with nullable `id`) + - `details->lineItems[]->priceId` is now nullable + - `items[]->priceId` is now nullable + - `details->lineItems[]->product` can now return `Product` (with `id`) or `TransactionPreviewProduct` (with nullable `id`) ### Added - `TransactionsClient::create()` now supports operation items with optional properties: diff --git a/src/Entities/Shared/TransactionLineItemPreview.php b/src/Entities/Shared/TransactionLineItemPreview.php index 80a902d..642a31b 100644 --- a/src/Entities/Shared/TransactionLineItemPreview.php +++ b/src/Entities/Shared/TransactionLineItemPreview.php @@ -12,6 +12,7 @@ namespace Paddle\SDK\Entities\Shared; use Paddle\SDK\Entities\Product; +use Paddle\SDK\Entities\Transaction\TransactionPreviewProduct; class TransactionLineItemPreview { @@ -21,19 +22,21 @@ private function __construct( public string $taxRate, public UnitTotals $unitTotals, public Totals $totals, - public Product $product, + public Product|TransactionPreviewProduct $product, ) { } public static function from(array $data): self { return new self( - $data['price_id'], + $data['price_id'] ?? null, $data['quantity'], $data['tax_rate'], UnitTotals::from($data['unit_totals']), Totals::from($data['totals']), - Product::from($data['product']), + isset($data['product']['id']) + ? Product::from($data['product']) + : TransactionPreviewProduct::from($data['product']), ); } } diff --git a/src/Entities/Transaction/TransactionItemPreviewWithPrice.php b/src/Entities/Transaction/TransactionItemPreviewWithPrice.php index a6ef78c..74e2446 100644 --- a/src/Entities/Transaction/TransactionItemPreviewWithPrice.php +++ b/src/Entities/Transaction/TransactionItemPreviewWithPrice.php @@ -26,7 +26,9 @@ private function __construct( public static function from(array $data): self { return new self( - isset($data['price']['id']) ? Price::from($data['price']) : TransactionPreviewPrice::from($data['price']), + isset($data['price']['id']) && isset($data['price']['product_id']) + ? Price::from($data['price']) + : TransactionPreviewPrice::from($data['price']), $data['quantity'], $data['include_in_totals'], isset($data['proration']) ? TransactionProration::from($data['proration']) : null, diff --git a/src/Entities/Transaction/TransactionPreviewPrice.php b/src/Entities/Transaction/TransactionPreviewPrice.php index 38eee34..241bd66 100644 --- a/src/Entities/Transaction/TransactionPreviewPrice.php +++ b/src/Entities/Transaction/TransactionPreviewPrice.php @@ -13,7 +13,6 @@ use Paddle\SDK\Entities\DateTime; use Paddle\SDK\Entities\Entity; -use Paddle\SDK\Entities\Product; use Paddle\SDK\Entities\Shared\CatalogType; use Paddle\SDK\Entities\Shared\CustomData; use Paddle\SDK\Entities\Shared\ImportMeta; @@ -31,7 +30,7 @@ class TransactionPreviewPrice implements Entity */ private function __construct( public string|null $id, - public string $productId, + public string|null $productId, public string|null $name, public string $description, public CatalogType|null $type, @@ -44,7 +43,6 @@ private function __construct( public Status $status, public CustomData|null $customData, public ImportMeta|null $importMeta, - public Product|null $product, public \DateTimeInterface $createdAt, public \DateTimeInterface $updatedAt, ) { @@ -53,8 +51,8 @@ private function __construct( public static function from(array $data): self { return new self( - id: $data['id'], - productId: $data['product_id'], + id: $data['id'] ?? null, + productId: $data['product_id'] ?? null, name: $data['name'] ?? null, description: $data['description'], type: CatalogType::from($data['type'] ?? ''), @@ -70,7 +68,6 @@ public static function from(array $data): self status: Status::from($data['status']), customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, importMeta: isset($data['import_meta']) ? ImportMeta::from($data['import_meta']) : null, - product: isset($data['product']) ? Product::from($data['product']) : null, createdAt: DateTime::from($data['created_at']), updatedAt: DateTime::from($data['updated_at']), ); diff --git a/src/Entities/Transaction/TransactionPreviewProduct.php b/src/Entities/Transaction/TransactionPreviewProduct.php new file mode 100644 index 0000000..25c59e9 --- /dev/null +++ b/src/Entities/Transaction/TransactionPreviewProduct.php @@ -0,0 +1,55 @@ +items[0]->price; self::assertInstanceOf(Price::class, $price); self::assertSame('pri_01gsz8z1q1n00f12qt82y31smh', $price->id); + self::assertSame('pro_01gsz4t5hdjse780zja8vvr7jg', $price->productId); $transactionPreviewPrice = $preview->items[1]->price; self::assertInstanceOf(TransactionPreviewPrice::class, $transactionPreviewPrice); self::assertNull($transactionPreviewPrice->id); + self::assertSame('pro_01gsz97mq9pa4fkyy0wqenepkz', $transactionPreviewPrice->productId); + + $transactionPreviewPriceWithoutProductId = $preview->items[2]->price; + self::assertInstanceOf(TransactionPreviewPrice::class, $transactionPreviewPriceWithoutProductId); + self::assertSame('pri_01gsz8z1q1n00f12qt82y31smh', $price->id); + self::assertNull($transactionPreviewPriceWithoutProductId->productId); self::assertNull($preview->details->lineItems[0]->priceId); self::assertSame('pri_01gsz8z1q1n00f12qt82y31smh', $preview->details->lineItems[1]->priceId); + + $product = $preview->details->lineItems[1]->product; + self::assertInstanceOf(Product::class, $product); + self::assertSame('pro_01gsz97mq9pa4fkyy0wqenepkz', $product->id); + + $previewProduct = $preview->details->lineItems[0]->product; + self::assertInstanceOf(TransactionPreviewProduct::class, $previewProduct); + self::assertNull($previewProduct->id); } /** diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json b/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json index e2f5562..e0a6d67 100644 --- a/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json +++ b/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json @@ -75,6 +75,41 @@ "quantity": 1, "proration": null, "include_in_totals": false + }, + { + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "description": "One-time charge", + "name": "One-time charge", + "product_id": null, + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active", + "custom_data": null, + "created_at": "2023-08-16T14:38:08.3Z", + "updated_at": "2023-08-16T14:38:08.3Z" + }, + "quantity": 1, + "proration": null, + "include_in_totals": false } ], "details": { @@ -113,7 +148,7 @@ "total": "540000" }, "product": { - "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "id": null, "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",