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

كيف تعمل الدفعة بالبطاقة، و3-D Secure، وحالات الرفض وإعادة المحاولة، وكيف ترسل عميلك إلى صفحة البطاقة من صفحتك الخاصة.

يكتب عميلك بيانات بطاقته في **صفحة البطاقة الآمنة لشريكنا في البطاقات**، لا في موقعك
ولا في موقعنا أبدًا. لا ترى رقم بطاقة ولا ترسله ولا تخزّنه أبدًا، وترفض واجهة API أي نص
يشبهه. في وضع الاختبار، تلك الصفحة هي محاكاتنا التي تحمل علامة TEST، ولا تقبل إلا
[بطاقات الاختبار](https://pay-docs.adpromax.com/ar/sandbox/test-cards).

هناك طريقتان لإيصال عميلك إليها:

* **صفحة الدفع المستضافة** (موصى بها): تعرض صفحتنا البطاقة، والدفع نقدًا إن أردت، وتنفّذ عنك كل
  الخطوات أدناه. انظر [صفحة الدفع المستضافة](https://pay-docs.adpromax.com/ar/payments/hosted-checkout).
* **صفحتك الخاصة**: تؤكد نية الدفع (payment intent) بنفسك وترسل متصفح عميلك إلى
  صفحة البطاقة، كما تبيّن هذه الصفحة.

نموذج بطاقة مضمَّن في صفحتك الخاصة مخطط له لاحقًا. سنعلن عنه في
[سجل التغييرات](https://pay-docs.adpromax.com/ar/changelog).

## ما يحدث أثناء الدفع بالبطاقة

1. تُؤكَّد نية دفع للبطاقة: تبدأ **محاولة** (`ch_…`)، وتصبح نية الدفع
   `requires_action`.
2. يصل متصفح عميلك إلى صفحة البطاقة، فيكتب بيانات بطاقته.
3. إذا طلب بنكه ذلك، يؤكد عبر **3-D Secure** في صفحة تابعة لبنكه. لا شيء
   عليك فعله: يجري ذلك في صفحة البطاقة. وفشل التحقق ينهي المحاولة بالرمز
   `authentication_failed`.
4. يؤكد لنا شريك البطاقات الدفعة. تصبح نية الدفع `succeeded` وتتلقى
   `payment_intent.succeeded`.

تبقى صفحة البطاقة صالحة للاستخدام نحو **30 دقيقة**؛ بعدها تنتهي المحاولة بحالة `expired` وتعود
نية الدفع إلى `requires_payment_method`. لا تقل الدفعة بالبطاقة عن **1.00 MAD**
(`100`).

## من صفحتك الخاصة

أنشئ نية دفع للبطاقة، ثم أكّدها مع:

* `payment_method_type`: `card`؛
* `return_url`: العنوان الذي يعود إليه عميلك بعد صفحة البطاقة، بـ `https`. ضع فيه مرجع
  طلبيتك، مثل `https://shop.example.com/orders/1042/return`: لا تحمل العودة أي
  دليل على الدفع.

يحتاج كل من الطلبين إلى `Idempotency-Key` خاص به. الاستجابة هي نية الدفع، في
`requires_action`، مع `next_action` من النوع `form_post`: عنوان (`url`)، و`method`،
و`fields` يجب أن يرسلها متصفح عميلك للوصول إلى صفحة البطاقة. تتغير الحقول من
دفعة إلى أخرى: أرسلها كلها كما هي.

```bash
PI=$(curl -s https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-create" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card"]}' | jq -r .id)

curl https://pay-api.adpromax.com/v1/payment_intents/$PI/confirm \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirm" \
  -d '{"payment_method_type": "card",
       "return_url": "https://shop.example.com/orders/1042/return"}'
```

```js title="card.mjs"
const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`

async function post(path, key, body) {
  const response = await fetch(`${api}${path}`, {
    method: 'POST',
    headers: {
      'Authorization': auth,
      'Content-Type': 'application/json',
      'Idempotency-Key': key,
    },
    body: JSON.stringify(body),
  })
  const answer = await response.json()
  if (!response.ok)
    throw new Error(answer.error.message)
  return answer
}

const intent = await post('/payment_intents', 'order-1042-create', {
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card'],
})
const confirmed = await post(`/payment_intents/${intent.id}/confirm`,
  'order-1042-confirm', {
    payment_method_type: 'card',
    return_url: 'https://shop.example.com/orders/1042/return',
  })
const formPost = confirmed.next_action.form_post
```

```ts title="card.ts"
interface FormPost {
  url: string
  method: string
  fields: Record<string, string>
}

interface PaymentIntent {
  id: string
  object: 'payment_intent'
  status: string
  next_action: { type: 'form_post', form_post: FormPost } | 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}`

async function post(path: string, key: string, body: object): Promise<PaymentIntent> {
  const response = await fetch(`${api}${path}`, {
    method: 'POST',
    headers: {
      'Authorization': auth,
      'Content-Type': 'application/json',
      'Idempotency-Key': key,
    },
    body: 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 PaymentIntent
}

const intent = await post('/payment_intents', 'order-1042-create', {
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card'],
})
const confirmed = await post(`/payment_intents/${intent.id}/confirm`,
  'order-1042-confirm', {
    payment_method_type: 'card',
    return_url: 'https://shop.example.com/orders/1042/return',
  })
const formPost = confirmed.next_action?.form_post
```

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

import requests

API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")

def post(path, key, body):
    response = requests.post(
        f"{API}{path}", auth=AUTH, json=body,
        headers={"Idempotency-Key": key},
    )
    answer = response.json()
    response.raise_for_status()
    return answer

intent = post("/payment_intents", "order-1042-create", {
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card"],
})
confirmed = post(f"/payment_intents/{intent['id']}/confirm",
                 "order-1042-confirm", {
                     "payment_method_type": "card",
                     "return_url": "https://shop.example.com/orders/1042/return",
                 })
form_post = confirmed["next_action"]["form_post"]
```

```php title="card.php"
<?php
function post(string $path, string $key, array $body): array
{
    $ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
    curl_setopt_array($ch, [
        CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Idempotency-Key: ' . $key,
        ],
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $answer = json_decode(curl_exec($ch), true);
    return $answer;
}

$intent = post('/payment_intents', 'order-1042-create', [
    'amount' => 25000,
    'currency' => 'mad',
    'payment_method_types' => ['card'],
]);
$confirmed = post("/payment_intents/{$intent['id']}/confirm",
    'order-1042-confirm', [
        'payment_method_type' => 'card',
        'return_url' => 'https://shop.example.com/orders/1042/return',
    ]);
$formPost = $confirmed['next_action']['form_post'];
```

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

API = 'https://pay-api.adpromax.com/v1'

def post(path, key, body)
  uri = URI("#{API}#{path}")
  request = Net::HTTP::Post.new(uri)
  request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
  request['Content-Type'] = 'application/json'
  request['Idempotency-Key'] = key
  request.body = JSON.generate(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

intent = post('/payment_intents', 'order-1042-create',
              amount: 25000, currency: 'mad', payment_method_types: ['card'])
confirmed = post("/payment_intents/#{intent['id']}/confirm", 'order-1042-confirm',
                 payment_method_type: 'card',
                 return_url: 'https://shop.example.com/orders/1042/return')
form_post = confirmed.dig('next_action', 'form_post')
```

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

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

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

type FormPost struct {
	URL    string            `json:"url"`
	Method string            `json:"method"`
	Fields map[string]string `json:"fields"`
}

type PaymentIntent struct {
	ID         string `json:"id"`
	Status     string `json:"status"`
	NextAction struct {
		FormPost FormPost `json:"form_post"`
	} `json:"next_action"`
}

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

func post(path, key string, body any) (*PaymentIntent, error) {
	payload, _ := json.Marshal(body)
	req, err := http.NewRequest(http.MethodPost, api+path, bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", key)
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		return nil, errors.New(apiErr.Error.Message)
	}
	var intent PaymentIntent
	return &intent, json.NewDecoder(res.Body).Decode(&intent)
}

func main() {
	intent, err := post("/payment_intents", "order-1042-create", map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card"},
	})
	if err != nil {
		log.Fatal(err)
	}
	confirmed, err := post("/payment_intents/"+intent.ID+"/confirm",
		"order-1042-confirm", map[string]any{
			"payment_method_type": "card",
			"return_url":          "https://shop.example.com/orders/1042/return",
		})
	if err != nil {
		log.Fatal(err)
	}
	formPost := confirmed.NextAction.FormPost
	fmt.Println(formPost.Method, formPost.URL)
}
```

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

public class Card {
    static final String API = "https://pay-api.adpromax.com/v1";
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String post(String path, String key, String body) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", key)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response =
            HTTP.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        return response.body();
    }

    public static void main(String[] args) throws Exception {
        String intent = post("/payment_intents", "order-1042-create", """
            {"amount": 25000, "currency": "mad", "payment_method_types": ["card"]}
            """);
        // The JDK has no JSON parser: a regex reads the id here.
        var id = Pattern.compile("\"id\"\\s*:\\s*\"(pi_[^\"]+)\"").matcher(intent);
        if (!id.find()) {
            throw new IllegalStateException("No id: " + intent);
        }
        String confirmed = post("/payment_intents/" + id.group(1) + "/confirm",
            "order-1042-confirm", """
            {"payment_method_type": "card",
             "return_url": "https://shop.example.com/orders/1042/return"}
            """);
        // next_action.form_post: url, method, fields
        System.out.println(confirmed);
    }
}
```

```csharp title="Card.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 { BaseAddress = new Uri("https://pay-api.adpromax.com/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

var intent = await Post("payment_intents", "order-1042-create", new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card" },
});
var confirm = $"payment_intents/{intent.Id}/confirm";
var confirmed = await Post(confirm, "order-1042-confirm", new
{
    payment_method_type = "card",
    return_url = "https://shop.example.com/orders/1042/return",
});
var formPost = confirmed.NextAction!.FormPost;
Console.WriteLine($"{formPost.Method} {formPost.Url}");

async Task<PaymentIntent> Post(string path, string key, object body)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, path)
    {
        Content = JsonContent.Create(body),
    };
    request.Headers.Add("Idempotency-Key", 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))!;
}

record PaymentIntent(string Id, string Status, NextAction? NextAction);
record NextAction(string Type, FormPost FormPost);
record FormPost(string Url, string Method, Dictionary<string, string> Fields);
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 reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::{json, Value};

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

#[derive(Deserialize)]
struct PaymentIntent {
    id: String,
    next_action: Option<NextAction>,
}

#[derive(Deserialize)]
struct NextAction {
    form_post: FormPost,
}

#[derive(Deserialize)]
struct FormPost {
    url: String,
    method: String,
    fields: HashMap<String, String>,
}

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

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

fn post(
    client: &Client,
    path: &str,
    key: &str,
    body: Value,
) -> Result<PaymentIntent, Box<dyn std::error::Error>> {
    let response = client
        .post(format!("{API}{path}"))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", key)
        .json(&body)
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    Ok(response.json()?)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let create = json!({
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card"],
    });
    let intent = post(&client, "/payment_intents", "order-1042-create", create)?;
    let confirm = json!({
        "payment_method_type": "card",
        "return_url": "https://shop.example.com/orders/1042/return",
    });
    let path = format!("/payment_intents/{}/confirm", intent.id);
    let confirmed = post(&client, &path, "order-1042-confirm", confirm)?;
    let next_action = confirmed.next_action.ok_or("no form to post")?;
    let form_post = next_action.form_post;
    let fields = form_post.fields.len();
    println!("{} {} ({fields} fields)", form_post.method, form_post.url);
    Ok(())
}
```

```json title="200 OK" response
{
  "id": "pi_01k7e5r2t8w4y6b9c3n7m1x5qa",
  "object": "payment_intent",
  "amount": 25000,
  "currency": "mad",
  "status": "requires_action",
  "payment_method_types": ["card"],
  "latest_charge": "ch_01k7e5r3a6s9d2f5g8h1j4k7mb",
  "next_action": {
    "type": "form_post",
    "form_post": {
      "url": "https://pay.example.com/…",
      "method": "POST",
      "fields": { "…": "…" }
    }
  },
  "livemode": false
}
```

## أرسل عميلك إلى صفحة البطاقة

أجب متصفح عميلك بصفحة فيها نموذج مبني من `next_action.form_post`: حقل
إدخال مخفي لكل حقل، يُرسل إلى `url`. يرسل النموذج نفسه فورًا؛ وزر *متابعة* الظاهر
هو البديل عند حظر السكربتات. هرِّب (escape) كل قيمة تكتبها في الصفحة.

إذا أعاد عميلك تحميل الصفحة قبل الدفع، فاقرأ نية الدفع من جديد وأرسل
`form_post` نفسه: يبقى صالحًا ما دامت المحاولة في الانتظار.

```bash
# Shows what the page must post: the form_post of the confirmed payment intent.
curl -s https://pay-api.adpromax.com/v1/payment_intents/pi_01k7e5r2t8w4y6b9c3n7m1x5qa \
  -u "$APX_SECRET_KEY:" | jq .next_action.form_post
```

```js title="card-form.mjs"
const escape = value => String(value)
  .replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`)

// formPost is next_action.form_post of the confirmed payment intent.
export function cardForm(formPost) {
  const inputs = Object.entries(formPost.fields)
    .map(([name, value]) =>
      `<input type="hidden" name="${escape(name)}" value="${escape(value)}">`)
    .join('\n')
  return `<!doctype html>
<form id="card" method="${escape(formPost.method)}" action="${escape(formPost.url)}">
${inputs}
<button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>`
}

// Express: res.send(cardForm(confirmed.next_action.form_post))
```

```ts title="card-form.ts"
import type { Response } from 'express'

interface FormPost {
  url: string
  method: string
  fields: Record<string, string>
}

const escape = (value: string): string => value
  .replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`)

// formPost is next_action.form_post of the confirmed payment intent.
export function cardForm(formPost: FormPost): string {
  const inputs = Object.entries(formPost.fields)
    .map(([name, value]) =>
      `<input type="hidden" name="${escape(name)}" value="${escape(value)}">`)
    .join('\n')
  return `<!doctype html>
<form id="card" method="${escape(formPost.method)}" action="${escape(formPost.url)}">
${inputs}
<button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>`
}

// In an Express route: sendCardForm(res, confirmed.next_action.form_post)
export function sendCardForm(res: Response, formPost: FormPost): void {
  res.type('html').send(cardForm(formPost))
}
```

```python title="app.py"
from flask import render_template_string

CARD_FORM = """<!doctype html>
<form id="card" method="{{ form.method }}" action="{{ form.url }}">
  {% for name, value in form.fields.items() %}
  <input type="hidden" name="{{ name }}" value="{{ value }}">
  {% endfor %}
  <button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>"""

def card_form(form_post):
    # Jinja escapes every value.
    return render_template_string(CARD_FORM, form=form_post)
```

```php title="card-form.php"
<?php
// $formPost is next_action.form_post of the confirmed payment intent.
$e = fn ($value) => htmlspecialchars((string) $value, ENT_QUOTES);
?>
<!doctype html>
<form id="card" method="<?= $e($formPost['method']) ?>" action="<?= $e($formPost['url']) ?>">
<?php foreach ($formPost['fields'] as $name => $value): ?>
  <input type="hidden" name="<?= $e($name) ?>" value="<?= $e($value) ?>">
<?php endforeach; ?>
  <button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>
```

```ruby title="card_form.rb"
require 'sinatra'

CARD_FORM = <<~HTML
  <!doctype html>
  <form id="card" method="<%= h form['method'] %>" action="<%= h form['url'] %>">
  <% form['fields'].each do |name, value| %>
    <input type="hidden" name="<%= h name %>" value="<%= h value %>">
  <% end %>
    <button type="submit">Continue</button>
  </form>
  <script>document.getElementById('card').submit()</script>
HTML

helpers do
  def h(value)
    Rack::Utils.escape_html(value.to_s)
  end

  # form_post is next_action.form_post of the confirmed payment intent.
  def card_form(form_post)
    erb CARD_FORM, locals: { form: form_post }
  end
end
```

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

import (
	"html/template"
	"net/http"
)

type FormPost struct {
	URL    string            `json:"url"`
	Method string            `json:"method"`
	Fields map[string]string `json:"fields"`
}

// html/template escapes every value.
var cardFormPage = template.Must(template.New("card").Parse(`<!doctype html>
<form id="card" method="{{.Method}}" action="{{.URL}}">
  {{range $name, $value := .Fields}}
  <input type="hidden" name="{{$name}}" value="{{$value}}">
  {{end}}
  <button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>`))

// formPost is next_action.form_post of the confirmed payment intent.
func cardForm(w http.ResponseWriter, formPost FormPost) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	cardFormPage.Execute(w, formPost)
}
```

```java title="CardFormController.java"
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.HtmlUtils;

public class CardFormController {
    record FormPost(String url, String method, Map<String, String> fields) {}

    // formPost is next_action.form_post of the confirmed payment intent.
    static ResponseEntity<String> cardForm(FormPost formPost) {
        StringBuilder inputs = new StringBuilder();
        formPost.fields().forEach((name, value) -> inputs.append(
            "<input type=\"hidden\" name=\"%s\" value=\"%s\">\n"
                .formatted(escape(name), escape(value))));
        String page = """
            <!doctype html>
            <form id="card" method="%s" action="%s">
            %s<button type="submit">Continue</button>
            </form>
            <script>document.getElementById('card').submit()</script>
            """.formatted(escape(formPost.method()), escape(formPost.url()), inputs);
        return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(page);
    }

    static String escape(String value) {
        return HtmlUtils.htmlEscape(value);
    }
}
```

```csharp title="Program.cs"
using System.Net;
using System.Text;

// formPost is next_action.form_post of the confirmed payment intent.
// In a route: return CardForm(confirmed.NextAction.FormPost);
static IResult CardForm(FormPost formPost)
{
    var inputs = new StringBuilder();
    foreach (var (name, value) in formPost.Fields)
    {
        inputs.AppendLine(
            $"<input type=\"hidden\" name=\"{E(name)}\" value=\"{E(value)}\">");
    }
    var page = $"""
        <!doctype html>
        <form id="card" method="{E(formPost.Method)}" action="{E(formPost.Url)}">
        {inputs}<button type="submit">Continue</button>
        </form>
        <script>document.getElementById('card').submit()</script>
        """;
    return Results.Content(page, "text/html");
}

static string E(string value) => WebUtility.HtmlEncode(value);

record FormPost(string Url, string Method, Dictionary<string, string> Fields);
```

```rust title="main.rs"
// Cargo.toml: axum = "0.8", serde = { version = "1", features = ["derive"] }
use std::collections::BTreeMap;

use axum::response::Html;
use serde::Deserialize;

#[derive(Deserialize)]
struct FormPost {
    url: String,
    method: String,
    fields: BTreeMap<String, String>,
}

fn escape(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

// form_post is next_action.form_post of the confirmed payment intent.
fn card_form(form_post: &FormPost) -> Html<String> {
    let inputs: String = form_post
        .fields
        .iter()
        .map(|(name, value)| {
            format!(
                "<input type=\"hidden\" name=\"{}\" value=\"{}\">\n",
                escape(name),
                escape(value)
            )
        })
        .collect();
    Html(format!(
        r#"<!doctype html>
<form id="card" method="{}" action="{}">
{}<button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>"#,
        escape(&form_post.method),
        escape(&form_post.url),
        inputs
    ))
}
```

## عندما يعود عميلك

بعد صفحة البطاقة، يعود متصفح عميلك إلى `return_url` مهما حدث.
هذه العودة لا تثبت شيئًا: اعرض «نحن بصدد تأكيد دفعتك»، ثم اقرأ نية الدفع
بمفتاحك السري، وسلّم الطلبية عند وصول إشعار الـ webhook `payment_intent.succeeded`. قد يصل التأكيد
بعد عودة عميلك: انظر [كيف تعمل المدفوعات](https://pay-docs.adpromax.com/ar/payments/how-payments-work#إعادة-التوجيه-وإشعارات-webhook-بأيهما-تثق).

## الرفض وإعادة المحاولة

المحاولة المرفوضة تعيد نية الدفع إلى `requires_payment_method`، مع السبب في
`last_payment_error`، وتتلقى `payment_intent.payment_failed`.

| `last_payment_error.code` | ما حدث                                                             | ما يستطيع عميلك فعله                   |
| ------------------------- | ------------------------------------------------------------------ | -------------------------------------- |
| `card_declined`           | رفض البنك. وقد يوضح `decline_code` أكثر، مثل `insufficient_funds`. | تجربة بطاقة أخرى.                      |
| `expired_card`            | انتهت صلاحية البطاقة.                                              | تجربة بطاقة أخرى.                      |
| `authentication_failed`   | لم ينجح التحقق عبر 3-D Secure.                                     | المحاولة من جديد، أو تجربة بطاقة أخرى. |
| `processing_error`        | تعذّرت معالجة الدفعة.                                              | المحاولة من جديد.                      |

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

* مع صفحة الدفع المستضافة، تعرض الصفحة *جرّب بطاقة أخرى*، و*ادفع نقدًا بدلًا من ذلك* إن كنت
  عرضت الدفع نقدًا.
* مع صفحتك الخاصة، أكّد نية الدفع من جديد بقيمة **جديدة** لـ `Idempotency-Key`، وأرسل
  عميلك إلى `form_post` الجديد.

اعرض لعميلك جملة واضحة، لا الرمز. وتجد الأرقام التي تُحدث كل حالة في وضع الاختبار
في [بطاقات الاختبار](https://pay-docs.adpromax.com/ar/sandbox/test-cards).

## الاسترداد

يمكن استرداد الدفعة الناجحة بالبطاقة، كليًا أو جزئيًا: انظر [المبالغ المستردة](https://pay-docs.adpromax.com/ar/payments/refunds).
