بيئة الاختبار
واجهة API لأدوات الاختبار المساعدة
أدِّ دور الدافع من اختباراتك الخاصة، دون متصفح: أكمل دفعة بالبطاقة، وادفع رمز دفع نقدي أو أنهِ صلاحيته.
في هذه الصفحة
تتيح أدوات الاختبار المساعدة لاختبار آلي أن يفعل ما يفعله شخص في وضع الاختبار: كتابة بطاقة اختبار في
صفحة الدفع، أو سداد رمز دفع نقدي لدى وكيل. توجد تحت /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 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)
endpackage 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 بعد ذلك. |
تجد ما تفعله كل نتيجة بالدفعة في سيناريوهات اختبار الدفع النقدي.
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 خاصًا به؛ وقيمة جديدة في كل تشغيل للاختبار.
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
endpackage 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(())
}
}في مواضع أخرى من وضع الاختبار
- تحمل صفحة القسيمة التي يراها عميلك محاكاة الدفع وباقي نتائج الدفع النقدي.
- في لوحة التحكم، تتضمن صفحة دفعة الاختبار بطاقة أدوات الاختبار المساعدة بالإجراءات نفسها.
- لا توجد أي منهما في الوضع المباشر، حيث لا يحرّك الدفعة إلا دفع حقيقي.