定制 mfadul24/kount-commerce 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

mfadul24/kount-commerce

Composer 安装命令:

composer require mfadul24/kount-commerce

包简介

PHP SDK for the Kount 360 Commerce APIs (Payments Fraud 2.0) — PSR-18/PSR-17 based, OAuth2 client-credentials with optional PSR-6 token caching, plus a legacy RIS migration facade.

README 文档

README

Sponsor

Modern, PSR-first PHP SDK for the Kount 360 Commerce APIs (Payments Fraud 2.0) — evaluate orders for fraud risk and receive a typed Approve / Decline / Review decision.

  • PSR-18 / PSR-17 transport — bring your own HTTP client; nothing concrete is forced on you. php-http/discovery finds an installed client automatically.
  • OAuth2 client-credentials built in — with an optional PSR-6 cache so the ~20-minute bearer token is reused across requests and processes.
  • Typed, generated-from-OpenAPI models hydrated by symfony/serializer — enums, nested objects and dates, no raw arrays.
  • One exception marker interface (Kount\Exception\KountException) covering every failure mode.
  • Legacy RIS migration facade for consumers of the old Kount/kount-ris-php-sdk.

Installation

The SDK requires a PSR-18 client and PSR-17 factories at runtime (declared as virtual packages). If your project doesn't already ship one:

composer require mfadul24/kount-commerce symfony/http-client nyholm/psr7

Any other PSR-18/PSR-17 implementation (Guzzle 7, httplug adapters, ...) works exactly the same.

Quickstart

use Kount\KountCommerceApi;
use Kount\Models\Components\OrderPostBody;
use Kount\Models\Components\Item;

$kount = KountCommerceApi::builder()
    ->setOAuth('your-client-id', 'your-client-secret')
    ->build();

$result = $kount->orders->evaluate(
    OrderPostBody::for('ORDER-4711')
        ->withDeviceSession($deviceSessionId) // unique for >= 30 days
        ->addItem(new Item(name: 'Widget', quantity: 2)),
    riskInquiry: true,
);

$decision = $result->order?->riskInquiry?->decision;   // enum: Approve | Decline | Review
$omniscore = $result->order?->riskInquiry?->omniscore; // float 0-99

Available operations on $kount->orders:

Method Endpoint
evaluate(OrderPostBody $body, ?bool $riskInquiry, ?bool $excludeDevice, ?bool $excludeFromPaymentsModel) POST /v2/orders
get(string $orderId) GET /v2/orders/{orderId}
update(string $orderId, OrderPatchBody $body) PATCH /v2/orders/{orderId}
batchUpdateReversals(BatchUpdateReversalsRequest $request) PATCH /v2/orders:batchUpdateReversals

Credentials

Kount issues the OAuth2 client-credentials secret in one of two shapes, and the builder has a method for each:

You were given Use Header the SDK sends
a discrete client id + client secret setOAuth($clientId, $clientSecret) Basic base64("$clientId:$clientSecret")
a single pre-encoded API key (the ready-made base64 of clientId:clientSecret) setApiKey($apiKey) Basic $apiKey — used verbatim

Some tenants — for example those onboarded via Digistore24 — never receive a separate id/secret pair, only the encoded API key. Pass it straight through; the SDK does not decode or split it:

$kount = KountCommerceApi::builder()
    ->setApiKey('bXktY2xpZW50LWlkOm15LWNsaWVudC1zZWNyZXQ=')
    ->build();

setApiKey() behaves exactly like setOAuth() otherwise — same token endpoint, the same 401 → refresh → retry, and the same optional PSR-6 cache and tokenUrl: override:

$kount = KountCommerceApi::builder()
    ->setApiKey($apiKey, tokenCache: new FilesystemAdapter())
    ->useSandbox()
    ->build();

Under the hood both route to Kount\Auth\ClientCredentialsTokenProvider, which stores only the encoded Basic credential:

use Kount\Auth\ClientCredentialsTokenProvider;

