Heatrail

Arendajatele

PHP SDK.

Sõltuvusteta klient poodidele, kellel on oma PHP kassa, ühilduv PHP 7.3 kuni 8.5-ga. Ta omab kolme asja, mida on kerge valesti teha: identifikaatorite räsi, HR1 päringu allkirjastamine, ja fail-open ajalimiit. Tema kaks käitumislepingut on kirjutatud nii, et nad ei saa vaikselt kohti vahetada.

01

Paigaldus

Composer sõltuvusi ei ole, teadlikult.

Käitusaeg
PHP 7.3 kuni 8.5, koos ext-curl ja ext-json. See on kogu sõltuvuste nimekiri. Poed vanal, külmutatud PHP-l on täpselt need poed, kes pettusesignaali vajavad, seega SDK on kirjutatud PHP 7.3 ühilduvas süntaksis.
Nimeruum
Heatrail\Sdk\, PSR-4, seotud arhiivi src/ kataloogiga.
Kust ta tuleb
Arhiivi serveeritakse partnerportaalist, ja ta on ka WooCommerce lisa sees. Ta ilmub Downloads lehele, kui teie taotlus on kinnitatud, nii Free kui Pro plaanil. Teda ei ole Packagistis.
Iga väljalase ehitatakse sildistatud commitist reprodutseeritavalt ja tema SHA-256 avaldatakse API viites. Kontrollige arhiiv selle väärtuse vastu enne, kui te ta vendorite.

Kui te juba jooksutate Composerit, suunake path repository lahtipakitud arhiivile. Kui ei, pange lähtekood paika ja registreerige neli rida autolaadurit:

spl_autoload_register(function (string $class): void {
    $prefix = 'Heatrail\\Sdk\\';

    if (strncmp($class, $prefix, strlen($prefix)) !== 0) {
        return;
    }

    $file = __DIR__ . '/heatrail-sdk/' . str_replace('\\', '/', substr($class, strlen($prefix))) . '.php';

    if (is_file($file)) {
        require $file;
    }
});
02

Klient

Konstrueerige ta üks kord, seadistusest.

HeatrailClient::forShop() on valikute massiivi tehas, sõbralikum kui üksteist positsioonilist argumenti PHP versioonil, millel nimega argumente ei ole. Konstruktor ise on avalik, kui eelistate teda.

use Heatrail\Sdk\HeatrailClient;

$client = HeatrailClient::forShop(
    $keyId,                              // hk_…
    $secret,                             // signing secret, server-side only
    $baseUrl,                            // https://api.heatrail.eu
    [
        'network_salt' => $networkSalt,  // identifier hashing only
        'timeout_ms'   => 800,           // default; must be positive
        'queue'        => $queue,        // optional QueueStore
        'logger'       => $logger,       // optional (level, message, context)
    ]
);
network_salt
Vaja ainult hashEmail() ja hashPhone() jaoks. Andke tühi string, kui räsite identifikaatorid ise mujal.
timeout_ms
Vaikimisi HeatrailClient::DEFAULT_TIMEOUT_MS, mis on 800. Mittepositiivne väärtus viskab konstrueerimisel, selle asemel et vastu võtta: cURL loeb null ajalimiiti kui üldse ilma ajalimiidita, mis lööks kogu fail-open eelarve vaikselt maha. Langetage, kui soovite. Kaugele üles ärge tõstke.
queue
QueueStore. Ilma selleta logitakse ebaõnnestunud kontroll ja ta kaob. Sellega läheb ta korduseks järjekorda. Osa 05.
logger
PSR-3 kujuline callable, mis võtab taseme, sõnumi ja konteksti. Väga soovitatav: just nii muutub vaikselt degradeerunud liidestus nähtavaks.
transport
Vaikimisi kaasasolev cURL transport. Vahetatav, mis on peamiselt see, kuidas SDK enda testid võrku väldivad. sleeper, clock ja nonce_factory on samal põhjusel.
03

Räsimine

null tähendab jätmist. See ei tähenda tühja.

hashEmail(?string $raw) ja hashPhone(?string $raw, string $country) normaliseerivad väärtuse ja rakendavad esimese astme HMAC-i teie võrgusoolaga, tagastades 64 väiketähega hex märki, või null.

