# معالجة الأخطاء

كائن الخطأ، وأنواع الأخطاء السبعة، والرموز التي تصادفها أولًا، وكيف تعالج كلًّا منها.

عندما يفشل طلب، تجيب واجهة API بحالة HTTP قيمتها `400` أو أكثر مع **كائن خطأ**. يقرر
كودك ما يفعله بناءً على `type` و`code` في الخطأ؛ أما `message` فللأشخاص.

## كائن الخطأ

```json title="404 Not Found" response
{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_missing",
    "message": "No such payment_intent: 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'",
    "doc_url": "https://pay-docs.adpromax.com/errors#resource_missing",
    "request_id": "req_01k7d9x3f6g9h2j5k8m1n4p7qs"
  }
}
```

| الحقل          | ما يحمله                                                                                                                          |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `type`         | أحد الأنواع السبعة أدناه: الصنف العام للفشل.                                                                                      |
| `code`         | السبب الدقيق، ثابت مع الوقت: فرّع كودك بحسبه.                                                                                     |
| `decline_code` | للبطاقة المرفوضة: سبب البنك، مثل `insufficient_funds`.                                                                            |
| `param`        | المعامل المخالف، على شكل مسار (`metadata.order_id`)، إن وُجد.                                                                     |
| `message`      | شرح موجّه لشخص، بلغة طلبك (الإنجليزية أو الفرنسية أو العربية، حسب ترويسة `Accept-Language`). قد يتغير: لا تفرّع كودك بحسبه أبدًا. |
| `doc_url`      | الصفحة التي تشرح الرمز.                                                                                                           |
| `request_id`   | معرّف الطلب (`req_…`)، ويوجد أيضًا في ترويسة `Request-Id` لكل استجابة.                                                            |

## أنواع الأخطاء

