Bac à sable

API des assistants de test

Jouez le rôle du payeur depuis vos propres tests, sans navigateur : terminer un paiement par carte, payer ou faire expirer un code espèces.

Ouvrir en Markdown
Sur cette page
  1. Terminer un paiement par carte
  2. Payer, faire expirer, refuser ou suspendre un code espèces
  3. Un paiement par carte complet, sans navigateur
  4. Ailleurs en mode test

Les assistants de test permettent à un test automatisé de faire ce qu’une personne fait en mode test : taper une carte de test sur la page de paiement, ou payer un code espèces chez un agent. Ils se trouvent sous /v1/test_helpers et n’acceptent qu’une clé secrète de test. Avec une clé de production, ils répondent 403, permission_error, test_mode_only.

Un assistant agit sur une tentative (ch_…) : le latest_charge d’une intention de paiement qui attend son payeur, en requires_action. Il joue le rôle du payeur ou du réseau, rien de plus : le paiement change ensuite comme il le ferait en production, par la notification du prestataire ou une vérification de statut, en général en quelques secondes. Relisez l’intention de paiement, ou attendez le webhook.

Terminer un paiement par carte

POST /v1/test_helpers/charges/{charge}/complete soumet un numéro de carte sur la page de paiement d’une tentative carte en attente, comme le ferait votre client.

  • card_number : l’une des cartes de test, avec ou sans espaces. Tout autre numéro est refusé.
  • three_d_secure, facultatif : pour la carte 3-D Secure, la réponse au défi simulé (voir la référence de l’API).
  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
complete.mjscomplete.tscomplete.pycomplete.phpcomplete.rbcomplete.goComplete.javaComplete.csmain.rs
curl https://pay-api.adpromax.com/v1/test_helpers/charges/ch_01k7d2j8m3n6b9v2c5x8z1a4qd/complete \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{"card_number": "4242424242424242"}'
const charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
const path = `/v1/test_helpers/charges/${charge}/complete`
await fetch(`https://pay-api.adpromax.com${path}`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ card_number: '4242424242424242' }),
})
interface CompleteCharge {
  /** One of the test cards, with or without spaces. */
  card_number: string
}

const charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
const path = `/v1/test_helpers/charges/${charge}/complete`
const body: CompleteCharge = { card_number: '4242424242424242' }
await fetch(`https://pay-api.adpromax.com${path}`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
})
import os

import requests

charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd"
requests.post(
    f"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    json={"card_number": "4242424242424242"},
)
<?php
$charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd';
$path = "/v1/test_helpers/charges/{$charge}/complete";
$ch = curl_init('https://pay-api.adpromax.com' . $path);
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['card_number' => '4242424242424242']),
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
require 'json'
require 'net/http'

charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
uri = URI("https://pay-api.adpromax.com/v1/test_helpers/charges/#{charge}/complete")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(card_number: '4242424242424242')

Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
package main

import (
	"log"
	"net/http"
	"os"
	"strings"
)