null tuleb tagasi tühja väärtuse, ainult tühikutest koosneva väärtuse, tuntud kohatäite nagu n/a või test@test.com, ja kõige mitteloetava peale. Kui ta tuleb, jätke väli payloadist täielikult välja. Ärge saatke tühja stringi. Ärge asendage varuväärtusega: jagatud näidise räsi saaks üheks hiiglaslikuks sõlmeks, mis seoks iga külaliskassa võrgus iga teisega, ja SDK kõige alumine kiht keeldub teda üldse arvutamast.

Telefoni normaliseerimine katab Eesti numbrid. Number, mida klient parsida ei suuda, tagastab null ja väli jäetakse ära, mitte ei arvata. Laiema katte jaoks normaliseerige ise E.164 peale ja räsige tulemus sama esimese astme konstruktsiooniga.

04

Kaks lepingut

Üks meetod ei viska kunagi. Üks viskab alati.

Asümmeetria ongi disain. Lugege ta läbi enne, kui kirjutate koodi kummagi meetodi vastu.

check() on fail-open

$result = $client->check(array $payload, string $idempotencyKey): array;

Õnnestumisel saate API keha muutmata, väli välja haaval nii, nagu teda kirjeldab API viide. Iga tõrke peale, ajalimiit, ühenduse viga, non-2xx, kodeerimatu payload, vigane JSON, saate selle asemel degradeerunud kuju:

['degraded' => true, 'score' => 0, 'reasons' => []]

Ta ei viska kunagi. Tõrge logitakse, ja kontroll lükatakse järjekorda, kui see on seadistatud. Skoor on null sellepärast, et teatada ei ole midagi, mitte sellepärast, et tellimus hinnati ja tunnistati korras olevaks. Hargnege oma koodis degraded pealt enne, kui hargnete score pealt.

Joonis 1

Mida SDK teeb, kui Heatrail ei vasta: müük läheb edasi ja check jääb hilisemaks alles.Ülemises reas viib kast Kassa, kus tellimuse rida on juba olemas, noolega kasti check, mis on piiratud 800 ms peale ja ei viska kunagi erindit, ja sealt kasti Iga tõrge, olgu see ajalimiit, non-2xx vastus või vigane JSON, millele vastatakse väärtusega degraded true ja skooriga null. Kastist check langeb katkendlik nool halli kasti Ost peatatakse, ja tema kõrval olev märkus ütleb, et seda katkendlikku haru ei ole olemas. Tõrkekastist läheb pidev joon alla kolme kasti ritta lehe jalal: check läheb järjekorda koos täpsete baitide ja algse võtmega, tühjendatakse hiljem pärast tervisesondi, ja loetakse üks kord, sest sama võti tagastab hoitud vastuse. Lõpurida ütleb, et ostu ei peatata, ei viivitata ega tühistata kunagi, ja et puudu jääb signaal, mitte müük.Heatrail ei vastaKassatellimuse ridaon juba olemascheck()piiratud 800 ms pealeei viska kunagiIga tõrgeajalimiit, non-2xx, vigane JSONdegraded true, skoor 0Ost peatataksesellist haru ei oleKatkendlikku teedei ole olemas.Järjekordatäpsed baidid, algne võtiTühjendus hiljemtervisesond, siis kordusLoetakse üks kordsama võti, hoitud vastusOstu ei peatata, ei viivitata ega tühistata kunagi. Puudu jääb signaal, mitte müük.
Tõrketee lõpeb järjekorras. Ostuni ta ei jõua kunagi.

Üks tõrge jäetakse teadlikult järjekorda panemata: 409. See tähendab, et API hoiab seda idempotentsusvõtit juba, tavaliselt kassatee, mis tulistab sama kontrolli kaks korda, seega korduseks ei ole midagi. Ta logitakse eraldi, kandes API eristajat, et healoomuline võidujooks oleks näha healoomulisena ja võtme kokkupõrge teie enda koodis oleks näha veana.

label() viskab

use Heatrail\Sdk\HeatrailLabelException;

try {
    $confirmed = $client->label(
        $orderRef,
        'fraud',                       // fraud | chargeback | real
        $operatorIdentifier,           // recorded as labelled_by
        $idempotencyKey,
        [
            'reason_code'        => 'stolen_card',
            'evidence_type'      => 'merchant_confirmation',
            'order_value_bucket' => '100-250',
        ]
    );

    // ['ok' => true, 'incident_id' => '…']
} catch (HeatrailLabelException $e) {
    // Tell the operator. Do not record the label as applied.
}

Transpordiviga, non-2xx, vigane JSON, või isegi 2xx, mis saabus ilma intsidendi identifikaatorita, kõik tõstavad HeatrailLabelException. Meetod tagastab õnnestumise kuju ainult siis, kui API sildi kinnitas, sest silt on selge inimese otsus, ja selle väljamõtlemine rikkuks jagatud võrku ühe võrgutõrke pealt.

