ف فواترا

أمثلة بلغات متعدّدة

لا نُلزمك بأي مكتبة — الواجهة REST + JSON عادية. هذه دوال جاهزة للنسخ، كلّها تُرسل Idempotency-Key وتتعامل مع status بشكل صحيح.

لا تُدرج المفتاح في الكود مباشرة. اقرأه من متغيّر بيئة، ونادِ واجهتنا من الخادم فقط.
<?php
declare(strict_types=1);

final class Fawtara
{
    private string $base = 'https://fawtara.codexal.co/api/v1';

    public function __construct(private string $apiKey) {}

    public function createInvoice(array $invoice, string $idempotencyKey): array
    {
        return $this->request('POST', '/invoices', $invoice, [
            'Idempotency-Key: ' . $idempotencyKey,
        ]);
    }

    public function getInvoice(string $id): array
    {
        return $this->request('GET', '/invoices/' . rawurlencode($id));
    }

    public function retry(string $id): array
    {
        return $this->request('POST', '/invoices/' . rawurlencode($id) . '/retry');
    }

    private function request(string $method, string $path, ?array $body = null, array $headers = []): array
    {
        $ch = curl_init($this->base . $path);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 60,
            CURLOPT_HTTPHEADER     => array_merge([
                'Authorization: Bearer ' . $this->apiKey,
                'Content-Type: application/json',
                'Accept: application/json',
            ], $headers),
            CURLOPT_POSTFIELDS => $body === null
                ? null
                : json_encode($body, JSON_UNESCAPED_UNICODE),
        ]);

        $raw  = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($raw === false) {
            throw new RuntimeException('Network error: ' . $err);
        }

        $data = json_decode((string) $raw, true) ?? [];

        if ($code >= 400) {
            $e = $data['error'] ?? [];
            throw new RuntimeException(
                sprintf('[%d %s] %s', $code, $e['type'] ?? 'unknown', $e['message'] ?? $raw)
            );
        }

        return $data;
    }
}

// ----- الاستخدام -----
$fawtara = new Fawtara(getenv('FAWTARA_API_KEY'));

$res = $fawtara->createInvoice([
    'invoice_number' => 'INV-1001',
    'payment_method' => 'cash',
    'customer'       => ['name' => 'شركة المثال', 'identity_type' => 'tax', 'identity_number' => '12345678'],
    'lines'          => [
        ['name' => 'خدمة استشارية', 'quantity' => 1, 'unit_price' => 150.000],
    ],
], 'order-99813');

$inv = $res['data'];

if ($inv['status'] !== 'accepted') {
    // لا تُعلن النجاح للمستخدم — سجّل الخطأ وأبلغ المسؤول
    error_log('Invoice not accepted: ' . json_encode($inv['error'], JSON_UNESCAPED_UNICODE));
} else {
    echo "QR: {$inv['qr_content']}\n";
}
<?php
// config/services.php
'fawtara' => [
    'key'  => env('FAWTARA_API_KEY'),
    'base' => env('FAWTARA_BASE', 'https://fawtara.codexal.co/api/v1'),
],

// app/Services/FawtaraClient.php
namespace App\Services;

use Illuminate\Support\Facades\Http;
use RuntimeException;

class FawtaraClient
{
    protected function http()
    {
        return Http::withToken(config('services.fawtara.key'))
            ->baseUrl(config('services.fawtara.base'))
            ->acceptJson()
            ->timeout(60)
            ->retry(3, 1000, throw: false);
    }

    public function createInvoice(array $payload, string $idempotencyKey): array
    {
        $res = $this->http()
            ->withHeaders(['Idempotency-Key' => $idempotencyKey])
            ->post('/invoices', $payload);

        if ($res->failed()) {
            throw new RuntimeException(
                $res->json('error.type') . ': ' . $res->json('error.message')
            );
        }

        return $res->json('data');
    }
}

