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.
Paigaldus
Composer sõltuvusi ei ole, teadlikult.
- Käitusaeg
- PHP 7.3 kuni 8.5, koos
ext-curljaext-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 arhiivisrc/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;
}
});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()jahashPhone()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. queueQueueStore. 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,clockjanonce_factoryon samal põhjusel.
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.
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
Ü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.
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.
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();