Silte järjekorda ei panda. Korduskatse on operaatori selge valik, mitte taustatöö oma.

Kaks väiksemat meetodit

explain(string $orderRef)
Loeb skoori, põhjused ja kontrolli aja uuesti ühe teie enda tellimuse kohta. Pehme tõrge nagu check(). Iga tõrge tagastab ['degraded' => true, 'status' => …] ja ta ei viska kunagi, sest ainult lugemiseks mõeldud mugavus ei tohi tellimuse ekraani maha võtta. Järjekorda teda ei panda, sest korduseks ei ole midagi ja operaator klõpsab uuesti.
health()
Allkirjastatud elussond, mis tagastab ['status' => 'up'] või ['status' => 'degraded']. Kasutage teda volituste kontrollimiseks seadistamise ajal. Järjekorra tühjendus kutsub teda esimesena ise.
05

Järjekord

Järjekord on koht, kus fail-open lakkab tähendamast andmekadu.

Andke kliendile QueueStore ja iga ebaõnnestunud kontroll püütakse koos kõigega, mida täpseks korduseks vaja: meetod, tee, täpsed keha baidid, ja algne idempotentsusvõti. Liides on teadlikult väike, et saaksite teda toetada sellega, mis teie poes juba on.

interface QueueStore
{
    /** @param array{method: string, path: string, body: string, idempotency_key: string, enqueued_at: string} $job */
    public function push(array $job): void;

    /** $sender returns true when delivered (row removed), false to keep it. */
    public function drain(callable $sender): int;
}

drainQueue($queue) sondib esmalt health() ja jätab kogu tsükli vahele, kui API ei teata up, et taastuv teenus ei saaks peksma. Siis kordab ta iga töö, taandudes eksponentsiaalselt koos jitteriga järjestikuste tõrgete vahel. Ta tagastab edukalt tühjendatud tööde arvu.

Kaks ajatempli, mida ei tohi segi ajada

Iga saatmine, algne või kordus, allkirjastatakse värske ajatempli ja värske noncega, sest need on transporditaseme korduskaitse ja API lükkab vananenud tagasi. Töö enqueued_at on järjestuse metaandmed ja teda ei saadeta kunagi päisena. Mis teeb korduse ohutuks, on muutmata idempotentsusvõti: kui algne kontroll API-ni jõudis ja te lihtsalt vastuse maha magasite, tagastab API hoitud vastuse ilma midagi uuesti jooksutamata.

Viitehoidla, ja mida produktsioonihoidla juurde lisab

FileQueueStore kirjutab ühe JSON rea töö kohta eksklusiivse luku all, ja kirjutab faili üks kord ümber, tühjenduse lõpus, et krahh keset käiku jätaks järjekorra terveks. Ta on ühe hosti oma, ja tal ei ole mahupiiri ega väljatõstmist. Kuni API on kättesaamatu ja kassad avanevad fail-open, see fail kasvab ja iga käik proovib iga töö uuesti.

Produktsioonis implementeerige samad kaks meetodit oma tabeli vastu ja lisage säilitus: katsete loendur, vanuse või katsete lagi, mille järel töö pargitakse, mitte ei kustutata, ja mahupiir. Järjekord, mis kirjeid vaikselt kaotab, on hullem kui järjekorra puudumine. WooCommerce lisa toob sellise hoidla kaasa, ja tema leht ütleb kasutatud arvud välja.

06

Töötav näide

Kohandatud kassa, otsast lõpuni.

Üks klass, mis mähkib kliendi, kolme kutsekohaga, mida päris pood vajab: hinda tellimus, märgi tulemus, tühjenda järjekord. Order seisab selle eest, mis teie enda tellimuse objekt on.

<?php

declare(strict_types=1);

use Heatrail\Sdk\HeatrailClient;
use Heatrail\Sdk\HeatrailLabelException;
use Heatrail\Sdk\Queue\FileQueueStore;

final class FraudSignal
{
    /** @var HeatrailClient */
    private $client;

    /** @var FileQueueStore */
    private $queue;

