Cliente e notifier
Helpers mínimos em PHP e TypeScript para assinar pedidos e notificar a AACT após confirmação de pagamento.
O que construir
No backend de origem, mantenha duas peças pequenas: um client que assina e faz POST dos eventos, e um notifier/hook invocado só depois do PSP confirmar que a venda está paga. Prefira uma queue à volta do notifier para que falhas da AACT nunca revertam uma encomenda paga.
- Client: construir JSON raw → HMAC → POST /api/v1/integration/events
- Notifier: mapear campos da encomenda/pagamento para payloads sale (e depois refund)
- Hook: chamar o notifier a partir do webhook paid / evidence do fornecedor
Ambiente
AACT_URL=http://localhost:18080
AACT_TOKEN=aact_…
AACT_HMAC_SECRET=…Crie a credencial em Admin → Integrations (palavra-passe de step-up). Use um source estável como node_checkout.
Cliente TypeScript
import { createHmac, randomBytes } from "node:crypto";
type AactConfig = {
baseUrl: string; // e.g. http://localhost:18080
token: string;
hmacSecret: string;
};
export async function postAactEvent(
config: AactConfig,
body: Record<string, unknown>,
): Promise<{ status: number; json: unknown }> {
const rawBody = JSON.stringify(body);
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomBytes(16).toString("hex");
const signature = createHmac("sha256", config.hmacSecret)
.update(`${timestamp}.${nonce}.${rawBody}`)
.digest("hex");
const res = await fetch(`${config.baseUrl}/api/v1/integration/events`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.token}`,
"Content-Type": "application/json",
Accept: "application/json",
"X-AACT-Timestamp": timestamp,
"X-AACT-Nonce": nonce,
"X-AACT-Signature": signature,
},
body: rawBody, // must be the same string that was signed
});
return { status: res.status, json: await res.json() };
}Notifier TypeScript (alvo do hook)
// Call after your PSP webhook confirms the sale is paid.
export async function notifyAactSale(input: {
orderId: string;
amountMinor: number;
currency: string;
referralCode?: string | null;
paidAt: Date;
}) {
const config = {
baseUrl: process.env.AACT_URL!,
token: process.env.AACT_TOKEN!,
hmacSecret: process.env.AACT_HMAC_SECRET!,
};
const body = {
external_event_id: `ord-${input.orderId}`,
event_type: "sale",
occurred_at: input.paidAt.toISOString(),
amount_minor: input.amountMinor,
currency: input.currency.toUpperCase(),
referral_code: input.referralCode ?? undefined,
metadata: { channel: "node_checkout" },
};
// Prefer a queue/job in production — do not fail the paid sale if AACT is down.
const { status, json } = await postAactEvent(config, body);
if (status !== 200 && status !== 201) {
throw new Error(`AACT sale failed: ${status} ${JSON.stringify(json)}`);
}
return json;
}Ligue notifyAactSale ao handler do webhook paid (ou a um job que ele despacha). Reutilize o mesmo external_event_id nos retries; novo timestamp e nonce em cada tentativa.
Cliente PHP
<?php
final class AactClient
{
public function __construct(
private readonly string $baseUrl,
private readonly string $token,
private readonly string $hmacSecret,
) {}
/** @param array<string, mixed> $body */
public function postEvent(array $body): array
{
$rawBody = json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = hash_hmac('sha256', $timestamp.'.'.$nonce.'.'.$rawBody, $this->hmacSecret);
$ch = curl_init(rtrim($this->baseUrl, '/').'/api/v1/integration/events');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.$this->token,
'Content-Type: application/json',
'Accept: application/json',
'X-AACT-Timestamp: '.$timestamp,
'X-AACT-Nonce: '.$nonce,
'X-AACT-Signature: '.$signature,
],
CURLOPT_POSTFIELDS => $rawBody,
CURLOPT_TIMEOUT => 15,
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
throw new RuntimeException('AACT request failed: '.curl_error($ch));
}
curl_close($ch);
return [
'status' => $status,
'body' => json_decode($response, true, 512, JSON_THROW_ON_ERROR),
];
}
}Notifier PHP (alvo do hook)
<?php
/** Call after your PSP confirms the payment is paid (never on checkout start). */
final class AactSaleNotifier
{
public function __construct(private readonly AactClient $client) {}
public function notifyPaidSale(
string $orderId,
int $amountMinor,
string $currency,
?string $referralCode,
\DateTimeInterface $paidAt,
): ?array {
$body = [
'external_event_id' => 'ord-'.$orderId,
'event_type' => 'sale',
'occurred_at' => $paidAt->format(DATE_ATOM),
'amount_minor' => $amountMinor,
'currency' => strtoupper($currency),
'metadata' => ['channel' => 'node_checkout'],
];
if ($referralCode !== null && $referralCode !== '') {
$body['referral_code'] = $referralCode;
}
// Dispatch to a queue in production so AACT downtime cannot unwind a paid sale.
$result = $this->client->postEvent($body);
if (! in_array($result['status'], [200, 201], true)) {
throw new RuntimeException('AACT sale failed with HTTP '.$result['status']);
}
return $result;
}
}// Dentro do handler "paid" do PSP, após persistência local:
$notifier = new AactSaleNotifier(
new AactClient(
getenv('AACT_URL') ?: '',
getenv('AACT_TOKEN') ?: '',
getenv('AACT_HMAC_SECRET') ?: '',
),
);
$notifier->notifyPaidSale(
orderId: $order->id,
amountMinor: $order->amount_cents,
currency: $order->currency,
referralCode: $order->referral_code,
paidAt: $order->paid_at,
);Esboço de refund
Use um novo external_event_id e aponte related_external_event_id para o id da sale original.
await postAactEvent(config, {
external_event_id: `refund-${refundId}`,
event_type: "refund",
related_external_event_id: `ord-${orderId}`,
occurred_at: new Date().toISOString(),
amount_minor: refundAmountMinor,
currency: "EUR",
});Seguinte
- Narrativa de checkout
Fluxo completo e env de origem
- Autenticação
Headers e regras de assinatura
- Idempotência
Retries e comportamento de replay