بيئة الاختبار

واجهة API لأدوات الاختبار المساعدة

أدِّ دور الدافع من اختباراتك الخاصة، دون متصفح: أكمل دفعة بالبطاقة، وادفع رمز دفع نقدي أو أنهِ صلاحيته.

فتح بصيغة Markdown
في هذه الصفحة
  1. أكمل دفعة بالبطاقة
  2. ادفع رمز دفع نقدي أو أنهِ صلاحيته أو ارفضه أو علّقه
  3. دفعة كاملة بالبطاقة، دون متصفح
  4. في مواضع أخرى من وضع الاختبار

تتيح أدوات الاختبار المساعدة لاختبار آلي أن يفعل ما يفعله شخص في وضع الاختبار: كتابة بطاقة اختبار في صفحة الدفع، أو سداد رمز دفع نقدي لدى وكيل. توجد تحت /v1/test_helpers ولا تقبل إلا مفتاحًا سريًا للاختبار. ومع مفتاح للوضع المباشر تجيب بـ 403، permission_error، test_mode_only.

تعمل الأداة المساعدة على محاولة (ch_…): أي latest_charge في نية دفع (payment intent) تنتظر دافعها، في requires_action. تؤدي دور الدافع أو دور الشبكة، لا أكثر: ثم تتغير الدفعة كما كانت ستتغير في الوضع المباشر، عبر إشعار المزوّد أو فحص حالة، خلال ثوانٍ في العادة. اقرأ نية الدفع من جديد، أو انتظر الـ webhook.

أكمل دفعة بالبطاقة

يرسل POST /v1/test_helpers/charges/{charge}/complete رقم بطاقة في صفحة الدفع لمحاولة بطاقة منتظرة، كما كان عميلك سيفعل.

  • card_number: إحدى بطاقات الاختبار، بمسافات أو من دونها. يُرفض أي رقم آخر.
  • three_d_secure، اختياري: لبطاقة 3-D Secure، الإجابة عن التحدي المحاكى (انظر مرجع 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(())
}

ادفع رمز دفع نقدي أو أنهِ صلاحيته أو ارفضه أو علّقه

أربع أدوات مساعدة تعمل على محاولة دفع نقدي منتظرة، كما كانت شبكة الدفع ستفعل. كل واحدة منها طلب POST بلا نص، ما عدا pay:

الأداة المساعدةما يحدث
/v1/test_helpers/cash_vouchers/{charge}/payيُسدَّد الرمز. amount، اختياري، بالسنتيم: لسداد مبلغ غير مبلغ القسيمة.
/v1/test_helpers/cash_vouchers/{charge}/expireيبلغ الرمز أجله دون دفع.
/v1/test_helpers/cash_vouchers/{charge}/refuseترفض الشبكة الرمز.
/v1/test_helpers/cash_vouchers/{charge}/suspendيُعلَّق الرمز؛ ثم سدّده بـ pay بعد ذلك.

تجد ما تفعله كل نتيجة بالدفعة في سيناريوهات اختبار الدفع النقدي.

  • 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(())
}

دفعة كاملة بالبطاقة، دون متصفح

يمكن لاختبار تكاملك تشغيل دفعة من البداية إلى النهاية: أنشئ نية دفع، وأكّدها للبطاقة، وأكمل محاولتها ببطاقة اختبار، ثم انتظر succeeded. يحمل كل طلب إنشاء وكل طلب تأكيد Idempotency-Key خاصًا به؛ وقيمة جديدة في كل تشغيل للاختبار.

  • 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(())
    }
}

في مواضع أخرى من وضع الاختبار

  • تحمل صفحة القسيمة التي يراها عميلك محاكاة الدفع وباقي نتائج الدفع النقدي.
  • في لوحة التحكم، تتضمن صفحة دفعة الاختبار بطاقة أدوات الاختبار المساعدة بالإجراءات نفسها.
  • لا توجد أي منهما في الوضع المباشر، حيث لا يحرّك الدفعة إلا دفع حقيقي.