| `type`                  | الحالة              | ما حدث                                                                                                        | ما تفعله                                                                                   |
| ----------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `invalid_request_error` | `400`، `404`، `409` | الطلب خاطئ: معامل ناقص أو غير معروف أو غير صالح، أو كائن أو عنوان غير موجود، أو إجراء لا تسمح به حالة الكائن. | صحّح الطلب. إرساله من جديد دون تغيير يفشل بالطريقة نفسها.                                  |
| `authentication_error`  | `401`               | لا مفتاح، أو مفتاح لا يعمل.                                                                                   | تحقّق من المفتاح ومن وضعه.                                                                 |
| `permission_error`      | `403`               | المفتاح يعمل لكن لا يحق له هذا.                                                                               | انظر الرمز: مفتاح للوضع المباشر قبل التفعيل، أو أداة اختبار مساعدة بمفتاح للوضع المباشر…   |
| `card_error`            | `402`               | رُفضت بطاقة.                                                                                                  | اعرض لعميلك رسالة ودعه يجرّب بطاقة أخرى.                                                   |
| `idempotency_error`     | `400`، `409`        | أُعيد استخدام `Idempotency-Key` لطلب آخر، أو ما زال طلبها الأول قيد التنفيذ.                                  | انظر [عدم التكرار](https://pay-docs.adpromax.com/ar/api-reference/idempotency) (idempotency).                              |
| `rate_limit_error`      | `429`               | طلبات كثيرة جدًا في وقت قصير.                                                                                 | انتظر مهلة `Retry-After`، ثم أعد المحاولة. انظر [حدود المعدل](https://pay-docs.adpromax.com/ar/api-reference/rate-limits). |
| `api_error`             | `500`، `503`        | فشل شيء من جهتنا، أو هو غير متاح للحظات.                                                                      | أعد المحاولة بقيمة `Idempotency-Key` نفسها؛ وإذا استمر الخطأ، فراسل الدعم مع `request_id`. |

## الرموز التي تصادفها أولًا

### الطلبات

| `code`                              | ما حدث                                                                                                                                                                                          |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parameter_missing`                 | معامل إلزامي غائب؛ ويذكره `param`.                                                                                                                                                              |
| `parameter_unknown`                 | معامل لا يقبله الطلب؛ ويذكره `param`. تحقّق من كتابته.                                                                                                                                          |
| `parameter_invalid`                 | لمعامل نوع أو قيمة خاطئة؛ ويذكره `param`. يُرفض بهذه الطريقة أيضًا أي حقل نصي يشبه رقم بطاقة: لا تمر أرقام البطاقات عبر API أبدًا. وكذلك قائمة تُطلب بـ `starting_after` و`ending_before` معًا. |
| `resource_missing`                  | لا يوجد كائن بهذا المعرّف، في هذا الوضع. وإذا كان موجودًا في الوضع الآخر، تذكر الرسالة ذلك: استخدمت على الأرجح المفتاح الخطأ.                                                                   |
| `unrecognized_request_url`          | لا يوجد مسار في API بهذه الطريقة وهذا العنوان؛ وتذكرهما الرسالة. تحقّق من المسار ومن طريقة HTTP.                                                                                                |
| `api_version_invalid`               | تذكر ترويسة `X-AdProMaxPay-Version`، أو `api_version` في نقطة استقبال webhook، إصدارًا غير موجود. وتسرد الرسالة الإصدارات الموجودة.                                                             |
| `amount_too_small`                  | المبلغ أقل من الحد الأدنى لطريقة الدفع: 100.00 MAD للدفع نقدًا، و1.00 MAD للبطاقة.                                                                                                              |
| `amount_too_large`                  | استرداد يتجاوز ما بقي قابلًا للاسترداد من الدفعة.                                                                                                                                               |
| `payment_method_unavailable`        | لا يمكن استخدام طريقة الدفع لحسابك في هذا الوضع.                                                                                                                                                |
| `payment_intent_unexpected_state`   | حالة نية الدفع (payment intent) لا تسمح بهذا الإجراء، مثل تأكيد دفعة سبق أن نجحت.                                                                                                               |
| `payment_intent_busy`               | طلب آخر يؤكد هذه الدفعة أو يعدّلها الآن. لم يتغير شيء: أعد إرسال الطلب بعد لحظات.                                                                                                               |
| `charge_unexpected_state`           | حالة المحاولة لا تسمح بأداة الاختبار المساعدة هذه، مثل إتمام محاولة سبق أن نجحت.                                                                                                                |
| `refund_unsupported_payment_method` | لا يمكن استرداد دفعة نقدية.                                                                                                                                                                     |
| `url_not_allowed`                   | عنوان webhook ليس `https`، أو يحمل اسم مستخدم أو كلمة مرور، أو ليس عامًا، أو يذكر مضيفًا لا يمكن حل اسمه.                                                                                       |
| `webhook_endpoint_limit_reached`    | تملك المؤسسة 16 نقطة استقبال webhook في هذا الوضع من قبل.                                                                                                                                       |
| `webhook_endpoints_busy`            | التقى تغييران لنقاط استقبالك في اللحظة نفسها (`409`). أرسل الطلب من جديد.                                                                                                                       |
| `idempotency_key_missing`           | أُرسل طلب يحرّك أموالًا دون `Idempotency-Key`.                                                                                                                                                  |
| `invalid_request`                   | طلب مرفوض دون رمز أدق؛ وتوضح الرسالة السبب.                                                                                                                                                     |

### المفاتيح والصلاحيات

| `code`                    | ما حدث                                                               |
| ------------------------- | -------------------------------------------------------------------- |
| `api_key_missing`         | لم يحمل الطلب أي مفتاح.                                              |
| `api_key_invalid`         | المفتاح غير موجود. تحقّق من عدم نقصان أي حرف.                        |
| `api_key_revoked`         | أُبطل المفتاح. استخدم مفتاحًا آخر.                                   |
| `api_key_expired`         | جُدِّد المفتاح وانتهت فترة التداخل الخاصة به. استخدم المفتاح الجديد. |
| `secret_key_required`     | استُخدم مفتاح قابل للنشر حيث يلزم مفتاح سري.                         |
| `live_mode_not_activated` | استُخدم مفتاح للوضع المباشر قبل تفعيل الحساب.                        |
| `account_inactive`        | الحساب موقوف أو مغلق.                                                |
| `test_mode_only`          | استُدعيت أداة اختبار مساعدة بمفتاح للوضع المباشر.                    |
| `authentication_required` | رد `401` دون رمز أدق؛ وتوضح الرسالة السبب.                           |
| `permission_denied`       | رد `403` دون رمز أدق؛ وتوضح الرسالة السبب.                           |

### المدفوعات وعدم التكرار والحدود

| `code`                    | ما حدث                                                          |
| ------------------------- | --------------------------------------------------------------- |
| `card_declined`           | رُفضت البطاقة؛ وقد يوضح `decline_code` السبب.                   |
| `idempotency_key_reused`  | استُخدمت قيمة `Idempotency-Key` هذه لطلب آخر.                   |
| `idempotency_in_progress` | الطلب الأول بقيمة `Idempotency-Key` هذه ما زال قيد التنفيذ.     |
| `idempotency_error`       | رفض يخص `Idempotency-Key` دون رمز أدق؛ وتوضح الرسالة السبب.     |
| `rate_limit_exceeded`     | طلبات كثيرة جدًا؛ وتذكر الرسالة الحد الذي بُلغ ومدة الانتظار.   |
| `test_daily_cap_reached`  | بُلغ السقف اليومي لوضع الاختبار، حتى منتصف الليل بتوقيت المغرب. |
| `api_error`               | خطأ من جهتنا.                                                   |

الدفعة التي يجريها عميلك في الصفحة المستضافة لا تُفشل استدعاءك لـ API عند رفض
البطاقة: يرى عميلك ذلك ويستطيع المحاولة من جديد، وترى أنت السبب في
`last_payment_error` ضمن نية الدفع وفي الحدث `payment_intent.payment_failed`. تجد قائمة الأسباب في
[كيف تعمل المدفوعات](https://pay-docs.adpromax.com/ar/payments/how-payments-work).

## قراءة خطأ في كودك

```bash
curl -i https://pay-api.adpromax.com/v1/payment_intents/pi_01k7d9x2c5v8b1n4m7q0w3e6rt \
  -u "$APX_SECRET_KEY:"
```

```js title="errors.mjs"
const id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}`, {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
if (!response.ok) {
  const { error } = await response.json()
  if (error.code === 'resource_missing')
    console.log('No such payment here:', error.message)
  else
    console.log(error.type, error.code, error.request_id)
}
```

```ts title="errors.ts"
interface ApiError {
  error: {
    type: string
    code: string
    message: string
    request_id: string
    doc_url: string
    param?: string
  }
}

const id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}`, {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  if (error.code === 'resource_missing')
    console.log('No such payment here:', error.message)
  else
    console.log(error.type, error.code, error.request_id)
}
```

```python title="errors.py"
import os

import requests

payment_intent_id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt"
response = requests.get(
    f"https://pay-api.adpromax.com/v1/payment_intents/{payment_intent_id}",
    auth=(os.environ["APX_SECRET_KEY"], ""),
)
if not response.ok:
    error = response.json()["error"]
    if error["code"] == "resource_missing":
        print("No such payment here:", error["message"])
    else:
        print(error["type"], error["code"], error["request_id"])
```

```php title="errors.php"
<?php
$id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt';
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents/' . $id);
curl_setopt($ch, CURLOPT_USERPWD, getenv('APX_SECRET_KEY') . ':');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
    $error = $body['error'];
    if ($error['code'] === 'resource_missing') {
        echo 'No such payment here: ', $error['message'], PHP_EOL;
    } else {
        echo $error['type'], ' ', $error['code'], ' ', $error['request_id'], PHP_EOL;
    }
}
```

```ruby title="errors.rb"
require 'json'
require 'net/http'

