# اختبر تكاملك

كل مسار قد يسلكه عملاؤك، وكل حالة فشل، ومعالجتك لإشعارات webhook، مختبرة من البداية إلى النهاية في بيئة الاختبار قبل الانتقال إلى الوضع المباشر.

يشغّل وضع الاختبار الكود نفسه الذي يشغّله الوضع المباشر: التحقق من صحة البيانات نفسه، والحالات نفسها،
والأحداث نفسها، وإشعارات الـ webhook نفسها (انظر [بيئة الاختبار والوضع المباشر: الفروق](https://pay-docs.adpromax.com/ar/sandbox/differences)). ما يعمل
هنا يعمل في الوضع المباشر. اختبر أولًا المسار الذي يسلكه عملاؤك، ثم كل طريقة قد تفشل بها دفعة،
ثم ما يفعله خادمك بإشعارات الـ webhook التي يتلقاها.

## قبل أن تبدأ

* **مفتاح سري للاختبار** في `APX_SECRET_KEY`: انظر [مفاتيح API الخاصة بك](https://pay-docs.adpromax.com/ar/get-started/api-keys).
* **نقطة استقبال webhook في وضع الاختبار**، مع سرّها في `APX_WEBHOOK_SECRET`. ولاستقبال الأحداث على
  حاسوبك، اجعل خادمك متاحًا عبر نفق HTTPS: انظر
  [البدء السريع للبطاقة](https://pay-docs.adpromax.com/ar/get-started/quickstart-card#5-أنشئ-نقطة-استقبال-webhook).
* **لوحة التحكم، في وضع الاختبار**، لمتابعة ما يحدث:
  * تعرض **المطوّرون ← سجلات الطلبات** كل استدعاء لـ API مع حالته و`Request-Id` الخاص به؛
  * تعرض **المطوّرون ← الأحداث** كل حدث، مع عمليات تسليمه إلى كل نقطة استقبال؛
  * تعرض صفحة نقطة الاستقبال في **المطوّرون ← Webhooks** كل عملية تسليم وإجاباتك عنها.

## الدفع بالبطاقة

ادفع في كل سيناريو على الصفحة المستضافة باستخدام [بطاقة الاختبار](https://pay-docs.adpromax.com/ar/sandbox/test-cards) المذكورة، أو دون
متصفح عبر [أدوات الاختبار المساعدة](https://pay-docs.adpromax.com/ar/sandbox/test-helpers).

- **نجاح** (`4242 4242 4242 4242`): ينفّذ الطلبية مرة واحدة، عند `payment_intent.succeeded`.

- **مرفوضة** (`4000 0000 0000 0002`): يُظهر أن الدفعة فشلت ويتيح لعميلك المحاولة من جديد. تبقى الطلبية مفتوحة.

- **3-D Secure** (`4000 0000 0000 3220`, مرة بخيار **موافقة** ومرة بخيار **فشل**): عند الموافقة: كما في النجاح. عند الفشل: كما في الرفض، مع `authentication_failed`.

- **تأكيد بطيء** (`4000 0000 0000 0077`): يعود عميلك إلى `success_url` قبل تأكيد الدفعة: اعرض «نحن بصدد تأكيد دفعتك»، وسلّم الطلبية عند وصول الـ webhook، بعد نحو 60 ثانية.

- **إشعارات متكررة** (`4000 0000 0000 0085`): تنفيذ واحد: بريد إلكتروني واحد، وحركة مخزون واحدة.

- **تأكيد مفقود** (`4000 0000 0000 0093`): لا يسلّم أبدًا: لا تنجح الدفعة، وتنتهي صلاحية محاولتها بعد نحو 30 دقيقة.

## الدفع نقدًا

تصرّف في كل رمز اختبار بأزرار صفحة القسيمة، أو عبر
[أدوات الاختبار المساعدة](https://pay-docs.adpromax.com/ar/sandbox/test-helpers). وتجد ما تفعله كل نتيجة في
[سيناريوهات اختبار الدفع النقدي](https://pay-docs.adpromax.com/ar/sandbox/cash-scenarios).

- **مدفوع** (**محاكاة الدفع**، أو `pay`): ينفّذ الطلبية عند `payment_intent.succeeded`.

- **منتهي الصلاحية** (`expire`): يحرّر الطلبية أو المخزون، ويتيح لعميلك الحصول على رمز جديد.

- **مدفوع بمبلغ آخر** (`pay` مع `amount` مختلف): لا يسلّم شيئًا: تحصل الدفعة على `review` ولا يُضاف شيء إلى الرصيد. وينتظر.

- **مدفوع بعد انتهاء الصلاحية** (`expire`، ثم `pay`): الأمر نفسه: `review`، ولا إضافة إلى الرصيد، ولا تسليم.

- **أقل من 100.00 MAD** (دفعة نقدية بمبلغ `5000`): يعالج الرفض، `amount_too_small`: اعرض البطاقة فقط للمبالغ الصغيرة.

## الاسترداد

- **استرداد جزئي لدفعة بالبطاقة** (استردّ جزءًا من دفعة ناجحة بالبطاقة): يسجّل ما تم استرداده؛ ويزداد `amount_refunded` في نية الدفع (payment intent).

- **استرداد كامل لدفعة بالبطاقة** (استردّ الباقي): يضع على الطلبية علامة «مستردة» عند نجاح الاسترداد.

- **استرداد دفعة نقدية** (حاول استرداد دفعة نقدية): يعالج الرفض، `refund_unsupported_payment_method`: يتولى الأمر إجراؤك الخاص بالدفع نقدًا. انظر [المبالغ المستردة](https://pay-docs.adpromax.com/ar/payments/refunds).

## إشعارات webhook

- **توقيع خاطئ** (أرسل إلى معالجك عملية تسليم بنص معدَّل ([اختبر معالجك](https://pay-docs.adpromax.com/ar/webhooks/signatures#اختبر-معالجك))): يجيب بـ `400` ولا يفعل شيئًا آخر.

- **الحدث نفسه مرتين** (اختر **إعادة الإرسال** لعملية تسليم من لوحة التحكم): يعالجه مرة واحدة: `id` الحدث معروف مسبقًا. انظر [إزالة التكرار](https://pay-docs.adpromax.com/ar/webhooks/retries).

- **نقطة استقبالك متوقفة** (أوقف خادمك، وادفع، وشغّله من جديد، ثم اختر **إعادة الإرسال** أو انتظر إعادة المحاولة): ينفّذ الطلبية عندما تنجح عملية التسليم.

## الطلبات

- **الإنشاء نفسه مرتين** (أرسل طلب إنشاء مرتين بالقيمة نفسها لـ `Idempotency-Key`): دفعة واحدة؛ وتحمل الاستجابة الثانية `Idempotent-Replayed: true`. انظر [عدم التكرار](https://pay-docs.adpromax.com/ar/api-reference/idempotency) (idempotency).

- **طلبات كثيرة جدًا** (في اختباراتك الخاصة، أعِد إلى برنامجك الرد `429`): ينتظر مدة `Retry-After`، ثم يعيد المحاولة بالمفتاح نفسه. انظر [حدود المعدل](https://pay-docs.adpromax.com/ar/api-reference/rate-limits).

- **مفتاح خاطئ** (مفتاح اختبار ينقصه حرف واحد): يعالج `401`، `api_key_invalid`: تنبيه لك، لا إعادة محاولة أبدًا.

## اختبار آلي: رمز دفع نقدي تنتهي صلاحيته

يمكن لمجموعة اختباراتك تشغيل أي من هذه السيناريوهات على بيئة الاختبار. ينشئ هذا الاختبار دفعة
نقدية، ويؤكدها باسم عميلك ومدينته، وينهي صلاحية رمزها بأداة الاختبار المساعدة، ثم
ينتظر حتى تعود الدفعة إلى `requires_payment_method` مع `voucher_expired`.

يحمل كل طلب إنشاء وكل طلب تأكيد `Idempotency-Key` خاصًا به، يُنشأ من جديد في كل تشغيل.
وتذكر استجابة التأكيد المحاولة في `latest_charge`: وعليها تعمل الأداة المساعدة.

```bash
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": 15000, "currency": "mad",
       "payment_method_types": ["cash"]}' | jq -r .id)

CH=$(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": "cash",
       "payer": {"name": "Salma Idrissi", "city": "Casablanca"}}' \
  | jq -r .latest_charge)

curl -s -X POST $API/test_helpers/cash_vouchers/$CH/expire \
  -u "$APX_SECRET_KEY:" > /dev/null

for i in 1 2 3 4 5 6 7 8 9 10; do
  INTENT=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:")
  STATUS=$(echo "$INTENT" | jq -r .status)
  [ "$STATUS" = requires_payment_method ] && break
  sleep 1
done
echo "$PI $STATUS $(echo "$INTENT" | jq -r .last_payment_error.code)"
```

```js title="cash-expiry.test.mjs"
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: 15000,
  currency: 'mad',
  payment_method_types: ['cash'],
}, 'create')
const confirmed = await call('POST', `/payment_intents/${intent.id}/confirm`, {
  payment_method_type: 'cash',
  payer: { name: 'Salma Idrissi', city: 'Casablanca' },
}, 'confirm')
const charge = confirmed.latest_charge
await call('POST', `/test_helpers/cash_vouchers/${charge}/expire`)

let latest
for (let i = 0; i < 10; i++) {
  await new Promise(resolve => setTimeout(resolve, 1000))
  latest = await call('GET', `/payment_intents/${intent.id}`)
  if (latest.status === 'requires_payment_method')
    break
}
// Expected: requires_payment_method voucher_expired
console.log(intent.id, latest.status, latest.last_payment_error?.code)
```

```ts title="cash-expiry.test.ts"
import { randomUUID } from 'node:crypto'

interface PaymentIntent {
  id: string
  status: string
  latest_charge: string | null
  last_payment_error: { code: string, message: 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>(
  method: string,
  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<PaymentIntent>('POST', '/payment_intents', {
  amount: 15000,
  currency: 'mad',
  payment_method_types: ['cash'],
}, 'create')
const confirm = `/payment_intents/${intent.id}/confirm`
const confirmed = await call<PaymentIntent>('POST', confirm, {
  payment_method_type: 'cash',
  payer: { name: 'Salma Idrissi', city: 'Casablanca' },
}, 'confirm')
const charge = confirmed.latest_charge
await call('POST', `/test_helpers/cash_vouchers/${charge}/expire`)

let latest = confirmed
for (let i = 0; i < 10; i++) {
  await new Promise(resolve => setTimeout(resolve, 1000))
  latest = await call<PaymentIntent>('GET', `/payment_intents/${intent.id}`)
  if (latest.status === 'requires_payment_method')
    break
}
// Expected: requires_payment_method voucher_expired
console.log(intent.id, latest.status, latest.last_payment_error?.code)
```

```python title="cash_expiry_test.py"
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": 15000,
    "currency": "mad",
    "payment_method_types": ["cash"],
}, "create")
confirmed = call("POST", f"/payment_intents/{intent['id']}/confirm", {
    "payment_method_type": "cash",
    "payer": {"name": "Salma Idrissi", "city": "Casablanca"},
}, "confirm")
charge = confirmed["latest_charge"]
call("POST", f"/test_helpers/cash_vouchers/{charge}/expire")

latest = None
for _ in range(10):
    time.sleep(1)
    latest = call("GET", f"/payment_intents/{intent['id']}")
    if latest["status"] == "requires_payment_method":
        break
# Expected: requires_payment_method voucher_expired
error = latest["last_payment_error"] or {}
print(intent["id"], latest["status"], error.get("code"))
```

```php title="cash_expiry_test.php"
<?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' => 15000,
    'currency' => 'mad',
    'payment_method_types' => ['cash'],
], "{$run}-create");
$confirmed = call('POST', "/payment_intents/{$intent['id']}/confirm", [
    'payment_method_type' => 'cash',
    'payer' => ['name' => 'Salma Idrissi', 'city' => 'Casablanca'],
], "{$run}-confirm");
$charge = $confirmed['latest_charge'];
call('POST', "/test_helpers/cash_vouchers/{$charge}/expire");

for ($i = 0; $i < 10; $i++) {
    sleep(1);
    $latest = call('GET', "/payment_intents/{$intent['id']}");
    if ($latest['status'] === 'requires_payment_method') {
        break;
    }
}
// Expected: requires_payment_method voucher_expired
echo $intent['id'], ' ', $latest['status'], ' ',
    $latest['last_payment_error']['code'] ?? '', PHP_EOL;
```

```ruby title="cash_expiry_test.rb"
require 'json'
require 'minitest/autorun'
require 'net/http'
require 'securerandom'

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

  def call(verb, path, body = nil, key = nil)
    uri = URI("#{API}#{path}")
    request = Net::HTTP.const_get(verb).new(uri)
    request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
    request['Content-Type'] = 'application/json'
    request['Idempotency-Key'] = "#{RUN}-#{key}" if key
    request.body = JSON.generate(body) if body
    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_an_expired_code_reopens_the_payment
    intent = call(:Post, '/payment_intents', {
      amount: 15000,
      currency: 'mad',
      payment_method_types: ['cash']
    }, 'create')
    confirmed = call(:Post, "/payment_intents/#{intent['id']}/confirm", {
      payment_method_type: 'cash',
      payer: { name: 'Salma Idrissi', city: 'Casablanca' }
    }, 'confirm')
    charge = confirmed['latest_charge']
    call(:Post, "/test_helpers/cash_vouchers/#{charge}/expire")

    latest = nil
    10.times do
      sleep 1
      latest = call(:Get, "/payment_intents/#{intent['id']}")
      break if latest['status'] == 'requires_payment_method'
    end
    assert_equal 'requires_payment_method', latest['status']
    assert_equal 'voucher_expired', latest.dig('last_payment_error', 'code')
  end
end
```

```go title="cash_expiry_test.go"
package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"net/http"
	"os"
	"testing"
	"time"
)

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

// A fresh run, so that every run's Idempotency-Key is new.
var run = rand.Text()

type paymentError struct {
	Code string `json:"code"`
}

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

func call(
	t *testing.T, method, path string, body any, key string,
) paymentIntent {
	t.Helper()
	var payload bytes.Buffer
	if body != nil {
		json.NewEncoder(&payload).Encode(body)
	}
	req, err := http.NewRequest(method, api+path, &payload)
	if err != nil {
		t.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	if key != "" {
		req.Header.Set("Idempotency-Key", run+"-"+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
	json.NewDecoder(res.Body).Decode(&answer)
	return answer
}

func TestAnExpiredCodeReopensThePayment(t *testing.T) {
	intent := call(t, http.MethodPost, "/payment_intents", map[string]any{
		"amount":               15000,
		"currency":             "mad",
		"payment_method_types": []string{"cash"},
	}, "create")
	payer := map[string]string{"name": "Salma Idrissi", "city": "Casablanca"}
	confirmed := call(t, http.MethodPost,
		"/payment_intents/"+intent.ID+"/confirm", map[string]any{
			"payment_method_type": "cash",
			"payer":               payer,
		}, "confirm")
	expire := "/test_helpers/cash_vouchers/" + confirmed.LatestCharge + "/expire"
	call(t, http.MethodPost, expire, nil, "")

	var latest paymentIntent
	for i := 0; i < 10; i++ {
		time.Sleep(time.Second)
		latest = call(t, http.MethodGet, "/payment_intents/"+intent.ID, nil, "")
		if latest.Status == "requires_payment_method" {
			break
		}
	}
	code := ""
	if latest.LastPaymentError != nil {
		code = latest.LastPaymentError.Code
	}
	if latest.Status != "requires_payment_method" || code != "voucher_expired" {
		t.Fatalf("%s: %s %s", intent.ID, latest.Status, code)
	}
}
```

```java title="CashExpiryTest.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CashExpiryTest {
    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
                ? BodyPublishers.noBody() : 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();
    }

    // The JDK has no JSON parser: a small regex reads one text field, the
    // first with that name after `after`.
    static String field(String json, String after, String name) {
        Matcher match = Pattern
            .compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
            .matcher(json);
        int from = Math.max(json.indexOf(after), 0);
        return match.find(from) ? match.group(1) : "";
    }

    public static void main(String[] args) throws Exception {
        String created = call("POST", "/payment_intents", """
            {
              "amount": 15000,
              "currency": "mad",
              "payment_method_types": ["cash"]
            }
            """, "create");
        String intent = field(created, "", "id");
        String confirm = "/payment_intents/" + intent + "/confirm";
        String confirmed = call("POST", confirm, """
            {
              "payment_method_type": "cash",
              "payer": { "name": "Salma Idrissi", "city": "Casablanca" }
            }
            """, "confirm");
        String charge = field(confirmed, "", "latest_charge");
        String expire = "/test_helpers/cash_vouchers/" + charge + "/expire";
        call("POST", expire, null, null);

        String status = "";
        String code = "";
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            String latest = call("GET", "/payment_intents/" + intent, null, null);
            status = field(latest, "", "status");
            code = field(latest, "\"last_payment_error\"", "code");
            if (status.equals("requires_payment_method")) {
                break;
            }
        }
        // Expected: requires_payment_method voucher_expired
        boolean reopened = status.equals("requires_payment_method");
        if (!reopened || !code.equals("voucher_expired")) {
            throw new AssertionError(intent + " " + status + " " + code);
        }
        System.out.println(intent + " " + status + " " + code);
    }
}
```

```csharp title="CashExpiryTest.cs"
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

const string Api = "https://pay-api.adpromax.com/v1";
var run = Guid.NewGuid().ToString();
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

async Task<JsonElement> 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);
    var answer = await response.Content.ReadFromJsonAsync<JsonElement>();
    if (!response.IsSuccessStatusCode)
        throw new InvalidOperationException(
            answer.GetProperty("error").GetProperty("message").GetString());
    return answer;
}

var intent = await Call(HttpMethod.Post, "/payment_intents", new
{
    amount = 15000,
    currency = "mad",
    payment_method_types = new[] { "cash" },
}, "create");
var id = intent.GetProperty("id").GetString();
var confirmed = await Call(HttpMethod.Post, $"/payment_intents/{id}/confirm", new
{
    payment_method_type = "cash",
    payer = new { name = "Salma Idrissi", city = "Casablanca" },
}, "confirm");
var charge = confirmed.GetProperty("latest_charge").GetString();
await Call(HttpMethod.Post, $"/test_helpers/cash_vouchers/{charge}/expire");

var latest = confirmed;
for (var i = 0; i < 10; i++)
{
    await Task.Delay(1000);
    latest = await Call(HttpMethod.Get, $"/payment_intents/{id}");
    if (latest.GetProperty("status").GetString() == "requires_payment_method")
        break;
}
var status = latest.GetProperty("status").GetString();
var error = latest.GetProperty("last_payment_error");
var code = error.ValueKind == JsonValueKind.Object
    ? error.GetProperty("code").GetString()
    : null;
// Expected: requires_payment_method voucher_expired
if (status != "requires_payment_method" || code != "voucher_expired")
    throw new InvalidOperationException($"{id} {status} {code}");
Console.WriteLine($"{id} {status} {code}");
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
//             uuid = { version = "1", features = ["v4"] }
// Run it with `cargo test`.
use std::{thread::sleep, time::Duration};

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

const API: &str = "https://pay-api.adpromax.com/v1";

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

fn main() {}

#[test]
fn an_expired_code_reopens_the_payment() {
    // A fresh run, so that every run's Idempotency-Key is new.
    let run = uuid::Uuid::new_v4();
    let intent = call(
        Method::POST,
        "/payment_intents",
        Some(json!({
            "amount": 15000,
            "currency": "mad",
            "payment_method_types": ["cash"]
        })),
        Some(format!("{run}-create")),
    );
    let id = intent["id"].as_str().unwrap();
    let confirmed = call(
        Method::POST,
        &format!("/payment_intents/{id}/confirm"),
        Some(json!({
            "payment_method_type": "cash",
            "payer": { "name": "Salma Idrissi", "city": "Casablanca" }
        })),
        Some(format!("{run}-confirm")),
    );
    let charge = confirmed["latest_charge"].as_str().unwrap();
    let expire = format!("/test_helpers/cash_vouchers/{charge}/expire");
    call(Method::POST, &expire, None, None);

    let mut latest = confirmed.clone();
    for _ in 0..10 {
        sleep(Duration::from_secs(1));
        latest = call(Method::GET, &format!("/payment_intents/{id}"), None, None);
        if latest["status"] == "requires_payment_method" {
            break;
        }
    }
    assert_eq!(latest["status"], "requires_payment_method");
    assert_eq!(latest["last_payment_error"]["code"], "voucher_expired");
}
```

## ما يراه عملاؤك

يدفع عملاؤك في صفحاتنا، وغالبًا من الهاتف. انظر إليها كما سيراها عملاؤك:

* افتح `url` جلسة دفع (checkout session) على هاتف، بالفرنسية والإنجليزية والعربية (تتبع الصفحة لغة
  المتصفح). ادفع بالبطاقة، ثم اطلب رمز دفع نقدي.
* في صفحة القسيمة، اطبع القسيمة وشاركها: يجب أن يُقرأ الرمز والمبلغ الواجب دفعه والأجل
  بوضوح.
* غادر الصفحة إلى متجرك، ثم افتح `url` الجلسة من جديد: العميل الذي غادر يستطيع
  العودة إلى جلسة ما زالت مفتوحة.

## دع لوحة التحكم تساعدك

* بطاقة **التكامل** في الصفحة الرئيسية تؤشّر بنودها تلقائيًا كلما وصلت مدفوعات الاختبار وإشعارات الـ webhook ورموز الدفع النقدي.
* في وضع الاختبار، يملأ *إنشاء مدفوعات نموذجية* في الصفحة الرئيسية الشاشات بمدفوعات وعملاء
  نموذجيين، لترى ما تعرضه كل صفحة.
* *حذف كل بيانات الاختبار*، في **الإعدادات ← المؤسسة**، يعيدك إلى بيئة اختبار نظيفة.
  وتبقى مفاتيحك وفريقك.

## الخطوات التالية

عندما ينجح كل سيناريو في هذه الصفحة، اقرأ [قائمة التحقق قبل الوضع المباشر](https://pay-docs.adpromax.com/ar/get-started/go-live).
