openapi: 3.1.0
# ═══ واجهةُ زاد جو البرمجيّة للتجّار — المواصفةُ ١.٠ (ه-٣٦٢ ⑧) ═══
#
# 🔑 هذه المواصفةُ هي العقدُ الذي تُبنى عليه الدوالُّ على ذراع زاد جو
# (server/supabase-arm/functions/merchant-api — أرضُ الماستر). الصفحةُ
# zadgo.com/developers تعرضها للمطوّرين. **الحالةُ: قيد البناء** — لا يُوعد
# بنقطةٍ لم تُنشر، والصفحةُ تقول ذلك.
#
# ## المبادئ (لا تُكسر عند البناء)
# • المفتاحُ لكلِّ مطعمٍ لا لكلِّ حساب: restaurants/{id}.apiKeyHash — والقاعدةُ
#   لا تكشفه، والذراعُ تقابله بالتجزئة.
# • الواجهةُ لا تمسّ مالاً: لا تعديلَ لعمولةٍ ولا رسم (ب١ · د١) — الطلبُ يُقبل
#   ويُرفض ويُعلَن جاهزاً، لا يُسعَّر.
# • كلُّ كتابةٍ بمفتاح تكرارٍ (Idempotency-Key) — الشبكةُ تعيد الإرسال.
# • الويب هوك موقَّعٌ بـHMAC-SHA256 على الجسم بسرِّ التاجر — والمستقبلُ يتحقّق.
# • الحدودُ من الإعدادات لا من الكود (ج١): rateLimit في delivery_settings/api.
info:
  title: ZadGo Merchant API
  version: "1.0.0-draft"
  description: |
    واجهةُ زاد جو للتجّار: حالةُ المتجر · القائمةُ · دورةُ الطلب · الويب هوك.
    مفتاحٌ لكلِّ مطعم. لا تعديلَ لأيِّ رقمٍ ماليّ عبر الواجهة.
  contact:
    name: ZadGo Developers
    url: https://zadgo.com/developers
    email: info@zadgo.com
servers:
  - url: https://api.zadgo.co/v1
    description: الإنتاج (قيد البناء)
  - url: https://sandbox.api.zadgo.co/v1
    description: بيئةُ التجربة — طلباتٌ مصنوعة، لا كباتنَ حقيقيّين
security:
  - ApiKey: []
tags:
  - name: store
    description: حالةُ المتجر وساعاتُه
  - name: menu
    description: القائمة — مزامنةٌ وتوفّرٌ وعروض
  - name: orders
    description: دورةُ الطلب من جهة المطعم
  - name: webhooks
    description: اشتراكاتُ الأحداث
