Skip to content

Commit

Permalink
feat: Support partial simulation payloads (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgrayston-paddle authored Jan 30, 2025
1 parent 02596af commit c9cf681
Show file tree
Hide file tree
Showing 28 changed files with 1,753 additions and 335 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx

- Added `transactions.revise` operation to revise a transaction and added `revised_at` to `Transaction` entity, see [related changelog](https://developer.paddle.com/changelog/2024/revise-transaction-customer-information?utm_source=dx&utm_medium=paddle-php-sdk).
- Added support for `transaction.revised` notification, see [related changelog](https://developer.paddle.com/changelog/2024/revise-transaction-customer-information?utm_source=dx&utm_medium=paddle-php-sdk).
- Support for partial simulation payloads, see [related changelog](https://developer.paddle.com/changelog/2025/existing-data-simulations?utm_source=dx&utm_medium=paddle-php-sdk)

### Fixed
- Handle known entity types for events that are not supported by the current SDK version. `UndefinedEvent` will always return an `UndefinedEntity`.
Expand Down
80 changes: 51 additions & 29 deletions examples/simulations.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
use Paddle\SDK\Entities\Event\EventTypeName;
use Paddle\SDK\Exceptions\ApiError;
use Paddle\SDK\Exceptions\SdkExceptions\MalformedResponse;
use Paddle\SDK\Notifications\Entities\Address;
use Paddle\SDK\Notifications\Entities\DateTime;
use Paddle\SDK\Notifications\Entities\Shared\CountryCode;
use Paddle\SDK\Notifications\Entities\Shared\Status;
use Paddle\SDK\Notifications\Entities\Simulation\Address;
use Paddle\SDK\Resources\Shared\Operations\List\Pager;
use Paddle\SDK\Resources\Simulations\Operations\CreateSimulation;
use Paddle\SDK\Resources\Simulations\Operations\ListSimulations;
Expand Down Expand Up @@ -54,20 +57,20 @@
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',
]),
payload: new Address(
id: 'add_01hv8gq3318ktkfengj2r75gfx',
description: 'Head Office',
firstLine: '4050 Jefferson Plaza, 41st Floor',
secondLine: null,
city: 'New York',
postalCode: '10021',
region: 'NY',
countryCode: CountryCode::US(),
status: Status::Active(),
createdAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
updatedAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
),
),
);
} catch (ApiError|MalformedResponse $e) {
Expand Down Expand Up @@ -103,20 +106,39 @@
$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',
]),
payload: new Address(
id: 'add_01hv8gq3318ktkfengj2r75gfx',
description: 'Head Office',
firstLine: '4050 Jefferson Plaza, 41st Floor',
secondLine: null,
city: 'New York',
postalCode: '10021',
region: 'NY',
countryCode: CountryCode::US(),
status: Status::Active(),
createdAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
updatedAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
),
),
);
} catch (ApiError|MalformedResponse $e) {
var_dump($e);
exit;
}

