diff --git a/src/Contracts/HasBody.php b/src/Contracts/HasBody.php new file mode 100644 index 000000000..41def4b49 --- /dev/null +++ b/src/Contracts/HasBody.php @@ -0,0 +1,13 @@ +client = $client; + } + + public function send(Request $request): mixed + { + $path = $request->resolveResourcePath() + . $this->buildQueryString($request->getQuery()); + + $body = $request instanceof HasBody + ? $request->getBody() + : null; + + $result = $this->client->performHttpCall( + $request->getMethod(), + $path, + $body + ); + + if ($result->isEmpty()) { + return null; + } + + $targetResourceClass = $request->getTargetResourceClass(); + + if (is_subclass_of($targetResourceClass, BaseCollection::class)) { + $collection = $this->buildResultCollection($result->decode(), $targetResourceClass); + + if ($request instanceof IsIteratable && $request->iteratorEnabled()) { + /** @var CursorCollection $collection */ + return $collection->getAutoIterator($request->iteratesBackwards()); + } + + return $collection; + } + + if (is_subclass_of($targetResourceClass, BaseResource::class)) { + return ResourceFactory::createFromApiResult($this->client, $result->decode(), $targetResourceClass); + } + + return null; + } + + protected function buildResultCollection(object $result, string $targetCollectionClass): BaseCollection + { + return ResourceFactory::createBaseResourceCollection( + $this->client, + ($targetCollectionClass)::getResourceClass(), + $result->_embedded->{$targetCollectionClass::getCollectionResourceName()}, + $result->_links, + $targetCollectionClass + ); + } + + /** + * @param array $filters + * @return string + */ + protected function buildQueryString(array $filters): string + { + if (empty($filters)) { + return ""; + } + + foreach ($filters as $key => $value) { + if ($value === true) { + $filters[$key] = "true"; + } + + if ($value === false) { + $filters[$key] = "false"; + } + } + + return "?" . http_build_query($filters, "", "&"); + } +} diff --git a/src/Http/EndpointCollection/BalanceEndpointCollection.php b/src/Http/EndpointCollection/BalanceEndpointCollection.php new file mode 100644 index 000000000..288f17bad --- /dev/null +++ b/src/Http/EndpointCollection/BalanceEndpointCollection.php @@ -0,0 +1,20 @@ +send(new GetPaginatedBalancesRequest($from, $limit, $parameters)); + } +} diff --git a/src/Http/EndpointCollection/PaymentEndpointCollection.php b/src/Http/EndpointCollection/PaymentEndpointCollection.php new file mode 100644 index 000000000..fec8d3102 --- /dev/null +++ b/src/Http/EndpointCollection/PaymentEndpointCollection.php @@ -0,0 +1,77 @@ +send(new GetPaginatedPaymentsRequest($from, $limit, $filters)); + } + + /** + * Retrieve a single payment from Mollie. + * + * Will throw a ApiException if the payment id is invalid or the resource cannot be found. + * + * @param string $paymentId + * @param array $filters + * + * @return Payment + * @throws ApiException + */ + public function get(string $paymentId, array $filters = []): Payment + { + return $this->send(new GetPaymentRequest($paymentId, $filters)); + } + + /** + * Issue a refund for the given payment. + * + * The $data parameter may either be an array of endpoint parameters, a float value to + * initiate a partial refund, or empty to do a full refund. + * + * @param Payment $payment + * @param array|float|null $data + * + * @return Refund + * @throws ApiException + */ + public function refund(Payment $payment, $data = []): Refund + { + return $this->send(new RefundPaymentRequest($payment->id, $data)); + } + + /** + * Create an iterator for iterating over payments retrieved from Mollie. + * + * @param string $from The first resource ID you want to include in your list. + * @param int $limit + * @param array $filters + * @param bool $iterateBackwards Set to true for reverse order iteration (default is false). + * + * @return LazyCollection + */ + public function iterator(?string $from = null, ?int $limit = null, array $filters = [], bool $iterateBackwards = false): LazyCollection + { + return $this->send( + (new GetPaginatedPaymentsRequest($from, $limit, $filters)) + ->useIterator() + ->setIterationDirection($iterateBackwards) + ); + } +} diff --git a/src/Http/EndpointCollection/PaymentRefundEndpointCollection.php b/src/Http/EndpointCollection/PaymentRefundEndpointCollection.php new file mode 100644 index 000000000..b74aa3750 --- /dev/null +++ b/src/Http/EndpointCollection/PaymentRefundEndpointCollection.php @@ -0,0 +1,25 @@ +send(new GetPaginatedPaymentRefundsRequest( + $paymentId, + $filters + )); + } +} diff --git a/src/Http/Request.php b/src/Http/Request.php new file mode 100644 index 000000000..b0dfe4a90 --- /dev/null +++ b/src/Http/Request.php @@ -0,0 +1,53 @@ +method)) { + throw new LogicException('Your request is missing a HTTP method. You must add a method property like [protected Method $method = Method::GET]'); + } + + return $this->method; + } + + public function getQuery(): array + { + return []; + } + + public static function getTargetResourceClass(): string + { + if (empty(static::$targetResourceClass)) { + throw new \RuntimeException('Resource class is not set.'); + } + + return static::$targetResourceClass; + } + + /** + * Resolve the resource path. + * + * @return string + */ + abstract public function resolveResourcePath(): string; +} diff --git a/src/Http/Requests/GetPaginatedBalancesRequest.php b/src/Http/Requests/GetPaginatedBalancesRequest.php new file mode 100644 index 000000000..9339e2291 --- /dev/null +++ b/src/Http/Requests/GetPaginatedBalancesRequest.php @@ -0,0 +1,34 @@ +paymentId = $paymentId; + $this->filters = $filters; + } + + public function resolveResourcePath(): string + { + $id = urlencode($this->paymentId); + + return "payments/{$id}/refunds"; + } +} diff --git a/src/Http/Requests/GetPaginatedPaymentsRequest.php b/src/Http/Requests/GetPaginatedPaymentsRequest.php new file mode 100644 index 000000000..03e2dc075 --- /dev/null +++ b/src/Http/Requests/GetPaginatedPaymentsRequest.php @@ -0,0 +1,36 @@ +paymentId = $paymentId; + $this->filters = $filters; + } + + /** + * Resolve the resource path. + * + * @return string + */ + public function resolveResourcePath(): string + { + $id = urlencode($this->paymentId); + + return "payments/{$id}"; + } + + public function getQuery(): array + { + return $this->filters; + } +} diff --git a/src/Http/Requests/HasJsonBody.php b/src/Http/Requests/HasJsonBody.php new file mode 100644 index 000000000..684abe73e --- /dev/null +++ b/src/Http/Requests/HasJsonBody.php @@ -0,0 +1,13 @@ +body); + } +} diff --git a/src/Http/Requests/IsIteratableRequest.php b/src/Http/Requests/IsIteratableRequest.php new file mode 100644 index 000000000..e428021ba --- /dev/null +++ b/src/Http/Requests/IsIteratableRequest.php @@ -0,0 +1,34 @@ +iteratorEnabled; + } + + public function iteratesBackwards(): bool + { + return $this->iterateBackwards; + } + + public function useIterator(): self + { + $this->iteratorEnabled = true; + + return $this; + } + + public function setIterationDirection(bool $iterateBackwards = false): self + { + $this->iterateBackwards = $iterateBackwards; + + return $this; + } +} diff --git a/src/Http/Requests/IsPaginatedRequest.php b/src/Http/Requests/IsPaginatedRequest.php new file mode 100644 index 000000000..b3d5c02ad --- /dev/null +++ b/src/Http/Requests/IsPaginatedRequest.php @@ -0,0 +1,30 @@ +from = $from; + $this->limit = $limit; + $this->filters = $filters; + } + + public function getQuery(): array + { + return array_merge([ + 'from' => $this->from, + 'limit' => $this->limit, + ], $this->filters); + } +} diff --git a/src/Http/Requests/RefundPaymentRequest.php b/src/Http/Requests/RefundPaymentRequest.php new file mode 100644 index 000000000..d45e19c31 --- /dev/null +++ b/src/Http/Requests/RefundPaymentRequest.php @@ -0,0 +1,38 @@ +paymentId = $paymentId; + $this->body = $data; + } + + public function resolveResourcePath(): string + { + $id = urlencode($this->paymentId); + + return "payments/{$id}/refunds"; + } +} diff --git a/src/MollieApiClient.php b/src/MollieApiClient.php index f5dd3db07..cf9ce6bd3 100644 --- a/src/MollieApiClient.php +++ b/src/MollieApiClient.php @@ -48,7 +48,12 @@ use Mollie\Api\Exceptions\ApiException; use Mollie\Api\Exceptions\IncompatiblePlatform; use Mollie\Api\Http\Adapter\MollieHttpAdapterPicker; +use Mollie\Api\Http\EndpointCollection\BalanceEndpointCollection; +use Mollie\Api\Http\EndpointCollection\PaymentEndpointCollection; +use Mollie\Api\Contracts\HasBody; +use Mollie\Api\Http\EndpointCollection\PaymentRefundEndpointCollection; use Mollie\Api\Idempotency\IdempotencyKeyGeneratorContract; +use Mollie\Api\Http\Request; /** * @property BalanceEndpoint $balances @@ -177,7 +182,11 @@ public function __construct( private function initializeEndpoints(): void { $endpointClasses = [ - 'balances' => BalanceEndpoint::class, + 'balances' => BalanceEndpointCollection::class, + 'payments' => PaymentEndpointCollection::class, + 'paymentRefunds' => PaymentRefundEndpointCollection::class, + + // 'balances' => BalanceEndpoint::class, 'balanceReports' => BalanceReportEndpoint::class, 'balanceTransactions' => BalanceTransactionEndpoint::class, 'chargebacks' => ChargebackEndpoint::class, @@ -200,9 +209,9 @@ private function initializeEndpoints(): void 'paymentChargebacks' => PaymentChargebackEndpoint::class, 'paymentLinks' => PaymentLinkEndpoint::class, 'paymentLinkPayments' => PaymentLinkPaymentEndpoint::class, - 'paymentRefunds' => PaymentRefundEndpoint::class, + // 'paymentRefunds' => PaymentRefundEndpoint::class, 'paymentRoutes' => PaymentRouteEndpoint::class, - 'payments' => PaymentEndpoint::class, + // 'payments' => PaymentEndpoint::class, 'permissions' => PermissionEndpoint::class, 'profiles' => ProfileEndpoint::class, 'profileMethods' => ProfileMethodEndpoint::class,