    public function __construct()
    {
        $this->queue = new FileQueueStore('/var/lib/heatrail/queue.jsonl');

        $this->client = HeatrailClient::forShop(
            (string) getenv('HEATRAIL_KEY_ID'),
            (string) getenv('HEATRAIL_API_SECRET'),
            (string) getenv('HEATRAIL_API_BASE'),
            [
                'network_salt' => (string) getenv('HEATRAIL_NETWORK_SALT'),
                'queue' => $this->queue,
                'logger' => static function (string $level, string $message, array $context): void {
                    error_log('[heatrail] ' . $level . ' ' . $message . ' ' . json_encode($context));
                },
            ]
        );
    }

    /**
     * Call once, straight after the order row exists and before the order is
     * handed to the payment provider.
     *
     * @return array{score: int, reasons: array}|null null = no signal
     */
    public function scoreOrder(Order $order): ?array
    {
        $payload = [
            'order_ref' => $order->reference,   // ^[A-Za-z0-9._:-]{1,128}$
            'ip' => $order->clientIp,           // required
            'context' => [
                'product_risk_class' => $order->hasGiftCard ? 'gift_card' : 'physical',
                'qty' => $order->itemCount,
                'amount_bucket' => self::amountBucket($order->totalEur),
                'account_age_class' => $order->isGuest ? 'new' : 'existing',
            ],
        ];

        if ($order->sessionId !== null) {
            $payload['session_ref'] = $order->sessionId;
        }

        // null means OMIT. Never send '', never hash a fallback.
        $emailHash = $this->client->hashEmail($order->email);

        if ($emailHash !== null) {
            $payload['email_h'] = $emailHash;
        }

        $phoneHash = $this->client->hashPhone($order->phone, 'EE');

        if ($phoneHash !== null) {
            $payload['phone_h'] = $phoneHash;
        }

        $result = $this->client->check($payload, self::checkKey($order->reference));

        // Fail-open: "no signal" is NOT "low risk". Say so in your own code.
        if (! empty($result['degraded'])) {
            return null;
        }

        return [
            'score' => isset($result['score']) ? (int) $result['score'] : 0,
            'reasons' => isset($result['reasons']) && is_array($result['reasons']) ? $result['reasons'] : [],
        ];
    }

    /**
     * Returns false when Heatrail did not confirm. Show that to the operator
     * and never record a label the API did not accept.
     */
    public function applyLabel(Order $order, string $label, string $reasonCode, string $operator): bool
    {
        try {
            $this->client->label(
                $order->reference,
                $label,                                  // fraud | chargeback | real
                $operator,
                self::labelKey($order->reference, $label),
                [
                    'reason_code' => $reasonCode,        // ignored for 'real'
                    'evidence_type' => 'merchant_confirmation',
                    'order_value_bucket' => self::amountBucket($order->totalEur),
                ]
            );

            return true;
        } catch (HeatrailLabelException $e) {
            error_log('[heatrail] label not confirmed: ' . $e->getMessage());

            return false;
        }
    }

    /** Run from cron, once a minute. */
    public function drain(): int
    {
        return $this->client->drainQueue($this->queue);
    }

    /**
     * Deterministic per order, and built from a hash so that an order
     * reference containing '.' or ':' still yields a key matching the API's
     * ^[A-Za-z0-9_-]{16,128}$.
     */
    private static function checkKey(string $orderRef): string
    {
        return 'chk-' . substr(hash('sha256', $orderRef . '|' . getenv('HEATRAIL_KEY_ID')), 0, 32);
    }

    private static function labelKey(string $orderRef, string $label): string
    {
        return 'lbl-' . substr(hash('sha256', $orderRef . '|' . $label), 0, 32);
    }

    private static function amountBucket(float $total): string
    {
        if ($total < 50) {
            return '0-50';
        }

        if ($total < 100) {
            return '50-100';
        }

        if ($total < 250) {
            return '100-250';
        }

        return $total < 500 ? '250-500' : '500+';
    }
}

Näide kasutab tüübitud omadusi ja nullable tagastustüüpe, seega ta tahab PHP 7.4 või uuemat. SDK ise jääb 7.3 ühilduvaks, aga teie kood ei pea.

Ja kutsekohad:

// In checkout, after the order exists:
$signal = new FraudSignal();
$assessment = $signal->scoreOrder($order);

if ($assessment !== null && $assessment['score'] >= 70) {
    $order->flagForManualReview($assessment['reasons']);
}

// The order continues regardless. There is no else branch that stops a sale.
// bin/heatrail-drain.php, from cron, every minute:
(new FraudSignal())->drain();

Kõike, mida SDK teeb, saate teha ka otse.

Heatrail

This site is not public yet

Heatrail is in a private pre-launch period. Enter the access password to continue.

Enquiries: hello@heatrail.eu