Skip to content

Commit

Permalink
feat: Handle notification entity serialization (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgrayston-paddle committed Oct 17, 2024
1 parent f481133 commit b5102d5
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 21 deletions.
9 changes: 8 additions & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,6 +50,7 @@
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
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;
Expand Down Expand Up @@ -185,7 +187,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()],
);

Expand Down
14 changes: 4 additions & 10 deletions src/Entities/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 */
$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<NotificationEntity> $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,
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/Notifications/Entities/EntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public static function create(string $eventType, array $data): Entity

/** @var class-string<Entity> $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");
Expand Down
30 changes: 30 additions & 0 deletions src/Notifications/Entities/UndefinedEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/**
* |------
* | ! Generated code !
* | Altering this code will result in changes being overwritten |
* |-------------------------------------------------------------|.
*/

namespace Paddle\SDK\Notifications\Entities;

class UndefinedEntity implements Entity, \JsonSerializable
{
public function __construct(
public readonly array $data,
) {
}

public static function from(array $data): self
{
return new self($data);
}

public function jsonSerialize(): \stdClass
{
return (object) $this->data;
}
}
36 changes: 36 additions & 0 deletions src/Notifications/Events/UndefinedEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Paddle\SDK\Notifications\Events;

use Paddle\SDK\Entities\Event;
use Paddle\SDK\Entities\Event\EventTypeName;
use Paddle\SDK\Notifications\Entities\Entity;
use Paddle\SDK\Notifications\Entities\UndefinedEntity;

final class UndefinedEvent extends Event
{
private function __construct(
string $eventId,
EventTypeName $eventType,
\DateTimeInterface $occurredAt,
public readonly UndefinedEntity $entity,
string|null $notificationId,
) {
parent::__construct($eventId, $eventType, $occurredAt, $entity, $notificationId);
}

/**
* @param UndefinedEntity $data
*/
public static function fromEvent(
string $eventId,
EventTypeName $eventType,
\DateTimeInterface $occurredAt,
Entity $data,
string|null $notificationId = null,
): static {
return new self($eventId, $eventType, $occurredAt, $data, $notificationId);
}
}
36 changes: 36 additions & 0 deletions tests/Functional/Resources/Events/EventsClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
use Paddle\SDK\Entities\Shared\Status;
use Paddle\SDK\Entities\Shared\TaxMode;
use Paddle\SDK\Environment;
use Paddle\SDK\Notifications\Entities\Entity;
use Paddle\SDK\Notifications\Entities\Product;
use Paddle\SDK\Notifications\Entities\Shared\Interval;
use Paddle\SDK\Notifications\Entities\Subscription\SubscriptionPrice;
use Paddle\SDK\Notifications\Entities\UndefinedEntity;
use Paddle\SDK\Notifications\Events\UndefinedEvent;
use Paddle\SDK\Options;
use Paddle\SDK\Resources\Events\Operations\ListEvents;
use Paddle\SDK\Resources\Shared\Operations\List\Pager;
Expand Down Expand Up @@ -158,4 +161,37 @@ public function list_handles_subscription_events_with_price(): void
self::assertSame('2023-04-24T14:11:13.014+00:00', $price1->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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions tests/Functional/Resources/Simulations/SimulationsClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
]),
];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
"currency_code": "USD"
},
"payout_totals": {
"chargeback_fee": {
"amount": "1",
"original": {
"amount": "2",
"currency_code": "USD"
}
},
"subtotal": "92",
"tax": "8",
"total": "100",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"action": "refund",
"transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y",
"subscription_id": "sub_01hvccbx32q2gb40sqx7n42430",
"tax_rates_used": [],
"customer_id": "ctm_01hrffh7gvp29kc7xahm8wddwa",
"reason": "error",
"credit_applied_to_balance": null,
Expand Down Expand Up @@ -36,6 +37,13 @@
"currency_code": "USD"
},
"payout_totals": {
"chargeback_fee": {
"amount": "1",
"original": {
"amount": "2",
"currency_code": "USD"
}
},
"subtotal": "92",
"tax": "8",
"total": "100",
Expand Down
Loading

0 comments on commit b5102d5

Please sign in to comment.