Skip to content

Commit c9cf681

Browse files
feat: Support partial simulation payloads (#113)
1 parent 02596af commit c9cf681

28 files changed

+1753
-335
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx
1212

1313
- 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).
1414
- 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).
15+
- 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)
1516

1617
### Fixed
1718
- Handle known entity types for events that are not supported by the current SDK version. `UndefinedEvent` will always return an `UndefinedEntity`.

examples/simulations.php

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
use Paddle\SDK\Entities\Event\EventTypeName;
66
use Paddle\SDK\Exceptions\ApiError;
77
use Paddle\SDK\Exceptions\SdkExceptions\MalformedResponse;
8-
use Paddle\SDK\Notifications\Entities\Address;
8+
use Paddle\SDK\Notifications\Entities\DateTime;
9+
use Paddle\SDK\Notifications\Entities\Shared\CountryCode;
10+
use Paddle\SDK\Notifications\Entities\Shared\Status;
11+
use Paddle\SDK\Notifications\Entities\Simulation\Address;
912
use Paddle\SDK\Resources\Shared\Operations\List\Pager;
1013
use Paddle\SDK\Resources\Simulations\Operations\CreateSimulation;
1114
use Paddle\SDK\Resources\Simulations\Operations\ListSimulations;
@@ -54,20 +57,20 @@
5457
notificationSettingId: $notificationSettingId,
5558
type: EventTypeName::AddressCreated(),
5659
name: 'Simulate Address Creation',
57-
payload: Address::from([
58-
'id' => 'add_01hv8gq3318ktkfengj2r75gfx',
59-
'country_code' => 'US',
60-
'status' => 'active',
61-
'created_at' => '2024-04-12T06:42:58.785000Z',
62-
'updated_at' => '2024-04-12T06:42:58.785000Z',
63-
'customer_id' => 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
64-
'description' => 'Head Office',
65-
'first_line' => '4050 Jefferson Plaza, 41st Floor',
66-
'second_line' => null,
67-
'city' => 'New York',
68-
'postal_code' => '10021',
69-
'region' => 'NY',
70-
]),
60+
payload: new Address(
61+
id: 'add_01hv8gq3318ktkfengj2r75gfx',
62+
description: 'Head Office',
63+
firstLine: '4050 Jefferson Plaza, 41st Floor',
64+
secondLine: null,
65+
city: 'New York',
66+
postalCode: '10021',
67+
region: 'NY',
68+
countryCode: CountryCode::US(),
69+
status: Status::Active(),
70+
createdAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
71+
updatedAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
72+
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
73+
),
7174
),
7275
);
7376
} catch (ApiError|MalformedResponse $e) {
@@ -103,20 +106,39 @@
103106
$simulationId,
104107
new UpdateSimulation(
105108
type: EventTypeName::AddressCreated(),
106-
payload: Address::from([
107-
'id' => 'add_01hv8gq3318ktkfengj2r75gfx',
108-
'country_code' => 'US',
109-
'status' => 'active',
110-
'created_at' => '2024-04-12T06:42:58.785000Z',
111-
'updated_at' => '2024-04-12T06:42:58.785000Z',
112-
'customer_id' => 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
113-
'description' => 'Head Office',
114-
'first_line' => '4050 Jefferson Plaza, 41st Floor',
115-
'second_line' => null,
116-
'city' => 'New York',
117-
'postal_code' => '10021',
118-
'region' => 'NY',
119-
]),
109+
payload: new Address(
110+
id: 'add_01hv8gq3318ktkfengj2r75gfx',
111+
description: 'Head Office',
112+
firstLine: '4050 Jefferson Plaza, 41st Floor',
113+
secondLine: null,
114+
city: 'New York',
115+
postalCode: '10021',
116+
region: 'NY',
117+
countryCode: CountryCode::US(),
118+
status: Status::Active(),
119+
createdAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
120+
updatedAt: DateTime::from('2024-04-12T06:42:58.785000Z'),
121+
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
122+
),
123+
),
124+
);
125+
} catch (ApiError|MalformedResponse $e) {
126+
var_dump($e);
127+
exit;
128+
}
129+
130+
// ┌───
131+
// │ Update Simulation - Partial Payload │
132+
// └─────────────────────────────────────┘
133+
try {
134+
$simulation = $paddle->simulations->update(
135+
$simulationId,
136+
new UpdateSimulation(
137+
type: EventTypeName::AddressCreated(),
138+
payload: new Address(
139+
id: 'add_01hv8gq3318ktkfengj2r75gfx',
140+
description: 'Head Office',
141+
),
120142
),
121143
);
122144
} catch (ApiError|MalformedResponse $e) {

src/Entities/Event.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ protected function __construct(
2424

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

3029
/** @var class-string<Event> $event */
3130
$event = sprintf('\Paddle\SDK\Notifications\Events\%s', $identifier);

src/Entities/EventNameResolver.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Paddle\SDK\Entities;
6+
7+
/**
8+
* @internal
9+
*/
10+
final class EventNameResolver
11+
{
12+
public static function resolve(string $eventType): string
13+
{
14+
$type = explode('.', $eventType);
15+
16+
return str_replace('_', '', ucwords(implode('_', $type), '_'));
17+
}
18+
}

src/Entities/Simulation.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
use Paddle\SDK\Entities\Simulation\SimulationScenarioType;
99
use Paddle\SDK\Entities\Simulation\SimulationStatus;
1010
use Paddle\SDK\Notifications\Entities\Entity as NotificationEntity;
11-
use Paddle\SDK\Notifications\Entities\EntityFactory;
11+
use Paddle\SDK\Notifications\Entities\Simulation\SimulationEntity;
12+
use Paddle\SDK\Notifications\Entities\Simulation\SimulationEntityFactory;
1213

1314
class Simulation implements Entity
1415
{
@@ -18,7 +19,7 @@ private function __construct(
1819
public string $notificationSettingId,
1920
public string $name,
2021
public EventTypeName|SimulationScenarioType $type,
21-
public NotificationEntity|null $payload,
22+
public NotificationEntity|SimulationEntity|null $payload,
2223
public \DateTimeInterface|null $lastRunAt,
2324
public \DateTimeInterface $createdAt,
2425
public \DateTimeInterface $updatedAt,
@@ -33,7 +34,7 @@ public static function from(array $data): self
3334
notificationSettingId: $data['notification_setting_id'],
3435
name: $data['name'],
3536
type: EventTypeName::from($data['type'])->isKnown() ? EventTypeName::from($data['type']) : SimulationScenarioType::from($data['type']),
36-
payload: $data['payload'] ? EntityFactory::create($data['type'], $data['payload']) : null,
37+
payload: $data['payload'] ? SimulationEntityFactory::create($data['type'], $data['payload']) : null,
3738
lastRunAt: isset($data['last_run_at']) ? DateTime::from($data['last_run_at']) : null,
3839
createdAt: DateTime::from($data['created_at']),
3940
updatedAt: DateTime::from($data['updated_at']),

src/Notifications/Entities/EntityFactory.php

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,32 @@
44

55
namespace Paddle\SDK\Notifications\Entities;
66

7+
use Paddle\SDK\Entities\EventNameResolver;
8+
79
class EntityFactory
810
{
911
public static function create(string $eventType, array $data): Entity
1012
{
11-
// Map specific event entity types.
12-
$eventEntityTypes = [
13-
'payment_method.deleted' => DeletedPaymentMethod::class,
14-
];
15-
16-
$entity = $eventEntityTypes[$eventType] ?? self::resolveEntityClass($eventType);
17-
18-
return $entity::from($data);
13+
return self::resolveEntityClass($eventType)::from($data);
1914
}
2015

2116
/**
2217
* @return class-string<Entity>
2318
*/
2419
private static function resolveEntityClass(string $eventType): string
2520
{
26-
$type = explode('.', $eventType);
27-
$entity = self::snakeToPascalCase($type[0] ?? 'Unknown');
28-
$identifier = self::snakeToPascalCase(implode('_', $type));
29-
3021
/** @var class-string<Entity> $entity */
31-
$entity = sprintf('\Paddle\SDK\Notifications\Entities\%s', $entity);
32-
if (! class_exists($entity)) {
33-
$entity = UndefinedEntity::class;
22+
$entity = EntityNameResolver::resolveFqn($eventType);
23+
if ($entity === UndefinedEntity::class) {
24+
return $entity;
3425
}
3526

3627
if (! class_exists($entity) || ! in_array(Entity::class, class_implements($entity), true)) {
28+
$identifier = EventNameResolver::resolve($eventType);
29+
3730
throw new \UnexpectedValueException("Event type '{$identifier}' cannot be mapped to an object");
3831
}
3932

4033
return $entity;
4134
}
42-
43-
private static function snakeToPascalCase(string $string): string
44-
{
45-
return str_replace('_', '', ucwords($string, '_'));
46-
}
4735
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Paddle\SDK\Notifications\Entities;
6+
7+
/**
8+
* @internal
9+
*/
10+
final class EntityNameResolver
11+
{
12+
public static function resolve(string $eventType): string
13+
{
14+
// Map specific event entity types.
15+
$eventEntityTypes = [
16+
'payment_method.deleted' => 'DeletedPaymentMethod',
17+
];
18+
19+
return $eventEntityTypes[$eventType] ?? self::resolveNameFromEventType($eventType);
20+
}
21+
22+
public static function resolveFqn(string $eventType): string
23+
{
24+
$fqn = sprintf('\Paddle\SDK\Notifications\Entities\%s', self::resolve($eventType));
25+
26+
return class_exists($fqn) ? $fqn : UndefinedEntity::class;
27+
}
28+
29+
/**
30+
* @return class-string<Entity>
31+
*/
32+
private static function resolveNameFromEventType(string $eventType): string
33+
{
34+
$type = explode('.', $eventType);
35+
36+
return self::snakeToPascalCase($type[0] ?? 'Unknown');
37+
}
38+
39+
private static function snakeToPascalCase(string $string): string
40+
{
41+
return str_replace('_', '', ucwords($string, '_'));
42+
}
43+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Paddle\SDK\Notifications\Entities\Simulation;
6+
7+
use Paddle\SDK\FiltersUndefined;
8+
use Paddle\SDK\Notifications\Entities\DateTime;
9+
use Paddle\SDK\Notifications\Entities\Shared\CountryCode;
10+
use Paddle\SDK\Notifications\Entities\Shared\CustomData;
11+
use Paddle\SDK\Notifications\Entities\Shared\ImportMeta;
12+
use Paddle\SDK\Notifications\Entities\Shared\Status;
13+
use Paddle\SDK\Notifications\Entities\Simulation\Traits\OptionalProperties;
14+
use Paddle\SDK\Undefined;
15+
16+
final class Address implements SimulationEntity
17+
{
18+
use OptionalProperties;
19+
use FiltersUndefined;
20+
21+
public function __construct(
22+
public readonly string|Undefined $id = new Undefined(),
23+
public readonly string|Undefined|null $description = new Undefined(),
24+
public readonly string|Undefined|null $firstLine = new Undefined(),
25+
public readonly string|Undefined|null $secondLine = new Undefined(),
26+
public readonly string|Undefined|null $city = new Undefined(),
27+
public readonly string|Undefined|null $postalCode = new Undefined(),
28+
public readonly string|Undefined|null $region = new Undefined(),
29+
public readonly CountryCode|Undefined $countryCode = new Undefined(),
30+
public readonly CustomData|Undefined|null $customData = new Undefined(),
31+
public readonly Status|Undefined $status = new Undefined(),
32+
public readonly \DateTimeInterface|Undefined $createdAt = new Undefined(),
33+
public readonly \DateTimeInterface|Undefined $updatedAt = new Undefined(),
34+
public readonly ImportMeta|Undefined|null $importMeta = new Undefined(),
35+
public readonly string|Undefined|null $customerId = new Undefined(),
36+
) {
37+
}
38+
39+
public static function from(array $data): self
40+
{
41+
return new self(
42+
id: self::optional($data, 'id'),
43+
description: self::optional($data, 'description'),
44+
firstLine: self::optional($data, 'first_line'),
45+
secondLine: self::optional($data, 'second_line'),
46+
city: self::optional($data, 'city'),
47+
postalCode: self::optional($data, 'postal_code'),
48+
region: self::optional($data, 'region'),
49+
countryCode: self::optional($data, 'country_code', fn ($value) => CountryCode::from($value)),
50+
customData: self::optional($data, 'custom_data', fn ($value) => new CustomData($value)),
51+
status: self::optional($data, 'status', fn ($value) => Status::from($value)),
52+
createdAt: self::optional($data, 'created_at', fn ($value) => DateTime::from($value)),
53+
updatedAt: self::optional($data, 'updated_at', fn ($value) => DateTime::from($value)),
54+
importMeta: self::optional($data, 'import_meta', fn ($value) => ImportMeta::from($value)),
55+
customerId: self::optional($data, 'customer_id'),
56+
);
57+
}
58+
59+
public function jsonSerialize(): mixed
60+
{
61+
return $this->filterUndefined([
62+
'id' => $this->id,
63+
'description' => $this->description,
64+
'first_line' => $this->firstLine,
65+
'second_line' => $this->secondLine,
66+
'city' => $this->city,
67+
'postal_code' => $this->postalCode,
68+
'region' => $this->region,
69+
'country_code' => $this->countryCode,
70+
'custom_data' => $this->customData,
71+
'status' => $this->status,
72+
'created_at' => $this->createdAt,
73+
'updated_at' => $this->updatedAt,
74+
'import_meta' => $this->importMeta,
75+
'customer_id' => $this->customerId,
76+
]);
77+
}
78+
}

0 commit comments

Comments
 (0)