## Checkout integration SDK
Paysera PHP payments API integration SDK

## Code style
[PSR-12](https://www.php-fig.org/psr/psr-12)

## Used
<b>Built with</b>
- [PSR](https://www.php-fig.org/psr/)

## Requirements
- PHP 7.4 or higher
- ext-curl
- ext-json

## Installation

Install via Composer:

```bash
composer require paysera/lib-checkout-integration-sdk
```

## Features

- **JWT Token Validation**: Automatic validation of JWT tokens with project ID extraction
- **PSR-Compliant Caching**: Configurable cache pool for improved performance
- **Payment Processing**: Full payment order creation and link generation
- **Callback Processing**: Secure webhook verification and processing
- **Translations**: Namespace-based localization with SDK and plugin translation merging
- **Multiple Environments**: Support for both production and sandbox environments

## Quick Start

```php
<?php

use Paysera\CheckoutSdk\SdkFacadeBuilder;
use Paysera\CheckoutSdk\Entity\PaymentApiCredentials;

// Build SDK facade with minimal configuration
$sdkFacade = (new SdkFacadeBuilder())
    ->build()
;

// Authorize with your API credentials
$apiCredentials = new PaymentApiCredentials(
    'your-client-id',
    'your-client-secret'
);

$sdkFacade
    ->getAuthorizationFacade()
    ->authorize($apiCredentials)
;

// Now you're ready to process payments
```

## SDK Facade Configuration

The SDK facade can be customized with various options:

```php
<?php

use Paysera\CheckoutSdk\SdkFacadeBuilder;

$sdkFacade = (new SdkFacadeBuilder())
    // Optional: Set custom PSR-3 logger. NullLogger by default
    ->setLogger($logger)

    // Optional: Set custom PSR-18 HTTP client. CurlHttpClient by default
    ->setHttpClient($httpClient)

    // Optional: Set custom PSR-6 cache pool. InMemoryCache by default.
    // Caches Keycloak JWKS used for JWT signature verification.
    // Persistent pool (Redis/APCu/filesystem) is strongly recommended in
    // production — see "JWKS cache" section below.
    ->setCacheItemPool($cacheItemPool)

    // Optional: Set custom PSR-20 clock. System clock by default
    // Used for time-based operations like token expiration
    ->setClock($clock)

    // Optional: Set auth token repository. InMemoryRepository by default
    // Permanently stores payment API access tokens
    // Needed for using the same token between requests
    ->setPaymentApiAuthTokenRepository($paymentApiAuthTokenRepository)

    // Optional: Set credentials repository. InMemoryRepository by default
    // Permanently stores payment API credentials
    // Needed for automatically refreshing tokens
    ->setPaymentApiCredentialsRepository($paymentApiCredentialsRepository)

    // Optional: Set custom API client formatter. SecureApiClientFormatter by default
    // Controls how HTTP requests/responses are formatted in logs
    ->setApiClientFormatter($apiClientFormatter)

    ->build()
;
```

## JWKS cache

The SDK verifies JWT access token signatures against Keycloak's JWKS
endpoint (`/auth/realms/Paysera/protocol/openid-connect/certs`). The key
set is fetched once on cold start and cached for 30 days. The endpoint is
re-contacted only when an incoming JWT carries a `kid` that is not present
in the cached set (Keycloak key rotation).

To benefit from this strategy in production you **must** inject a
persistent PSR-6 cache pool via `setCacheItemPool()`. The default
`InMemoryCache` is scoped to a single PHP request and defeats the caching
completely in shared-nothing deployments (PHP-FPM, multi-container setups),
causing a `/certs` fetch on every validation.

Recommended backends: Redis, APCu, filesystem cache — anything that
survives across PHP request lifecycles. Examples:

- Symfony: `Symfony\Component\Cache\Adapter\RedisAdapter`
- Laravel: `Illuminate\Cache\Psr6\CachePool` wrapping the application cache
- WordPress/WooCommerce: any plugin bridging WP transients or object cache
  to PSR-6
- Plain PHP: `Symfony\Component\Cache\Adapter\FilesystemAdapter`

If `/certs` is unreachable during a rotation fallback, JWT validation
fails loudly — the SDK never accepts an unsigned or unverifiable token.

### Forcing a JWKS refresh

Normal Keycloak rotation is handled automatically via the unknown-`kid`
fallback. For incident response (revoked or compromised signing key),
the cached key set must be purged from the PSR-6 pool so the next JWT
validation cold-starts it from `/certs` again.

`Firebase\JWT\CachedKeySet` stores its entries under the `jwks` prefix,
but `setCacheKeys()` SHA-256-hashes the composite key whenever it exceeds
64 characters — and every real Paysera JWKS URL crosses that threshold.
The prefix is lost in the final storage key, which means a
`redis-cli --scan --pattern 'jwks*'` sweep will not match anything.

The reliable approach is to dedicate an isolated PSR-6 pool to the SDK
and clear it as a whole during an incident:

- Dedicated Redis DB: `redis-cli -n <sdk-db-index> FLUSHDB`
- Symfony Cache (dedicated pool): `bin/console cache:pool:clear <paysera-sdk-pool>`
- Dedicated filesystem cache: delete the cache directory
  (`rm -rf var/cache/paysera-sdk/*`) and reload the PHP-FPM pool
- APCu: `apcu_clear_cache()` only if APCu is not shared with unrelated
  application data; otherwise prefer a different backend

Sharing the SDK's PSR-6 pool with unrelated application caches defeats
this procedure — a full flush would also wipe business data. Keep the
pool SDK-scoped so incident response stays a one-liner.

### HTTP client timeouts

JWKS fetches go through the PSR-18 client injected via `setHttpClient()`
(or the default `CurlHttpClient` otherwise). During cold start and
rotation fallback, the call blocks the caller thread. A slow or
unreachable `/certs` endpoint with an unbounded client timeout turns
every such call into a hung checkout request.

**Required for production**: configure a finite connect timeout (~5 s)
and total-request timeout (~10 s) on the injected PSR-18 client.

- The default `CurlHttpClient` already sets `CURLOPT_CONNECTTIMEOUT=10`
  and `CURLOPT_TIMEOUT=30`.
- Symfony HttpClient: `HttpClient::create(['timeout' => 10, 'max_duration' => 10])`.
- Guzzle: `new Client(['connect_timeout' => 5, 'timeout' => 10])`.

## Documentation

### Use Cases
- [Authorization](docs/AUTHORIZE.md) - Authenticate with the Payment API and decode JWT tokens
- [Payment Initiation](docs/INITIATE_PAYMENT.md) - Create payment orders and generate payment links
- [Callback Processing](docs/PROCESS_PAYMENT_CALLBACK.md) - Securely process payment webhooks
- [Translations](docs/TRANSLATIONS.md) - Localization with SDK and plugin namespace support
- [Local Development](docs/LOCAL_DEVELOPMENT.md) - Setup and testing in development environment

## Version History

See [CHANGELOG.md](CHANGELOG.md) for detailed version history and upgrade notes.

## License

LGPL-3.0-or-later
