# المبالغ المستردة

استرداد دفعة بالبطاقة كليًا أو جزئيًا، والمدة التي يستغرقها الاسترداد، ولماذا لا تُسترد الدفعة النقدية نقدًا.

**الاسترداد** يعيد المال إلى عميلك عن دفعة نجحت. يمكن استرداد المدفوعات بالبطاقة،
كليًا أو جزئيًا. أما المدفوعات النقدية فلا: انظر [المدفوعات النقدية](#المدفوعات-النقدية) أدناه.

* لا تُسترد إلا دفعة حالتها `succeeded`.
* يمكنك إجراء عدة عمليات استرداد جزئية، ما دام مجموعها لا يتجاوز
  `amount_received` للدفعة. يُرفض أي استرداد يتجاوز المتبقي بالرمز `amount_too_large`.
* يُرفض استرداد الدفعة النقدية بالرمز `refund_unsupported_payment_method`، في وضع الاختبار كما في الوضع المباشر.

## استرداد دفعة بالبطاقة

أنشئ عملية استرداد عبر `POST /v1/refunds`. ترويسة `Idempotency-Key` إلزامية، كما في كل
طلب يحرّك أموالًا: الاسترداد الذي يُرسل مرتين بالمفتاح نفسه يُنفَّذ مرة واحدة.

| المعامل          | ما يحمله                                                                              |
| ---------------- | ------------------------------------------------------------------------------------- |
| `payment_intent` | الدفعة المراد استردادها (`pi_…`). أو أرسل `charge`، أي محاولتها الناجحة (`ch_…`).     |
| `amount`         | اختياري. بالسنتيم. من دونه، كل ما لم يُسترد بعد.                                      |
| `reason`         | اختياري. `requested_by_customer` أو `duplicate` أو `fraudulent`.                      |
| `metadata`       | اختياري. مراجعك الخاصة، مثل رقم الإرجاع: انظر [البيانات الوصفية](https://pay-docs.adpromax.com/ar/payments/metadata). |

الاستجابة هي عملية الاسترداد، وتكون `pending` في البداية.

```bash
curl https://pay-api.adpromax.com/v1/refunds \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-order-1042-1" \
  -d '{
    "payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
    "amount": 10000,
    "reason": "requested_by_customer",
    "metadata": { "return_id": "R-311" }
  }'
```

```js title="refund.mjs"
const response = await fetch('https://pay-api.adpromax.com/v1/refunds', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'refund-order-1042-1',
  },
  body: JSON.stringify({
    payment_intent: 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
    amount: 10000,
    reason: 'requested_by_customer',
    metadata: { return_id: 'R-311' },
  }),
})
const refund = await response.json()
if (!response.ok)
  throw new Error(refund.error.message)
console.log(refund.id, refund.status)
```

```ts title="refund.ts"
interface Refund {
  id: string
  object: 'refund'
  amount: number
  currency: string
  payment_intent: string
  reason: string | null
  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/refunds', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'refund-order-1042-1',
  },
  body: JSON.stringify({
    payment_intent: 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
    amount: 10000,
    reason: 'requested_by_customer',
    metadata: { return_id: 'R-311' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const refund = (await response.json()) as Refund
console.log(refund.id, refund.status)
```

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

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/refunds",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "refund-order-1042-1"},
    json={
        "payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
        "amount": 10000,
        "reason": "requested_by_customer",
        "metadata": {"return_id": "R-311"},
    },
)
refund = response.json()
response.raise_for_status()
print(refund["id"], refund["status"])
```

```php title="refund.php"
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/refunds');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: refund-order-1042-1',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'payment_intent' => 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
        'amount' => 10000,
        'reason' => 'requested_by_customer',
        'metadata' => ['return_id' => 'R-311'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$refund = json_decode(curl_exec($ch), true);
echo $refund['id'], ' ', $refund['status'], PHP_EOL;
```

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

uri = URI('https://pay-api.adpromax.com/v1/refunds')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'refund-order-1042-1'
request.body = JSON.generate(
  payment_intent: 'pi_01k7e7v2x5z8b1d4f7h0k3n6qs',
  amount: 10000,
  reason: 'requested_by_customer',
  metadata: { return_id: 'R-311' }
)

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

puts "#{refund['id']} #{refund['status']}"
```

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

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

type Refund struct {
	ID            string `json:"id"`
	Amount        int64  `json:"amount"`
	PaymentIntent string `json:"payment_intent"`
	Status        string `json:"status"`
}

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

func main() {
	body, err := json.Marshal(map[string]any{
		"payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
		"amount":         10000,
		"reason":         "requested_by_customer",
		"metadata":       map[string]string{"return_id": "R-311"},
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/refunds", 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", "refund-order-1042-1")

	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 refund Refund
	if err := json.NewDecoder(res.Body).Decode(&refund); err != nil {
		log.Fatal(err)
	}
	fmt.Println(refund.ID, refund.Status)
}
```

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

public class Refund {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
              "amount": 10000,
              "reason": "requested_by_customer",
              "metadata": { "return_id": "R-311" }
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/refunds"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "refund-order-1042-1")
            .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 refund as JSON: read its id and status with a JSON library.
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

```csharp title="Refund.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/refunds");
request.Headers.Add("Idempotency-Key", "refund-order-1042-1");
request.Content = JsonContent.Create(new
{
    payment_intent = "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
    amount = 10000,
    reason = "requested_by_customer",
    metadata = new { return_id = "R-311" },
});

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 refund = await response.Content.ReadFromJsonAsync<Refund>(json);
Console.WriteLine($"{refund!.Id} {refund.Status}");

record Refund(string Id, long Amount, string PaymentIntent, string Status);
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 serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct Refund {
    id: String,
    status: 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/refunds")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "refund-order-1042-1")
        .json(&json!({
            "payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
            "amount": 10000,
            "reason": "requested_by_customer",
            "metadata": { "return_id": "R-311" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let refund: Refund = response.json()?;
    println!("{} {}", refund.id, refund.status);
    Ok(())
}
```

```json title="200 OK" response
{
  "id": "re_01k7e7w3y6a9c2e5g8j1m4p7rt",
  "object": "refund",
  "amount": 10000,
  "currency": "mad",
  "payment_intent": "pi_01k7e7v2x5z8b1d4f7h0k3n6qs",
  "charge": "ch_01k7e7v3c6e9g2j5m8p1r4t7wb",
  "status": "pending",
  "reason": "requested_by_customer",
  "metadata": { "return_id": "R-311" },
  "created": 1790863800,
  "livemode": false
}
```

## حياة عملية الاسترداد

حالات عملية الاسترداد:

- `pending` → `succeeded`: نُفّذت
- `pending` → `failed`: رُفضت
- `pending` → `canceled`: تلغيها أنت

تبقى عملية الاسترداد `pending` إلى أن تُنفَّذ، ثم تصبح `succeeded`. تنتهي بحالة `failed` إذا رفضتها شبكة
البطاقات، وبحالة `canceled` إذا ألغيتها قبل ذلك.

* **في وضع الاختبار**، ينجح الاسترداد في ثوانٍ.
* **في الوضع المباشر**، قد يُعالَج استرداد الدفعة بالبطاقة **معالجة يدوية**: ينفّذه فريقنا مع
  شبكة البطاقات. يبقى `pending` إلى أن يتم ذلك، وقد يستغرق بضعة أيام عمل. وبعد أن
  يصبح `succeeded`، يأخذ بنك عميلك وقته الخاص لإظهار المال في بطاقته.

الحالات هي نفسها في الوضعين، فلا يتغير كودك بين وضع وآخر.

## إلغاء عملية استرداد

يمكن إلغاء عملية استرداد `pending` لم تُنفَّذ بعد عبر
`POST /v1/refunds/{id}/cancel`. تصبح `canceled`، ويمكن استرداد مبلغها من جديد لاحقًا.

```bash
curl -X POST https://pay-api.adpromax.com/v1/refunds/re_01k7e7w3y6a9c2e5g8j1m4p7rt/cancel \
  -u "$APX_SECRET_KEY:"
```

```js title="cancel-refund.mjs"
const id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/refunds/${id}/cancel`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const refund = await response.json()
console.log(refund.status) // canceled
```

```ts title="cancel-refund.ts"
interface Refund {
  id: string
  object: 'refund'
  status: string
}

const id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/refunds/${id}/cancel`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const refund = (await response.json()) as Refund
console.log(refund.status) // canceled
```

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

import requests

refund_id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt"
response = requests.post(
    f"https://pay-api.adpromax.com/v1/refunds/{refund_id}/cancel",
    auth=(os.environ["APX_SECRET_KEY"], ""),
)
print(response.json()["status"])  # canceled
```

```php title="cancel_refund.php"
<?php
$id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt';
$ch = curl_init("https://pay-api.adpromax.com/v1/refunds/{$id}/cancel");
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
]);
$refund = json_decode(curl_exec($ch), true);
echo $refund['status'], PHP_EOL; // canceled
```

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

id = 're_01k7e7w3y6a9c2e5g8j1m4p7rt'
uri = URI("https://pay-api.adpromax.com/v1/refunds/#{id}/cancel")
request = Net::HTTP::Post.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
puts JSON.parse(response.body)['status'] # canceled
```

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

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

type Refund struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func main() {
	id := "re_01k7e7w3y6a9c2e5g8j1m4p7rt"
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/refunds/"+id+"/cancel", 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()

	var refund Refund
	if err := json.NewDecoder(res.Body).Decode(&refund); err != nil {
		log.Fatal(err)
	}
	fmt.Println(refund.Status) // canceled
}
```

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

public class CancelRefund {
    public static void main(String[] args) throws Exception {
        String id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt";
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/refunds/" + id + "/cancel"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .POST(HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        // The refund as JSON: its status is now "canceled".
        System.out.println(response.body());
    }
}
```

```csharp title="CancelRefund.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 = "re_01k7e7w3y6a9c2e5g8j1m4p7rt";
using var response = await http.PostAsync(
    $"https://pay-api.adpromax.com/v1/refunds/{id}/cancel", null);
var refund = await response.Content.ReadFromJsonAsync<Refund>(json);
Console.WriteLine(refund!.Status); // canceled

record Refund(string Id, string Status);
```

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

#[derive(Deserialize)]
struct Refund {
    status: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let id = "re_01k7e7w3y6a9c2e5g8j1m4p7rt";
    let refund: Refund = reqwest::blocking::Client::new()
        .post(format!("https://pay-api.adpromax.com/v1/refunds/{id}/cancel"))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .send()?
        .json()?;
    println!("{}", refund.status); // canceled
    Ok(())
}
```

## الأحداث والأرصدة

| الحدث             | متى                                               |
| ----------------- | ------------------------------------------------- |
| `refund.created`  | أُنشئت عملية استرداد.                             |
| `refund.updated`  | تغيّرت حالة عملية استرداد، مثلًا إلى `succeeded`. |
| `refund.failed`   | فشلت عملية استرداد.                               |
| `charge.refunded` | استُردّت محاولة، كليًا أو جزئيًا.                 |

* يزداد `amount_refunded` في نية الدفع (payment intent) عندما **ينجح** استرداد. تبقى نية الدفع
  `succeeded`، حتى لو استُردّت كليًا.
* الاسترداد الفاشل أو الملغى يحرّر مبلغه: يمكنك استرداده من جديد.
* يُخصم الاسترداد الناجح من رصيدك؛ ويظهر كسطر استرداد في **الأرصدة**. لا تُعاد
  رسومنا على الدفعة: الدفعة المستردة كليًا تترك رصيدك أقل بمقدار تلك
  الرسوم.

## من لوحة التحكم

في صفحة الدفعة، يفتح **استرداد** نموذجًا:

* المبلغ، معبأ مسبقًا بما بقي للاسترداد، مع خياري *كامل* و*جزئي*؛
* السبب: مكرر، أو احتيالي، أو بطلب من العميل، أو غير ذلك مع ملاحظة؛
* في الوضع المباشر، تأكيد أخير قبل إرسال الاسترداد.

تعرض صفحة **المبالغ المستردة** كل عملية استرداد مع حالتها ودفعتها ومن أجراها.

من يستطيع الاسترداد في لوحة التحكم يتوقف على الدور: المالك والمسؤول والمالية والدعم يستطيعون.
يستطيع دور الدعم الاسترداد حتى سقف لكل عملية، 1,000.00 MAD افتراضيًا، ويعرضه النموذج قبل أن
تكتب. في الأدوار الأخرى يكون الزر معطّلًا ويذكر السبب. انظر
[الفريق والأدوار](https://pay-docs.adpromax.com/ar/account/team-and-roles). مفتاح API لا تقيّده الأدوار: يتصرف باسم المؤسسة
كلها.

## المدفوعات النقدية

الدفعة النقدية الناجحة **لا يمكن استردادها عبر AdProMaxPay**: لا نقدًا، ولا إلى بطاقة.

**السبب.** سلّم عميلك المال نقدًا لدى وكيل. لا توجد بطاقة ولا حساب
يُعاد إليه.

**ما يحدث.** تبقى الدفعة `succeeded` ويبقى مبلغها في رصيدك. في
لوحة التحكم، يكون **استرداد** معطّلًا في الدفعة النقدية ويذكر السبب؛ وتجيب واجهة API بـ
`refund_unsupported_payment_method`، في وضع الاختبار كما في الوضع المباشر.

**ما تفعله.** سوِّ الأمر مع عميلك مباشرة: تحويل بنكي، أو رصيد في متجرك، أو نقدًا في
محلّك. سجّل ذلك في دفاترك. وأخبر عملاءك بسياستك قبل أن يختاروا الدفع نقدًا.

رمز الدفع النقدي الذي لم يُدفع بعد لا يُسترد بل **يُلغى**: انظر
[إلغاء رمز لم تعد تريده](https://pay-docs.adpromax.com/ar/payments/cash-payments#إلغاء-رمز-لم-تعد-تريده).