id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
uri = URI("https://pay-api.adpromax.com/v1/payment_intents/#{id}")
request = Net::HTTP::Get.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
  error = JSON.parse(response.body)['error']
  if error['code'] == 'resource_missing'
    puts "No such payment here: #{error['message']}"
  else
    puts "#{error['type']} #{error['code']} #{error['request_id']}"
  end
end
```

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

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

type APIError struct {
	Error struct {
		Type      string `json:"type"`
		Code      string `json:"code"`
		Message   string `json:"message"`
		RequestID string `json:"request_id"`
	} `json:"error"`
}

func main() {
	id := "pi_01k7d9x2c5v8b1n4m7q0w3e6rt"
	req, err := http.NewRequest(http.MethodGet,
		"https://pay-api.adpromax.com/v1/payment_intents/"+id, nil)
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		var failure APIError
		if err := json.NewDecoder(res.Body).Decode(&failure); err != nil {
			log.Fatal(err)
		}
		if failure.Error.Code == "resource_missing" {
			fmt.Println("No such payment here:", failure.Error.Message)
		} else {
			fmt.Println(failure.Error.Type, failure.Error.Code,
				failure.Error.RequestID)
		}
	}
}
```

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

public class Errors {
    // The JDK has no JSON parser: a small regex reads one field of the error.
    static String field(String json, String name) {
        Matcher match = Pattern
            .compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
            .matcher(json);
        return match.find() ? match.group(1) : "";
    }

    public static void main(String[] args) throws Exception {
        String id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents/" + id))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            String error = response.body();
            if (field(error, "code").equals("resource_missing")) {
                System.out.println(
                    "No such payment here: " + field(error, "message"));
            } else {
                System.out.println(field(error, "type") + " "
                    + field(error, "code") + " " + field(error, "request_id"));
            }
        }
    }
}
```

```csharp title="Errors.cs"
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 id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
using var response = await http.GetAsync($"https://pay-api.adpromax.com/v1/payment_intents/{id}");
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    var error = failure!.Error;
    if (error.Code == "resource_missing")
        Console.WriteLine($"No such payment here: {error.Message}");
    else
        Console.WriteLine($"{error.Type} {error.Code} {error.RequestId}");
}

record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Type, string Code, string Message, string RequestId);
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
use serde::Deserialize;

#[derive(Deserialize)]
struct ApiError {
    error: ErrorBody,
}

#[derive(Deserialize)]
struct ErrorBody {
    r#type: String,
    code: String,
    message: String,
    request_id: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
    let response = reqwest::blocking::Client::new()
        .get(format!("https://pay-api.adpromax.com/v1/payment_intents/{id}"))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .send()?;
    if !response.status().is_success() {
        let ApiError { error } = response.json()?;
        if error.code == "resource_missing" {
            println!("No such payment here: {}", error.message);
        } else {
            println!("{} {} {}", error.r#type, error.code, error.request_id);
        }
    }
    Ok(())
}
```

## معرّفات الطلبات

تحمل كل استجابة، نجاحًا كانت أو فشلًا، ترويسة `Request-Id` (`req_…`). تعثر سجلات الطلبات في لوحة
التحكم على الطلب به، مع ما أرسلته وما أجابت به واجهة API. احتفظ به في سجلاتك الخاصة،
وقدّمه عندما تراسل الدعم: فهو أسرع طريق إلى الجواب.