// app/Jobs/SubmitInvoice.php — الإرسال داخل Job أفضل من داخل الطلب
public function handle(FawtaraClient $fawtara): void
{
    $order = $this->order;

    $invoice = $fawtara->createInvoice([
        'invoice_number' => $order->number,
        'issue_date'     => $order->created_at->toDateString(),
        'payment_method' => $order->is_credit ? 'receivable' : 'cash',
        'customer'       => [
            'name'            => $order->customer_name,
            'identity_type'   => 'tax',
            'identity_number' => $order->customer_tin,
            'phone'           => $order->customer_phone,
        ],
        'lines' => $order->items->map(fn ($i) => [
            'name'        => $i->name,
            'quantity'    => $i->qty,
            'unit_price'  => $i->price,
            'discount'    => $i->discount,
            'tax_percent' => $i->tax_rate,
        ])->all(),
        'metadata' => ['order_id' => $order->id],
    ], "order-{$order->id}");

    $order->update([
        'fawtara_id'  => $invoice['id'],
        'tax_uuid'    => $invoice['uuid'],
        'tax_status'  => $invoice['status'],
        'qr_content'  => $invoice['qr_content'],
    ]);
}
// fawtara.js
const BASE = process.env.FAWTARA_BASE || 'https://fawtara.codexal.co/api/v1';
const KEY  = process.env.FAWTARA_API_KEY;

class FawtaraError extends Error {
  constructor(status, type, message, body) {
    super(`[${status} ${type}] ${message}`);
    this.status = status;
    this.type   = type;
    this.body   = body;
  }
}

async function call(method, path, body, extraHeaders = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      Accept: 'application/json',
      ...extraHeaders,
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const data = await res.json().catch(() => ({}));

  if (!res.ok) {
    const e = data.error || {};
    throw new FawtaraError(res.status, e.type || 'unknown', e.message || '', data);
  }
  return data;
}

export const fawtara = {
  createInvoice: (invoice, idempotencyKey) =>
    call('POST', '/invoices', invoice, { 'Idempotency-Key': idempotencyKey }),

  getInvoice: (id) => call('GET', `/invoices/${encodeURIComponent(id)}`),

  retry: (id) => call('POST', `/invoices/${encodeURIComponent(id)}/retry`),

  usage: () => call('GET', '/usage'),
};

// ----- الاستخدام -----
try {
  const { data: inv } = await fawtara.createInvoice({
    invoice_number: 'INV-1001',
    payment_method: 'cash',
    customer: { name: 'شركة المثال', identity_type: 'tax', identity_number: '12345678' },
    lines: [{ name: 'خدمة استشارية', quantity: 1, unit_price: 150.0 }],
  }, 'order-99813');

  if (inv.status !== 'accepted') {
    console.error('not accepted:', inv.error);
  } else {
    console.log('QR:', inv.qr_content);
  }
} catch (err) {
  if (err.type === 'rate_limited') {
    await new Promise(r => setTimeout(r, (err.body.error.retry_after ?? 5) * 1000));
    // أعد المحاولة بنفس مفتاح منع التكرار
  }
  throw err;
}
# fawtara.py
import os, json, time, uuid
import requests

BASE = os.getenv("FAWTARA_BASE", "https://fawtara.codexal.co/api/v1")
KEY  = os.environ["FAWTARA_API_KEY"]


class FawtaraError(Exception):
    def __init__(self, status, type_, message, body=None):
        super().__init__(f"[{status} {type_}] {message}")
        self.status, self.type, self.body = status, type_, body


class Fawtara:
    def __init__(self, api_key: str = KEY, base: str = BASE):
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        })
        self.base = base

    def _call(self, method, path, body=None, headers=None, retries=3):
        for attempt in range(retries):
            res = self.session.request(
                method, self.base + path,
                data=json.dumps(body, ensure_ascii=False).encode() if body else None,
                headers=headers or {}, timeout=60,
            )
            if res.status_code == 429 and attempt < retries - 1:
                time.sleep(int(res.headers.get("Retry-After", 5)))
                continue

            data = res.json() if res.content else {}
            if res.status_code >= 400:
                err = data.get("error", {})
                raise FawtaraError(res.status_code, err.get("type", "unknown"),
                                   err.get("message", ""), data)
            return data
        raise FawtaraError(429, "rate_limited", "exhausted retries")

    def create_invoice(self, invoice: dict, idempotency_key: str) -> dict:
        return self._call("POST", "/invoices", invoice,
                          {"Idempotency-Key": idempotency_key})["data"]

    def get_invoice(self, invoice_id: str) -> dict:
        return self._call("GET", f"/invoices/{invoice_id}")["data"]

    def retry(self, invoice_id: str) -> dict:
        return self._call("POST", f"/invoices/{invoice_id}/retry")["data"]


