{
  "openapi": "3.0.3",
  "info": {
    "title": "FN Shipping API",
    "version": "1.4.0",
    "description": "FN Shipping API — gateway pengiriman FN CREATIVE untuk merchant.\n\n## Ringkasan\n- Cek ongkir real-time multi-kurir\n- Autocomplete wilayah Indonesia\n- Buat resi setelah pembayaran lunas\n- Lacak status pengiriman\n\n## Harga ongkir\nGunakan `rates[].price` dari response quote sebagai **ongkir final** di checkout merchant.\n\n## Autentikasi integrasi (server-to-server)\nHeader `x-api-key` + `id_merchant` — sama dengan payment gateway pay.fncreative.org.\n\n## Alur merchant\n1. Login & isi profil shipping (kode pos asal wajib)\n2. POST /shipping/quote saat checkout\n3. POST /shipping/orders setelah pembayaran lunas\n4. GET /shipping/orders/{id}/tracking untuk lacak paket\n\nProfil per merchant — setiap toko punya asal kirim & default paket sendiri.",
    "contact": {
      "name": "FN CREATIVE",
      "url": "https://fncreative.org/kontak.html"
    }
  },
  "servers": [
    { "url": "https://ship.fncreative.org/api", "description": "Production" }
  ],
  "tags": [
    { "name": "System", "description": "Health & status" },
    { "name": "Auth", "description": "Login akun FN CREATIVE (dashboard)" },
    { "name": "Profile", "description": "Profil shipping per merchant" },
    { "name": "Shipping", "description": "Cek ongkir & wilayah (API key)" },
    { "name": "Orders", "description": "Buat resi & lacak pengiriman (API key)" }
  ],
  "security": [{ "ClientAuth": [] }],
  "components": {
    "securitySchemes": {
      "ClientAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "API key FN CREATIVE (sama dengan payment gateway). Wajib dikombinasikan dengan header id_merchant."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Session token dari POST /auth/login. Untuk dashboard & kelola profil."
      },
      "MerchantId": {
        "type": "apiKey",
        "in": "header",
        "name": "id_merchant",
        "description": "ID merchant FN CREATIVE"
      }
    },
    "schemas": {
      "ApiEnvelope": {
        "type": "object",
        "properties": {
          "status": { "type": "boolean" },
          "message": { "type": "string" },
          "data": { "type": "object" }
        }
      },
      "LoginRequest": {
        "type": "object",
        "required": ["email", "password"],
        "properties": {
          "email": { "type": "string", "format": "email", "example": "merchant@example.com" },
          "password": { "type": "string", "example": "password-anda" }
        }
      },
      "MerchantProfile": {
        "type": "object",
        "required": ["origin_postal_code"],
        "properties": {
          "origin_postal_code": { "type": "string", "example": "60111", "description": "Kode pos gudang/toko asal (wajib)" },
          "origin_city": { "type": "string", "example": "Kota Surabaya" },
          "origin_province": { "type": "string", "example": "Jawa Timur" },
          "origin_label": { "type": "string", "example": "Gudang Surabaya Timur" },
          "package_weight_grams": { "type": "integer", "example": 500, "description": "Berat default paket (gram)" },
          "default_item_value": { "type": "integer", "example": 250000, "description": "Nilai barang default (Rp)" },
          "default_item_name": { "type": "string", "example": "Paket Fashion" },
          "couriers": { "type": "string", "example": "jne,jnt,sicepat" },
          "cod": {
            "type": "object",
            "properties": {
              "enabled": { "type": "boolean" },
              "fee": { "type": "integer" },
              "local_city": { "type": "string" },
              "area_note": { "type": "string" },
              "eta": { "type": "string" }
            }
          },
          "profile_ready": { "type": "boolean", "readOnly": true },
          "updated_at": { "type": "string", "format": "date-time", "readOnly": true }
        }
      },
      "QuoteRequestMinimal": {
        "type": "object",
        "required": ["method", "city", "province", "postal_code"],
        "description": "Request minimal — asal kirim & default paket dari profil merchant",
        "properties": {
          "method": { "type": "string", "enum": ["expedition", "cod"], "example": "expedition" },
          "city": { "type": "string", "example": "Jakarta Selatan" },
          "province": { "type": "string", "example": "DKI Jakarta" },
          "postal_code": { "type": "string", "example": "12190" }
        }
      },
      "QuoteRequest": {
        "type": "object",
        "required": ["method"],
        "properties": {
          "method": { "type": "string", "enum": ["expedition", "cod"], "example": "expedition" },
          "city": { "type": "string", "example": "Jakarta Selatan" },
          "province": { "type": "string", "example": "DKI Jakarta" },
          "postal_code": { "type": "string", "example": "12190" },
          "site_config": {
            "type": "object",
            "description": "Override profil merchant (opsional)",
            "properties": {
              "originPostalCode": { "type": "string", "example": "64129" },
              "originLabel": { "type": "string" },
              "couriers": { "type": "string", "example": "jne,jnt,sicepat" },
              "packageWeightGrams": { "type": "integer", "example": 300 },
              "cod": {
                "type": "object",
                "properties": {
                  "enabled": { "type": "boolean" },
                  "fee": { "type": "integer" },
                  "eta": { "type": "string" },
                  "localCity": { "type": "string" }
                }
              }
            }
          },
          "item": {
            "type": "object",
            "description": "Override berat/nilai barang (opsional)",
            "properties": {
              "name": { "type": "string" },
              "value": { "type": "integer" },
              "weight": { "type": "integer" },
              "quantity": { "type": "integer", "default": 1 }
            }
          }
        }
      },
      "ShippingRate": {
        "type": "object",
        "description": "Opsi kurir dari hasil quote. Gunakan price sebagai ongkir final di checkout.",
        "required": ["id", "price"],
        "properties": {
          "id": { "type": "string", "example": "fn:jne:reg", "description": "ID rate — simpan untuk POST /shipping/orders" },
          "courier": { "type": "string", "example": "JNE" },
          "service": { "type": "string", "example": "Reguler" },
          "price": { "type": "integer", "description": "Ongkir final — gunakan nilai ini di checkout merchant", "example": 21290 },
          "eta": { "type": "string", "example": "2 - 3 days" }
        }
      },
      "OrderDestination": {
        "type": "object",
        "required": ["contact_name", "contact_phone", "address", "postal_code"],
        "properties": {
          "contact_name": { "type": "string", "example": "Budi Santoso" },
          "contact_phone": { "type": "string", "example": "081234567890" },
          "contact_email": { "type": "string", "example": "budi@example.com" },
          "address": { "type": "string", "example": "Jl. Sudirman No. 1" },
          "postal_code": { "type": "string", "example": "12190" },
          "city": { "type": "string", "example": "Jakarta Selatan" },
          "province": { "type": "string", "example": "DKI Jakarta" },
          "note": { "type": "string" }
        }
      },
      "CreateOrderRequest": {
        "type": "object",
        "required": ["rate_id", "destination"],
        "properties": {
          "reference_id": { "type": "string", "example": "ORDER-2026-001", "description": "ID order di sistem merchant (unik)" },
          "rate_id": { "type": "string", "example": "fn:jne:reg", "description": "Dari rates[].id hasil quote" },
          "destination": { "$ref": "#/components/schemas/OrderDestination" },
          "order_note": { "type": "string" },
          "collection_method": { "type": "string", "enum": ["pickup", "drop_off"], "default": "pickup" },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": { "type": "string" },
                "value": { "type": "integer" },
                "weight": { "type": "integer" },
                "quantity": { "type": "integer" }
              }
            }
          }
        }
      },
      "ShippingOrder": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "example": "ORDER-2026-001" },
          "reference_id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "processing", "shipped", "delivered", "cancelled"] },
          "courier": { "type": "string", "example": "JNE" },
          "service": { "type": "string", "example": "reg" },
          "rate_id": { "type": "string", "example": "fn:jne:reg" },
          "waybill_id": { "type": "string" },
          "tracking_id": { "type": "string" },
          "price": { "type": "integer" },
          "origin_postal_code": { "type": "string" },
          "destination": { "$ref": "#/components/schemas/OrderDestination" },
          "tracking_events": { "type": "array", "items": { "type": "object" } },
          "provider": { "type": "string", "example": "live" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "tags": ["System"],
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": { "description": "Service online" }
        }
      }
    },
    "/auth/login": {
      "post": {
        "tags": ["Auth"],
        "summary": "Login akun FN CREATIVE",
        "description": "Email & password sama dengan pay.fncreative.org. Response berisi session_token untuk dashboard.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LoginRequest" }
            }
          }
        },
        "responses": {
          "200": { "description": "Login berhasil — session_token & id_merchant di data" },
          "401": { "description": "Email atau password salah" },
          "429": { "description": "Terlalu banyak percobaan login" }
        }
      }
    },
    "/auth/me": {
      "get": {
        "tags": ["Auth"],
        "summary": "Data merchant & API key",
        "description": "Butuh Bearer session_token. api_key hanya tersedia jika akun sudah disetujui.",
        "security": [{ "BearerAuth": [] }],
        "responses": {
          "200": { "description": "Data merchant termasuk api_key" },
          "401": { "description": "Sesi tidak valid" }
        }
      }
    },
    "/shipping/profile": {
      "get": {
        "tags": ["Profile"],
        "summary": "Baca profil shipping",
        "description": "Profil asal kirim & default paket merchant. Butuh login (Bearer).",
        "security": [{ "BearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Profil merchant",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ApiEnvelope" },
                    { "properties": { "data": { "$ref": "#/components/schemas/MerchantProfile" } } }
                  ]
                }
              }
            }
          },
          "401": { "description": "Sesi tidak valid" }
        }
      },
      "put": {
        "tags": ["Profile"],
        "summary": "Simpan profil shipping",
        "description": "Wajib isi origin_postal_code sebelum API quote bisa dipakai. Setiap merchant punya profil sendiri.",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/MerchantProfile" }
            }
          }
        },
        "responses": {
          "200": { "description": "Profil disimpan — profile_ready: true" },
          "400": { "description": "origin_postal_code wajib diisi" },
          "401": { "description": "Sesi tidak valid" }
        }
      }
    },
    "/shipping/quote": {
      "post": {
        "tags": ["Shipping"],
        "summary": "Hitung tarif ongkir / COD",
        "description": "Mode minimal: kirim city, province, postal_code saja — asal & default paket dari profil merchant.\nMode override: tambahkan site_config dan/atau item.",
        "security": [{ "ClientAuth": [] }, { "MerchantId": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/QuoteRequest" },
              "examples": {
                "minimal": {
                  "summary": "Request minimal (profil sudah diisi)",
                  "value": {
                    "method": "expedition",
                    "city": "Jakarta Selatan",
                    "province": "DKI Jakarta",
                    "postal_code": "12190"
                  }
                },
                "override": {
                  "summary": "Override berat & asal",
                  "value": {
                    "method": "expedition",
                    "city": "Jakarta Selatan",
                    "province": "DKI Jakarta",
                    "postal_code": "12190",
                    "item": { "name": "Jaket", "value": 450000, "weight": 800 }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Daftar rates[] — gunakan rates[].price di checkout" },
          "400": { "description": "Validasi input gagal" },
          "401": { "description": "API key tidak valid" },
          "403": { "description": "Akun belum disetujui admin" },
          "422": { "description": "profile_incomplete — isi profil shipping dulu" }
        }
      }
    },
    "/shipping/areas": {
      "get": {
        "tags": ["Shipping"],
        "summary": "Cari wilayah (autocomplete)",
        "security": [{ "ClientAuth": [] }, { "MerchantId": [] }],
        "parameters": [
          {
            "name": "input",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "example": "Surabaya"
          }
        ],
        "responses": {
          "200": { "description": "Daftar wilayah" },
          "401": { "description": "API key tidak valid" }
        }
      }
    },
    "/shipping/orders": {
      "post": {
        "tags": ["Orders"],
        "summary": "Buat pesanan pengiriman (resi)",
        "description": "Panggil setelah pembayaran/checkout lunas. Wajib gunakan `rate_id` dari hasil POST /shipping/quote (format: fn:kurir:layanan). `reference_id` unik — request ulang dengan ID sama mengembalikan resi yang sudah ada.",
        "security": [{ "ClientAuth": [] }, { "MerchantId": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateOrderRequest" }
            }
          }
        },
        "responses": {
          "201": { "description": "Resi dibuat" },
          "200": { "description": "Resi sudah ada (reference_id duplikat)" },
          "400": { "description": "Validasi gagal" },
          "401": { "description": "API key tidak valid" },
          "422": { "description": "Profil shipping belum lengkap" }
        }
      }
    },
    "/shipping/orders/{id}": {
      "get": {
        "tags": ["Orders"],
        "summary": "Detail pesanan pengiriman",
        "security": [{ "ClientAuth": [] }, { "MerchantId": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "reference_id atau id order FN Shipping"
          }
        ],
        "responses": {
          "200": { "description": "Detail order" },
          "404": { "description": "Tidak ditemukan" }
        }
      }
    },
    "/shipping/orders/{id}/tracking": {
      "get": {
        "tags": ["Orders"],
        "summary": "Lacak pengiriman",
        "description": "Status terbaru, nomor resi (waybill_id), dan riwayat perjalanan paket. Disarankan untuk polling dari backend merchant.",
        "security": [{ "ClientAuth": [] }, { "MerchantId": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Status & riwayat tracking" },
          "404": { "description": "Tidak ditemukan" }
        }
      }
    }
  }
}
