openapi: 3.0.3
info:
  title: Bosta ↔ Shopify Bridge API
  version: 1.0.0
  description: |
    Links Shopify orders to Bosta deliveries, mirrors Bosta shipment states into Shopify fulfillment events,
    and tracks COD collection / payout. All `/api/*` routes require header `X-Api-Key`.
servers:
  - url: http://localhost:3900
components:
  securitySchemes:
    ApiKey: { type: apiKey, in: header, name: X-Api-Key }
  schemas:
    Shipment:
      type: object
      properties:
        tracking_number: { type: string, example: "48089608" }
        bosta_id: { type: string }
        shopify_order_id: { type: string, example: "gid://shopify/Order/6123456789" }
        shopify_order_name: { type: string, example: "#1042" }
        shopify_fulfillment_id: { type: string }
        delivery_type: { type: string, enum: [SEND, EXCHANGE, CRP, RTO] }
        bosta_state: { type: integer, example: 41 }
        bosta_state_name: { type: string, example: Picked up }
        bucket: { type: string, enum: [new, in_transit, out_for_delivery, delivered, exception, returned, closed, unknown] }
        shopify_status: { type: string, example: OUT_FOR_DELIVERY }
        customer_name: { type: string }
        customer_phone: { type: string }
        cod: { type: number }
        shipment_fees: { type: number }
        attempts: { type: integer }
        exception_reason: { type: string }
        delivered_at: { type: string, format: date-time }
        cod_collected: { type: boolean }
        settlement_status: { type: string, enum: [none, pending, paid] }
        settled_at: { type: string }
        payout_ref: { type: string }
        last_error: { type: string }
        tracking_url: { type: string }
        updated_at: { type: string }
    Event:
      type: object
      properties:
        id: { type: integer }
        tracking_number: { type: string }
        source: { type: string, enum: [webhook, poll, manual] }
        bosta_state: { type: integer }
        bosta_state_name: { type: string }
        shopify_status: { type: string }
        event_ts: { type: integer, description: Bosta timeStamp, epoch ms }
        payload: { type: object }
        processed: { type: integer }
        error: { type: string }
        received_at: { type: string }
    BostaWebhook:
      type: object
      required: [trackingNumber, state]
      properties:
        _id: { type: string }
        trackingNumber: { type: string }
        state: { type: integer, description: Bosta state code (10,20,21,24,30,41,45,46,47,48,49,...) }
        type: { type: string, example: SEND }
        timeStamp: { type: integer, example: 1689252908261 }
        cod: { type: number, description: only on Delivered }
        isConfirmedDelivery: { type: boolean }
        deliveryPromiseDate: { type: string, example: "13-07-2023" }
        exceptionReason: { type: string }
        exceptionCode: { type: integer }
        businessReference: { type: string, description: value you sent at creation (use the Shopify order name) }
        numberOfAttempts: { type: integer }
    LinkRequest:
      type: object
      properties:
        trackingNumber: { type: string, description: existing Bosta tracking number }
        orderId: { type: string, description: Shopify order numeric id or gid }
        orderName: { type: string, example: "#1042" }
        cod: { type: number }
        customer: { type: object, properties: { name: { type: string }, phone: { type: string } } }
        createDelivery:
          type: object
          description: If present, the bridge creates the Bosta delivery (POST /deliveries?apiVersion=1) with this body and uses the returned tracking number. businessReference is filled from the order.
          example:
            type: 10
            cod: 350
            specs: { packageType: Parcel, packageDetails: { itemsCount: 2, description: "2x T-shirt" } }
            dropOffAddress: { city: Cairo, firstLine: "12 Street 9, Maadi", secondLine: "near metro" }
            receiver: { firstName: Ahmed, lastName: Ali, phone: "01000000000" }
    SettleRequest:
      type: object
      properties:
        trackingNumbers: { type: array, items: { type: string } }
        deliveredUpTo: { type: string, format: date, description: alternative to trackingNumbers; settles every pending COD delivered on/before this date }
        payoutRef: { type: string }
        note: { type: string }