// ┌───
// │ Update Simulation - Partial Payload │
// └─────────────────────────────────────┘
try {
$simulation = $paddle->simulations->update(
$simulationId,
new UpdateSimulation(
type: EventTypeName::AddressCreated(),
payload: new Address(
id: 'add_01hv8gq3318ktkfengj2r75gfx',
description: 'Head Office',
),
),
);
} catch (ApiError|MalformedResponse $e) {
Expand Down
3 changes: 1 addition & 2 deletions src/Entities/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ protected function __construct(

public static function from(array $data): self
{
$type = explode('.', (string) $data['event_type']);
$identifier = str_replace('_', '', ucwords(implode('_', $type), '_'));
$identifier = EventNameResolver::resolve((string) $data['event_type']);

/** @var class-string<Event> $event */
$event = sprintf('\Paddle\SDK\Notifications\Events\%s', $identifier);
Expand Down
18 changes: 18 additions & 0 deletions src/Entities/EventNameResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Paddle\SDK\Entities;

/**
* @internal
*/
final class EventNameResolver
{
public static function resolve(string $eventType): string
{
$type = explode('.', $eventType);

return str_replace('_', '', ucwords(implode('_', $type), '_'));
}
}
7 changes: 4 additions & 3 deletions src/Entities/Simulation.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
use Paddle\SDK\Entities\Simulation\SimulationScenarioType;
use Paddle\SDK\Entities\Simulation\SimulationStatus;
use Paddle\SDK\Notifications\Entities\Entity as NotificationEntity;
use Paddle\SDK\Notifications\Entities\EntityFactory;
use Paddle\SDK\Notifications\Entities\Simulation\SimulationEntity;
use Paddle\SDK\Notifications\Entities\Simulation\SimulationEntityFactory;

class Simulation implements Entity
{
Expand All @@ -18,7 +19,7 @@ private function __construct(
public string $notificationSettingId,
public string $name,
public EventTypeName|SimulationScenarioType $type,
public NotificationEntity|null $payload,
public NotificationEntity|SimulationEntity|null $payload,
public \DateTimeInterface|null $lastRunAt,
public \DateTimeInterface $createdAt,
public \DateTimeInterface $updatedAt,
Expand All @@ -33,7 +34,7 @@ public static function from(array $data): self
notificationSettingId: $data['notification_setting_id'],
name: $data['name'],
type: EventTypeName::from($data['type'])->isKnown() ? EventTypeName::from($data['type']) : SimulationScenarioType::from($data['type']),
payload: $data['payload'] ? EntityFactory::create($data['type'], $data['payload']) : null,
payload: $data['payload'] ? SimulationEntityFactory::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']),
Expand Down
28 changes: 8 additions & 20 deletions src/Notifications/Entities/EntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,32 @@

namespace Paddle\SDK\Notifications\Entities;

use Paddle\SDK\Entities\EventNameResolver;

class EntityFactory
{
public static function create(string $eventType, array $data): Entity
{
// Map specific event entity types.
$eventEntityTypes = [
'payment_method.deleted' => DeletedPaymentMethod::class,
];

$entity = $eventEntityTypes[$eventType] ?? self::resolveEntityClass($eventType);

return $entity::from($data);
return self::resolveEntityClass($eventType)::from($data);
}

/**
* @return class-string<Entity>
*/
private static function resolveEntityClass(string $eventType): string
{
$type = explode('.', $eventType);
$entity = self::snakeToPascalCase($type[0] ?? 'Unknown');
$identifier = self::snakeToPascalCase(implode('_', $type));

/** @var class-string<Entity> $entity */
$entity = sprintf('\Paddle\SDK\Notifications\Entities\%s', $entity);
if (! class_exists($entity)) {
$entity = UndefinedEntity::class;
$entity = EntityNameResolver::resolveFqn($eventType);
if ($entity === UndefinedEntity::class) {
return $entity;
}

if (! class_exists($entity) || ! in_array(Entity::class, class_implements($entity), true)) {
$identifier = EventNameResolver::resolve($eventType);

throw new \UnexpectedValueException("Event type '{$identifier}' cannot be mapped to an object");
}

return $entity;
}

private static function snakeToPascalCase(string $string): string
{
return str_replace('_', '', ucwords($string, '_'));
}
}
43 changes: 43 additions & 0 deletions src/Notifications/Entities/EntityNameResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Paddle\SDK\Notifications\Entities;

/**
* @internal
*/
final class EntityNameResolver
{
public static function resolve(string $eventType): string
{
// Map specific event entity types.
$eventEntityTypes = [
'payment_method.deleted' => 'DeletedPaymentMethod',
];

return $eventEntityTypes[$eventType] ?? self::resolveNameFromEventType($eventType);
}

public static function resolveFqn(string $eventType): string
{
$fqn = sprintf('\Paddle\SDK\Notifications\Entities\%s', self::resolve($eventType));

return class_exists($fqn) ? $fqn : UndefinedEntity::class;
}

/**
* @return class-string<Entity>
*/
private static function resolveNameFromEventType(string $eventType): string
{
$type = explode('.', $eventType);

return self::snakeToPascalCase($type[0] ?? 'Unknown');
}

private static function snakeToPascalCase(string $string): string
{
return str_replace('_', '', ucwords($string, '_'));
}
}
78 changes: 78 additions & 0 deletions src/Notifications/Entities/Simulation/Address.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Paddle\SDK\Notifications\Entities\Simulation;

use Paddle\SDK\FiltersUndefined;
use Paddle\SDK\Notifications\Entities\DateTime;
use Paddle\SDK\Notifications\Entities\Shared\CountryCode;
use Paddle\SDK\Notifications\Entities\Shared\CustomData;
use Paddle\SDK\Notifications\Entities\Shared\ImportMeta;
use Paddle\SDK\Notifications\Entities\Shared\Status;
use Paddle\SDK\Notifications\Entities\Simulation\Traits\OptionalProperties;
use Paddle\SDK\Undefined;

final class Address implements SimulationEntity
{
use OptionalProperties;
use FiltersUndefined;

public function __construct(
public readonly string|Undefined $id = new Undefined(),
public readonly string|Undefined|null $description = new Undefined(),
public readonly string|Undefined|null $firstLine = new Undefined(),
public readonly string|Undefined|null $secondLine = new Undefined(),
public readonly string|Undefined|null $city = new Undefined(),
public readonly string|Undefined|null $postalCode = new Undefined(),
public readonly string|Undefined|null $region = new Undefined(),
public readonly CountryCode|Undefined $countryCode = new Undefined(),
public readonly CustomData|Undefined|null $customData = new Undefined(),
public readonly Status|Undefined $status = new Undefined(),
public readonly \DateTimeInterface|Undefined $createdAt = new Undefined(),
public readonly \DateTimeInterface|Undefined $updatedAt = new Undefined(),
public readonly ImportMeta|Undefined|null $importMeta = new Undefined(),
public readonly string|Undefined|null $customerId = new Undefined(),
) {
}

public static function from(array $data): self
{
return new self(
id: self::optional($data, 'id'),
description: self::optional($data, 'description'),
firstLine: self::optional($data, 'first_line'),
secondLine: self::optional($data, 'second_line'),
city: self::optional($data, 'city'),
postalCode: self::optional($data, 'postal_code'),
region: self::optional($data, 'region'),
countryCode: self::optional($data, 'country_code', fn ($value) => CountryCode::from($value)),
customData: self::optional($data, 'custom_data', fn ($value) => new CustomData($value)),
status: self::optional($data, 'status', fn ($value) => Status::from($value)),
createdAt: self::optional($data, 'created_at', fn ($value) => DateTime::from($value)),
updatedAt: self::optional($data, 'updated_at', fn ($value) => DateTime::from($value)),
importMeta: self::optional($data, 'import_meta', fn ($value) => ImportMeta::from($value)),
customerId: self::optional($data, 'customer_id'),
);
}

public function jsonSerialize(): mixed
{
return $this->filterUndefined([
'id' => $this->id,
'description' => $this->description,
'first_line' => $this->firstLine,
'second_line' => $this->secondLine,
'city' => $this->city,
'postal_code' => $this->postalCode,
'region' => $this->region,
'country_code' => $this->countryCode,
'custom_data' => $this->customData,
'status' => $this->status,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt,
'import_meta' => $this->importMeta,
'customer_id' => $this->customerId,
]);
}
}
Loading

0 comments on commit c9cf681

Please sign in to comment.