# ----- الاستخدام -----
if __name__ == "__main__":
    api = Fawtara()
    inv = api.create_invoice({
        "invoice_number": "INV-1001",
        "payment_method": "cash",
        "customer": {"name": "شركة المثال", "identity_type": "tax",
                     "identity_number": "12345678"},
        "lines": [{"name": "خدمة استشارية", "quantity": 1, "unit_price": 150.0}],
    }, idempotency_key=str(uuid.uuid4()))

    if inv["status"] != "accepted":
        print("رفض:", inv["error"])
    else:
        print("QR:", inv["qr_content"])
// FawtaraClient.cs — .NET 8
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

public sealed class FawtaraException(int status, string type, string message)
    : Exception($"[{status} {type}] {message}")
{
    public int Status { get; } = status;
    public string Type { get; } = type;
}

public sealed class FawtaraClient
{
    private readonly HttpClient _http;

    public FawtaraClient(string apiKey, string baseUrl = "https://fawtara.codexal.co/api/v1/")
    {
        _http = new HttpClient { BaseAddress = new Uri(baseUrl), Timeout = TimeSpan.FromSeconds(60) };
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
        _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    public async Task<JsonElement> CreateInvoiceAsync(object invoice, string idempotencyKey)
    {
        using var req = new HttpRequestMessage(HttpMethod.Post, "invoices")
        {
            Content = JsonContent.Create(invoice)
        };
        req.Headers.Add("Idempotency-Key", idempotencyKey);

        using var res  = await _http.SendAsync(req);
        var       json = await res.Content.ReadFromJsonAsync<JsonElement>();

        if (!res.IsSuccessStatusCode)
        {
            var err = json.GetProperty("error");
            throw new FawtaraException(
                (int)res.StatusCode,
                err.GetProperty("type").GetString() ?? "unknown",
                err.GetProperty("message").GetString() ?? "");
        }

        return json.GetProperty("data");
    }
}

// ----- الاستخدام -----
var client = new FawtaraClient(Environment.GetEnvironmentVariable("FAWTARA_API_KEY")!);

var data = await client.CreateInvoiceAsync(new
{
    invoice_number  = "INV-1001",
    payment_method  = "cash",
    customer = new { name = "شركة المثال", identity_type = "tax", identity_number = "12345678" },
    lines = new[] { new { name = "خدمة استشارية", quantity = 1, unit_price = 150.000 } }
}, idempotencyKey: $"order-99813");

Console.WriteLine(data.GetProperty("status").GetString());
// FawtaraClient.java — Java 17+
import java.net.URI;
import java.net.http.*;
import java.time.Duration;

public class FawtaraClient {

    private static final String BASE = "https://fawtara.codexal.co/api/v1";
    private final HttpClient http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(15)).build();
    private final String apiKey;

    public FawtaraClient(String apiKey) { this.apiKey = apiKey; }

    public String createInvoice(String jsonBody, String idempotencyKey) throws Exception {
        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(BASE + "/invoices"))
                .timeout(Duration.ofSeconds(60))
                .header("Authorization", "Bearer " + apiKey)
                .header("Content-Type", "application/json")
                .header("Idempotency-Key", idempotencyKey)
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody, java.nio.charset.StandardCharsets.UTF_8))
                .build();

        HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());

        if (res.statusCode() >= 400) {
            throw new RuntimeException("Fawtara error " + res.statusCode() + ": " + res.body());
        }
        return res.body();
    }

    public static void main(String[] args) throws Exception {
        String body = """
            {
              "invoice_number": "INV-1001",
              "payment_method": "cash",
              "customer": { "name": "شركة المثال" },
              "lines": [ { "name": "خدمة استشارية", "quantity": 1, "unit_price": 150.000 } ]
            }
            """;

        var client = new FawtaraClient(System.getenv("FAWTARA_API_KEY"));
        System.out.println(client.createInvoice(body, "order-99813"));
    }
}

مواصفة OpenAPI

لتوليد عميل جاهز بأي لغة، استخدم المواصفة: https://fawtara.codexal.co/app/docs/openapi.json

# مثال بـ openapi-generator
openapi-generator-cli generate \
  -i https://fawtara.codexal.co/app/docs/openapi.json \
  -g typescript-fetch \
  -o ./fawtara-client