security: [{ ApiKey: [] }]
paths:
  /health:
    get: { security: [], summary: Liveness + config warnings, responses: { "200": { description: ok } } }
  /webhooks/bosta:
    post:
      security: []
      summary: Bosta state-change webhook target
      description: Register this URL in Bosta Dashboard → Settings → API Integration → Set Up Your Webhook, with Authorization Key name = BOSTA_WEBHOOK_HEADER and value = BOSTA_WEBHOOK_SECRET.
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/BostaWebhook" } } } }
      responses: { "200": { description: accepted (processed asynchronously) }, "401": { description: bad secret } }
  /webhooks/shopify/orders:
    post:
      security: []
      summary: Optional Shopify orders/create webhook → auto-create Bosta delivery
      responses: { "200": { description: accepted } }
  /api/stats:
    get: { summary: Counts per bucket, responses: { "200": { description: ok } } }
  /api/states:
    get: { summary: Bosta state codes and the Shopify FulfillmentEventStatus each maps to, responses: { "200": { description: ok } } }
  /api/shipments:
    get:
      summary: List shipments
      parameters:
        - { name: q, in: query, schema: { type: string }, description: search tracking/order/phone/name }
        - { name: status, in: query, schema: { type: string, enum: [new, in_transit, out_for_delivery, delivered, exception, returned, closed] } }
        - { name: settlement, in: query, schema: { type: string, enum: [none, pending, paid] } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 500 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses: { "200": { description: "{ ok, total, items: Shipment[] }" } }
    post:
      summary: Link a Shopify order to a Bosta delivery (creates the Shopify fulfillment with tracking)
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/LinkRequest" } } } }
      responses: { "201": { description: "{ ok, shipment }" }, "404": { description: Shopify order not found } }
  /api/shipments/by-order/{order}:
    get:
      summary: Find the shipment of a Shopify order
      parameters: [{ name: order, in: path, required: true, schema: { type: string }, description: "#1042, 1042, or numeric order id" }]
      responses: { "200": { description: "{ ok, shipment (+events) }" }, "404": { description: not found } }
  /api/shipments/{tn}:
    get:
      summary: Shipment detail with event timeline
      parameters: [{ name: tn, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: "{ ok, shipment: Shipment & { events: Event[] } }" }, "404": { description: not found } }
  /api/shipments/{tn}/events:
    get:
      summary: Event timeline only
      parameters: [{ name: tn, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: "{ ok, events }" } }
    post:
      summary: Manually push a Bosta state (replay a missed webhook)
      parameters: [{ name: tn, in: path, required: true, schema: { type: string } }]
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/BostaWebhook" } } } }
      responses: { "200": { description: "{ ok, result }" } }
  /api/shipments/{tn}/sync:
    post:
      summary: Poll Bosta now (GET /deliveries/business/{tn}) and apply the current state
      parameters: [{ name: tn, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: "{ ok, result }" } }
  /api/bosta/track/{tn}:
    get:
      summary: Raw Bosta delivery record (proxy)
      parameters: [{ name: tn, in: path, required: true, schema: { type: string } }]
      responses: { "200": { description: "{ ok, delivery }" } }
  /api/reconcile:
    post: { summary: Poll Bosta for every non-terminal shipment, responses: { "200": { description: "{ ok, checked, changed, failed }" } } }
  /api/accounting/summary:
    get:
      summary: COD money position
      description: |
        `local` is computed from the bridge's records: expected_cod_in_transit, collected_pending_payout, paid_out,
        returned_or_failed_cod, shipment_fees (each `{v: amount, n: count}`).
        `bosta` is live from Bosta: unpaid_cod_amount (GET /businesses/{id}/transactions), expected_cod / collected_cod
        (GET /deliveries/analytics/total-deliveries) and the raw counts.
      responses: { "200": { description: ok } }
  /api/accounting/transactions:
    get:
      summary: COD orders ledger
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [pending, paid, collected, none] } }
        - { name: from, in: query, schema: { type: string, format: date }, description: delivered from }
        - { name: to, in: query, schema: { type: string, format: date }, description: delivered to }
        - { name: limit, in: query, schema: { type: integer, default: 100, maximum: 1000 } }
        - { name: offset, in: query, schema: { type: integer } }
      responses: { "200": { description: "{ ok, total, sum, items }" } }
  /api/accounting/settle:
    post:
      summary: Mark collected COD as paid out by Bosta
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/SettleRequest" } } } }
      responses: { "200": { description: "{ ok, count, amount, trackingNumbers }" } }
  /api/logs:
    get:
      summary: Service logs
      parameters:
        - { name: level, in: query, schema: { type: string, enum: [info, warn, error] } }
        - { name: source, in: query, schema: { type: string, enum: [webhook, bosta, shopify, reconcile, api, system] } }
        - { name: tn, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 200, maximum: 2000 } }
      responses: { "200": { description: "{ ok, items }" } }
  /api/events:
    get: { summary: Latest raw Bosta events across all shipments, responses: { "200": { description: ok } } }
