diff --git a/src/Client.php b/src/Client.php index d8ab568..a4a42b2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -17,6 +17,7 @@ use Http\Discovery\HttpAsyncClientDiscovery; use Http\Discovery\Psr17FactoryDiscovery; use Http\Message\Authentication\Bearer; +use Paddle\SDK\Entities\DateTime; use Paddle\SDK\Logger\Formatter; use Paddle\SDK\Resources\Addresses\AddressesClient; use Paddle\SDK\Resources\Adjustments\AdjustmentsClient; @@ -48,6 +49,7 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -184,7 +186,12 @@ private function requestRaw(string $method, string|UriInterface $uri, array|\Jso $request = $this->requestFactory->createRequest($method, $uri); $serializer = new Serializer( - [new BackedEnumNormalizer(), new JsonSerializableNormalizer(), new ObjectNormalizer(nameConverter: new CamelCaseToSnakeCaseNameConverter())], + [ + new BackedEnumNormalizer(), + new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => DateTime::PADDLE_RFC3339]), + new JsonSerializableNormalizer(), + new ObjectNormalizer(nameConverter: new CamelCaseToSnakeCaseNameConverter()), + ], [new JsonEncoder()], ); diff --git a/src/Entities/Event.php b/src/Entities/Event.php index 729c2db..f5db070 100644 --- a/src/Entities/Event.php +++ b/src/Entities/Event.php @@ -6,6 +6,8 @@ use Paddle\SDK\Entities\Event\EventTypeName; use Paddle\SDK\Notifications\Entities\Entity as NotificationEntity; +use Paddle\SDK\Notifications\Entities\EntityFactory; +use Paddle\SDK\Notifications\Events\UndefinedEvent; use Psr\Http\Message\ServerRequestInterface; abstract class Event implements Entity @@ -22,28 +24,20 @@ protected function __construct( public static function from(array $data): self { $type = explode('.', (string) $data['event_type']); - $entity = $type[0] ?? 'Unknown'; $identifier = str_replace('_', '', ucwords(implode('_', $type), '_')); /** @var class-string $event */ $event = sprintf('\Paddle\SDK\Notifications\Events\%s', $identifier); if (! class_exists($event) || ! is_subclass_of($event, self::class)) { - throw new \UnexpectedValueException("Event type '{$identifier}' cannot be mapped to an object"); - } - - /** @var class-string $entity */ - $entity = sprintf('\Paddle\SDK\Notifications\Entities\%s', ucfirst($entity)); - - if (! class_exists($entity) || ! in_array(NotificationEntity::class, class_implements($entity), true)) { - throw new \UnexpectedValueException("Event type '{$identifier}' cannot be mapped to an object"); + $event = UndefinedEvent::class; } return $event::fromEvent( $data['event_id'], EventTypeName::from($data['event_type']), DateTime::from($data['occurred_at']), - $entity::from($data['data']), + EntityFactory::create($data['event_type'], $data['data']), $data['notification_id'] ?? null, ); } diff --git a/src/Notifications/Entities/Adjustment.php b/src/Notifications/Entities/Adjustment.php index ec4e291..0865628 100644 --- a/src/Notifications/Entities/Adjustment.php +++ b/src/Notifications/Entities/Adjustment.php @@ -38,7 +38,7 @@ private function __construct( public array $items, public AdjustmentTotals $totals, public PayoutTotalsAdjustment|null $payoutTotals, - public array $taxRatesUsed, + public array|null $taxRatesUsed, public \DateTimeInterface $createdAt, public \DateTimeInterface|null $updatedAt, ) { @@ -59,7 +59,9 @@ public static function from(array $data): self items: array_map(fn (array $item): AdjustmentItem => AdjustmentItem::from($item), $data['items']), totals: AdjustmentTotals::from($data['totals']), payoutTotals: isset($data['payout_totals']) ? PayoutTotalsAdjustment::from($data['payout_totals']) : null, - taxRatesUsed: array_map(fn (array $taxRateUsed): AdjustmentTaxRatesUsed => AdjustmentTaxRatesUsed::from($taxRateUsed), $data['tax_rates_used'] ?? []), + taxRatesUsed: isset($data['tax_rates_used']) + ? array_map(fn (array $taxRateUsed): AdjustmentTaxRatesUsed => AdjustmentTaxRatesUsed::from($taxRateUsed), $data['tax_rates_used'] ?? []) + : null, createdAt: DateTime::from($data['created_at']), updatedAt: isset($data['updated_at']) ? DateTime::from($data['updated_at']) : null, ); diff --git a/src/Notifications/Entities/EntityFactory.php b/src/Notifications/Entities/EntityFactory.php index 63c4a47..db0d9b8 100644 --- a/src/Notifications/Entities/EntityFactory.php +++ b/src/Notifications/Entities/EntityFactory.php @@ -14,6 +14,9 @@ public static function create(string $eventType, array $data): Entity /** @var class-string $entity */ $entity = sprintf('\Paddle\SDK\Notifications\Entities\%s', ucfirst($entity)); + if (! class_exists($entity)) { + $entity = UndefinedEntity::class; + } if (! class_exists($entity) || ! in_array(Entity::class, class_implements($entity), true)) { throw new \UnexpectedValueException("Event type '{$identifier}' cannot be mapped to an object"); diff --git a/src/Notifications/Entities/Shared/PayoutTotalsAdjustment.php b/src/Notifications/Entities/Shared/PayoutTotalsAdjustment.php index 551c994..eee9407 100644 --- a/src/Notifications/Entities/Shared/PayoutTotalsAdjustment.php +++ b/src/Notifications/Entities/Shared/PayoutTotalsAdjustment.php @@ -11,14 +11,19 @@ namespace Paddle\SDK\Notifications\Entities\Shared; -class PayoutTotalsAdjustment +use Paddle\SDK\FiltersUndefined; +use Paddle\SDK\Undefined; + +class PayoutTotalsAdjustment implements \JsonSerializable { + use FiltersUndefined; + private function __construct( public string $subtotal, public string $tax, public string $total, public string $fee, - public ChargebackFee|null $chargebackFee, + public ChargebackFee|Undefined|null $chargebackFee, public string $earnings, public CurrencyCodePayouts $currencyCode, ) { @@ -31,9 +36,22 @@ public static function from(array $data): self tax: $data['tax'], total: $data['total'], fee: $data['fee'], - chargebackFee: isset($data['chargeback_fee']) ? ChargebackFee::from($data['chargeback_fee']) : null, + chargebackFee: isset($data['chargeback_fee']) ? ChargebackFee::from($data['chargeback_fee']) : new Undefined(), earnings: $data['earnings'], currencyCode: CurrencyCodePayouts::from($data['currency_code']), ); } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'subtotal' => $this->subtotal, + 'tax' => $this->tax, + 'total' => $this->total, + 'fee' => $this->fee, + 'chargeback_fee' => $this->chargebackFee, + 'earnings' => $this->earnings, + 'currency_code' => $this->currencyCode, + ]); + } } diff --git a/src/Notifications/Entities/UndefinedEntity.php b/src/Notifications/Entities/UndefinedEntity.php new file mode 100644 index 0000000..abcfbaa --- /dev/null +++ b/src/Notifications/Entities/UndefinedEntity.php @@ -0,0 +1,30 @@ +data; + } +} diff --git a/src/Notifications/Events/UndefinedEvent.php b/src/Notifications/Events/UndefinedEvent.php new file mode 100644 index 0000000..7efef86 --- /dev/null +++ b/src/Notifications/Events/UndefinedEvent.php @@ -0,0 +1,36 @@ +createdAt->format(\DATE_RFC3339_EXTENDED)); self::assertSame('2023-11-24T14:12:05.528+00:00', $price1->updatedAt->format(\DATE_RFC3339_EXTENDED)); } + + /** + * @test + */ + public function list_handles_unknown_events(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_default'))); + $events = $this->client->events->list(new ListEvents()); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + + $undefinedEvents = array_values( + array_filter( + iterator_to_array($events), + fn ($event) => (string) $event->eventType === 'unknown_entity.updated', + ), + ); + + $undefinedEvent = $undefinedEvents[0]; + self::assertInstanceOf(UndefinedEvent::class, $undefinedEvent); + self::assertSame($undefinedEvent->entity, $undefinedEvent->data); + self::assertInstanceOf(UndefinedEntity::class, $undefinedEvent->entity); + self::assertInstanceOf(UndefinedEntity::class, $undefinedEvent->data); + self::assertInstanceOf(Entity::class, $undefinedEvent->data); + self::assertEquals( + [ + 'key' => 'value', + ], + $undefinedEvent->entity->data, + ); + } } diff --git a/tests/Functional/Resources/Events/_fixtures/response/list_default.json b/tests/Functional/Resources/Events/_fixtures/response/list_default.json index 377afbd..b4eb9d6 100644 --- a/tests/Functional/Resources/Events/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Events/_fixtures/response/list_default.json @@ -2801,6 +2801,14 @@ "updated_at": "2023-11-23T15:33:19.238230688Z", "billed_at": "2023-11-23T15:33:01.930479Z" } + }, + { + "event_id": "evt_01hfyd0v4xppkwmjaca5xyzh5d", + "event_type": "unknown_entity.updated", + "occurred_at": "2023-11-23T15:33:19.645134Z", + "data": { + "key": "value" + } } ], "meta": { diff --git a/tests/Functional/Resources/Notifications/NotificationsClientTest.php b/tests/Functional/Resources/Notifications/NotificationsClientTest.php index 0e8dacc..af592e1 100644 --- a/tests/Functional/Resources/Notifications/NotificationsClientTest.php +++ b/tests/Functional/Resources/Notifications/NotificationsClientTest.php @@ -7,8 +7,12 @@ use GuzzleHttp\Psr7\Response; use Http\Mock\Client as MockClient; use Paddle\SDK\Client; +use Paddle\SDK\Entities\Notification; use Paddle\SDK\Entities\Notification\NotificationStatus; use Paddle\SDK\Environment; +use Paddle\SDK\Notifications\Entities\Entity; +use Paddle\SDK\Notifications\Entities\UndefinedEntity; +use Paddle\SDK\Notifications\Events\UndefinedEvent; use Paddle\SDK\Options; use Paddle\SDK\Resources\Notifications\Operations\ListNotifications; use Paddle\SDK\Resources\Shared\Operations\List\Pager; @@ -171,4 +175,40 @@ public function replay_hits_expected_uri(): void ); self::assertSame('ntf_01h46h1s2zabpkdks7yt4vkgkc', $replayId); } + + /** + * @test + */ + public function list_handles_unknown_events(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_default'))); + $notifications = $this->client->notifications->list(new ListNotifications()); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + + $undefinedEventNotifications = array_values( + array_filter( + iterator_to_array($notifications), + fn (Notification $notification) => (string) $notification->type === 'unknown_entity.updated', + ), + ); + + $undefinedEventNotification = $undefinedEventNotifications[0]; + self::assertInstanceOf(Notification::class, $undefinedEventNotification); + + $undefinedEvent = $undefinedEventNotification->payload; + self::assertInstanceOf(UndefinedEvent::class, $undefinedEvent); + self::assertSame($undefinedEvent->entity, $undefinedEvent->data); + self::assertInstanceOf(Entity::class, $undefinedEvent->data); + self::assertInstanceOf(UndefinedEntity::class, $undefinedEvent->data); + self::assertInstanceOf(UndefinedEntity::class, $undefinedEvent->entity); + self::assertEquals( + [ + 'key' => 'value', + ], + $undefinedEvent->entity->data, + ); + } } diff --git a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json index bd92d24..29b9362 100644 --- a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json +++ b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json @@ -452,6 +452,28 @@ "retry_at": null, "times_attempted": 1, "notification_setting_id": "ntfset_01h7zcdzf04a7wvyja9k9p1n3p" + }, + { + "id": "ntf_01h8441jz6fr97hv7zemswj8cw", + "type": "unknown_entity.updated", + "status": "delivered", + "payload": { + "data": { + "key": "value" + }, + "event_id": "evt_01h8441jx8x1q971q9ksksqh82", + "event_type": "unknown_entity.updated", + "occurred_at": "2023-08-18T10:46:18.792661Z", + "notification_id": "ntf_01h8441jz6fr97hv7zemswj8cw" + }, + "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/SimulationRunEvents/_fixtures/response/full_entity.json b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json index 18b52cd..fe66a71 100644 --- a/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json +++ b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json @@ -8,9 +8,9 @@ "city": "New York", "region": "NY", "status": "active", - "created_at": "2024-04-12T06:42:58.785Z", + "created_at": "2024-04-12T06:42:58.785000Z", "first_line": "4050 Jefferson Plaza, 41st Floor", - "updated_at": "2024-04-12T06:42:58.785Z", + "updated_at": "2024-04-12T06:42:58.785000Z", "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "description": "Head Office", diff --git a/tests/Functional/Resources/Simulations/SimulationsClientTest.php b/tests/Functional/Resources/Simulations/SimulationsClientTest.php index 7477ac3..aea1e7d 100644 --- a/tests/Functional/Resources/Simulations/SimulationsClientTest.php +++ b/tests/Functional/Resources/Simulations/SimulationsClientTest.php @@ -70,6 +70,22 @@ public static function createOperationsProvider(): \Generator new Response(200, body: self::readRawJsonFixture('response/full_entity')), self::readRawJsonFixture('request/create_basic'), ]; + + yield 'Undefined' => [ + new CreateSimulation( + notificationSettingId: 'ntfset_01j82d983j814ypzx7m1fw2jpz', + type: EventTypeName::from('unknown_entity.created'), + name: 'Some Simulation', + payload: EntityFactory::create('unknown_entity.created', ['some' => 'data']), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + json_encode([ + 'notification_setting_id' => 'ntfset_01j82d983j814ypzx7m1fw2jpz', + 'name' => 'Some Simulation', + 'type' => 'unknown_entity.created', + 'payload' => ['some' => 'data'], + ]), + ]; } /** diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json b/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json index 432e7b3..f160c96 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json +++ b/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json @@ -3,9 +3,9 @@ "city": "New York", "region": "NY", "status": "active", - "created_at": "2024-04-12T06:42:58.785Z", + "created_at": "2024-04-12T06:42:58.785000Z", "first_line": "4050 Jefferson Plaza, 41st Floor", - "updated_at": "2024-04-12T06:42:58.785Z", + "updated_at": "2024-04-12T06:42:58.785000Z", "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "description": "Head Office", diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json b/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json index 75cfafe..6577e27 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json +++ b/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json @@ -7,9 +7,9 @@ "city": "New York", "region": "NY", "status": "active", - "created_at": "2024-04-12T06:42:58.785Z", + "created_at": "2024-04-12T06:42:58.785000Z", "first_line": "4050 Jefferson Plaza, 41st Floor", - "updated_at": "2024-04-12T06:42:58.785Z", + "updated_at": "2024-04-12T06:42:58.785000Z", "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "description": "Head Office", diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json b/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json index 56b48a8..6485a62 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json +++ b/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json @@ -8,6 +8,7 @@ "action": "refund", "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "tax_rates_used": null, "customer_id": "ctm_01hrffh7gvp29kc7xahm8wddwa", "reason": "error", "credit_applied_to_balance": null, diff --git a/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json b/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json index 6097b6a..379be80 100644 --- a/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json +++ b/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json @@ -10,9 +10,9 @@ "city": "New York", "region": "NY", "status": "active", - "created_at": "2024-04-12T06:42:58.785Z", + "created_at": "2024-04-12T06:42:58.785000Z", "first_line": "4050 Jefferson Plaza, 41st Floor", - "updated_at": "2024-04-12T06:42:58.785Z", + "updated_at": "2024-04-12T06:42:58.785000Z", "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "description": "Head Office", diff --git a/tests/Unit/Entities/EventTest.php b/tests/Unit/Entities/EventTest.php index 6e6a046..fc83893 100644 --- a/tests/Unit/Entities/EventTest.php +++ b/tests/Unit/Entities/EventTest.php @@ -333,6 +333,27 @@ public static function eventDataProvider(): iterable } } + /** + * @test + */ + public function it_creates_event_for_undefined_entity(): void + { + $event = Event::from([ + 'event_id' => 'evt_01h8bzakzx3hm2fmen703n5q45', + 'event_type' => 'unknown_event.created', + 'occurred_at' => '2023-08-21T11:57:47.390028Z', + 'notification_id' => 'ntf_01h8bzam1z32agrxjwhjgqk8w6', + 'data' => [ + 'some' => 'data', + ], + ]); + + self::assertSame('ntf_01h8bzam1z32agrxjwhjgqk8w6', $event->notificationId); + + self::assertInstanceOf(Event::class, $event); + self::assertInstanceOf(Entity::class, $event->data); + } + /** * @test */ diff --git a/tests/Unit/Entities/_fixtures/notification/entity/address.created.json b/tests/Unit/Entities/_fixtures/notification/entity/address.created.json index 10dcb1e..6103eac 100644 --- a/tests/Unit/Entities/_fixtures/notification/entity/address.created.json +++ b/tests/Unit/Entities/_fixtures/notification/entity/address.created.json @@ -3,9 +3,9 @@ "city": "New York", "region": "NY", "status": "active", - "created_at": "2024-04-12T06:42:58.785Z", + "created_at": "2024-04-12T06:42:58.785000Z", "first_line": "4050 Jefferson Plaza, 41st Floor", - "updated_at": "2024-04-12T06:42:58.785Z", + "updated_at": "2024-04-12T06:42:58.785000Z", "custom_data": null, "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", "description": "Head Office",