func main() {
	charge := "ch_01k7d2j8m3n6b9v2c5x8z1a4qd"
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/test_helpers/charges/"+charge+"/complete",
		strings.NewReader(`{"card_number": "4242424242424242"}`))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	res.Body.Close()
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Complete {
    public static void main(String[] args) throws Exception {
        String charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
        String body = """
            {"card_number": "4242424242424242"}
            """;
        HttpRequest request = HttpRequest.newBuilder(URI.create(
                "https://pay-api.adpromax.com/v1/test_helpers/charges/" + charge + "/complete"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.discarding());
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

var charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
using var response = await http.PostAsJsonAsync(
    $"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete",
    new { card_number = "4242424242424242" });
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
    reqwest::blocking::Client::new()
        .post(format!(
            "https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete"
        ))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .json(&json!({ "card_number": "4242424242424242" }))
        .send()?;
    Ok(())
}

Payer, faire expirer, refuser ou suspendre un code espèces

Quatre assistants agissent sur une tentative espèces en attente, comme le ferait le réseau de paiement. Chacun est un POST sans corps, sauf pay :

AssistantCe qui se passe
/v1/test_helpers/cash_vouchers/{charge}/payLe code est payé. amount, facultatif, en centimes : payer un autre montant que celui du bon.
/v1/test_helpers/cash_vouchers/{charge}/expireLe code atteint son échéance sans être payé.
/v1/test_helpers/cash_vouchers/{charge}/refuseLe réseau refuse le code.
/v1/test_helpers/cash_vouchers/{charge}/suspendLe code est mis en attente ; payez-le ensuite avec pay.

L’effet de chaque issue sur le paiement est décrit dans Scénarios de test en espèces.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
cash.mjscash.tscash.pycash.phpcash.rbcash.goCash.javaCash.csmain.rs
CH=ch_01k7d5f3a1s5d9f2g6h4j8k3nb
curl -X POST https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
  -u "$APX_SECRET_KEY:"

# or pay another amount: half of a 150.00 MAD payment
curl https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{"amount": 7500}'
const charge = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
const helper = action =>
  `https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/${charge}/${action}`
const headers = {
  'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
  'Content-Type': 'application/json',
}

await fetch(helper('pay'), { method: 'POST', headers })
// or: helper('expire'), helper('refuse'), helper('suspend'),
// or pay another amount:
// fetch(helper('pay'), { method: 'POST', headers,
//   body: JSON.stringify({ amount: 7500 }) })
type CashAction = 'pay' | 'expire' | 'refuse' | 'suspend'

const charge = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
const helper = (action: CashAction): string =>
  `https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/${charge}/${action}`
const headers = {
  'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
  'Content-Type': 'application/json',
}

await fetch(helper('pay'), { method: 'POST', headers })
// or: helper('expire'), helper('refuse'), helper('suspend'),
// or pay another amount:
// fetch(helper('pay'), { method: 'POST', headers,
//   body: JSON.stringify({ amount: 7500 }) })
import os

import requests

AUTH = (os.environ["APX_SECRET_KEY"], "")
charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb"


def cash_helper(action, body=None):
    url = f"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{charge}/{action}"
    return requests.post(url, auth=AUTH, json=body or {})


cash_helper("pay")
# or "expire", "refuse", "suspend"; another amount:
# cash_helper("pay", {"amount": 7500})
<?php
function cash_helper(string $charge, string $action, array $body = []): void
{
    $path = "/v1/test_helpers/cash_vouchers/{$charge}/{$action}";
    $ch = curl_init('https://pay-api.adpromax.com' . $path);
    curl_setopt_array($ch, [
        CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode((object) $body),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    curl_exec($ch);
}

cash_helper('ch_01k7d5f3a1s5d9f2g6h4j8k3nb', 'pay');
// or 'expire', 'refuse', 'suspend'; another amount:
// cash_helper('ch_…', 'pay', ['amount' => 7500]);
require 'json'
require 'net/http'

CHARGE = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'

def cash_helper(action, body = {})
  uri = URI("https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/#{CHARGE}/#{action}")
  request = Net::HTTP::Post.new(uri)
  request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
  request['Content-Type'] = 'application/json'
  request.body = JSON.generate(body)
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.request(request)
  end
end

cash_helper('pay')
# or 'expire', 'refuse', 'suspend'; another amount:
# cash_helper('pay', amount: 7500)
package main

import (
	"bytes"
	"encoding/json"
	"log"
	"net/http"
	"os"
)

const charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb"

func cashHelper(action string, body map[string]any) error {
	payload, err := json.Marshal(body)
	if err != nil {
		return err
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/"+charge+"/"+action,
		bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	return res.Body.Close()
}

func main() {
	if err := cashHelper("pay", map[string]any{}); err != nil {
		log.Fatal(err)
	}
	// or "expire", "refuse", "suspend"; another amount:
	// cashHelper("pay", map[string]any{"amount": 7500})
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Cash {
    static final String CHARGE = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";

    static void cashHelper(String action, String body) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(
                "https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/"
                    + CHARGE + "/" + action))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.discarding());
    }

    public static void main(String[] args) throws Exception {
        cashHelper("pay", "{}");
        // or "expire", "refuse", "suspend"; another amount:
        // cashHelper("pay", "{\"amount\": 7500}");
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

const string charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";

Task<HttpResponseMessage> CashHelper(string action, object? body = null) =>
    http.PostAsJsonAsync(
        $"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{charge}/{action}",
        body ?? new { });

await CashHelper("pay");
// or "expire", "refuse", "suspend"; another amount:
// await CashHelper("pay", new { amount = 7500 });
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
use serde_json::{json, Value};

const CHARGE: &str = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";

fn cash_helper(action: &str, body: Value) -> Result<(), Box<dyn std::error::Error>> {
    reqwest::blocking::Client::new()
        .post(format!(
            "https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{CHARGE}/{action}"
        ))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .json(&body)
        .send()?;
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    cash_helper("pay", json!({}))?;
    // or "expire", "refuse", "suspend"; another amount:
    // cash_helper("pay", json!({ "amount": 7500 }))?;
    Ok(())
}

Un paiement par carte complet, sans navigateur

Un test de votre intégration peut faire un paiement de bout en bout : créer une intention de paiement, la confirmer pour une carte, terminer sa tentative avec une carte de test, puis attendre succeeded. Chaque création et chaque confirmation porte sa propre Idempotency-Key, nouvelle à chaque exécution du test.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
card-payment.test.mjscard-payment.test.tscard_payment_test.pycard_payment_test.phpcard_payment_test.rbcard_payment_test.goCardPaymentTest.javaCardPaymentTest.csmain.rs
API=https://pay-api.adpromax.com/v1
RUN=$(date +%s)
PI=$(curl -s $API/payment_intents -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: test-$RUN-create" \
  -d '{"amount": 50000, "currency": "mad",
       "payment_method_types": ["card"]}' | jq -r .id)

curl -s $API/payment_intents/$PI/confirm -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: test-$RUN-confirm" \
  -d '{"payment_method_type": "card",
       "return_url": "https://shop.example.com/done"}' > /dev/null

CH=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:" \
  | jq -r .latest_charge)
curl -s $API/test_helpers/charges/$CH/complete -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{"card_number": "4242424242424242"}' > /dev/null

for i in 1 2 3 4 5 6 7 8 9 10; do
  STATUS=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:" \
    | jq -r .status)
  [ "$STATUS" = succeeded ] && break
  sleep 1
done
echo "$PI $STATUS"
import { randomUUID } from 'node:crypto'

const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
const run = randomUUID()

async function call(method, path, body, key) {
  const headers = { Authorization: auth }
  if (body !== undefined)
    headers['Content-Type'] = 'application/json'
  if (key !== undefined)
    headers['Idempotency-Key'] = `${run}-${key}`
  const response = await fetch(`${api}${path}`, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  })
  const answer = await response.json()
  if (!response.ok)
    throw new Error(answer.error.message)
  return answer
}

const intent = await call('POST', '/payment_intents', {
  amount: 50000,
  currency: 'mad',
  payment_method_types: ['card'],
}, 'create')
await call('POST', `/payment_intents/${intent.id}/confirm`, {
  payment_method_type: 'card',
  return_url: 'https://shop.example.com/done',
}, 'confirm')
const charge = (await call('GET', `/payment_intents/${intent.id}`)).latest_charge
await call('POST', `/test_helpers/charges/${charge}/complete`, {
  card_number: '4242424242424242',
})

let status
for (let i = 0; i < 10 && status !== 'succeeded'; i++) {
  await new Promise(resolve => setTimeout(resolve, 1000))
  status = (await call('GET', `/payment_intents/${intent.id}`)).status
}
console.log(intent.id, status)
import { randomUUID } from 'node:crypto'

interface PaymentIntent {
  id: string
  status: string
  latest_charge: string | null
}

interface ApiError {
  error: { type: string, code: string, message: string, request_id: string }
}

const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
const run = randomUUID()

async function call<T = PaymentIntent>(
  method: 'GET' | 'POST',
  path: string,
  body?: object,
  key?: string,
): Promise<T> {
  const headers: Record<string, string> = { Authorization: auth }
  if (body !== undefined)
    headers['Content-Type'] = 'application/json'
  if (key !== undefined)
    headers['Idempotency-Key'] = `${run}-${key}`
  const response = await fetch(`${api}${path}`, {
    method,
    headers,
    body: body === undefined ? undefined : JSON.stringify(body),
  })
  if (!response.ok) {
    const { error } = (await response.json()) as ApiError
    throw new Error(`${error.code}: ${error.message}`)
  }
  return (await response.json()) as T
}

const intent = await call('POST', '/payment_intents', {
  amount: 50000,
  currency: 'mad',
  payment_method_types: ['card'],
}, 'create')
await call('POST', `/payment_intents/${intent.id}/confirm`, {
  payment_method_type: 'card',
  return_url: 'https://shop.example.com/done',
}, 'confirm')
const charge = (await call('GET', `/payment_intents/${intent.id}`)).latest_charge
await call<unknown>('POST', `/test_helpers/charges/${charge}/complete`, {
  card_number: '4242424242424242',
})

let status: string | undefined
for (let i = 0; i < 10 && status !== 'succeeded'; i++) {
  await new Promise(resolve => setTimeout(resolve, 1000))
  status = (await call('GET', `/payment_intents/${intent.id}`)).status
}
console.log(intent.id, status)
import os
import time
import uuid

import requests

API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
RUN = uuid.uuid4().hex


def call(method, path, body=None, key=None):
    headers = {"Idempotency-Key": f"{RUN}-{key}"} if key else {}
    response = requests.request(
        method, f"{API}{path}", auth=AUTH, json=body, headers=headers
    )
    answer = response.json()
    response.raise_for_status()
    return answer


intent = call("POST", "/payment_intents", {
    "amount": 50000,
    "currency": "mad",
    "payment_method_types": ["card"],
}, "create")
call("POST", f"/payment_intents/{intent['id']}/confirm", {
    "payment_method_type": "card",
    "return_url": "https://shop.example.com/done",
}, "confirm")
charge = call("GET", f"/payment_intents/{intent['id']}")["latest_charge"]
call("POST", f"/test_helpers/charges/{charge}/complete", {
    "card_number": "4242424242424242",
})

status = None
for _ in range(10):
    time.sleep(1)
    status = call("GET", f"/payment_intents/{intent['id']}")["status"]
    if status == "succeeded":
        break
print(intent["id"], status)
<?php
function call(
    string $method,
    string $path,
    ?array $body = null,
    ?string $key = null
): array {
    $headers = ['Content-Type: application/json'];
    if ($key !== null) {
        $headers[] = "Idempotency-Key: {$key}";
    }
    $ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
    curl_setopt_array($ch, [
        CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $answer = json_decode(curl_exec($ch), true);
    return $answer;
}

$run = bin2hex(random_bytes(8));
$intent = call('POST', '/payment_intents', [
    'amount' => 50000,
    'currency' => 'mad',
    'payment_method_types' => ['card'],
], "{$run}-create");
call('POST', "/payment_intents/{$intent['id']}/confirm", [
    'payment_method_type' => 'card',
    'return_url' => 'https://shop.example.com/done',
], "{$run}-confirm");
$charge = call('GET', "/payment_intents/{$intent['id']}")['latest_charge'];
call('POST', "/test_helpers/charges/{$charge}/complete", [
    'card_number' => '4242424242424242',
]);

for ($i = 0, $status = null; $i < 10 && $status !== 'succeeded'; $i++) {
    sleep(1);
    $status = call('GET', "/payment_intents/{$intent['id']}")['status'];
}
echo $intent['id'], ' ', $status, PHP_EOL;
require 'json'
require 'minitest/autorun'
require 'net/http'
require 'securerandom'

class CardPaymentTest < Minitest::Test
  API = 'https://pay-api.adpromax.com/v1'
  RUN = SecureRandom.uuid

  def api(method, path, body = nil, key = nil)
    uri = URI("#{API}#{path}")
    request = (method == :get ? Net::HTTP::Get : Net::HTTP::Post).new(uri)
    request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
    request['Idempotency-Key'] = "#{RUN}-#{key}" if key
    if body
      request['Content-Type'] = 'application/json'
      request.body = JSON.generate(body)
    end
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end
    answer = JSON.parse(response.body)
    raise answer.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

    answer
  end

  def test_a_card_payment_succeeds
    intent = api(:post, '/payment_intents', {
      amount: 50_000,
      currency: 'mad',
      payment_method_types: ['card']
    }, 'create')
    api(:post, "/payment_intents/#{intent['id']}/confirm", {
      payment_method_type: 'card',
      return_url: 'https://shop.example.com/done'
    }, 'confirm')
    charge = api(:get, "/payment_intents/#{intent['id']}")['latest_charge']
    api(:post, "/test_helpers/charges/#{charge}/complete",
        { card_number: '4242424242424242' })

    status = nil
    10.times do
      sleep 1
      status = api(:get, "/payment_intents/#{intent['id']}")['status']
      break if status == 'succeeded'
    end
    assert_equal 'succeeded', status
  end
end
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"testing"
	"time"
)

const api = "https://pay-api.adpromax.com/v1"

type paymentIntent struct {
	ID           string `json:"id"`
	Status       string `json:"status"`
	LatestCharge string `json:"latest_charge"`
}

func call(t *testing.T, method, path string, body any, key string) paymentIntent {
	t.Helper()
	var payload io.Reader
	if body != nil {
		encoded, err := json.Marshal(body)
		if err != nil {
			t.Fatal(err)
		}
		payload = bytes.NewReader(encoded)
	}
	req, err := http.NewRequest(method, api+path, payload)
	if err != nil {
		t.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if key != "" {
		req.Header.Set("Idempotency-Key", key)
	}
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		t.Fatalf("%s %s: %s", method, path, res.Status)
	}
	var answer paymentIntent
	if err := json.NewDecoder(res.Body).Decode(&answer); err != nil {
		t.Fatal(err)
	}
	return answer
}

func TestCardPayment(t *testing.T) {
	run := fmt.Sprint(time.Now().UnixNano())
	intent := call(t, "POST", "/payment_intents", map[string]any{
		"amount":               50000,
		"currency":             "mad",
		"payment_method_types": []string{"card"},
	}, run+"-create")
	path := "/payment_intents/" + intent.ID
	call(t, "POST", path+"/confirm", map[string]any{
		"payment_method_type": "card",
		"return_url":          "https://shop.example.com/done",
	}, run+"-confirm")
	charge := call(t, "GET", path, nil, "").LatestCharge
	call(t, "POST", "/test_helpers/charges/"+charge+"/complete",
		map[string]any{"card_number": "4242424242424242"}, "")

	status := ""
	for i := 0; i < 10 && status != "succeeded"; i++ {
		time.Sleep(time.Second)
		status = call(t, "GET", path, nil, "").Status
	}
	if status != "succeeded" {
		t.Fatalf("%s is %q, want succeeded", intent.ID, status)
	}
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CardPaymentTest {
    static final String API = "https://pay-api.adpromax.com/v1";
    static final String RUN = UUID.randomUUID().toString();
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String call(String method, String path, String body, String key)
            throws Exception {
        HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .method(method, body == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(body));
        if (key != null) {
            request.header("Idempotency-Key", RUN + "-" + key);
        }
        HttpResponse<String> response = HTTP.send(
            request.build(), HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        return response.body();
    }

    // No JSON library here: a small regex reads the first field of that name.
    static String field(String json, String name) {
        Matcher match = Pattern
            .compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
            .matcher(json);
        if (!match.find()) {
            throw new IllegalStateException("no " + name + " in " + json);
        }
        return match.group(1);
    }

    public static void main(String[] args) throws Exception {
        String intent = field(call("POST", "/payment_intents", """
            {
              "amount": 50000,
              "currency": "mad",
              "payment_method_types": ["card"]
            }
            """, "create"), "id");
        String path = "/payment_intents/" + intent;
        call("POST", path + "/confirm", """
            {
              "payment_method_type": "card",
              "return_url": "https://shop.example.com/done"
            }
            """, "confirm");
        String charge = field(call("GET", path, null, null), "latest_charge");
        call("POST", "/test_helpers/charges/" + charge + "/complete", """
            {"card_number": "4242424242424242"}
            """, null);

        String status = null;
        for (int i = 0; i < 10 && !"succeeded".equals(status); i++) {
            Thread.sleep(1000);
            status = field(call("GET", path, null, null), "status");
        }
        if (!"succeeded".equals(status)) {
            throw new AssertionError(intent + " is " + status);
        }
        System.out.println(intent + " " + status);
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var api = "https://pay-api.adpromax.com/v1";
var run = Guid.NewGuid().ToString();

async Task<PaymentIntent> Call(
    HttpMethod method, string path, object? body = null, string? key = null)
{
    using var request = new HttpRequestMessage(method, api + path);
    if (body is not null)
        request.Content = JsonContent.Create(body);
    if (key is not null)
        request.Headers.Add("Idempotency-Key", $"{run}-{key}");
    using var response = await http.SendAsync(request);
    if (!response.IsSuccessStatusCode)
    {
        var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
        throw new InvalidOperationException(failure?.Error.Message);
    }
    return (await response.Content.ReadFromJsonAsync<PaymentIntent>(json))!;
}

var intent = await Call(HttpMethod.Post, "/payment_intents", new
{
    amount = 50000,
    currency = "mad",
    payment_method_types = new[] { "card" },
}, "create");
var path = $"/payment_intents/{intent.Id}";
await Call(HttpMethod.Post, $"{path}/confirm", new
{
    payment_method_type = "card",
    return_url = "https://shop.example.com/done",
}, "confirm");
var charge = (await Call(HttpMethod.Get, path)).LatestCharge;
await Call(HttpMethod.Post, $"/test_helpers/charges/{charge}/complete",
    new { card_number = "4242424242424242" });

string? status = null;
for (var i = 0; i < 10 && status != "succeeded"; i++)
{
    await Task.Delay(1000);
    status = (await Call(HttpMethod.Get, path)).Status;
}
if (status != "succeeded")
    throw new InvalidOperationException($"{intent.Id} is {status}");
Console.WriteLine($"{intent.Id} {status}");

record PaymentIntent(string Id, string Status, string? LatestCharge);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
// Run it with `cargo test`.
fn main() {}

#[cfg(test)]
mod tests {
    use std::thread;
    use std::time::{Duration, SystemTime, UNIX_EPOCH};

    use reqwest::{blocking::Client, Method};
    use serde_json::{json, Value};

    const API: &str = "https://pay-api.adpromax.com/v1";
    type Error = Box<dyn std::error::Error>;

    fn call(
        method: Method,
        path: &str,
        body: Option<Value>,
        key: Option<String>,
    ) -> Result<Value, Error> {
        let mut request = Client::new()
            .request(method, format!("{API}{path}"))
            .bearer_auth(std::env::var("APX_SECRET_KEY")?);
        if let Some(body) = body {
            request = request.json(&body);
        }
        if let Some(key) = key {
            request = request.header("Idempotency-Key", key);
        }
        let response = request.send()?;
        let failed = !response.status().is_success();
        let answer: Value = response.json()?;
        if failed {
            let message = answer["error"]["message"].as_str();
            return Err(message.unwrap_or("request failed").into());
        }
        Ok(answer)
    }

    #[test]
    fn a_card_payment_succeeds() -> Result<(), Error> {
        let run = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
        let intent = call(
            Method::POST,
            "/payment_intents",
            Some(json!({
                "amount": 50000,
                "currency": "mad",
                "payment_method_types": ["card"]
            })),
            Some(format!("{run}-create")),
        )?;
        let id = intent["id"].as_str().ok_or("no id")?;
        let path = format!("/payment_intents/{id}");
        call(
            Method::POST,
            &format!("{path}/confirm"),
            Some(json!({
                "payment_method_type": "card",
                "return_url": "https://shop.example.com/done"
            })),
            Some(format!("{run}-confirm")),
        )?;
        let waiting = call(Method::GET, &path, None, None)?;
        let charge = waiting["latest_charge"].as_str().ok_or("no charge")?;
        call(
            Method::POST,
            &format!("/test_helpers/charges/{charge}/complete"),
            Some(json!({ "card_number": "4242424242424242" })),
            None,
        )?;

        let mut status = String::new();
        for _ in 0..10 {
            thread::sleep(Duration::from_secs(1));
            let current = call(Method::GET, &path, None, None)?;
            status = current["status"].as_str().unwrap_or_default().to_owned();
            if status == "succeeded" {
                break;
            }
        }
        assert_eq!(status, "succeeded", "{id}");
        Ok(())
    }
}

Ailleurs en mode test

  • La page du bon que voit votre client porte Simuler le paiement et les autres issues espèces.
  • Sur le tableau de bord, la page d’un paiement de test a une carte Assistants de test avec les mêmes actions.
  • Ni l’une ni l’autre n’existe en production, où seul un vrai paiement fait avancer un paiement.