ClientCredentialsTokenProvider::fromApiKey($apiKey, $tokenUrl);                    // pre-encoded key
ClientCredentialsTokenProvider::fromClientCredentials($id, $secret, $tokenUrl);   // id + secret pair

Token caching (PSR-6)

Without a cache the bearer token is memoized per process only — under PHP-FPM that means one token exchange per worker. Inject any PSR-6 pool to share the token (TTL is expires_in − 60s safety skew). The example uses FilesystemAdapter, which requires composer require symfony/cache:

use Symfony\Component\Cache\Adapter\FilesystemAdapter;

$kount = KountCommerceApi::builder()
    ->setOAuth('your-client-id', 'your-client-secret', tokenCache: new FilesystemAdapter())
    ->build();

On a 401 the SDK invalidates the cached token, re-authenticates and retries the request exactly once; a second 401 throws AuthenticationException.

Bringing your own HTTP client

Retry and timeout policy intentionally belong to your client, not the SDK:

use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Component\HttpClient\HttpClient;

$kount = KountCommerceApi::builder()
    ->setClient(new Psr18Client(HttpClient::create(['timeout' => 10])))
    ->setOAuth('your-client-id', 'your-client-secret')
    ->build();

setRequestFactory() / setStreamFactory() accept custom PSR-17 factories; setLogger() accepts any PSR-3 logger.

Tracing outbound requests

Need per-request traceability — duration, request body, response body — for every external call the SDK makes? Decorate your PSR-18 client. Every outbound call flows through the one client you inject, so a single decorator captures both the OAuth token POST and the /v2/orders calls (tell them apart by URL). Timing and traceability policy belong to the transport layer, exactly like retries.

The SDK ships Kount\Http\SensitiveDataRedactor so your trace never records a live credential: it masks the whole Authorization header (Basic on the token call, Bearer on order calls) and any JSON body key whose name contains token (the token endpoint's access_token). Everything else — order payloads, error documents, the token request's grant_type/scope form body — is left verbatim.

use Kount\Http\SensitiveDataRedactor;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

final class TracingClient implements ClientInterface
{
    public function __construct(
        private readonly ClientInterface $inner,
        private readonly YourTraceRecorder $recorder,
        private readonly SensitiveDataRedactor $redactor = new SensitiveDataRedactor(),
    ) {}

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        $requestBody = (string) $request->getBody(); // (string) rewinds a seekable stream

        $start = microtime(true);
        $response = $this->inner->sendRequest($request);
        $durationSeconds = microtime(true) - $start;

        $this->recorder->record(
            method: $request->getMethod(),
            uri: (string) $request->getUri(),
            requestHeaders: $this->redactor->redactHeaders($request->getHeaders()),
            requestBody: $this->redactor->redactBody($requestBody),
            statusCode: $response->getStatusCode(),
            responseBody: $this->redactor->redactBody((string) $response->getBody()),
            durationSeconds: $durationSeconds,
        );

        return $response;
    }
}

$kount = KountCommerceApi::builder()
    ->setClient(new TracingClient($inner, $recorder))
    ->setOAuth('your-client-id', 'your-client-secret')
    ->build();

Two things to keep in mind:

  • Seekable streams. Reading a body consumes its stream. The (string) cast above seeks back to the start first (PSR-7 __toString), and the SDK re-reads the response the same way — so as long as your PSR-18 client returns seekable streams (guzzle and nyholm do), reading for the trace doesn't starve the SDK. With a non-seekable stream you must buffer the body yourself.
  • Redaction is yours to keep. Only the values above are masked. If you also want to keep order PII (email, payment fields) out of traces, extend the redactor: new SensitiveDataRedactor(sensitiveKeySubstrings: ['token', 'email', 'pan']), or pass extra header names via the first argument.

Sandbox

$kount = KountCommerceApi::builder()
    ->setOAuth('sandbox-client-id', 'sandbox-client-secret')
    ->useSandbox()
    ->build();

