Docs AACT
|
Início

Guias

Visão geral
Começar
Integrações
  • Checkout
  • Cliente e notifier
  • Eventos
  • Autenticação
  • Idempotência
  • Reembolsos
API
Segurança
Operações

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

Só no servidor

Mantenha AACT_TOKEN e AACT_HMAC_SECRET fora do browser. Assine exactamente a string raw body que envia.

Ambiente

bash · backend de origem
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

typescript · postAactEvent
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)

typescript · notifyAactSale
// 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 · AactClient
<?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 · AactSaleNotifier
<?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;
    }
}
php · após confirmação paid
// 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.

typescript
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

Anterior

Integração de checkout

Seguinte

Eventos comerciais