# البيانات الوصفية

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

**البيانات الوصفية** (metadata) مجموعة من مفاتيحك وقيمك الخاصة على كائن: رقم طلبية، أو معرّف سلة، أو المتجر
أو القناة التي جاء منها البيع. يخزّنها AdProMaxPay ويعيدها كما هي أينما ظهر
الكائن. ولا تغيّر أبدًا طريقة عمل الدفعة.

## أضف بيانات وصفية إلى دفعة

أرسل `metadata` ككائن من السلاسل النصية عند إنشاء نية الدفع (payment intent). تعود مراجعك عندئذ
في استجابات API وفي كل حدث يخص هذه الدفعة: عندما يتلقى الـ webhook لديك
`payment_intent.succeeded`، يذكر `data.object.metadata.order_id` الطلبية المراد تنفيذها.

```bash
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1045-create" \
  -d '{
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card", "cash"],
    "metadata": { "order_id": "1045", "channel": "whatsapp" }
  }'
```

```js title="metadata.mjs"
const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1045-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    metadata: { order_id: '1045', channel: 'whatsapp' },
  }),
})
const intent = await response.json()
if (!response.ok)
  throw new Error(intent.error.message)
console.log(intent.id, intent.metadata.order_id)
```

```ts title="metadata.ts"
interface PaymentIntent {
  id: string
  object: 'payment_intent'
  amount: number
  currency: string
  status: string
  metadata: Record<string, string>
}

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

const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1045-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    metadata: { order_id: '1045', channel: 'whatsapp' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const intent = (await response.json()) as PaymentIntent
console.log(intent.id, intent.metadata.order_id)
```

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

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/payment_intents",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "order-1045-create"},
    json={
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card", "cash"],
        "metadata": {"order_id": "1045", "channel": "whatsapp"},
    },
)
intent = response.json()
response.raise_for_status()
print(intent["id"], intent["metadata"]["order_id"])
```

```php title="metadata.php"
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: order-1045-create',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 25000,
        'currency' => 'mad',
        'payment_method_types' => ['card', 'cash'],
        'metadata' => ['order_id' => '1045', 'channel' => 'whatsapp'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$intent = json_decode(curl_exec($ch), true);
echo $intent['id'], ' ', $intent['metadata']['order_id'], PHP_EOL;
```

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

uri = URI('https://pay-api.adpromax.com/v1/payment_intents')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'order-1045-create'
request.body = JSON.generate(
  amount: 25000,
  currency: 'mad',
  payment_method_types: %w[card cash],
  metadata: { order_id: '1045', channel: 'whatsapp' }
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
intent = JSON.parse(response.body)
raise intent.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

puts "#{intent['id']} #{intent['metadata']['order_id']}"
```

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

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

type PaymentIntent struct {
	ID       string            `json:"id"`
	Status   string            `json:"status"`
	Metadata map[string]string `json:"metadata"`
}

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

func main() {
	metadata := map[string]string{"order_id": "1045", "channel": "whatsapp"}
	body, err := json.Marshal(map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card", "cash"},
		"metadata":             metadata,
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/payment_intents", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "order-1045-create")

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

	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		log.Fatal(apiErr.Error.Message)
	}
	var intent PaymentIntent
	if err := json.NewDecoder(res.Body).Decode(&intent); err != nil {
		log.Fatal(err)
	}
	fmt.Println(intent.ID, intent.Metadata["order_id"])
}
```

```java title="Metadata.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Metadata {
    public static void main(String[] args) throws Exception {
        // Your own references, as strings.
        String metadata = """
            { "order_id": "1045", "channel": "whatsapp" }""";
        String body = """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card", "cash"],
              "metadata": %s
            }
            """.formatted(metadata);
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "order-1045-create")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        // The payment intent as JSON: its metadata comes back as you sent it.
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

```csharp title="Metadata.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"));

using var request = new HttpRequestMessage(
    HttpMethod.Post, "https://pay-api.adpromax.com/v1/payment_intents");
request.Headers.Add("Idempotency-Key", "order-1045-create");
request.Content = JsonContent.Create(new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card", "cash" },
    metadata = new { order_id = "1045", channel = "whatsapp" },
});

using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    throw new InvalidOperationException(failure?.Error.Message);
}
var intent = await response.Content.ReadFromJsonAsync<PaymentIntent>(json);
Console.WriteLine($"{intent!.Id} {intent.Metadata["order_id"]}");

record PaymentIntent(string Id, string Status, Dictionary<string, string> Metadata);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
```

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

use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct PaymentIntent {
    id: String,
    metadata: HashMap<String, String>,
}

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

#[derive(Deserialize)]
struct ErrorBody {
    message: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/payment_intents")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "order-1045-create")
        .json(&json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"],
            "metadata": { "order_id": "1045", "channel": "whatsapp" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let intent: PaymentIntent = response.json()?;
    println!("{} {}", intent.id, intent.metadata["order_id"]);
    Ok(())
}
```

## الحدود

* حتى **50 مفتاحًا** في الكائن الواحد.
* المفتاح حتى **40 حرفًا**؛ والقيمة حتى **500 حرف**.
* القيم **سلاسل نصية** فقط. أرسل الرقم كنص: `"1045"` لا `1045`.

يُرفض الطلب الذي يتجاوز هذه الحدود بـ `400`، `parameter_invalid`، مع `param` يذكر المفتاح
المخالف، مثل `metadata.order_id`. ولا يُنشأ شيء. اقرأ `param` لتعرف المفتاح: فحقل
`message` جملة عامة لا تذكر أي حد جرى تجاوزه. في هذه الاستجابة، المفتاح
أطول من 40 حرفًا.

```json title="400 Bad Request" response
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "param": "metadata.customer_reference_from_our_previous_system",
    "message": "This value is not valid.",
    "doc_url": "https://pay-docs.adpromax.com/errors#parameter_invalid",
    "request_id": "req_01k7e5s3d7f1g5h9j3k7m1n5pz"
  }
}
```

## الكائنات التي تقبل البيانات الوصفية

| الكائن                   | التعيين                      | التغيير                                                                   |
| ------------------------ | ---------------------------- | ------------------------------------------------------------------------- |
| نية الدفع                | `POST /v1/payment_intents`   | `POST /v1/payment_intents/{id}`، ما دامت الدفعة `requires_payment_method` |
| جلسة الدفع               | `POST /v1/checkout/sessions` | —                                                                         |
| العميل                   | `POST /v1/customers`         | `POST /v1/customers/{id}`، في أي وقت                                      |
| الاسترداد                | `POST /v1/refunds`           | —                                                                         |
| نقطة استقبال الـ webhook | `POST /v1/webhook_endpoints` | `POST /v1/webhook_endpoints/{id}`                                         |

تغيير البيانات الوصفية لنقطة استقبال webhook **يُدمج** مع ما تحمله: تُعيَّن المفاتيح التي ترسلها،
وتبقى الأخرى، ويُحذف المفتاح المرسل بقيمة فارغة (`""`).

## تبقى البيانات الوصفية حيث وضعتها

تنتمي البيانات الوصفية إلى الكائن الذي وضعتها عليه، ولا تُنسخ أبدًا إلى كائن آخر. تكون
`metadata` جلسة الدفع (checkout session) على الجلسة وفي أحداثها `checkout.session.*`؛ ولا تحملها نية الدفع التي
تنشئها الجلسة.

مع صفحة الدفع المستضافة، احتفظ بمعرّف نية الدفع مع طلبيتك، كما يفعل
[البدء السريع للبطاقة](https://pay-docs.adpromax.com/ar/get-started/quickstart-card)، أو اقرأ الجلسة عند وصول
`checkout.session.completed`.

## أين تراها

* في API: كل استجابة تحمل الكائن.
* في إشعارات الـ webhook: يحمل `data.object` في الحدث البيانات الوصفية للكائن كما كانت عند وقوع
  الحدث.
* في لوحة التحكم: تعرض صفحة الدفعة بياناتها الوصفية، للقراءة فقط، ويمكن تصفية قائمة المدفوعات
  حسب البيانات الوصفية. وتُعدَّل البيانات الوصفية للعميل في صفحة العميل.

## ما لا يوضع أبدًا في البيانات الوصفية

البيانات الوصفية للمراجع، لا للأسرار. يراها فريقك كله في لوحة التحكم، وتتلقاها
نقاط استقبال الـ webhook لديك.

* **لا أرقام بطاقات.** لا تمر أرقام البطاقات عبر API أبدًا: تُرفض أي قيمة تشبه رقم
  بطاقة بالرمز `parameter_invalid`.
* **لا كلمات مرور ولا مفاتيح ولا رموز وصول.**
* **لا بيانات شخصية حساسة**، مثل أرقام الهوية أو المعلومات الصحية. لتعرف من دفع، استخدم
  [عميلًا](https://pay-docs.adpromax.com/ar/payments/customers) واحتفظ بالباقي في أنظمتك، تحت معرّفك.
