From d370f6c748da81fb084c0cf280a1a9307d41b459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Saborit?= Date: Thu, 17 Oct 2024 16:53:46 +0200 Subject: [PATCH] feat: Simulator API (#84) Co-authored-by: davidgrayston-paddle --- CHANGELOG.md | 2 + examples/catalog_management.php | 2 +- examples/simulations.php | 130 +++++ src/Client.php | 21 +- .../Collections/SimulationCollection.php | 30 + .../Collections/SimulationRunCollection.php | 30 + .../SimulationRunEventCollection.php | 30 + .../Collections/SimulationTypeCollection.php | 30 + src/Entities/Event.php | 14 +- src/Entities/Simulation.php | 42 ++ src/Entities/Simulation/SimulationKind.php | 24 + .../Simulation/SimulationScenarioType.php | 30 + src/Entities/Simulation/SimulationStatus.php | 24 + src/Entities/SimulationRun.php | 37 ++ .../SimulationRun/SimulationRunStatus.php | 26 + src/Entities/SimulationRunEvent.php | 41 ++ .../SimulationRunEventRequest.php | 27 + .../SimulationRunEventResponse.php | 29 + .../SimulationRunEventStatus.php | 28 + src/Entities/SimulationType.php | 40 ++ src/Notifications/Entities/EntityFactory.php | 27 + .../Entities/UndefinedEntity.php | 30 + src/Notifications/Events/UndefinedEvent.php | 36 ++ .../Operations/ListSimulationRunEvents.php | 36 ++ .../SimulationRunEventsClient.php | 71 +++ .../Operations/GetSimulationRuns.php | 28 + .../SimulationRuns/Operations/Includes.php | 15 + .../Operations/ListSimulationRuns.php | 42 ++ .../SimulationRuns/SimulationRunsClient.php | 72 +++ .../SimulationTypes/SimulationTypesClient.php | 39 ++ .../Operations/CreateSimulation.php | 34 ++ .../Operations/ListSimulations.php | 51 ++ .../Operations/UpdateSimulation.php | 37 ++ .../Simulations/SimulationsClient.php | 86 +++ .../Resources/Events/EventsClientTest.php | 36 ++ .../_fixtures/response/list_default.json | 8 + .../Notifications/NotificationsClientTest.php | 40 ++ .../_fixtures/response/list_default.json | 22 + .../SimulationRunEventsClientTest.php | 202 +++++++ .../_fixtures/response/full_entity.json | 30 + .../_fixtures/response/list_default.json | 127 +++++ .../response/list_paginated_page_one.json | 70 +++ .../response/list_paginated_page_two.json | 70 +++ .../SimulationRunsClientTest.php | 190 +++++++ .../_fixtures/response/full_entity.json | 12 + .../_fixtures/response/list_default.json | 27 + .../response/list_paginated_page_one.json | 20 + .../response/list_paginated_page_two.json | 20 + .../SimulationTypesClientTest.php | 47 ++ .../_fixtures/response/list_default.json | 523 ++++++++++++++++++ .../Simulations/SimulationsClientTest.php | 270 +++++++++ .../request/address_created_payload.json | 16 + .../request/adjustment_updated_payload.json | 50 ++ .../_fixtures/request/create_basic.json | 21 + .../_fixtures/request/update_full.json | 57 ++ .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 31 ++ .../full_entity_adjustment_updated.json | 58 ++ .../_fixtures/response/list_default.json | 35 ++ .../response/list_paginated_page_one.json | 24 + .../response/list_paginated_page_two.json | 24 + tests/Unit/Entities/EventTest.php | 21 + .../notification/entity/address.created.json | 4 +- 64 files changed, 3289 insertions(+), 14 deletions(-) create mode 100644 examples/simulations.php create mode 100644 src/Entities/Collections/SimulationCollection.php create mode 100644 src/Entities/Collections/SimulationRunCollection.php create mode 100644 src/Entities/Collections/SimulationRunEventCollection.php create mode 100644 src/Entities/Collections/SimulationTypeCollection.php create mode 100644 src/Entities/Simulation.php create mode 100644 src/Entities/Simulation/SimulationKind.php create mode 100644 src/Entities/Simulation/SimulationScenarioType.php create mode 100644 src/Entities/Simulation/SimulationStatus.php create mode 100644 src/Entities/SimulationRun.php create mode 100644 src/Entities/SimulationRun/SimulationRunStatus.php create mode 100644 src/Entities/SimulationRunEvent.php create mode 100644 src/Entities/SimulationRunEvent/SimulationRunEventRequest.php create mode 100644 src/Entities/SimulationRunEvent/SimulationRunEventResponse.php create mode 100644 src/Entities/SimulationRunEvent/SimulationRunEventStatus.php create mode 100644 src/Entities/SimulationType.php create mode 100644 src/Notifications/Entities/EntityFactory.php create mode 100644 src/Notifications/Entities/UndefinedEntity.php create mode 100644 src/Notifications/Events/UndefinedEvent.php create mode 100644 src/Resources/SimulationRunEvents/Operations/ListSimulationRunEvents.php create mode 100644 src/Resources/SimulationRunEvents/SimulationRunEventsClient.php create mode 100644 src/Resources/SimulationRuns/Operations/GetSimulationRuns.php create mode 100644 src/Resources/SimulationRuns/Operations/Includes.php create mode 100644 src/Resources/SimulationRuns/Operations/ListSimulationRuns.php create mode 100644 src/Resources/SimulationRuns/SimulationRunsClient.php create mode 100644 src/Resources/SimulationTypes/SimulationTypesClient.php create mode 100644 src/Resources/Simulations/Operations/CreateSimulation.php create mode 100644 src/Resources/Simulations/Operations/ListSimulations.php create mode 100644 src/Resources/Simulations/Operations/UpdateSimulation.php create mode 100644 src/Resources/Simulations/SimulationsClient.php create mode 100644 tests/Functional/Resources/SimulationRunEvents/SimulationRunEventsClientTest.php create mode 100644 tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_one.json create mode 100644 tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_two.json create mode 100644 tests/Functional/Resources/SimulationRuns/SimulationRunsClientTest.php create mode 100644 tests/Functional/Resources/SimulationRuns/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/SimulationRuns/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_one.json create mode 100644 tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_two.json create mode 100644 tests/Functional/Resources/SimulationTypes/SimulationTypesClientTest.php create mode 100644 tests/Functional/Resources/SimulationTypes/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Simulations/SimulationsClientTest.php create mode 100644 tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/request/adjustment_updated_payload.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/response/full_entity_adjustment_updated.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_one.json create mode 100644 tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_two.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e7dcf9..7a030f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx ### Added +- Added simulations api [related changelog](https://developer.paddle.com/changelog/2024/webhook-simulator?utm_source=dx&utm_medium=paddle-php-sdk) - Added `traffic_source` property to `NotificationSetting` entity - Support notification settings `traffic_source` filter - Support new payment methods `offline`, `unknown`, `wire_transfer` @@ -24,6 +25,7 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx - `items[]->priceId` is now nullable - `details->lineItems[]->product` can now return `Product` (with `id`) or `TransactionPreviewProduct` (with nullable `id`) - Empty custom data array will now serialize to empty JSON object `{}` +- `EventsClient::list` and `Notification->payload` will now return `UndefinedEvent` for unknown event types. ### Added - `TransactionsClient::create()` now supports operation items with optional properties: diff --git a/examples/catalog_management.php b/examples/catalog_management.php index f2fcb01..c36c0f1 100644 --- a/examples/catalog_management.php +++ b/examples/catalog_management.php @@ -24,7 +24,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$environment = Paddle\SDK\Environment::tryFrom(getenv('PAD') ?: '') ?? Paddle\SDK\Environment::SANDBOX; +$environment = Paddle\SDK\Environment::tryFrom(getenv('PADDLE_ENVIRONMENT') ?: '') ?? Paddle\SDK\Environment::SANDBOX; $apiKey = getenv('PADDLE_API_KEY') ?: null; if (is_null($apiKey)) { diff --git a/examples/simulations.php b/examples/simulations.php new file mode 100644 index 0000000..0e4c3ba --- /dev/null +++ b/examples/simulations.php @@ -0,0 +1,130 @@ +simulations->list(new ListSimulations(new Pager(perPage: 10))); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo "List Simulations\n"; + +foreach ($simulations as $simulation) { + echo sprintf("- %s:\n", $simulation->name); + echo sprintf(" - ID: %s\n", $simulation->id); + echo sprintf(" - Type: %s\n", $simulation->type->getValue()); + echo sprintf(" - Notification Setting ID: %s\n", $simulation->notificationSettingId); +} + +// ┌─── +// │ Create Simulation │ +// └───────────────────┘ +try { + $simulation = $paddle->simulations->create( + new CreateSimulation( + notificationSettingId: $notificationSettingId, + type: EventTypeName::AddressCreated(), + name: 'Simulate Address Creation', + payload: Address::from([ + 'id' => 'add_01hv8gq3318ktkfengj2r75gfx', + 'country_code' => 'US', + 'status' => 'active', + 'created_at' => '2024-04-12T06:42:58.785000Z', + 'updated_at' => '2024-04-12T06:42:58.785000Z', + 'customer_id' => 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + 'description' => 'Head Office', + 'first_line' => '4050 Jefferson Plaza, 41st Floor', + 'second_line' => null, + 'city' => 'New York', + 'postal_code' => '10021', + 'region' => 'NY', + ]), + ), + ); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Created Simulation: %s\n", $simulation->name); +echo sprintf("- ID: %s\n", $simulation->id); +echo sprintf("- Type: %s\n", $simulation->type->getValue()); +echo sprintf("- Notification Setting ID: %s\n", $simulation->notificationSettingId); + +// ┌─── +// │ Get Simulation │ +// └────────────────┘ +try { + $simulation = $paddle->simulations->get($simulationId); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Get Simulation: %s\n", $simulation->name); +echo sprintf("- ID: %s\n", $simulation->id); +echo sprintf("- Type: %s\n", $simulation->type->getValue()); +echo sprintf("- Notification Setting ID: %s\n", $simulation->notificationSettingId); + +// ┌─── +// │ Update Simulation │ +// └───────────────────┘ +try { + $simulation = $paddle->simulations->update( + $simulationId, + new UpdateSimulation( + type: EventTypeName::AddressCreated(), + payload: Address::from([ + 'id' => 'add_01hv8gq3318ktkfengj2r75gfx', + 'country_code' => 'US', + 'status' => 'active', + 'created_at' => '2024-04-12T06:42:58.785000Z', + 'updated_at' => '2024-04-12T06:42:58.785000Z', + 'customer_id' => 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + 'description' => 'Head Office', + 'first_line' => '4050 Jefferson Plaza, 41st Floor', + 'second_line' => null, + 'city' => 'New York', + 'postal_code' => '10021', + 'region' => 'NY', + ]), + ), + ); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Updated Simulation: %s\n", $simulation->name); +echo sprintf("- ID: %s\n", $simulation->id); +echo sprintf("- Type: %s\n", $simulation->type->getValue()); +echo sprintf("- Notification Setting ID: %s\n", $simulation->notificationSettingId); diff --git a/src/Client.php b/src/Client.php index 3bf87de..25bdd0c 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; @@ -32,6 +33,10 @@ use Paddle\SDK\Resources\PricingPreviews\PricingPreviewsClient; use Paddle\SDK\Resources\Products\ProductsClient; use Paddle\SDK\Resources\Reports\ReportsClient; +use Paddle\SDK\Resources\SimulationRunEvents\SimulationRunEventsClient; +use Paddle\SDK\Resources\SimulationRuns\SimulationRunsClient; +use Paddle\SDK\Resources\Simulations\SimulationsClient; +use Paddle\SDK\Resources\SimulationTypes\SimulationTypesClient; use Paddle\SDK\Resources\Subscriptions\SubscriptionsClient; use Paddle\SDK\Resources\Transactions\TransactionsClient; use Psr\Http\Message\RequestFactoryInterface; @@ -45,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; @@ -73,6 +79,10 @@ class Client public readonly NotificationsClient $notifications; public readonly NotificationLogsClient $notificationLogs; public readonly ReportsClient $reports; + public readonly SimulationsClient $simulations; + public readonly SimulationRunsClient $simulationRuns; + public readonly SimulationRunEventsClient $simulationRunEvents; + public readonly SimulationTypesClient $simulationTypes; private readonly HttpAsyncClient $httpClient; private readonly RequestFactoryInterface $requestFactory; @@ -116,6 +126,10 @@ public function __construct( $this->notifications = new NotificationsClient($this); $this->notificationLogs = new NotificationLogsClient($this); $this->reports = new ReportsClient($this); + $this->simulations = new SimulationsClient($this); + $this->simulationRuns = new SimulationRunsClient($this); + $this->simulationRunEvents = new SimulationRunEventsClient($this); + $this->simulationTypes = new SimulationTypesClient($this); } public function getRaw(string|UriInterface $uri, array|HasParameters $parameters = []): ResponseInterface @@ -173,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()], ); diff --git a/src/Entities/Collections/SimulationCollection.php b/src/Entities/Collections/SimulationCollection.php new file mode 100644 index 0000000..95162d4 --- /dev/null +++ b/src/Entities/Collections/SimulationCollection.php @@ -0,0 +1,30 @@ + Simulation::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Simulation + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SimulationRunCollection.php b/src/Entities/Collections/SimulationRunCollection.php new file mode 100644 index 0000000..75ec8dc --- /dev/null +++ b/src/Entities/Collections/SimulationRunCollection.php @@ -0,0 +1,30 @@ + SimulationRun::from($item), $itemsData), + $paginator, + ); + } + + public function current(): SimulationRun + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SimulationRunEventCollection.php b/src/Entities/Collections/SimulationRunEventCollection.php new file mode 100644 index 0000000..3ccb41c --- /dev/null +++ b/src/Entities/Collections/SimulationRunEventCollection.php @@ -0,0 +1,30 @@ + SimulationRunEvent::from($item), $itemsData), + $paginator, + ); + } + + public function current(): SimulationRunEvent + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SimulationTypeCollection.php b/src/Entities/Collections/SimulationTypeCollection.php new file mode 100644 index 0000000..6435d89 --- /dev/null +++ b/src/Entities/Collections/SimulationTypeCollection.php @@ -0,0 +1,30 @@ + SimulationType::from($item), $itemsData), + $paginator, + ); + } + + public function current(): SimulationType + { + return parent::current(); + } +} 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/Entities/Simulation.php b/src/Entities/Simulation.php new file mode 100644 index 0000000..e2c0001 --- /dev/null +++ b/src/Entities/Simulation.php @@ -0,0 +1,42 @@ +isKnown() ? EventTypeName::from($data['type']) : SimulationScenarioType::from($data['type']), + payload: $data['payload'] ? EntityFactory::create($data['type'], $data['payload']) : null, + lastRunAt: isset($data['last_run_at']) ? DateTime::from($data['last_run_at']) : null, + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + ); + } +} diff --git a/src/Entities/Simulation/SimulationKind.php b/src/Entities/Simulation/SimulationKind.php new file mode 100644 index 0000000..87ed29e --- /dev/null +++ b/src/Entities/Simulation/SimulationKind.php @@ -0,0 +1,24 @@ + $events + */ + private function __construct( + public string $id, + public SimulationRunStatus $status, + public EventTypeName|SimulationScenarioType $type, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public array $events, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: SimulationRunStatus::from($data['status']), + type: EventTypeName::from($data['type'])->isKnown() ? EventTypeName::from($data['type']) : SimulationScenarioType::from($data['type']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + events: array_map(fn (array $event): SimulationRunEvent => SimulationRunEvent::from($event), $data['events'] ?? []), + ); + } +} diff --git a/src/Entities/SimulationRun/SimulationRunStatus.php b/src/Entities/SimulationRun/SimulationRunStatus.php new file mode 100644 index 0000000..39d0e3e --- /dev/null +++ b/src/Entities/SimulationRun/SimulationRunStatus.php @@ -0,0 +1,26 @@ + EventTypeName::from($event), $data['events']), + ); + } +} diff --git a/src/Notifications/Entities/EntityFactory.php b/src/Notifications/Entities/EntityFactory.php new file mode 100644 index 0000000..db0d9b8 --- /dev/null +++ b/src/Notifications/Entities/EntityFactory.php @@ -0,0 +1,27 @@ + $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"); + } + + return $entity::from($data); + } +} 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 @@ + $ids + */ + public function __construct( + private readonly Pager|null $pager = null, + private readonly array $ids = [], + ) { + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->getValue(); + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + ]), + ); + } +} diff --git a/src/Resources/SimulationRunEvents/SimulationRunEventsClient.php b/src/Resources/SimulationRunEvents/SimulationRunEventsClient.php new file mode 100644 index 0000000..cb5da49 --- /dev/null +++ b/src/Resources/SimulationRunEvents/SimulationRunEventsClient.php @@ -0,0 +1,71 @@ +client->getRaw("/simulations/{$simulationId}/runs/{$runId}/events", $listOperation), + ); + + return SimulationRunEventCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), SimulationRunEventCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $simulationId, string $runId, string $id): SimulationRunEvent + { + $parser = new ResponseParser( + $this->client->getRaw("/simulations/{$simulationId}/runs/{$runId}/events/{$id}"), + ); + + return SimulationRunEvent::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function replay(string $simulationId, string $runId, string $id): SimulationRunEvent + { + $parser = new ResponseParser( + $this->client->postRaw("/simulations/{$simulationId}/runs/{$runId}/events/{$id}/replay"), + ); + + return SimulationRunEvent::from($parser->getData()); + } +} diff --git a/src/Resources/SimulationRuns/Operations/GetSimulationRuns.php b/src/Resources/SimulationRuns/Operations/GetSimulationRuns.php new file mode 100644 index 0000000..51d4203 --- /dev/null +++ b/src/Resources/SimulationRuns/Operations/GetSimulationRuns.php @@ -0,0 +1,28 @@ +includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->getValue(); + + return array_filter([ + 'include' => implode(',', array_map($enumStringify, $this->includes)), + ]); + } +} diff --git a/src/Resources/SimulationRuns/Operations/Includes.php b/src/Resources/SimulationRuns/Operations/Includes.php new file mode 100644 index 0000000..31adf66 --- /dev/null +++ b/src/Resources/SimulationRuns/Operations/Includes.php @@ -0,0 +1,15 @@ + $ids + */ + public function __construct( + private readonly Pager|null $pager = null, + private readonly array $ids = [], + private readonly array $includes = [], + ) { + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->getValue(); + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + 'include' => implode(',', array_map($enumStringify, $this->includes)), + ]), + ); + } +} diff --git a/src/Resources/SimulationRuns/SimulationRunsClient.php b/src/Resources/SimulationRuns/SimulationRunsClient.php new file mode 100644 index 0000000..af29042 --- /dev/null +++ b/src/Resources/SimulationRuns/SimulationRunsClient.php @@ -0,0 +1,72 @@ +client->getRaw("/simulations/{$simulationId}/runs", $listOperation), + ); + + return SimulationRunCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), SimulationRunCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $simulationId, string $id, GetSimulationRuns $getSimulationRuns = new GetSimulationRuns()): SimulationRun + { + $parser = new ResponseParser( + $this->client->getRaw("/simulations/{$simulationId}/runs/{$id}", $getSimulationRuns), + ); + + return SimulationRun::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(string $simulationId): SimulationRun + { + $parser = new ResponseParser( + $this->client->postRaw("/simulations/{$simulationId}/runs"), + ); + + return SimulationRun::from($parser->getData()); + } +} diff --git a/src/Resources/SimulationTypes/SimulationTypesClient.php b/src/Resources/SimulationTypes/SimulationTypesClient.php new file mode 100644 index 0000000..217e606 --- /dev/null +++ b/src/Resources/SimulationTypes/SimulationTypesClient.php @@ -0,0 +1,39 @@ +client->getRaw('/simulation-types'), + ); + + return SimulationTypeCollection::from($parser->getData()); + } +} diff --git a/src/Resources/Simulations/Operations/CreateSimulation.php b/src/Resources/Simulations/Operations/CreateSimulation.php new file mode 100644 index 0000000..bccaf60 --- /dev/null +++ b/src/Resources/Simulations/Operations/CreateSimulation.php @@ -0,0 +1,34 @@ +filterUndefined([ + 'notification_setting_id' => $this->notificationSettingId, + 'type' => $this->type, + 'name' => $this->name, + 'payload' => $this->payload, + ]); + } +} diff --git a/src/Resources/Simulations/Operations/ListSimulations.php b/src/Resources/Simulations/Operations/ListSimulations.php new file mode 100644 index 0000000..9a5add5 --- /dev/null +++ b/src/Resources/Simulations/Operations/ListSimulations.php @@ -0,0 +1,51 @@ + $notificationSettingIds + * @param array $ids + * @param array $statuses + */ + public function __construct( + private readonly Pager|null $pager = null, + private readonly array $notificationSettingIds = [], + private readonly array $ids = [], + private readonly array $statuses = [], + ) { + if ($invalid = array_filter($this->notificationSettingIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('notificationSettingIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof SimulationStatus)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', SimulationStatus::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->getValue(); + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'notification_setting_id' => implode(',', $this->notificationSettingIds), + 'id' => implode(',', $this->ids), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + ]), + ); + } +} diff --git a/src/Resources/Simulations/Operations/UpdateSimulation.php b/src/Resources/Simulations/Operations/UpdateSimulation.php new file mode 100644 index 0000000..de2cc97 --- /dev/null +++ b/src/Resources/Simulations/Operations/UpdateSimulation.php @@ -0,0 +1,37 @@ +filterUndefined([ + 'notification_setting_id' => $this->notificationSettingId, + 'type' => $this->type, + 'name' => $this->name, + 'status' => $this->status, + 'payload' => $this->payload, + ]); + } +} diff --git a/src/Resources/Simulations/SimulationsClient.php b/src/Resources/Simulations/SimulationsClient.php new file mode 100644 index 0000000..0e509f3 --- /dev/null +++ b/src/Resources/Simulations/SimulationsClient.php @@ -0,0 +1,86 @@ +client->getRaw('/simulations', $listOperation), + ); + + return SimulationCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), SimulationCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id): Simulation + { + $parser = new ResponseParser( + $this->client->getRaw("/simulations/{$id}"), + ); + + return Simulation::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateSimulation $createOperation): Simulation + { + $parser = new ResponseParser( + $this->client->postRaw('/simulations', $createOperation), + ); + + return Simulation::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateSimulation $operation): Simulation + { + $parser = new ResponseParser( + $this->client->patchRaw("/simulations/{$id}", $operation), + ); + + return Simulation::from($parser->getData()); + } +} diff --git a/tests/Functional/Resources/Events/EventsClientTest.php b/tests/Functional/Resources/Events/EventsClientTest.php index 489a010..304bef2 100644 --- a/tests/Functional/Resources/Events/EventsClientTest.php +++ b/tests/Functional/Resources/Events/EventsClientTest.php @@ -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; @@ -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, + ); + } } 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/SimulationRunEventsClientTest.php b/tests/Functional/Resources/SimulationRunEvents/SimulationRunEventsClientTest.php new file mode 100644 index 0000000..0ab4cd7 --- /dev/null +++ b/tests/Functional/Resources/SimulationRunEvents/SimulationRunEventsClientTest.php @@ -0,0 +1,202 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + */ + public function it_uses_expected_payload_on_replay(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/full_entity'))); + $this->client->simulationRunEvents->replay('ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', 'ntfsimevt_01j82j3tr93j99gfv26tsngc27'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events/ntfsimevt_01j82j3tr93j99gfv26tsngc27/replay', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString('{}', (string) $request->getBody()); + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + string $simulationId, + string $runId, + ListSimulationRunEvents $listOperation, + string $expectedUri, + ): void { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_default'))); + $this->client->simulationRunEvents->list($simulationId, $runId, $listOperation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents(), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With ids filter' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents(ids: ['ntfsimevt_01j82hf8jrwjsf9337a35tqghp', 'ntfsimevt_03j82hf8jrwjsf9337a35tqghx']), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?id=ntfsimevt_01j82hf8jrwjsf9337a35tqghp,ntfsimevt_03j82hf8jrwjsf9337a35tqghx', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With default pagination' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents( + new Pager(), + ), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?order_by=id[asc]&per_page=50', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With pagination after' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents( + new Pager(after: 'ntfsimevt_01j82hf8jrwjsf9337a35tqghp'), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_01j82hf8jrwjsf9337a35tqghp&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID asc' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents( + new Pager(after: 'ntfsimevt_01j82hf8jrwjsf9337a35tqghp', orderBy: OrderBy::idAscending()), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_01j82hf8jrwjsf9337a35tqghp&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID desc' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents( + new Pager(after: 'ntfsimevt_01j82hf8jrwjsf9337a35tqghp', orderBy: OrderBy::idDescending()), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_01j82hf8jrwjsf9337a35tqghp&order_by=id[desc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID asc, per page' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + new ListSimulationRunEvents( + new Pager(after: 'ntfsimevt_01j82hf8jrwjsf9337a35tqghp', orderBy: OrderBy::idDescending(), perPage: 10), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_01j82hf8jrwjsf9337a35tqghp&order_by=id[desc]&per_page=10', + Environment::SANDBOX->baseUrl(), + ), + ]; + } + + /** @test */ + public function it_can_paginate(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_one'))); + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_two'))); + + $collection = $this->client->simulationRunEvents->list('ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p'); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events', + urldecode((string) $request->getUri()), + ); + + $allSimulations = iterator_to_array($collection); + self::assertCount(2, $allSimulations); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_01j82hf8jrwjsf9337a35tqghp', + urldecode((string) $request->getUri()), + ); + } + + /** + * @test + * + * @dataProvider getRequestProvider + */ + public function get_hits_expected_uri( + string $simulationId, + string $runId, + string $id, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->simulationRunEvents->get($simulationId, $runId, $id); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Default' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p', + 'ntfsimevt_01j82j3tr93j99gfv26tsngc27', + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events/ntfsimevt_01j82j3tr93j99gfv26tsngc27', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json new file mode 100644 index 0000000..fe66a71 --- /dev/null +++ b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/full_entity.json @@ -0,0 +1,30 @@ +{ + "data": { + "id": "ntfsimevt_01j82j3tr93j99gfv26tsngc27", + "status": "pending", + "event_type": "address.created", + "payload": { + "id": "add_01hv8gq3318ktkfengj2r75gfx", + "city": "New York", + "region": "NY", + "status": "active", + "created_at": "2024-04-12T06:42:58.785000Z", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "updated_at": "2024-04-12T06:42:58.785000Z", + "custom_data": null, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "description": "Head Office", + "import_meta": null, + "postal_code": "10021", + "second_line": null, + "country_code": "US" + }, + "request": null, + "response": null, + "created_at": "2024-09-18T12:36:01.929359Z", + "updated_at": "2024-09-18T12:37:35.659788Z" + }, + "meta": { + "request_id": "0369cb98-380b-40a9-bfd8-8ac4856c05f6" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_default.json b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_default.json new file mode 100644 index 0000000..1393c85 --- /dev/null +++ b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_default.json @@ -0,0 +1,127 @@ +{ + "data": [ + { + "id": "ntfsimevt_01j82hf8jrwjsf9337a35tqghp", + "status": "success", + "event_type": "adjustment.updated", + "payload": { + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "type": "partial", + "amount": "100", + "totals": { + "tax": "8", + "total": "100", + "subtotal": "92" + }, + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "proration": null + } + ], + "action": "refund", + "reason": "error", + "status": "pending_approval", + "totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "currency_code": "USD", + "payout_totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "credit_applied_to_balance": null + }, + "request": { + "body": "{\"event_id\":\"ntfsimevt_01j82hf8jrwjsf9337a35tqghp\",\"event_type\":\"adjustment.updated\",\"occurred_at\":\"2024-09-18T12:24:47.960617Z\",\"data\":{\"id\":\"adj_01hvgf2s84dr6reszzg29zbvcm\",\"items\":[{\"id\":\"adjitm_01hvgf2s84dr6reszzg2gx70gj\",\"type\":\"partial\",\"amount\":\"100\",\"totals\":{\"tax\":\"8\",\"total\":\"100\",\"subtotal\":\"92\"},\"item_id\":\"txnitm_01hvcc94b7qgz60qmrqmbm19zw\",\"proration\":null}],\"action\":\"refund\",\"reason\":\"error\",\"status\":\"pending_approval\",\"totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"created_at\":\"2024-04-15T08:48:20.239695Z\",\"updated_at\":\"2024-04-15T08:48:20.239695Z\",\"customer_id\":\"ctm_01hv6y1jedq4p1n0yqn5ba3ky4\",\"currency_code\":\"USD\",\"payout_totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"transaction_id\":\"txn_01hvcc93znj3mpqt1tenkjb04y\",\"subscription_id\":\"sub_01hvccbx32q2gb40sqx7n42430\",\"credit_applied_to_balance\":null}}" + }, + "response": { + "body": "{\"status\":\"NOTICE\",\"message\":\"Unknown source ID. If you just created that source, it can take a minute to propagate. Be assured that if the source is valid, the webhook was handled successfully. Check your dashboard to inspect the request: https://dashboard.hookdeck.com/\",\"request_id\":\"req_6ob9lGLoCA2qKWS6VGXR\"}", + "status_code": 200 + }, + "created_at": "2024-09-18T12:24:47.960617Z", + "updated_at": "2024-09-18T12:24:48.309530Z" + }, + { + "id": "ntfsimevt_03j82hf8jrwjsf9337a35tqghx", + "status": "success", + "event_type": "adjustment.updated", + "payload": { + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "type": "partial", + "amount": "100", + "totals": { + "tax": "8", + "total": "100", + "subtotal": "92" + }, + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "proration": null + } + ], + "action": "refund", + "reason": "error", + "status": "pending_approval", + "totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "currency_code": "USD", + "payout_totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "credit_applied_to_balance": null + }, + "request": { + "body": "{\"event_id\":\"ntfsimevt_01j82hf8jrwjsf9337a35tqghp\",\"event_type\":\"adjustment.updated\",\"occurred_at\":\"2024-09-18T12:24:47.960617Z\",\"data\":{\"id\":\"adj_01hvgf2s84dr6reszzg29zbvcm\",\"items\":[{\"id\":\"adjitm_01hvgf2s84dr6reszzg2gx70gj\",\"type\":\"partial\",\"amount\":\"100\",\"totals\":{\"tax\":\"8\",\"total\":\"100\",\"subtotal\":\"92\"},\"item_id\":\"txnitm_01hvcc94b7qgz60qmrqmbm19zw\",\"proration\":null}],\"action\":\"refund\",\"reason\":\"error\",\"status\":\"pending_approval\",\"totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"created_at\":\"2024-04-15T08:48:20.239695Z\",\"updated_at\":\"2024-04-15T08:48:20.239695Z\",\"customer_id\":\"ctm_01hv6y1jedq4p1n0yqn5ba3ky4\",\"currency_code\":\"USD\",\"payout_totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"transaction_id\":\"txn_01hvcc93znj3mpqt1tenkjb04y\",\"subscription_id\":\"sub_01hvccbx32q2gb40sqx7n42430\",\"credit_applied_to_balance\":null}}" + }, + "response": { + "body": "{\"status\":\"NOTICE\",\"message\":\"Unknown source ID. If you just created that source, it can take a minute to propagate. Be assured that if the source is valid, the webhook was handled successfully. Check your dashboard to inspect the request: https://dashboard.hookdeck.com/\",\"request_id\":\"req_6ob9lGLoCA2qKWS6VGXR\"}", + "status_code": 200 + }, + "created_at": "2024-09-18T13:24:47.960617Z", + "updated_at": "2024-09-18T13:24:48.309530Z" + } + ], + "meta": { + "pagination": { + "per_page": 50, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_03j82hf8jrwjsf9337a35tqghx", + "has_more": false + }, + "request_id": "f1726ebc-383d-45da-ab71-2c811b3c6ce8" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 0000000..463c867 --- /dev/null +++ b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,70 @@ +{ + "data": [ + { + "id": "ntfsimevt_01j82hf8jrwjsf9337a35tqghp", + "status": "success", + "event_type": "adjustment.updated", + "payload": { + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "type": "partial", + "amount": "100", + "totals": { + "tax": "8", + "total": "100", + "subtotal": "92" + }, + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "proration": null + } + ], + "action": "refund", + "reason": "error", + "status": "pending_approval", + "totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "currency_code": "USD", + "payout_totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "credit_applied_to_balance": null + }, + "request": { + "body": "{\"event_id\":\"ntfsimevt_01j82hf8jrwjsf9337a35tqghp\",\"event_type\":\"adjustment.updated\",\"occurred_at\":\"2024-09-18T12:24:47.960617Z\",\"data\":{\"id\":\"adj_01hvgf2s84dr6reszzg29zbvcm\",\"items\":[{\"id\":\"adjitm_01hvgf2s84dr6reszzg2gx70gj\",\"type\":\"partial\",\"amount\":\"100\",\"totals\":{\"tax\":\"8\",\"total\":\"100\",\"subtotal\":\"92\"},\"item_id\":\"txnitm_01hvcc94b7qgz60qmrqmbm19zw\",\"proration\":null}],\"action\":\"refund\",\"reason\":\"error\",\"status\":\"pending_approval\",\"totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"created_at\":\"2024-04-15T08:48:20.239695Z\",\"updated_at\":\"2024-04-15T08:48:20.239695Z\",\"customer_id\":\"ctm_01hv6y1jedq4p1n0yqn5ba3ky4\",\"currency_code\":\"USD\",\"payout_totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"transaction_id\":\"txn_01hvcc93znj3mpqt1tenkjb04y\",\"subscription_id\":\"sub_01hvccbx32q2gb40sqx7n42430\",\"credit_applied_to_balance\":null}}" + }, + "response": { + "body": "{\"status\":\"NOTICE\",\"message\":\"Unknown source ID. If you just created that source, it can take a minute to propagate. Be assured that if the source is valid, the webhook was handled successfully. Check your dashboard to inspect the request: https://dashboard.hookdeck.com/\",\"request_id\":\"req_6ob9lGLoCA2qKWS6VGXR\"}", + "status_code": 200 + }, + "created_at": "2024-09-18T12:24:47.960617Z", + "updated_at": "2024-09-18T12:24:48.309530Z" + } + ], + "meta": { + "pagination": { + "per_page": 1, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_01j82hf8jrwjsf9337a35tqghp", + "has_more": true + }, + "request_id": "f1726ebc-383d-45da-ab71-2c811b3c6ce8" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 0000000..94dee9f --- /dev/null +++ b/tests/Functional/Resources/SimulationRunEvents/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,70 @@ +{ + "data": [ + { + "id": "ntfsimevt_03j82hf8jrwjsf9337a35tqghx", + "status": "success", + "event_type": "adjustment.updated", + "payload": { + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "type": "partial", + "amount": "100", + "totals": { + "tax": "8", + "total": "100", + "subtotal": "92" + }, + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "proration": null + } + ], + "action": "refund", + "reason": "error", + "status": "pending_approval", + "totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "currency_code": "USD", + "payout_totals": { + "fee": "5", + "tax": "8", + "total": "100", + "earnings": "87", + "subtotal": "92", + "currency_code": "USD" + }, + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "credit_applied_to_balance": null + }, + "request": { + "body": "{\"event_id\":\"ntfsimevt_01j82hf8jrwjsf9337a35tqghp\",\"event_type\":\"adjustment.updated\",\"occurred_at\":\"2024-09-18T12:24:47.960617Z\",\"data\":{\"id\":\"adj_01hvgf2s84dr6reszzg29zbvcm\",\"items\":[{\"id\":\"adjitm_01hvgf2s84dr6reszzg2gx70gj\",\"type\":\"partial\",\"amount\":\"100\",\"totals\":{\"tax\":\"8\",\"total\":\"100\",\"subtotal\":\"92\"},\"item_id\":\"txnitm_01hvcc94b7qgz60qmrqmbm19zw\",\"proration\":null}],\"action\":\"refund\",\"reason\":\"error\",\"status\":\"pending_approval\",\"totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"created_at\":\"2024-04-15T08:48:20.239695Z\",\"updated_at\":\"2024-04-15T08:48:20.239695Z\",\"customer_id\":\"ctm_01hv6y1jedq4p1n0yqn5ba3ky4\",\"currency_code\":\"USD\",\"payout_totals\":{\"fee\":\"5\",\"tax\":\"8\",\"total\":\"100\",\"earnings\":\"87\",\"subtotal\":\"92\",\"currency_code\":\"USD\"},\"transaction_id\":\"txn_01hvcc93znj3mpqt1tenkjb04y\",\"subscription_id\":\"sub_01hvccbx32q2gb40sqx7n42430\",\"credit_applied_to_balance\":null}}" + }, + "response": { + "body": "{\"status\":\"NOTICE\",\"message\":\"Unknown source ID. If you just created that source, it can take a minute to propagate. Be assured that if the source is valid, the webhook was handled successfully. Check your dashboard to inspect the request: https://dashboard.hookdeck.com/\",\"request_id\":\"req_6ob9lGLoCA2qKWS6VGXR\"}", + "status_code": 200 + }, + "created_at": "2024-09-18T13:24:47.960617Z", + "updated_at": "2024-09-18T13:24:48.309530Z" + } + ], + "meta": { + "pagination": { + "per_page": 1, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p/events?after=ntfsimevt_03j82hf8jrwjsf9337a35tqghx", + "has_more": false + }, + "request_id": "f1726ebc-383d-45da-ab71-2c811b3c6ce8" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRuns/SimulationRunsClientTest.php b/tests/Functional/Resources/SimulationRuns/SimulationRunsClientTest.php new file mode 100644 index 0000000..65e4cbb --- /dev/null +++ b/tests/Functional/Resources/SimulationRuns/SimulationRunsClientTest.php @@ -0,0 +1,190 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + */ + public function it_uses_expected_payload_on_create(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/full_entity'))); + $this->client->simulationRuns->create('ntfsim_01j82g2mggsgjpb3mjg0xq6p5k'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString('{}', (string) $request->getBody()); + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + string $simulationId, + ListSimulationRuns $listOperation, + string $expectedUri, + ): void { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_default'))); + $this->client->simulationRuns->list($simulationId, $listOperation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns(), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With ids filter' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns(ids: ['ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', 'ntfsimrun_01j82gjx7fxdc27bsqxf311cbe']), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?id=ntfsim_01j82g2mggsgjpb3mjg0xq6p5k,ntfsimrun_01j82gjx7fxdc27bsqxf311cbe', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With default pagination' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns( + new Pager(), + ), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?order_by=id[asc]&per_page=50', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With pagination after' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns( + new Pager(after: 'ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3'), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID asc' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns( + new Pager(after: 'ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3', orderBy: OrderBy::idAscending()), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID desc' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns( + new Pager(after: 'ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3', orderBy: OrderBy::idDescending()), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3&order_by=id[desc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID asc, per page' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new ListSimulationRuns( + new Pager(after: 'ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3', orderBy: OrderBy::idDescending(), perPage: 10), + ), + sprintf( + '%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3&order_by=id[desc]&per_page=10', + Environment::SANDBOX->baseUrl(), + ), + ]; + } + + /** @test */ + public function it_can_paginate(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_one'))); + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_two'))); + + $collection = $this->client->simulationRuns->list('ntfsim_01j82g2mggsgjpb3mjg0xq6p5k'); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs', + urldecode((string) $request->getUri()), + ); + + $allSimulations = iterator_to_array($collection); + self::assertCount(2, $allSimulations); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3', + urldecode((string) $request->getUri()), + ); + } + + /** + * @test + * + * @dataProvider getRequestProvider + */ + public function get_hits_expected_uri( + string $simulationId, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->simulationRuns->get($simulationId, 'ntfsimrun_01j82h13n87yq2sfv187hm2r0p'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Default' => [ + 'ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs/ntfsimrun_01j82h13n87yq2sfv187hm2r0p', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/SimulationRuns/_fixtures/response/full_entity.json b/tests/Functional/Resources/SimulationRuns/_fixtures/response/full_entity.json new file mode 100644 index 0000000..e9a1bbd --- /dev/null +++ b/tests/Functional/Resources/SimulationRuns/_fixtures/response/full_entity.json @@ -0,0 +1,12 @@ +{ + "data": { + "id": "ntfsimrun_01j82h13n87yq2sfv187hm2r0p", + "status": "pending", + "type": "subscription_creation", + "created_at": "2024-09-18T12:17:04.168467Z", + "updated_at": "2024-09-18T12:17:04.168467Z" + }, + "meta": { + "request_id": "2201997e-d23b-4a49-acdb-8a9f8cc72ff6" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_default.json b/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_default.json new file mode 100644 index 0000000..674b511 --- /dev/null +++ b/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_default.json @@ -0,0 +1,27 @@ +{ + "data": [ + { + "id": "ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3", + "status": "completed", + "type": "subscription_creation", + "created_at": "2024-09-18T12:14:15.628606Z", + "updated_at": "2024-09-18T12:14:27.244695Z" + }, + { + "id": "ntfsimrun_01j82gjx7fxdc27bsqxf311cbe", + "status": "completed", + "type": "subscription_creation", + "created_at": "2024-09-18T12:09:18.831870Z", + "updated_at": "2024-09-18T12:09:30.613445Z" + } + ], + "meta": { + "pagination": { + "per_page": 50, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gjx7fxdc27bsqxf311cbe", + "has_more": false + }, + "request_id": "12a52c1d-e899-4401-baf4-e836e3178062" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 0000000..28efb03 --- /dev/null +++ b/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,20 @@ +{ + "data": [ + { + "id": "ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3", + "status": "completed", + "type": "subscription_creation", + "created_at": "2024-09-18T12:14:15.628606Z", + "updated_at": "2024-09-18T12:14:27.244695Z" + } + ], + "meta": { + "pagination": { + "per_page": 1, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gvz2cgw08p7mak3gcd3a3", + "has_more": true + }, + "request_id": "12a52c1d-e899-4401-baf4-e836e3178062" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 0000000..3618f69 --- /dev/null +++ b/tests/Functional/Resources/SimulationRuns/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,20 @@ +{ + "data": [ + { + "id": "ntfsimrun_01j82gjx7fxdc27bsqxf311cbe", + "status": "completed", + "type": "subscription_creation", + "created_at": "2024-09-18T12:09:18.831870Z", + "updated_at": "2024-09-18T12:09:30.613445Z" + } + ], + "meta": { + "pagination": { + "per_page": 1, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k/runs?after=ntfsimrun_01j82gjx7fxdc27bsqxf311cbe", + "has_more": false + }, + "request_id": "12a52c1d-e899-4401-baf4-e836e3178062" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/SimulationTypes/SimulationTypesClientTest.php b/tests/Functional/Resources/SimulationTypes/SimulationTypesClientTest.php new file mode 100644 index 0000000..2775959 --- /dev/null +++ b/tests/Functional/Resources/SimulationTypes/SimulationTypesClientTest.php @@ -0,0 +1,47 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** @test */ + public function list_hits_expected_uri(): void + { + $expectedUri = sprintf('%s/simulation-types', Environment::SANDBOX->baseUrl()); + + $this->mockClient->addResponse( + new Response(200, body: self::readRawJsonFixture('response/list_default')), + ); + $this->client->simulationTypes->list(); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } +} diff --git a/tests/Functional/Resources/SimulationTypes/_fixtures/response/list_default.json b/tests/Functional/Resources/SimulationTypes/_fixtures/response/list_default.json new file mode 100644 index 0000000..91bbdad --- /dev/null +++ b/tests/Functional/Resources/SimulationTypes/_fixtures/response/list_default.json @@ -0,0 +1,523 @@ +{ + "data": [ + { + "name": "subscription_creation", + "label": "Subscription created from a checkout", + "description": "Occurs when a subscription is created.", + "group": "Subscription", + "type": "scenario", + "events": [ + "transaction.created", + "customer.created", + "address.created", + "transaction.updated", + "transaction.ready", + "transaction.updated", + "transaction.paid", + "subscription.created", + "subscription.activated", + "transaction.updated", + "transaction.updated", + "transaction.completed" + ] + }, + { + "name": "subscription_renewal", + "label": "Subscription renewed successfully", + "description": "Occurs when a subscription is renewed.", + "group": "Subscription", + "type": "scenario", + "events": [ + "subscription.updated", + "transaction.created", + "transaction.billed", + "transaction.updated", + "transaction.paid", + "transaction.updated", + "transaction.completed" + ] + }, + { + "name": "subscription_pause", + "label": "Subscription paused by customer", + "description": "Occurs when a subscription is paused.", + "group": "Subscription", + "type": "scenario", + "events": [ + "subscription.updated", + "subscription.paused" + ] + }, + { + "name": "subscription_resume", + "label": "Subscription resumed after being paused", + "description": "Occurs when a subscription is resumed after being paused.", + "group": "Subscription", + "type": "scenario", + "events": [ + "subscription.updated", + "subscription.resumed", + "transaction.created", + "transaction.billed", + "transaction.updated", + "transaction.paid", + "transaction.updated", + "transaction.completed" + ] + }, + { + "name": "subscription_cancellation", + "label": "Subscription canceled by customer", + "description": "Occurs when a subscription is canceled.", + "group": "Subscription", + "type": "scenario", + "events": [ + "subscription.updated", + "subscription.canceled" + ] + }, + { + "name": "subscription.activated", + "label": "subscription.activated", + "description": "Occurs when a subscription becomes active. Its status field changes to active. This means any trial period has elapsed and Paddle has successfully billed the customer.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.activated" + ] + }, + { + "name": "subscription.canceled", + "label": "subscription.canceled", + "description": "Occurs when a subscription is canceled. Its status field changes to canceled.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.canceled" + ] + }, + { + "name": "subscription.created", + "label": "subscription.created", + "description": "Occurs when a subscription is created. subscription.trialing or subscription.activated typically follow.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.created" + ] + }, + { + "name": "subscription.past_due", + "label": "subscription.past_due", + "description": "Occurs when a subscription has an unpaid transaction. Its status changes to past_due.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.past_due" + ] + }, + { + "name": "subscription.paused", + "label": "subscription.paused", + "description": "Occurs when a subscription is paused. Its status field changes to paused.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.paused" + ] + }, + { + "name": "subscription.resumed", + "label": "subscription.resumed", + "description": "Occurs when a subscription is resumed after being paused. Its status field changes to active.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.resumed" + ] + }, + { + "name": "subscription.trialing", + "label": "subscription.trialing", + "description": "Occurs when a subscription enters trial period.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.trialing" + ] + }, + { + "name": "subscription.updated", + "label": "subscription.updated", + "description": "Occurs when a subscription is updated.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.updated" + ] + }, + { + "name": "subscription.imported", + "label": "subscription.imported", + "description": "Occurs when a subscription is imported.", + "group": "Subscription", + "type": "single_event", + "events": [ + "subscription.imported" + ] + }, + { + "name": "address.created", + "label": "address.created", + "description": "Occurs when an address is created.", + "group": "Address", + "type": "single_event", + "events": [ + "address.created" + ] + }, + { + "name": "address.updated", + "label": "address.updated", + "description": "Occurs when an address is updated.", + "group": "Address", + "type": "single_event", + "events": [ + "address.updated" + ] + }, + { + "name": "address.imported", + "label": "address.imported", + "description": "Occurs when a address is imported.", + "group": "Address", + "type": "single_event", + "events": [ + "address.imported" + ] + }, + { + "name": "adjustment.created", + "label": "adjustment.created", + "description": "Occurs when an adjustment is created.", + "group": "Adjustment", + "type": "single_event", + "events": [ + "adjustment.created" + ] + }, + { + "name": "adjustment.updated", + "label": "adjustment.updated", + "description": "Occurs when an adjustment is updated, the only time an adjustment will be updated is when the status changes from pending to approved or from pending to rejected.", + "group": "Adjustment", + "type": "single_event", + "events": [ + "adjustment.updated" + ] + }, + { + "name": "business.created", + "label": "business.created", + "description": "Occurs when a business is created.", + "group": "Business", + "type": "single_event", + "events": [ + "business.created" + ] + }, + { + "name": "business.updated", + "label": "business.updated", + "description": "Occurs when a business is updated.", + "group": "Business", + "type": "single_event", + "events": [ + "business.updated" + ] + }, + { + "name": "business.imported", + "label": "business.imported", + "description": "Occurs when a business is imported.", + "group": "Business", + "type": "single_event", + "events": [ + "business.imported" + ] + }, + { + "name": "customer.created", + "label": "customer.created", + "description": "Occurs when a customer is created.", + "group": "Customer", + "type": "single_event", + "events": [ + "customer.created" + ] + }, + { + "name": "customer.updated", + "label": "customer.updated", + "description": "Occurs when a customer is updated.", + "group": "Customer", + "type": "single_event", + "events": [ + "customer.updated" + ] + }, + { + "name": "customer.imported", + "label": "customer.imported", + "description": "Occurs when a customer is imported.", + "group": "Customer", + "type": "single_event", + "events": [ + "customer.imported" + ] + }, + { + "name": "payment_method.saved", + "label": "payment_method.saved", + "description": "Occurs when a customer saves a payment method to their account.", + "group": "Payment Method", + "type": "single_event", + "events": [ + "payment_method.saved" + ] + }, + { + "name": "payment_method.deleted", + "label": "payment_method.deleted", + "description": "Occurs when a customer removes a payment method from their account.", + "group": "Payment Method", + "type": "single_event", + "events": [ + "payment_method.deleted" + ] + }, + { + "name": "discount.created", + "label": "discount.created", + "description": "Occurs when a discount is created.", + "group": "Discount", + "type": "single_event", + "events": [ + "discount.created" + ] + }, + { + "name": "discount.updated", + "label": "discount.updated", + "description": "Occurs when a discount is updated.", + "group": "Discount", + "type": "single_event", + "events": [ + "discount.updated" + ] + }, + { + "name": "discount.imported", + "label": "discount.imported", + "description": "Occurs when a discount is imported.", + "group": "Discount", + "type": "single_event", + "events": [ + "discount.imported" + ] + }, + { + "name": "payout.created", + "label": "payout.created", + "description": "Occurs when a payout is created.", + "group": "Payout", + "type": "single_event", + "events": [ + "payout.created" + ] + }, + { + "name": "payout.paid", + "label": "payout.paid", + "description": "Occurs when a payout is paid.", + "group": "Payout", + "type": "single_event", + "events": [ + "payout.paid" + ] + }, + { + "name": "price.created", + "label": "price.created", + "description": "Occurs when a price is created.", + "group": "Price", + "type": "single_event", + "events": [ + "price.created" + ] + }, + { + "name": "price.updated", + "label": "price.updated", + "description": "Occurs when a price is updated.", + "group": "Price", + "type": "single_event", + "events": [ + "price.updated" + ] + }, + { + "name": "price.imported", + "label": "price.imported", + "description": "Occurs when a price is imported.", + "group": "Price", + "type": "single_event", + "events": [ + "price.imported" + ] + }, + { + "name": "product.created", + "label": "product.created", + "description": "Occurs when a product is created.", + "group": "Product", + "type": "single_event", + "events": [ + "product.created" + ] + }, + { + "name": "product.updated", + "label": "product.updated", + "description": "Occurs when a product is updated.", + "group": "Product", + "type": "single_event", + "events": [ + "product.updated" + ] + }, + { + "name": "product.imported", + "label": "product.imported", + "description": "Occurs when a product is imported.", + "group": "Product", + "type": "single_event", + "events": [ + "product.imported" + ] + }, + { + "name": "report.created", + "label": "report.created", + "description": "Occurs when a report is created.", + "group": "Report", + "type": "single_event", + "events": [ + "report.created" + ] + }, + { + "name": "report.updated", + "label": "report.updated", + "description": "Occurs when a report is updated.", + "group": "Report", + "type": "single_event", + "events": [ + "report.updated" + ] + }, + { + "name": "transaction.billed", + "label": "transaction.billed", + "description": "Occurs when a transaction is billed. Its status field changes to billed and billed_at is populated.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.billed" + ] + }, + { + "name": "transaction.canceled", + "label": "transaction.canceled", + "description": "Occurs when a transaction is canceled. Its status field changes to canceled.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.canceled" + ] + }, + { + "name": "transaction.completed", + "label": "transaction.completed", + "description": "Occurs when a transaction is completed. Its status field changes to completed.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.completed" + ] + }, + { + "name": "transaction.created", + "label": "transaction.created", + "description": "Occurs when a transaction is created.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.created" + ] + }, + { + "name": "transaction.paid", + "label": "transaction.paid", + "description": "Occurs when a transaction is paid. Its status field changes to paid.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.paid" + ] + }, + { + "name": "transaction.past_due", + "label": "transaction.past_due", + "description": "Occurs when a transaction becomes past due. Its status field changes to past_due.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.past_due" + ] + }, + { + "name": "transaction.payment_failed", + "label": "transaction.payment_failed", + "description": "Occurs when a payment fails for a transaction. The payments array is updated with details of the payment attempt.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.payment_failed" + ] + }, + { + "name": "transaction.ready", + "label": "transaction.ready", + "description": "Occurs when a transaction is ready to be billed. Its status field changes to ready.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.ready" + ] + }, + { + "name": "transaction.updated", + "label": "transaction.updated", + "description": "Occurs when a transaction is updated.", + "group": "Transaction", + "type": "single_event", + "events": [ + "transaction.updated" + ] + } + ], + "meta": { + "request_id": "76905a23-8206-44ca-8821-7237b26f59d1" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/SimulationsClientTest.php b/tests/Functional/Resources/Simulations/SimulationsClientTest.php new file mode 100644 index 0000000..aea1e7d --- /dev/null +++ b/tests/Functional/Resources/Simulations/SimulationsClientTest.php @@ -0,0 +1,270 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateSimulation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->simulations->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/simulations', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateSimulation( + notificationSettingId: 'ntfset_01j82d983j814ypzx7m1fw2jpz', + type: EventTypeName::AddressCreated(), + name: 'New US address created for CRM', + payload: EntityFactory::create('address.created', json_decode(self::readRawJsonFixture('request/address_created_payload'), true)), + ), + 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'], + ]), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateSimulation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->simulations->update('ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateSimulation(status: SimulationStatus::Archived()), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateSimulation( + notificationSettingId: 'ntfset_01j82d983j814ypzx7m1fw2jpz', + name: 'New simulation name', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateSimulation( + notificationSettingId: 'ntfset_01j82d983j814ypzx7m1fw2jpz', + type: EventTypeName::AdjustmentUpdated(), + name: 'Refund approved', + status: SimulationStatus::Active(), + payload: EntityFactory::create('adjustment.updated', json_decode(self::readRawJsonFixture('request/adjustment_updated_payload'), true)), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity_adjustment_updated')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListSimulations $listOperation, + string $expectedUri, + ): void { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_default'))); + $this->client->simulations->list($listOperation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListSimulations(), + sprintf('%s/simulations', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With status filter' => [ + new ListSimulations(statuses: [SimulationStatus::Active()]), + sprintf('%s/simulations?status=active', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With default pagination' => [ + new ListSimulations( + new Pager(), + ), + sprintf('%s/simulations?order_by=id[asc]&per_page=50', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With pagination after' => [ + new ListSimulations( + new Pager('ntfsim_01j82d9tc19c67jds5vzbzjcns'), + ), + sprintf( + '%s/simulations?after=ntfsim_01j82d9tc19c67jds5vzbzjcns&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID asc' => [ + new ListSimulations( + new Pager('ntfsim_01j82d9tc19c67jds5vzbzjcns', OrderBy::idAscending()), + ), + sprintf( + '%s/simulations?after=ntfsim_01j82d9tc19c67jds5vzbzjcns&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID desc' => [ + new ListSimulations( + new Pager('ntfsim_01j82d9tc19c67jds5vzbzjcns', OrderBy::idDescending()), + ), + sprintf( + '%s/simulations?after=ntfsim_01j82d9tc19c67jds5vzbzjcns&order_by=id[desc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'With pagination after, order by ID asc, per page' => [ + new ListSimulations( + new Pager('ntfsim_01j82d9tc19c67jds5vzbzjcns', OrderBy::idDescending(), 10), + ), + sprintf( + '%s/simulations?after=ntfsim_01j82d9tc19c67jds5vzbzjcns&order_by=id[desc]&per_page=10', + Environment::SANDBOX->baseUrl(), + ), + ]; + } + + /** @test */ + public function it_can_paginate(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_one'))); + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_two'))); + + $collection = $this->client->simulations->list(); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/simulations', + urldecode((string) $request->getUri()), + ); + + $allSimulations = iterator_to_array($collection); + self::assertCount(2, $allSimulations); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/simulations?after=ntfsim_01j82fs5pvrdse93e1kawqy2fr', + urldecode((string) $request->getUri()), + ); + } + + /** + * @test + * + * @dataProvider getRequestProvider + */ + public function get_hits_expected_uri( + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->simulations->get('ntfsim_01j82g2mggsgjpb3mjg0xq6p5k'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Default' => [ + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/simulations/ntfsim_01j82g2mggsgjpb3mjg0xq6p5k', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json b/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json new file mode 100644 index 0000000..f160c96 --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/request/address_created_payload.json @@ -0,0 +1,16 @@ +{ + "id": "add_01hv8gq3318ktkfengj2r75gfx", + "city": "New York", + "region": "NY", + "status": "active", + "created_at": "2024-04-12T06:42:58.785000Z", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "updated_at": "2024-04-12T06:42:58.785000Z", + "custom_data": null, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "description": "Head Office", + "import_meta": null, + "postal_code": "10021", + "second_line": null, + "country_code": "US" +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/adjustment_updated_payload.json b/tests/Functional/Resources/Simulations/_fixtures/request/adjustment_updated_payload.json new file mode 100644 index 0000000..3ec5b3d --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/request/adjustment_updated_payload.json @@ -0,0 +1,50 @@ +{ + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "action": "refund", + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "customer_id": "ctm_01hrffh7gvp29kc7xahm8wddwa", + "reason": "error", + "credit_applied_to_balance": null, + "currency_code": "USD", + "status": "approved", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "type": "partial", + "amount": "100", + "proration": null, + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100" + } + } + ], + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "payout_totals": { + "chargeback_fee": { + "amount": "1", + "original": { + "amount": "2", + "currency_code": "USD" + } + }, + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z" +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json b/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json new file mode 100644 index 0000000..6577e27 --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/request/create_basic.json @@ -0,0 +1,21 @@ +{ + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz", + "name": "New US address created for CRM", + "type": "address.created", + "payload": { + "id": "add_01hv8gq3318ktkfengj2r75gfx", + "city": "New York", + "region": "NY", + "status": "active", + "created_at": "2024-04-12T06:42:58.785000Z", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "updated_at": "2024-04-12T06:42:58.785000Z", + "custom_data": null, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "description": "Head Office", + "import_meta": null, + "postal_code": "10021", + "second_line": null, + "country_code": "US" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json b/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json new file mode 100644 index 0000000..d9623f3 --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/request/update_full.json @@ -0,0 +1,57 @@ +{ + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz", + "name": "Refund approved", + "type": "adjustment.updated", + "status": "active", + "payload": { + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "action": "refund", + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "tax_rates_used": [], + "customer_id": "ctm_01hrffh7gvp29kc7xahm8wddwa", + "reason": "error", + "credit_applied_to_balance": null, + "currency_code": "USD", + "status": "approved", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "type": "partial", + "amount": "100", + "proration": null, + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100" + } + } + ], + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "payout_totals": { + "chargeback_fee": { + "amount": "1", + "original": { + "amount": "2", + "currency_code": "USD" + } + }, + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/update_partial.json b/tests/Functional/Resources/Simulations/_fixtures/request/update_partial.json new file mode 100644 index 0000000..ebae47d --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "name": "New simulation name", + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz" +} diff --git a/tests/Functional/Resources/Simulations/_fixtures/request/update_single.json b/tests/Functional/Resources/Simulations/_fixtures/request/update_single.json new file mode 100644 index 0000000..38b00de --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "status": "archived" +} diff --git a/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json b/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json new file mode 100644 index 0000000..379be80 --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/response/full_entity.json @@ -0,0 +1,31 @@ +{ + "data": { + "id": "ntfsim_01j82g2mggsgjpb3mjg0xq6p5k", + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz", + "name": "New US address created for CRM", + "type": "address.created", + "status": "active", + "payload": { + "id": "add_01hv8gq3318ktkfengj2r75gfx", + "city": "New York", + "region": "NY", + "status": "active", + "created_at": "2024-04-12T06:42:58.785000Z", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "updated_at": "2024-04-12T06:42:58.785000Z", + "custom_data": null, + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "description": "Head Office", + "import_meta": null, + "postal_code": "10021", + "second_line": null, + "country_code": "US" + }, + "last_run_at": null, + "created_at": "2024-09-18T12:00:25.616392Z", + "updated_at": "2024-09-18T12:00:25.616392Z" + }, + "meta": { + "request_id": "72351248-13bf-45be-befe-b3a5b5234588" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/response/full_entity_adjustment_updated.json b/tests/Functional/Resources/Simulations/_fixtures/response/full_entity_adjustment_updated.json new file mode 100644 index 0000000..ddf035e --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/response/full_entity_adjustment_updated.json @@ -0,0 +1,58 @@ +{ + "data": { + "id": "ntfsim_01j82fs5pvrdse93e1kawqy2fr", + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz", + "name": "Refund approved", + "type": "adjustment.updated", + "status": "active", + "payload": { + "id": "adj_01hvgf2s84dr6reszzg29zbvcm", + "action": "refund", + "transaction_id": "txn_01hvcc93znj3mpqt1tenkjb04y", + "subscription_id": "sub_01hvccbx32q2gb40sqx7n42430", + "customer_id": "ctm_01hrffh7gvp29kc7xahm8wddwa", + "reason": "error", + "credit_applied_to_balance": null, + "currency_code": "USD", + "status": "approved", + "items": [ + { + "id": "adjitm_01hvgf2s84dr6reszzg2gx70gj", + "item_id": "txnitm_01hvcc94b7qgz60qmrqmbm19zw", + "type": "partial", + "amount": "100", + "proration": null, + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100" + } + } + ], + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "created_at": "2024-04-15T08:48:20.239695Z", + "updated_at": "2024-04-15T08:48:20.239695Z" + }, + "last_run_at": "2024-09-18T11:55:18.261049Z", + "created_at": "2024-09-18T11:55:15.547675Z", + "updated_at": "2024-09-18T12:07:08.475015Z" + }, + "meta": { + "request_id": "ab96e6ec-6a2b-4f3a-9e65-3cf7ac29ea99" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/response/list_default.json b/tests/Functional/Resources/Simulations/_fixtures/response/list_default.json new file mode 100644 index 0000000..f426f93 --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/response/list_default.json @@ -0,0 +1,35 @@ +{ + "data": [ + { + "id": "ntfsim_01j82fs5pvrdse93e1kawqy2fr", + "notification_setting_id": "ntfset_01j8259dtga48jwekrv2pmk0kp", + "name": "Refund or chargeback created", + "type": "adjustment.created", + "status": "active", + "payload": null, + "last_run_at": "2024-09-18T11:55:18.261049Z", + "created_at": "2024-09-18T11:55:15.547675Z", + "updated_at": "2024-09-18T11:55:18.261225Z" + }, + { + "id": "ntfsim_01j82d9tc19c67jds5vzbzjcns", + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz", + "name": "Subscription created using pricing page on website", + "type": "subscription_creation", + "status": "active", + "payload": null, + "last_run_at": null, + "created_at": "2024-09-18T11:11:55.265125Z", + "updated_at": "2024-09-18T11:54:18.543265Z" + } + ], + "meta": { + "pagination": { + "per_page": 50, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations?after=ntfsim_01j55cce7pz60k2a4dfeh1c9sa", + "has_more": false + }, + "request_id": "ad095054-41bc-4907-907d-da18310aea49" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 0000000..b9361cd --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "id": "ntfsim_01j82fs5pvrdse93e1kawqy2fr", + "notification_setting_id": "ntfset_01j8259dtga48jwekrv2pmk0kp", + "name": "Refund or chargeback created", + "type": "adjustment.created", + "status": "active", + "payload": null, + "last_run_at": "2024-09-18T11:55:18.261049Z", + "created_at": "2024-09-18T11:55:15.547675Z", + "updated_at": "2024-09-18T11:55:18.261225Z" + } + ], + "meta": { + "pagination": { + "per_page": 1, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations?after=ntfsim_01j82fs5pvrdse93e1kawqy2fr", + "has_more": true + }, + "request_id": "ad095054-41bc-4907-907d-da18310aea49" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 0000000..8ad0707 --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "id": "ntfsim_01j82d9tc19c67jds5vzbzjcns", + "notification_setting_id": "ntfset_01j82d983j814ypzx7m1fw2jpz", + "name": "Subscription created using pricing page on website", + "type": "subscription_creation", + "status": "active", + "payload": null, + "last_run_at": null, + "created_at": "2024-09-18T11:11:55.265125Z", + "updated_at": "2024-09-18T11:54:18.543265Z" + } + ], + "meta": { + "pagination": { + "per_page": 1, + "estimated_total": 2, + "next": "https://api.paddle.dev/simulations?after=ntfsim_01j82d9tc19c67jds5vzbzjcns", + "has_more": false + }, + "request_id": "ad095054-41bc-4907-907d-da18310aea49" + } +} \ No newline at end of file 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",