From 8a7432e493651e29e249d63f099b006cc704ed3a Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Wed, 14 Jan 2026 09:53:09 +0100 Subject: [PATCH 1/5] feat(sessions): align CreateSessionRequest with API specification - Replace method/checkoutFlow parameters with lines (required) - Add optional fields: billingAddress, shippingAddress, customerId, sequenceType, metadata, payment.webhookUrl, profileId - Make cancelUrl optional - Implement SupportsTestmodeInPayload interface - Update RequestFactory to support nested payload lookup (payment.webhookUrl) --- src/Factories/CreateSessionRequestFactory.php | 18 ++++- src/Factories/RequestFactory.php | 8 +- src/Http/Requests/CreateSessionRequest.php | 76 ++++++++++++++----- 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/Factories/CreateSessionRequestFactory.php b/src/Factories/CreateSessionRequestFactory.php index e72b72c1..d7eced3c 100644 --- a/src/Factories/CreateSessionRequestFactory.php +++ b/src/Factories/CreateSessionRequestFactory.php @@ -2,6 +2,7 @@ namespace Mollie\Api\Factories; +use Mollie\Api\Http\Data\Address; use Mollie\Api\Http\Requests\CreateSessionRequest; class CreateSessionRequestFactory extends RequestFactory @@ -9,12 +10,21 @@ class CreateSessionRequestFactory extends RequestFactory public function create(): CreateSessionRequest { return new CreateSessionRequest( - $this->payload('redirectUrl'), - $this->payload('cancelUrl'), MoneyFactory::new($this->payload('amount'))->create(), $this->payload('description'), - $this->payload('method'), - $this->payload('checkoutFlow') + $this->payload('redirectUrl'), + $this->transformFromPayload( + 'lines', + fn ($items) => OrderLineCollectionFactory::new($items)->create() + ), + $this->payload('cancelUrl'), + $this->transformFromPayload('billingAddress', fn ($item) => Address::fromArray($item)), + $this->transformFromPayload('shippingAddress', fn ($item) => Address::fromArray($item)), + $this->payload('customerId'), + $this->payload('sequenceType'), + $this->payload('metadata'), + $this->payload('webhookUrl', null, 'payment.'), + $this->payload('profileId') ); } } diff --git a/src/Factories/RequestFactory.php b/src/Factories/RequestFactory.php index 34dfc4f9..c05c5f8b 100644 --- a/src/Factories/RequestFactory.php +++ b/src/Factories/RequestFactory.php @@ -30,14 +30,14 @@ public function withQuery(array $query) return $this; } - protected function payload(?string $key = null, $default = null) + protected function payload(?string $key = null, $default = null, $backupKey = 'payment.') { - return $this->get($key, $default, $this->payload); + return $this->get($key, $default, $this->payload, $backupKey); } - protected function query(?string $key = null, $default = null) + protected function query(?string $key = null, $default = null, $backupKey = 'filters.') { - return $this->get($key, $default, $this->query); + return $this->get($key, $default, $this->query, $backupKey); } protected function payloadIncludes(string $key, $value) diff --git a/src/Http/Requests/CreateSessionRequest.php b/src/Http/Requests/CreateSessionRequest.php index 24b8836f..6c05d866 100644 --- a/src/Http/Requests/CreateSessionRequest.php +++ b/src/Http/Requests/CreateSessionRequest.php @@ -3,13 +3,16 @@ namespace Mollie\Api\Http\Requests; use Mollie\Api\Contracts\HasPayload; +use Mollie\Api\Contracts\SupportsTestmodeInPayload; +use Mollie\Api\Http\Data\Address; +use Mollie\Api\Http\Data\DataCollection; use Mollie\Api\Http\Data\Money; +use Mollie\Api\Http\Data\OrderLine; use Mollie\Api\Resources\Session; use Mollie\Api\Traits\HasJsonPayload; -use Mollie\Api\Types\CheckoutFlow; use Mollie\Api\Types\Method; -class CreateSessionRequest extends ResourceHydratableRequest implements HasPayload +class CreateSessionRequest extends ResourceHydratableRequest implements HasPayload, SupportsTestmodeInPayload { use HasJsonPayload; @@ -17,43 +20,78 @@ class CreateSessionRequest extends ResourceHydratableRequest implements HasPaylo protected $hydratableResource = Session::class; + private Money $amount; + + private string $description; + private string $redirectUrl; - private string $cancelUrl; + /** + * @var DataCollection + */ + private DataCollection $lines; - private Money $amount; + private ?string $cancelUrl; - private string $description; + private ?Address $billingAddress; + + private ?Address $shippingAddress; + + private ?string $customerId; + + private ?string $sequenceType; + + private ?array $metadata; - private string $paymentMethod; + private ?string $paymentWebhook; - private string $checkoutFlow; + private ?string $profileId; public function __construct( - string $redirectUrl, - string $cancelUrl, Money $amount, string $description, - string $method, - ?string $checkoutFlow = null + string $redirectUrl, + DataCollection $lines, + ?string $cancelUrl = null, + ?Address $billingAddress = null, + ?Address $shippingAddress = null, + ?string $customerId = null, + ?string $sequenceType = null, + ?array $metadata = null, + ?string $paymentWebhook = null, + ?string $profileId = null ) { - $this->redirectUrl = $redirectUrl; - $this->cancelUrl = $cancelUrl; $this->amount = $amount; $this->description = $description; - $this->paymentMethod = $method; - $this->checkoutFlow = $checkoutFlow ?? CheckoutFlow::EXPRESS; + $this->redirectUrl = $redirectUrl; + $this->lines = $lines; + $this->cancelUrl = $cancelUrl; + $this->billingAddress = $billingAddress; + $this->shippingAddress = $shippingAddress; + $this->customerId = $customerId; + $this->sequenceType = $sequenceType; + $this->metadata = $metadata; + $this->paymentWebhook = $paymentWebhook; + $this->profileId = $profileId; } protected function defaultPayload(): array { return [ - 'redirectUrl' => $this->redirectUrl, - 'cancelUrl' => $this->cancelUrl, 'amount' => $this->amount, 'description' => $this->description, - 'method' => $this->paymentMethod, - 'checkoutFlow' => $this->checkoutFlow, + 'redirectUrl' => $this->redirectUrl, + 'lines' => $this->lines, + 'cancelUrl' => $this->cancelUrl, + 'billingAddress' => $this->billingAddress, + 'shippingAddress' => $this->shippingAddress, + 'customerId' => $this->customerId, + 'sequenceType' => $this->sequenceType, + 'metadata' => $this->metadata, + 'profileId' => $this->profileId, + 'payment' => $this->paymentWebhook !== null ? [ + 'webhookUrl' => $this->paymentWebhook, + ] : null, ]; } From 29f6a979a03bad1fc55faf8551bb753880beb49c Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Wed, 14 Jan 2026 09:53:11 +0100 Subject: [PATCH 2/5] feat(sessions): update Session resource to match API specification - Add clientAccessToken property (required) - Add customerId, sequenceType, metadata, payment, lines properties - Remove deprecated properties: authenticationId, nextAction, method, methodDetails, failedAt - Update status helper methods: isCreated() -> isOpen(), hasFailed() -> isExpired(), remove isReadyForProcessing() --- src/Resources/Session.php | 80 +++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/src/Resources/Session.php b/src/Resources/Session.php index 3f814087..7ae90ccb 100644 --- a/src/Resources/Session.php +++ b/src/Resources/Session.php @@ -29,27 +29,11 @@ class Session extends BaseResource public $status; /** - * UTC datetime indicating the time at which the Session failed in ISO-8601 format. - * - * @example "2013-12-25T10:30:54+00:00" - * - * @var string|null - */ - public $failedAt; - - /** - * Unique identifier to record the Userʼs authentication with a method + * Client access token for the session. * * @var string */ - public $authenticationId; - - /** - * Indicates the next action to take in the payment preparation flow. - * - * @var string - */ - public $nextAction; + public $clientAccessToken; /** * The URL the buyer will be redirected to in case the @@ -85,36 +69,53 @@ class Session extends BaseResource public $description; /** - * Payment method currently selected by the shopper. + * The person and the address the payment is shipped to. * - * @var string + * @var \stdClass|null */ - public $method; + public $shippingAddress; /** - * All additional information relating to the selected method. + * The person and the address the payment is billed to. * - * @var \stdClass + * @var \stdClass|null */ - public $methodDetails; + public $billingAddress; /** - * The person and the address the payment is shipped to. + * ID of the customer the session is created for. + * + * @var string|null + */ + public $customerId; + + /** + * Sequence type for recurring payments. * - * @deprecated + * @var string|null + */ + public $sequenceType; + + /** + * Metadata associated with the session. * - * @var \stdClass + * @var object|array|null */ - public $shippingAddress; + public $metadata; /** - * The person and the address the payment is billed to. + * Payment settings for the session. * - * @deprecated + * @var \stdClass|null + */ + public $payment; + + /** + * Order lines for the session. * - * @var \stdClass + * @var array|object[]|null */ - public $billingAddress; + public $lines; /** * An object with several URL objects relevant to the customer. Every URL object will contain an href and a type field. @@ -123,24 +124,19 @@ class Session extends BaseResource */ public $_links; - public function isCreated() + public function isOpen() { - return $this->status === SessionStatus::STATUS_CREATED; + return $this->status === SessionStatus::OPEN; } - public function isReadyForProcessing() + public function isExpired() { - return $this->status === SessionStatus::STATUS_READY_FOR_PROCESSING; + return $this->status === SessionStatus::EXPIRED; } public function isCompleted() { - return $this->status === SessionStatus::STATUS_COMPLETED; - } - - public function hasFailed() - { - return $this->status === SessionStatus::STATUS_FAILED; + return $this->status === SessionStatus::COMPLETED; } /** From 52f5706660b26d51b7c6b03076a3a7b259da35ad Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Wed, 14 Jan 2026 09:53:14 +0100 Subject: [PATCH 3/5] refactor(sessions): update SessionStatus constants and remove CheckoutFlow - Replace STATUS_CREATED, STATUS_READY_FOR_PROCESSING, STATUS_FAILED with OPEN, EXPIRED - Change STATUS_COMPLETED to COMPLETED - Remove CheckoutFlow type (no longer used) --- src/Types/CheckoutFlow.php | 8 -------- src/Types/SessionStatus.php | 15 +++++---------- 2 files changed, 5 insertions(+), 18 deletions(-) delete mode 100644 src/Types/CheckoutFlow.php diff --git a/src/Types/CheckoutFlow.php b/src/Types/CheckoutFlow.php deleted file mode 100644 index 9e7f95ef..00000000 --- a/src/Types/CheckoutFlow.php +++ /dev/null @@ -1,8 +0,0 @@ - Date: Wed, 14 Jan 2026 09:53:17 +0100 Subject: [PATCH 4/5] test(sessions): update tests for new session endpoint structure - Update CreateSessionRequestTest to use lines parameter - Add CreateSessionRequestFactoryTest with full and minimal data tests - Update SessionEndpointCollectionTest to use new payload structure --- .../SessionEndpointCollectionTest.php | 18 +++- .../CreateSessionRequestFactoryTest.php | 96 +++++++++++++++++++ .../Requests/CreateSessionRequestTest.php | 31 ++++-- 3 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 tests/Factories/CreateSessionRequestFactoryTest.php diff --git a/tests/EndpointCollection/SessionEndpointCollectionTest.php b/tests/EndpointCollection/SessionEndpointCollectionTest.php index 3a5abdf4..31bc1ec8 100644 --- a/tests/EndpointCollection/SessionEndpointCollectionTest.php +++ b/tests/EndpointCollection/SessionEndpointCollectionTest.php @@ -39,11 +39,23 @@ public function create() /** @var Session $session */ $session = $client->sessions->create([ - 'redirectUrl' => 'https://example.com/redirect', - 'cancelUrl' => 'https://example.com/cancel', 'amount' => new Money('EUR', '10.00'), 'description' => 'Test Session', - 'method' => 'ideal', + 'redirectUrl' => 'https://example.com/redirect', + 'lines' => [ + [ + 'description' => 'Product A', + 'quantity' => 1, + 'unitPrice' => [ + 'currency' => 'EUR', + 'value' => '10.00', + ], + 'totalAmount' => [ + 'currency' => 'EUR', + 'value' => '10.00', + ], + ], + ], ]); $this->assertSession($session); diff --git a/tests/Factories/CreateSessionRequestFactoryTest.php b/tests/Factories/CreateSessionRequestFactoryTest.php new file mode 100644 index 00000000..1107b8d2 --- /dev/null +++ b/tests/Factories/CreateSessionRequestFactoryTest.php @@ -0,0 +1,96 @@ +withPayload([ + 'amount' => [ + 'currency' => 'EUR', + 'value' => '100.00', + ], + 'description' => 'Order #12345', + 'redirectUrl' => 'https://example.com/redirect', + 'cancelUrl' => 'https://example.com/cancel', + 'lines' => [ + [ + 'description' => 'Product A', + 'quantity' => 2, + 'vatRate' => '21.00', + 'unitPrice' => [ + 'currency' => 'EUR', + 'value' => '50.00', + ], + 'totalAmount' => [ + 'currency' => 'EUR', + 'value' => '100.00', + ], + ], + ], + 'billingAddress' => [ + 'streetAndNumber' => 'Main Street 1', + 'postalCode' => '1234AB', + 'city' => 'Amsterdam', + 'country' => 'NL', + ], + 'shippingAddress' => [ + 'streetAndNumber' => 'Main Street 1', + 'postalCode' => '1234AB', + 'city' => 'Amsterdam', + 'country' => 'NL', + ], + 'customerId' => 'cst_12345', + 'sequenceType' => 'oneoff', + 'metadata' => [ + 'order_id' => '12345', + ], + 'payment' => [ + 'webhookUrl' => 'https://example.com/webhook', + ], + 'profileId' => 'pfl_12345', + 'testmode' => true, + ]) + ->create(); + + $this->assertInstanceOf(CreateSessionRequest::class, $request); + } + + /** @test */ + public function create_returns_session_request_object_with_minimal_data() + { + $request = CreateSessionRequestFactory::new() + ->withPayload([ + 'amount' => [ + 'currency' => 'EUR', + 'value' => '100.00', + ], + 'description' => 'Order #12345', + 'redirectUrl' => 'https://example.com/redirect', + 'lines' => [ + [ + 'description' => 'Product A', + 'quantity' => 1, + 'unitPrice' => [ + 'currency' => 'EUR', + 'value' => '100.00', + ], + 'totalAmount' => [ + 'currency' => 'EUR', + 'value' => '100.00', + ], + ], + ], + ]) + ->create(); + + $this->assertInstanceOf(CreateSessionRequest::class, $request); + } +} diff --git a/tests/Http/Requests/CreateSessionRequestTest.php b/tests/Http/Requests/CreateSessionRequestTest.php index 77cd7ee3..646ce1ed 100644 --- a/tests/Http/Requests/CreateSessionRequestTest.php +++ b/tests/Http/Requests/CreateSessionRequestTest.php @@ -4,7 +4,9 @@ use Mollie\Api\Fake\MockMollieClient; use Mollie\Api\Fake\MockResponse; +use Mollie\Api\Http\Data\DataCollection; use Mollie\Api\Http\Data\Money; +use Mollie\Api\Http\Data\OrderLine; use Mollie\Api\Http\Requests\CreateSessionRequest; use Mollie\Api\Resources\Session; use PHPUnit\Framework\TestCase; @@ -18,12 +20,20 @@ public function it_can_create_session() CreateSessionRequest::class => MockResponse::created('session'), ]); + $lines = new DataCollection([ + new OrderLine( + 'Product A', + 1, + new Money('EUR', '10.00'), + new Money('EUR', '10.00') + ), + ]); + $request = new CreateSessionRequest( - 'https://example.com/redirect', - 'https://example.com/cancel', new Money('EUR', '10.00'), 'My product', - 'ideal', + 'https://example.com/redirect', + $lines ); /** @var Session */ @@ -36,13 +46,20 @@ public function it_can_create_session() /** @test */ public function it_resolves_correct_resource_path() { + $lines = new DataCollection([ + new OrderLine( + 'Product A', + 1, + new Money('EUR', '10.00'), + new Money('EUR', '10.00') + ), + ]); + $request = new CreateSessionRequest( - 'https://example.com/redirect', - 'https://example.com/cancel', new Money('EUR', '10.00'), 'My product', - 'ideal', - 'embedded' + 'https://example.com/redirect', + $lines ); $this->assertEquals('sessions', $request->resolveResourcePath()); } From 0f6274e24005b4ad955a4d2e8c2a0ec6f991fe34 Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Wed, 14 Jan 2026 09:53:20 +0100 Subject: [PATCH 5/5] test(sessions): update mock response to match API specification - Add clientAccessToken (required) - Update status to 'open' (valid values: open, expired, completed) - Add redirectUrl, cancelUrl, metadata, payment, lines - Remove deprecated fields: method, methodDetails, nextAction, createdAt --- src/Fake/Responses/session.json | 41 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Fake/Responses/session.json b/src/Fake/Responses/session.json index efc2b7b8..68f1cf82 100644 --- a/src/Fake/Responses/session.json +++ b/src/Fake/Responses/session.json @@ -1,31 +1,40 @@ { "resource": "session", "id": "{{ RESOURCE_ID }}", + "clientAccessToken": "dddiweodh23973yo23d2h...", + "status": "open", "mode": "live", - "status": "created", "amount": { - "value": "10.00", - "currency": "EUR" + "currency": "EUR", + "value": "10.00" }, "description": "Order #12345", - "method": "paypal", - "methodDetails": { - "checkoutFlow": "express" + "redirectUrl": "https://example.com/redirect", + "cancelUrl": "https://example.com/cancel", + "metadata": { + "orderId": "12345" }, - "nextAction": "redirect", - "createdAt": "2024-01-10T12:06:28+00:00", + "payment": { + "webhookUrl": "https://webshop.example.org/payments/webhook" + }, + "lines": [ + { + "description": "Product A", + "quantity": 1, + "unitPrice": { + "currency": "EUR", + "value": "10.00" + }, + "totalAmount": { + "currency": "EUR", + "value": "10.00" + } + } + ], "_links": { "self": { "href": "https://api.mollie.com/v2/sessions/{{ RESOURCE_ID }}", "type": "application/hal+json" - }, - "redirect": { - "href": "https://paypalc.com/order/dghjfidf;gj", - "type": "application/hal+json" - }, - "documentation": { - "href": "...", - "type": "text/html" } } }