Warning

The default production/sandbox hosts (https://api.kount.com/commerce, https://api-sandbox.kount.com/commerce) and the OAuth token URL are taken from the OpenAPI spec plus Kount's public documentation but have not yet been verified against live credentials. If your Kount onboarding material lists different endpoints, override them with setServerUrl() and the tokenUrl: argument of setOAuth() / useSandbox(). Running the gated live smoke test (below) settles this.

Error handling

Every exception the SDK throws implements Kount\Exception\KountException:

Exception Meaning
AuthenticationException Credentials rejected by the token endpoint, or persistent 401 after a forced refresh
TransportException The PSR-18 client failed (network, DNS, timeout) — including token endpoint calls; original exception in getPrevious()
ApiException Non-2xx API answer; carries statusCode, raw body, response and the typed error document: errorDetails (POST 400/500) or rpcStatus (all other error bodies)
MalformedResponseException 2xx answer that could not be decoded into the expected model
UnsupportedFeatureException Legacy RIS feature without a 2.0 equivalent (facade only)
InvalidArgumentException Pre-send validation (empty order id, missing credentials) — never reaches the network; extends \InvalidArgumentException
use Kount\Exception\ApiException;
use Kount\Exception\KountException;

try {
    $result = $kount->orders->evaluate($body);
} catch (ApiException $e) {
    $log->warning('Kount rejected the call', [
        'status' => $e->statusCode,
        'correlationId' => $e->errorDetails?->correlationId,
        'rpcMessage' => $e->rpcStatus?->message,
    ]);
} catch (KountException $e) {
    // transport / auth / decoding problems
}

The SDK parses responses strictly against the OpenAPI contract and fails closed: an unknown enum value or a number where the spec promises a string (e.g. the uint64-as-string amount fields) raises MalformedResponseException rather than degrading silently.

Auditability

Kount's cross-system trace id arrives as the X-Correlation-Id response header and is not part of any success model. After a call, $kount->orders->lastCorrelationId() returns the header of the most recent response on that instance (null when absent). It refers to the last call only and is not concurrency-safe across a shared instance — read it right after the call whose trace you want.

Migrating from the legacy RIS SDK

Kount\Legacy\RisClient offers familiar Inquiry / Response semantics on top of the 2.0 pipeline — see docs/MIGRATION.md for the full field mapping.

Upgrading between versions of this SDK? See docs/UPGRADING.md for breaking changes and migration steps.

use Kount\Legacy\Inquiry;
use Kount\Legacy\RisClient;

$ris = new RisClient($kount);

$response = $ris->process(
    (new Inquiry())
        ->setSessionId($deviceSessionId)
        ->setOrderNumber('ORDER-4711')
        ->setEmail('customer@example.com')
        ->setTotal(1999)          // cents, as in legacy RIS
        ->setCurrency('EUR'),
);

$response->getDecision();  // 'A' | 'D' | 'R'
$response->getOmniscore(); // float

Legacy-only features (Khash payment encoding, masked payment tokens) throw UnsupportedFeatureException instead of silently doing nothing.

Development

composer install
composer lint   # ECS code style
composer stan   # PHPStan, level max (zero baseline)
composer test   # unit + mocked integration tests

The live sandbox smoke test never runs in CI; it is gated on env vars:

KOUNT_SANDBOX_CLIENT_ID=... KOUNT_SANDBOX_CLIENT_SECRET=... composer test:live

Optional: KOUNT_SANDBOX_SERVER_URL / KOUNT_SANDBOX_TOKEN_URL to override the unverified default endpoints.

About this codebase

The typed models were originally bootstrapped from docs/swagger.json with a code generator; the SDK is hand-maintained since and has no generator toolchain dependency. docs/swagger.json remains as the API contract reference.

Sponsoring

If this SDK saves you time, consider supporting its maintenance by sponsoring @mfadul24 on GitHub. ❤️

License

MIT

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-08

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固