> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oneclickdz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# استطلاع الحالة

> تابع المعاملة حتى تصل إلى حالة نهائية دون أن تدفع مرتين

<div dir="ltr">
  ## نظرة عامة

  الاستكشاف والدفع عمليتان غير متزامنتين، لذا فإن الاستطلاع ليس تحسيناً — بل هو الآلية نفسها. كل ما تحتاج معرفته عن معاملة موجود في `status` الخاص بها، ويُقرأ من [الحصول على معاملة بالمعرّف](/ar/api-reference/bill-payment/check-by-id).

  <Note>
    لا يرسل دفع الفواتير إشعارات إليك. الاستطلاع هو الطريقة التي تصلك بها النتائج، للاستكشاف والدفع معاً.
  </Note>

  ## آلة الحالات

  كل انتقال يمكن أن تقوم به المعاملة:

  | من           | إلى          | المُشغِّل                                          |
  | ------------ | ------------ | -------------------------------------------------- |
  | —            | `PENDING`    | `POST /v3/bills/discover`                          |
  | `PENDING`    | `READY`      | انتهى الاستكشاف — `bills[]` موجودة، وقد تكون فارغة |
  | `PENDING`    | `FAILED`     | تعذّر إكمال الاستكشاف                              |
  | `READY`      | `PROCESSING` | `POST /v3/bills/pay`                               |
  | `PROCESSING` | `SUCCESS`    | تم الدفع                                           |
  | `PROCESSING` | `FAILED`     | مرفوض؛ لم يُخصم أي مبلغ                            |
  | `PROCESSING` | `REFUNDED`   | خُصم المبلغ ثم أُعيد بالكامل                       |
  | `PROCESSING` | `UNKNOWN`    | النتيجة غير مؤكدة                                  |
  | `UNKNOWN`    | `SUCCESS`    | تأكّد الدفع                                        |
  | `UNKNOWN`    | `REFUNDED`   | تأكّد عدم الدفع؛ أُعيد المال                       |

  `SUCCESS` و`FAILED` و`REFUNDED` حالات نهائية — لا تغادرها المعاملة أبداً.

  | الحالة       | المعنى                            | هل تستمر في الاستطلاع؟ | حركة المال        |
  | ------------ | --------------------------------- | ---------------------- | ----------------- |
  | `PENDING`    | قُبل الاستكشاف ولم ينتهِ بعد      | نعم                    | لا                |
  | `READY`      | انتهى الاستكشاف؛ `bills[]` موجودة | لا — حان دورك للتصرّف  | لا                |
  | `PROCESSING` | الدفع جارٍ                        | نعم                    | لم تتأكد بعد      |
  | `SUCCESS`    | تم الدفع                          | لا                     | نعم — خُصم المبلغ |
  | `FAILED`     | لم يتم                            | لا                     | لا                |
  | `REFUNDED`   | خُصم المبلغ ثم أُعيد بالكامل      | لا                     | صافي صفر          |
  | `UNKNOWN`    | النتيجة غير مؤكدة؛ قيد المراجعة   | **نعم**                | غير معروف بعد     |

  ## الإعدادات الموصى بها

  هذه نقاط انطلاق وليست ضمانات. قِس حركة مرورك الخاصة وعدّل بناءً عليها.

  | المرحلة               | الفاصل الزمني                | التوقف بعد                            |
  | --------------------- | ---------------------------- | ------------------------------------- |
  | الاستكشاف (`PENDING`) | 2 ثانية، تتزايد إلى 10 ثوانٍ | دقيقتان                               |
  | الدفع (`PROCESSING`)  | 3 ثوانٍ، تتزايد إلى 10 ثوانٍ | 5 دقائق                               |
  | المراجعة (`UNKNOWN`)  | 30 ثانية                     | لا تتوقف — سلّمها إلى مهمة خلفية أبطأ |

  <Warning>
    "التوقف" يعني **إيقاف الاستطلاع في المقدّمة**، لا "اعتبار أن الدفع فشل". المعاملة التي توقفت عن مراقبتها لا تزال لها نتيجة حقيقية؛ انقلها إلى مهمة مطابقة خلفية تواصل التحقق.
  </Warning>

  زِد الفاصل الزمني تدريجياً بدلاً من إرهاق الخدمة بفاصل ثابت. الدفعة التي لم تنتهِ خلال ثلاث ثوانٍ لن تنتهي أسرع لأنك سألت مجدداً.

  ## مستطلِع جاهز للإنتاج

  الشكل نفسه بأربع لغات: فاصل زمني أولي، وتزايد حتى سقف معيّن، ومهلة إجمالية، وفرع واحد لكل حالة.

  <CodeGroup>
    ```bash cURL theme={null}
    #!/usr/bin/env bash
    # Poll one transaction until it reaches a final state.
    BASE="https://billapi.oneclickdz.com"
    TXN="68b2f4c1a7d3e9f204c81a55"
    INTERVAL=3
    DEADLINE=$(( $(date +%s) + 300 ))

    while [ "$(date +%s)" -lt "$DEADLINE" ]; do
      STATUS=$(curl -s "$BASE/v3/bills/transactions/$TXN" \
        -H "X-Access-Token: YOUR_API_KEY" \
        | grep -o '"status":"[A-Z_]*"' | head -1 | cut -d'"' -f4)

      echo "status=$STATUS"

      case "$STATUS" in
        SUCCESS|FAILED|REFUNDED) exit 0 ;;
        UNKNOWN)                 INTERVAL=30 ;;
        *)                       [ "$INTERVAL" -lt 10 ] && INTERVAL=$((INTERVAL + 2)) ;;
      esac

      sleep "$INTERVAL"
    done

    echo "still not final — hand over to background reconciliation"
    exit 1
    ```

    ```javascript Node.js theme={null}
    const BASE = "https://billapi.oneclickdz.com";
    const KEY = process.env.ONECLICKDZ_API_KEY;

    const FINAL = new Set(["SUCCESS", "FAILED", "REFUNDED"]);
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

    async function getTransaction(transactionId) {
      const response = await fetch(`${BASE}/v3/bills/transactions/${transactionId}`, {
        headers: { "X-Access-Token": KEY },
      });

      const body = await response.json();

      // Transient — worth another attempt.
      if (["AUTH_UNAVAILABLE", "SERVICE_UNAVAILABLE"].includes(body?.error?.code)) {
        return null;
      }

      if (!body.success) {
        throw new Error(`${body.error.code}: ${body.error.message}`);
      }

      return body.data;
    }

    async function pollUntilFinal(transactionId, { timeoutMs = 300_000 } = {}) {
      const deadline = Date.now() + timeoutMs;
      let interval = 3000;

      while (Date.now() < deadline) {
        const transaction = await getTransaction(transactionId);

        if (transaction) {
          if (FINAL.has(transaction.status)) return transaction;

          // Under review — slow right down, but never stop.
          interval = transaction.status === "UNKNOWN" ? 30_000 : Math.min(interval + 2000, 10_000);
        }

        await sleep(interval);
      }

      // Not final yet. Hand over — never assume a failure.
      await queueForReconciliation(transactionId);
      return null;
    }
    ```

    ```python Python theme={null}
    import os
    import time
    import requests

    BASE = 'https://billapi.oneclickdz.com'
    KEY = os.getenv('ONECLICKDZ_API_KEY')

    FINAL = {'SUCCESS', 'FAILED', 'REFUNDED'}
    TRANSIENT = {'AUTH_UNAVAILABLE', 'SERVICE_UNAVAILABLE'}


    def get_transaction(transaction_id):
        response = requests.get(
            f'{BASE}/v3/bills/transactions/{transaction_id}',
            headers={'X-Access-Token': KEY}
        )

        body = response.json()

        # Transient — worth another attempt.
        if body.get('error', {}).get('code') in TRANSIENT:
            return None

        if not body['success']:
            raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")

        return body['data']


    def poll_until_final(transaction_id, timeout_s=300):
        deadline = time.monotonic() + timeout_s
        interval = 3.0

        while time.monotonic() < deadline:
            transaction = get_transaction(transaction_id)

            if transaction:
                if transaction['status'] in FINAL:
                    return transaction

                # Under review — slow right down, but never stop.
                interval = 30.0 if transaction['status'] == 'UNKNOWN' else min(interval + 2, 10)

            time.sleep(interval)

        # Not final yet. Hand over — never assume a failure.
        queue_for_reconciliation(transaction_id)
        return None
    ```

    ```php PHP theme={null}
    <?php
    const BASE = 'https://billapi.oneclickdz.com';
    const FINAL_STATUSES = ['SUCCESS', 'FAILED', 'REFUNDED'];
    const TRANSIENT = ['AUTH_UNAVAILABLE', 'SERVICE_UNAVAILABLE'];

    function getTransaction(string $transactionId): ?array
    {
        $ch = curl_init(BASE . "/v3/bills/transactions/$transactionId");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-Access-Token: ' . getenv('ONECLICKDZ_API_KEY')
        ]);

        $body = json_decode(curl_exec($ch), true);
        curl_close($ch);

        // Transient — worth another attempt.
        if (in_array($body['error']['code'] ?? '', TRANSIENT, true)) {
            return null;
        }

        if (!$body['success']) {
            throw new Exception($body['error']['code'] . ': ' . $body['error']['message']);
        }

        return $body['data'];
    }

    function pollUntilFinal(string $transactionId, int $timeoutSeconds = 300): ?array
    {
        $deadline = time() + $timeoutSeconds;
        $interval = 3;

        while (time() < $deadline) {
            $transaction = getTransaction($transactionId);

            if ($transaction !== null) {
                if (in_array($transaction['status'], FINAL_STATUSES, true)) {
                    return $transaction;
                }

                // Under review — slow right down, but never stop.
                $interval = $transaction['status'] === 'UNKNOWN'
                    ? 30
                    : min($interval + 2, 10);
            }

            sleep($interval);
        }

        // Not final yet. Hand over — never assume a failure.
        queueForReconciliation($transactionId);
        return null;
    }
    ?>
    ```
  </CodeGroup>

  ## التصرّف عند كل حالة

  فرع واحد لكل حالة، ودون فرع افتراضي يفترض الفشل.

  ```javascript theme={null}
  async function applyOutcome(order, transaction) {
    switch (transaction.status) {
      case "SUCCESS":
        // Money moved. Keep the proof.
        await db.orders.update(order.id, {
          state: "PAID",
          operationId: transaction.operationId,
          total: transaction.total,
        });
        await downloadReceipt(transaction.transactionId);
        await notifyCustomerPaid(order);
        return;

      case "FAILED":
        // Nothing was charged. Safe to release the customer's funds.
        await db.orders.update(order.id, {
          state: "FAILED",
          reason: transaction.error?.code,
        });
        await releaseCustomerFunds(order);
        return;

      case "REFUNDED":
        // Money moved and came back in full. Same customer outcome as FAILED.
        await db.orders.update(order.id, {
          state: "REFUNDED",
          reason: transaction.error?.code,
        });
        await releaseCustomerFunds(order);
        return;

      case "UNKNOWN":
        // Not an outcome. Keep watching, and keep the customer's money held.
        await db.orders.update(order.id, { state: "UNDER_REVIEW" });
        await queueForReconciliation(transaction.transactionId);
        return;

      case "PENDING":
      case "PROCESSING":
        // Still working. Nothing to do but poll.
        return;
    }
  }
  ```

  ## التعامل مع `UNKNOWN`

  `UNKNOWN` تعني أن النتيجة لم تتأكد بعد. ليست فشلاً وليست نجاحاً. وتُحسم من تلقاء نفسها إلى `SUCCESS` أو `REFUNDED`.

  <Warning>
    طالما أن المعاملة في حالة `UNKNOWN`:

    * **لا** تسترد المبلغ لعميلك أبداً.
    * **لا** تُعِد إرسال الدفعة أبداً.
    * **لا** تعرض "فشل الدفع" في واجهتك أبداً.

    أي فعل من هذه يحوّل دفعة غير مؤكدة إلى خسارة مؤكدة — فإما أن تسترد قيمة فاتورة دُفعت فعلاً، وإما أن تدفعها مرتين.
  </Warning>

  ما ينبغي فعله بدلاً من ذلك:

  <Steps>
    <Step title="احجز أموال العميل">
      أبقِ المبلغ محجوزاً لديك واعرض حالة محايدة — "جارٍ تأكيد الدفع"، لا "فشل".
    </Step>

    <Step title="أبطئ الاستطلاع كثيراً">
      انتقل إلى وتيرة كل 30 ثانية، أو إلى مهمة خلفية تتحقق دورياً. الاستطلاع المتكرر لا يُسرّع المراجعة.
    </Step>

    <Step title="تصرّف عند الحسم فقط">
      `SUCCESS` — سوِّ العملية واحتفظ بالإيصال. `REFUNDED` — حرّر الأموال. عندها فقط أخبر العميل.
    </Step>
  </Steps>

  ## التعامل مع `REFUNDED`

  `REFUNDED` تعني أن الدفعة خُصمت ثم أُعيدت بالكامل. النتيجة بالنسبة للعميل هي نفسها كما في `FAILED` — الفاتورة غير مدفوعة — لكن محاسبتك تختلف: المال خرج ثم عاد، لذا تنتمي الحركتان كلتاهما إلى دفتر حساباتك.

  يوضّح `error.code` سبب عدم ثبات الدفع:

  | `error.code`          | `error.message`                                         |
  | --------------------- | ------------------------------------------------------- |
  | `PAYMENT_DECLINED`    | The payment was declined by the bank or partner portal. |
  | `PARTNER_UNAVAILABLE` | The partner service is temporarily unavailable.         |
  | `INVALID_ACCOUNT`     | The provided account identifier is invalid.             |
  | `BILL_ALREADY_PAID`   | This bill has already been paid.                        |

  تظهر الأكواد الأربعة نفسها مع `FAILED`. تفرّع بناءً على `code`، ولا تتفرّع بناءً على `message` أبداً.

  ## التعامل مع الأخطاء العابرة أثناء الاستطلاع

  فشل استطلاع لا يعني معاملة فاشلة.

  | الاستجابة                 | المعنى                                        | ما العمل                                            |
  | ------------------------- | --------------------------------------------- | --------------------------------------------------- |
  | `503 AUTH_UNAVAILABLE`    | لم نتمكن من التحقق من مفتاحك في الوقت المناسب | انتظر مدة `Retry-After` (5 ثوانٍ) ثم استطلع مجدداً  |
  | `503 SERVICE_UNAVAILABLE` | صيانة مُخطط لها                               | انتظر مدة `Retry-After` ثم استطلع مجدداً            |
  | `500 INTERNAL_ERROR`      | حدث خطأ لدينا                                 | استطلع مجدداً؛ وصعّد الأمر مع `requestId` إذا استمر |
  | `404 NOT_FOUND`           | ليست معاملتك، أو البيئة خاطئة                 | تحقق من المعرّف ومن المفتاح — لا تعتبرها دفعة فاشلة |

  <Note>
    لا تدع استطلاعاً فاشلاً يغيّر حالة طلبك أبداً. لا يجوز ذلك إلا لقيمة `status` حقيقية آتية من قراءة ناجحة.
  </Note>

  ## ما لا يجب فعله أبداً

  | لا تفعل أبداً                                   | لماذا                        | البديل                                         |
  | ----------------------------------------------- | ---------------------------- | ---------------------------------------------- |
  | إعادة إرسال `pay` بعد انتهاء المهلة             | ربما نجحت المحاولة الأولى    | اقرأ المعاملة؛ `READY` وحدها تثبت أنها لم تبدأ |
  | استرداد المبلغ لعميلك عند `UNKNOWN`             | قد تُحسم إلى `SUCCESS`       | احجز الأموال واستمر في الاستطلاع               |
  | اعتبار خطأ الاستطلاع دفعة فاشلة                 | المعاملة لا تتأثر            | أعد محاولة الاستطلاع                           |
  | الاستطلاع بفاصل ثابت قدره ثانية واحدة إلى الأبد | يكلّف الطرفين ولا يفيد أحداً | زِد الفاصل الزمني حتى سقف معيّن                |
  | التفرّع بناءً على `completedAt`                 | يُضبط في حالات ليست نهائية   | تفرّع بناءً على `status`                       |

  ## أفضل الممارسات

  <CardGroup cols={2}>
    <Card title="مستطلِع واحد لمعاملة واحدة" icon="route">
      تابع المعاملة بمعرّفها. أما سرد المعاملات مراراً للعثور عليها فأبطأ وأثقل.
    </Card>

    <Card title="زِد الفاصل الزمني تدريجياً" icon="stopwatch">
      ابدأ ببضع ثوانٍ ثم تدرّج إلى نحو عشر. أبطئ إلى 30 ثانية بمجرد أن تصبح المعاملة `UNKNOWN`.
    </Card>

    <Card title="سلّم المهمة ولا تستسلم" icon="clock-rotate-left">
      عند انتهاء مهلة الاستطلاع في المقدّمة، أضف المعاملة إلى طابور المطابقة الخلفية.
    </Card>

    <Card title="سجّل requestId" icon="fingerprint">
      كل استطلاع يُعيد واحداً. احتفظ بآخر واحد مع طلبك — فهو ما يحتاجه الدعم.
    </Card>
  </CardGroup>

  ## الخطوة التالية

  <Card title="الخطوة 5: الإيصالات والمطابقة" icon="scale-balanced" href="/ar/bill-payment-guides/5-receipts-and-reconciliation">
    خزّن إثبات الدفع وطابق دفتر حساباتك يومياً
  </Card>

  ## صفحات ذات الصلة

  <CardGroup cols={2}>
    <Card title="الحصول على معاملة بالمعرّف" icon="id-card" href="/ar/api-reference/bill-payment/check-by-id">
      كائن المعاملة ومصفوفة الحقول
    </Card>

    <Card title="استراتيجيات الاستطلاع" icon="chart-line" href="/ar/polling-strategies">
      إرشادات الاستطلاع لجميع المنتجات
    </Card>

    <Card title="دفع الفواتير" icon="money-bill-transfer" href="/ar/bill-payment-guides/3-paying-bills">
      ما ينبغي فعله قبل الدفع
    </Card>

    <Card title="اختبار Sandbox" icon="flask" href="/ar/bill-payment-guides/6-sandbox-testing">
      أعد إنتاج `UNKNOWN` و`REFUNDED` عند الطلب
    </Card>
  </CardGroup>
</div>