paths:
  /store:
    get:
      tags: [store]
      summary: حالةُ المتجر الآن
      operationId: getStore
      responses:
        "200":
          description: المتجر
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Store" }
    patch:
      tags: [store]
      summary: فتحٌ/إغلاق · إيقافٌ مؤقّت · ساعاتُ العمل · سقفُ الطلبات الجارية
      operationId: patchStore
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StorePatch" }
      responses:
        "200": { description: الحالةُ بعد التعديل, content: { application/json: { schema: { $ref: "#/components/schemas/Store" } } } }
        "422": { $ref: "#/components/responses/Invalid" }
  /menu:
    get:
      tags: [menu]
      summary: القائمةُ كاملةً (أقسامٌ وأصناف)
      operationId: getMenu
      responses:
        "200": { description: القائمة, content: { application/json: { schema: { $ref: "#/components/schemas/Menu" } } } }
    put:
      tags: [menu]
      summary: مزامنةٌ كاملة — تحلّ محلَّ القائمة (الأصنافُ الغائبةُ تُطفأ لا تُحذف)
      operationId: syncMenu
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Menu" }
      responses:
        "200": { description: ملخّصُ المزامنة, content: { application/json: { schema: { $ref: "#/components/schemas/SyncResult" } } } }
        "422": { $ref: "#/components/responses/Invalid" }
  /menu/items/{itemId}:
    patch:
      tags: [menu]
      summary: توفّرُ صنفٍ أو سعرُه أو عرضُه (بحدود قاعدة العروض)
      operationId: patchItem
      parameters:
        - { name: itemId, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                isAvailable: { type: boolean }
                soldOutUntil: { type: string, format: date-time, nullable: true }
                price: { type: number, minimum: 0 }
                offerPrice: { type: number, minimum: 0, nullable: true, description: أقلُّ من السعر وإلّا رُفض }
                offerUntil: { type: string, format: date-time, nullable: true }
      responses:
        "200": { description: الصنفُ بعد التعديل, content: { application/json: { schema: { $ref: "#/components/schemas/Item" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
  /orders:
    get:
      tags: [orders]
      summary: طلباتُ المتجر
      operationId: listOrders
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [pending, active, delivered, failed] }, description: pending = ينتظر قبولك }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
      responses:
        "200": { description: قائمة, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Order" } } } } }
  /orders/{orderId}:
    get:
      tags: [orders]
      summary: طلبٌ واحد
      operationId: getOrder
      parameters: [{ name: orderId, in: path, required: true, schema: { type: string } }]
      responses:
        "200": { description: الطلب, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
  /orders/{orderId}/accept:
    post:
      tags: [orders]
      summary: قبولُ الطلب بزمن تحضير
      operationId: acceptOrder
      parameters:
        - { name: orderId, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        content:
          application/json:
            schema: { type: object, properties: { prepMinutes: { type: integer, minimum: 1, maximum: 180 } } }
      responses:
        "200": { description: قُبل, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        "409": { $ref: "#/components/responses/Conflict" }
  /orders/{orderId}/reject:
    post:
      tags: [orders]
      summary: رفضُ الطلب بسببٍ من القائمة المغلقة
      operationId: rejectOrder
      parameters:
        - { name: orderId, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: string, enum: [itemUnavailable, tooBusy, outsideHours, kitchenIssue] }
      responses:
        "200": { description: رُفض, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        "409": { $ref: "#/components/responses/Conflict" }
  /orders/{orderId}/ready:
    post:
      tags: [orders]
      summary: الطلبُ جاهزٌ للاستلام
      operationId: readyOrder
      parameters:
        - { name: orderId, in: path, required: true, schema: { type: string } }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      responses:
        "200": { description: جاهز, content: { application/json: { schema: { $ref: "#/components/schemas/Order" } } } }
        "409": { $ref: "#/components/responses/Conflict" }
  /webhooks:
    get:
      tags: [webhooks]
      summary: اشتراكاتُك
      operationId: listWebhooks
      responses:
        "200": { description: قائمة, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Webhook" } } } } }
    post:
      tags: [webhooks]
      summary: اشتراكٌ جديد — يُختبر بحدث ping قبل القبول
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, description: https فقط }
                events: { type: array, items: { $ref: "#/components/schemas/EventType" } }
      responses:
        "201": { description: أُنشئ ومعه السرّ (يُعرض مرّةً واحدة), content: { application/json: { schema: { $ref: "#/components/schemas/WebhookCreated" } } } }
  /webhooks/{id}:
    delete:
      tags: [webhooks]
      summary: إلغاءُ اشتراك
      operationId: deleteWebhook
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "204": { description: أُلغي }
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-ZadGo-Key
      description: مفتاحُ المطعم — يصدره المديرُ من لوحة زاد جو ويُعرض مرّةً واحدة
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, maxLength: 64 }
      description: مفتاحُ تكرارٍ يختاره العميل — الطلبُ نفسُه بالمفتاح نفسِه لا يُنفَّذ مرّتين
  responses:
    Invalid: { description: بياناتٌ مرفوضة — والسببُ في الجسم, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    NotFound: { description: غيرُ موجودٍ أو ليس لمتجرك, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Conflict: { description: الحالةُ لا تسمح (طلبٌ ليس بانتظارك), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  schemas:
    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: string, example: order_not_pending }
        message: { type: string, description: بالعربيّة }
    Store:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        isOpen: { type: boolean }
        pausedUntil: { type: string, format: date-time, nullable: true }
        openNow: { type: boolean, description: المحسومُ الذي يراه العميل (المفتاحُ والجدولُ والإيقافُ معاً) }
        openingHours: { type: object, additionalProperties: true }
        estimatedTimeMin: { type: integer }
        maxActiveOrders: { type: integer, nullable: true }
        minOrder: { type: number }
        rating: { type: number }
        ratingCount: { type: integer }
    StorePatch:
      type: object
      properties:
        isOpen: { type: boolean }
        pauseMinutes: { type: integer, minimum: 0, maximum: 240, description: صفر = استئناف }
        openingHours: { type: object, additionalProperties: true }
        maxActiveOrders: { type: integer, minimum: 0, nullable: true }
        minOrder: { type: number, minimum: 0 }
    Category:
      type: object
      required: [name]
      properties:
        id: { type: string }
        name: { type: string }
        sortOrder: { type: integer }
    Item:
      type: object
      required: [name, price, categoryId]
      properties:
        id: { type: string }
        externalId: { type: string, description: معرّفُك في نظامك — يُستعمل في المزامنة }
        categoryId: { type: string }
        name: { type: string }
        description: { type: string }
        price: { type: number, minimum: 0 }
        offerPrice: { type: number, nullable: true }
        offerUntil: { type: string, format: date-time, nullable: true }
        kcal: { type: integer, nullable: true }
        imageUrl: { type: string, nullable: true, description: للقراءة — الرفعُ من اللوحة أو التطبيق }
        isAvailable: { type: boolean }
        soldOutUntil: { type: string, format: date-time, nullable: true }
    Menu:
      type: object
      properties:
        categories: { type: array, items: { $ref: "#/components/schemas/Category" } }
        items: { type: array, items: { $ref: "#/components/schemas/Item" } }
    SyncResult:
      type: object
      properties:
        created: { type: integer }
        updated: { type: integer }
        disabled: { type: integer, description: أصنافٌ غابت عن المزامنة فأُطفئت }
        rejected: { type: array, items: { type: object, properties: { externalId: { type: string }, reason: { type: string } } } }
    OrderItem:
      type: object
      properties:
        name: { type: string }
        quantity: { type: integer }
        price: { type: number }
        notes: { type: string }
    Order:
      type: object
      properties:
        id: { type: string }
        orderNumber: { type: string }
        status: { type: string, enum: [restaurantPending, restaurantAccepted, preparing, readyForPickup, searchingDriver, driverAssigned, pickedUp, onTheWay, delivered, cancelled, restaurantRejected, noDriverFound, undelivered] }
        createdAt: { type: string, format: date-time }
        items: { type: array, items: { $ref: "#/components/schemas/OrderItem" } }
        itemsTotal: { type: number, description: قيمةُ الأصناف — بلا عمولةٍ ولا رسوم (لا تُعرض عبر الواجهة) }
        paymentMethod: { type: string, enum: [cash, card, wallet] }
        customerName: { type: string }
        deliveryDistrict: { type: string }
        prepMinutes: { type: integer, nullable: true }
        driver: { type: object, nullable: true, properties: { name: { type: string }, phone: { type: string } } }
        deliverySecret: { type: string, nullable: true, description: رمزُ التسليم — للمطعم عند التسليم للكابتن فقط }
    EventType:
      type: string
      enum: [order.created, order.status_changed, order.cancelled, menu.item_disabled, settlement.created, ping]
    Webhook:
      type: object
      properties:
        id: { type: string }
        url: { type: string }
        events: { type: array, items: { $ref: "#/components/schemas/EventType" } }
        createdAt: { type: string, format: date-time }
        failures24h: { type: integer, description: يُعطَّل بعد ٥٠ فشلاً متتالياً ويُبلَّغ صاحبُه }
    WebhookCreated:
      allOf:
        - { $ref: "#/components/schemas/Webhook" }
        - type: object
          properties:
            secret: { type: string, description: سرُّ التوقيع — يُعرض مرّةً واحدة }
    WebhookDelivery:
      description: |
        جسمُ كلِّ تسليم. الترويسة `X-ZadGo-Signature: sha256=<hex>` = HMAC-SHA256(الجسم الخام، السرّ).
        و`X-ZadGo-Delivery` معرّفٌ فريد — المستقبلُ يتجاهل المكرَّر. إعادةُ المحاولة بتراجعٍ أُسّيّ لساعة.
      type: object
      properties:
        id: { type: string }
        type: { $ref: "#/components/schemas/EventType" }
        at: { type: string, format: date-time }
        data: { type: object, additionalProperties: true }
