Imported from Ticketpark/SaferpayJsonApi (
AGENTS.md). Install upstream withnpx skills add Ticketpark/SaferpayJsonApi. Copyright stays with the author.
AGENTS.md
Guidance for AI agents working on Ticketpark/SaferpayJsonApi.
Project overview
PHP library (ticketpark/saferpay-json-api) that wraps the Saferpay JSON API. It serializes typed request objects to JSON, sends them via a PSR-18 HTTP client, and deserializes responses.
- Namespace:
Ticketpark\SaferpayJson - Autoload:
lib/SaferpayJson/(PSR-4) - PHP: 8.3–8.5
- Key dependencies:
jms/serializer,psr/http-client,guzzlehttp/guzzle(default HTTP client)
Check lib/SaferpayJson/Request/Container/RequestHeader.php and README.md for the current target API version.
Repository layout
lib/SaferpayJson/
Request/ # API request classes and shared request infrastructure
Container/ # Request payload containers (nested JSON objects)
Enum/ # Backed enums for request-side API vocabulary
PaymentPage/ # PaymentPage/* endpoints
Transaction/ # Transaction/* endpoints
SecureCardData/ # Alias/* endpoints
Response/ # API response classes
Container/ # Response payload containers
Enum/ # Backed enums for response-side API vocabulary
PaymentPage/
Transaction/
SecureCardData/
tests/SaferpayJson/Tests/Request/ # Mirrors Request/ structure
tests/SaferpayJson/Tests/Integration/ # Sandbox integration tests (optional credentials)
example/ # Runnable usage examples (need credentials.php)
.github/
contributing.md # Container conventions (authoritative)
pull_request_template.md
Architecture
Request flow
- Create a
RequestConfigwith API credentials and optional test mode. - Instantiate a concrete
Requestsubclass with required containers. - Call
execute()→ HTTP POST to{rootUrl}{API_PATH}with JSON body. - Success → typed
Response; 4xx →SaferpayErrorExceptionwithErrorResponse.
RequestHeader (including SpecVersion) is injected automatically via Request::getRequestHeader() and is not set on request classes directly.
Request classes
Each endpoint is a final class extending Request:
use RequestCommonsTraitpublic const string API_PATH = '/Payment/v1/...'public const string RESPONSE_CLASS = SomeResponse::class- Constructor takes
RequestConfigplus Saferpay-mandatory fields; callsparent::__construct($requestConfig) execute(): SomeResponsedelegates todoExecute()with a return type hint- Properties map to JSON via
#[SerializedName('FieldName')]attributes (PascalCase, matching Saferpay docs) - Enum-like API values use backed enums under
Request\Enum\(e.g.PaymentMethod,Wallet)
Containers
Request containers (lib/SaferpayJson/Request/Container/):
final class,#[SerializedName]on every property- Mandatory Saferpay fields → constructor parameters, no setters for constructor args
- Optional fields → nullable properties with getters/setters returning
self - Reuse existing containers when Saferpay reuses the same JSON structure (e.g.
ForeignRetailerin bothMarketplaceandMerchantFundDistributor)
Response containers (lib/SaferpayJson/Response/Container/):
- No constructors, no setters, all properties optional (
?type = null) - Getters only — even when Saferpay marks fields mandatory, keep them optional here
See .github/contributing.md for the full rationale.
Implemented endpoints
This library covers a subset of the Saferpay API. Currently implemented:
| Area | Endpoints |
|---|---|
| PaymentPage | Initialize, Assert |
| Transaction | Initialize, Authorize, AuthorizeDirect, AuthorizeReferenced, Capture, Refund, Cancel, Inquire |
| SecureCardData | Alias Insert, AssertInsert, InsertDirect, Update, Delete |
Not implemented (among others): MultipartCapture, AssertCapture, RefundDirect, RedirectPayment, AlternativePayment, DccInquiry, Batch, OmniChannel, Management API.
When bumping API versions, only add types for endpoints this library already implements, unless explicitly asked to add new endpoints.
Bumping the Saferpay API version
Follow the Saferpay changelog for the target version.
- Read the changelog entry for the new version (e.g. v1.43).
- Apply only changes relevant to implemented endpoints and containers.
- Update
RequestHeader::$specVersion(default spec version sent with every request). - Update the version link in
README.mdtohttps://saferpay.github.io/jsonapi/{version}/index.html. - Add new containers / fields / enum cases as described in the spec.
- Wire new optional containers into the matching
*Requestclasses with getter/setter. - Update integration test assertions if request/response shape or
SpecVersionchanged (see Integration tests). - Run tests and static analysis (see below).
Example (v1.43):
- New
MerchantFundDistributorcontainer → added toCaptureRequest CLICKTOPAYwallet → new case onRequest\Enum\WalletForeignRetailersubcontainer reused from v1.41
Branch and PR conventions:
- Branch:
feature/bump-saferpay-api-version-on-1-XX - Commit:
feat: Version bump to 1.XX - PR body: see Pull requests and fill in the template bullets (e.g.
Bump SpecVersion to 1.XX, one bullet per meaningful change).
Do not commit or open PRs unless the user asks.
Pull requests
Stick to .github/pull_request_template.md: Closes #<issue> followed by bullet points only.
- Never add a test plan section to the PR body.
- Never add a "created by …" signature to the PR.
Adding or changing code
New request container
final class ExampleContainer
{
#[SerializedName('FieldName')]
private ?string $field = null;
public function getField(): ?string { ... }
public function setField(?string $field): self { ... }
}
Place under Request/Container/. Use sub-namespaces when Saferpay groups containers (e.g. Request/Container/Transaction/).
New endpoint (only when requested)
- Add
*Requestin the appropriateRequest/subdirectory. - Add
*Responsein the matchingResponse/subdirectory extendingResponse. - Add containers for any new JSON structures.
- Add
tests/SaferpayJson/Tests/Request/.../*RequestTest.phpextendingCommonRequestTestCase. - Extend integration tests: add or update flows under
tests/SaferpayJson/Tests/Integration/and wireIntegrationRequestAssertions/IntegrationResponseAssertions(see Integration tests). - Optionally add an
example/script.
Enums
Documented API vocabulary lives in backed string enums, separated by direction:
- Request:
lib/SaferpayJson/Request/Enum/— used on request containers and request classes - Response:
lib/SaferpayJson/Response/Enum/— used on response containers and response classes
When Saferpay reuses the same JSON values on both sides (e.g. PaymentMethod, Wallet, Gender), define a case in each namespace. Keep case names and backing values in sync when bumping API versions.
Add a new enum file per vocabulary group; wire properties and getters/setters (requests) or getters only (responses) to the enum type. JMS enum support is enabled in SerializerFactory.
Style
declare(strict_types=1);in every file- Follow Symfony coding standards
finalon concrete classes- Minimal scope — match surrounding code; no drive-by refactors
Testing
composer install
composer test
composer test-integration # sandbox + Playwright; needs example/credentials.php
composer phpstan
composer rector-check
composer cs-check
composer cs-fix # apply coding standard fixes
Request tests extend CommonRequestTestCase:
- Implement
getInstance()returning a configured request - Call
doTestSuccessfulResponse(ResponseClass::class)for the happy path - Error path and
RequestConfigretry validation are inherited
Integration tests
Optional end-to-end tests against the Saferpay sandbox (composer test-integration). They require example/credentials.php (copy from credentials.dist.php) and Playwright for browser flows (Transaction/Initialize, PaymentPage, Alias/Insert).
In Docker: docker compose build && docker compose run --rm php composer install && docker compose run --rm php composer test-integration (see tests/README.md). The PHP image ships Node.js, Playwright OS deps, and Chromium at /opt/ms-playwright.
Location: tests/SaferpayJson/Tests/Integration/
| File | Role |
|---|---|
IntegrationTestCase.php |
Credentials, shared helpers (createTestPayment, createTestPayer, …) |
IntegrationRequestAssertions.php |
Mandatory JSON keys + optional fields on requests (serialize before execute()) |
IntegrationResponseAssertions.php |
All getters on responses and nested containers after execute() |
Flows call $this->executeIntegrationRequest($request), which asserts the serialized payload, posts to the API, then asserts the response. Unit tests (composer test) exclude this directory.
Update integration tests when you change:
- Add or remove an implemented endpoint → new/updated flow test +
assertIntegrationRequest()/ response assert method in the matching trait - Add/remove/rename request or response properties or containers → extend the corresponding assert helper and, if useful, set the field in a flow so the sandbox exercises it
- Change mandatory vs optional request fields → adjust mandatory-key checks in
IntegrationRequestAssertions - Bump
RequestHeader::$specVersion→ update expectedSpecVersioninassertRequestHeaderJson() - Change enum cases used in integration flows → update flow setup and enum JSON assertions
Do not skip integration assertion updates when altering request/response types that already have coverage — stale asserts hide spec drift.
CI (.github/workflows/tests.yml) runs PHPUnit on PHP 8.3–8.5 (prefer-lowest and prefer-stable). Static analysis on PHP 8.5 runs PHPStan level 8, Rector, php-cs-fixer, and composer validate. Integration tests are not run in CI by default (credentials required).
Common pitfalls
- Spec version: Must stay in sync across
RequestHeaderandREADME.md. - Mandatory vs optional: Follow Saferpay docs for requests; keep response fields optional regardless.
- RequestHeader: Never add it as a serialized property on request classes — it is a virtual property.
- Container reuse: Check for an existing container before creating a duplicate.
- Shared enums:
PaymentMethod,Wallet, andGenderexist in bothRequest\EnumandResponse\Enum; update both when Saferpay adds values. - Partial API coverage: Changelog entries may mention endpoints this library does not implement; skip those unless adding the endpoint is in scope.
- Tests: New request classes need a unit test class; existing unit tests mock Guzzle and do not hit the real API. Keep
IntegrationRequestAssertionsandIntegrationResponseAssertionsin sync with request/response changes (see Integration tests).