Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle notification entity serialization #92

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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;
Expand Down Expand Up @@ -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()],
);

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
Loading