openapi: 3.0.3 info: title: 'Yokito Food API API Documentation' description: 'REST API untuk platform Yokito Food — bakery homemade Padang. Menyediakan endpoint publik (produk, kategori, promo, pesanan) dan endpoint admin terproteksi (CRUD + manajemen pesanan).' version: 1.0.0 servers: - url: 'http://api-yokitofood.test' tags: - name: Authentication description: "\nLogin, logout, dan info profil admin." - name: 'Public — Categories' description: '' - name: 'Public — Products' description: '' - name: 'Public — Promos' description: '' - name: 'Public — Testimonials' description: '' - name: 'Public — Settings' description: '' - name: 'Public — Orders' description: '' - name: 'Admin — Categories' description: '' - name: 'Admin — Products' description: '' - name: 'Admin — Testimonials' description: '' - name: 'Admin — Promos' description: '' - name: 'Admin — Settings' description: '' - name: 'Admin — Orders' description: '' components: securitySchemes: default: type: http scheme: bearer description: 'Login via POST /api/v1/auth/login untuk mendapatkan token. Sertakan sebagai Authorization: Bearer {token} di setiap request admin.' security: - default: [] paths: /api/v1/auth/login: post: summary: 'Login admin' operationId: loginAdmin description: 'Mengembalikan Bearer token yang digunakan di semua endpoint admin.' parameters: [] responses: { } tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Email admin.' example: admin@yokitofood.test password: type: string description: Password. example: password required: - email - password security: [] /api/v1/admin/logout: post: summary: Logout operationId: logout description: 'Hapus token yang sedang aktif.' parameters: [] responses: { } tags: - Authentication /api/v1/admin/me: get: summary: 'Profil admin yang sedang login' operationId: profilAdminYangSedangLogin description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Authentication /api/v1/categories: get: summary: 'Daftar kategori aktif' operationId: daftarKategoriAktif description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: 'Kue Kering' slug: kue-kering description: null sort_order: 1 - id: 2 name: 'Kue Basah' slug: kue-basah description: null sort_order: 2 - id: 3 name: Roti slug: roti description: null sort_order: 3 - id: 4 name: Minuman slug: minuman description: null sort_order: 4 - id: 5 name: 'Snack Box' slug: snack-box description: null sort_order: 5 properties: data: type: array example: - id: 1 name: 'Kue Kering' slug: kue-kering description: null sort_order: 1 - id: 2 name: 'Kue Basah' slug: kue-basah description: null sort_order: 2 - id: 3 name: Roti slug: roti description: null sort_order: 3 - id: 4 name: Minuman slug: minuman description: null sort_order: 4 - id: 5 name: 'Snack Box' slug: snack-box description: null sort_order: 5 items: type: object properties: id: type: integer example: 1 name: type: string example: 'Kue Kering' slug: type: string example: kue-kering description: type: string example: null nullable: true sort_order: type: integer example: 1 tags: - 'Public — Categories' security: [] /api/v1/products: get: summary: 'Daftar produk aktif' operationId: daftarProdukAktif description: '' parameters: - in: query name: category description: 'Filter berdasarkan slug kategori.' example: kue-kering required: false schema: type: string description: 'Filter berdasarkan slug kategori.' example: kue-kering - in: query name: search description: 'Cari berdasarkan nama atau deskripsi.' example: brownies required: false schema: type: string description: 'Cari berdasarkan nama atau deskripsi.' example: brownies - in: query name: featured description: 'Tampilkan hanya produk unggulan.' example: true required: false schema: type: boolean description: 'Tampilkan hanya produk unggulan.' example: true - in: query name: per_page description: 'Jumlah item per halaman (max 48).' example: 12 required: false schema: type: integer description: 'Jumlah item per halaman (max 48).' example: 12 responses: 200: description: '' content: application/json: schema: type: object example: data: [] links: first: 'http://api-yokitofood.test/api/v1/products?page=1' last: 'http://api-yokitofood.test/api/v1/products?page=1' prev: null next: null meta: current_page: 1 from: null last_page: 1 links: - url: null label: '« Previous' page: null active: false - url: 'http://api-yokitofood.test/api/v1/products?page=1' label: '1' page: 1 active: true - url: null label: 'Next »' page: null active: false path: 'http://api-yokitofood.test/api/v1/products' per_page: 12 to: null total: 0 properties: data: type: array example: [] links: type: object properties: first: type: string example: 'http://api-yokitofood.test/api/v1/products?page=1' last: type: string example: 'http://api-yokitofood.test/api/v1/products?page=1' prev: type: string example: null nullable: true next: type: string example: null nullable: true meta: type: object properties: current_page: type: integer example: 1 from: type: string example: null nullable: true last_page: type: integer example: 1 links: type: array example: - url: null label: '« Previous' page: null active: false - url: 'http://api-yokitofood.test/api/v1/products?page=1' label: '1' page: 1 active: true - url: null label: 'Next »' page: null active: false items: type: object properties: url: type: string example: null nullable: true label: type: string example: '« Previous' page: type: string example: null nullable: true active: type: boolean example: false path: type: string example: 'http://api-yokitofood.test/api/v1/products' per_page: type: integer example: 12 to: type: string example: null nullable: true total: type: integer example: 0 tags: - 'Public — Products' security: [] '/api/v1/products/{slug}': get: summary: 'Detail produk by slug' operationId: detailProdukBySlug description: '' parameters: [] responses: 404: description: '' content: application/json: schema: type: object example: message: 'No query results for model [App\Models\Product].' properties: message: type: string example: 'No query results for model [App\Models\Product].' tags: - 'Public — Products' security: [] parameters: - in: path name: slug description: 'Slug produk.' example: coklat-brownies-premium required: true schema: type: string /api/v1/promos/active: get: summary: 'Promo yang sedang aktif' operationId: promoYangSedangAktif description: 'Mengembalikan promo yang aktif dan dalam periode berlaku.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: [] properties: data: type: array example: [] tags: - 'Public — Promos' security: [] /api/v1/testimonials: get: summary: 'Daftar testimoni aktif' operationId: daftarTestimoniAktif description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: [] properties: data: type: array example: [] tags: - 'Public — Testimonials' security: [] /api/v1/settings: get: summary: 'Setting publik (general, contact, social)' operationId: settingPublikgeneralContactSocial description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: general: site_name: 'Yokito Food' site_tagline: 'Homemade with Love' site_description: 'Kue dan makanan rumahan berkualitas.' contact: whatsapp_number: '6281234567890' whatsapp_message: 'Halo, saya ingin memesan: ' email: hello@yokitofood.test address: '' social: instagram: '' facebook: '' tiktok: '' properties: data: type: object properties: general: type: object properties: site_name: type: string example: 'Yokito Food' site_tagline: type: string example: 'Homemade with Love' site_description: type: string example: 'Kue dan makanan rumahan berkualitas.' contact: type: object properties: whatsapp_number: type: string example: '6281234567890' whatsapp_message: type: string example: 'Halo, saya ingin memesan: ' email: type: string example: hello@yokitofood.test address: type: string example: '' social: type: object properties: instagram: type: string example: '' facebook: type: string example: '' tiktok: type: string example: '' tags: - 'Public — Settings' security: [] /api/v1/orders: post: summary: 'Buat pesanan baru' operationId: buatPesananBaru description: "Idempotent via `client_id`: jika client_id sudah ada, kembalikan order yang sama (HTTP 200).\nDiskon promo dihitung otomatis jika kode valid." parameters: [] responses: { } tags: - 'Public — Orders' requestBody: required: true content: application/json: schema: type: object properties: client_id: type: string description: 'UUID unik yang dibuat client sebelum request.' example: 550e8400-e29b-41d4-a716-446655440000 customer_name: type: string description: 'Nama pemesan.' example: 'Rina Sari' customer_phone: type: string description: 'Nomor WhatsApp.' example: '08123456789' customer_address: type: string description: 'Alamat pengiriman.' example: 'Jl. Merdeka No. 10, Padang' nullable: true notes: type: string description: 'Catatan tambahan.' example: 'Tolong pakai pita merah' nullable: true promo_code: type: string description: 'Kode promo (opsional).' example: DISKON10 nullable: true items: type: array description: 'Daftar item pesanan.' example: - architecto items: type: string required: - client_id - customer_name - customer_phone - items security: [] /api/v1/admin/categories: get: summary: '' operationId: getApiV1AdminCategories description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Categories' post: summary: '' operationId: postApiV1AdminCategories description: '' parameters: [] responses: { } tags: - 'Admin — Categories' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 255 characters.' example: b description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true is_active: type: boolean description: '' example: false sort_order: type: integer description: 'Must be at least 0.' example: 60 required: - name '/api/v1/admin/categories/{category_id}': put: summary: '' operationId: putApiV1AdminCategoriesCategory_id description: '' parameters: [] responses: { } tags: - 'Admin — Categories' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 255 characters.' example: b description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true is_active: type: boolean description: '' example: true sort_order: type: integer description: 'Must be at least 0.' example: 60 delete: summary: '' operationId: deleteApiV1AdminCategoriesCategory_id description: '' parameters: [] responses: { } tags: - 'Admin — Categories' parameters: - in: path name: category_id description: 'The ID of the category.' example: 1 required: true schema: type: integer /api/v1/admin/products: get: summary: '' operationId: getApiV1AdminProducts description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Products' post: summary: '' operationId: postApiV1AdminProducts description: '' parameters: [] responses: { } tags: - 'Admin — Products' requestBody: required: true content: application/json: schema: type: object properties: category_id: type: string description: 'The id of an existing record in the categories table.' example: architecto name: type: string description: 'Must not be greater than 255 characters.' example: 'n' description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true price: type: integer description: 'Must be at least 0.' example: 60 stock_status: type: string description: '' example: sold_out enum: - available - sold_out - pre_order is_active: type: boolean description: '' example: false is_featured: type: boolean description: '' example: false sort_order: type: integer description: 'Must be at least 0.' example: 42 required: - category_id - name - price - stock_status '/api/v1/admin/products/{id}': get: summary: '' operationId: getApiV1AdminProductsId description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Products' post: summary: '' operationId: postApiV1AdminProductsId description: '' parameters: [] responses: { } tags: - 'Admin — Products' requestBody: required: false content: application/json: schema: type: object properties: category_id: type: string description: 'The id of an existing record in the categories table.' example: null name: type: string description: 'Must not be greater than 255 characters.' example: b description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true price: type: integer description: 'Must be at least 0.' example: 60 stock_status: type: string description: '' example: available enum: - available - sold_out - pre_order is_active: type: boolean description: '' example: false is_featured: type: boolean description: '' example: true sort_order: type: integer description: 'Must be at least 0.' example: 42 delete: summary: '' operationId: deleteApiV1AdminProductsId description: '' parameters: [] responses: { } tags: - 'Admin — Products' parameters: - in: path name: id description: 'The ID of the product.' example: 16 required: true schema: type: integer '/api/v1/admin/products/{id}/restore': post: summary: '' operationId: postApiV1AdminProductsIdRestore description: '' parameters: [] responses: { } tags: - 'Admin — Products' parameters: - in: path name: id description: 'The ID of the product.' example: 16 required: true schema: type: integer /api/v1/admin/testimonials: get: summary: '' operationId: getApiV1AdminTestimonials description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Testimonials' post: summary: '' operationId: postApiV1AdminTestimonials description: '' parameters: [] responses: { } tags: - 'Admin — Testimonials' requestBody: required: true content: application/json: schema: type: object properties: customer_name: type: string description: 'Must not be greater than 255 characters.' example: b customer_location: type: string description: 'Must not be greater than 255 characters.' example: 'n' nullable: true content: type: string description: '' example: architecto rating: type: integer description: 'Must be at least 1. Must not be greater than 5.' example: 2 is_active: type: boolean description: '' example: false sort_order: type: integer description: 'Must be at least 0.' example: 84 required: - customer_name - content '/api/v1/admin/testimonials/{testimonial_id}': put: summary: '' operationId: putApiV1AdminTestimonialsTestimonial_id description: '' parameters: [] responses: { } tags: - 'Admin — Testimonials' delete: summary: '' operationId: deleteApiV1AdminTestimonialsTestimonial_id description: '' parameters: [] responses: { } tags: - 'Admin — Testimonials' parameters: - in: path name: testimonial_id description: 'The ID of the testimonial.' example: 16 required: true schema: type: integer /api/v1/admin/promos: get: summary: '' operationId: getApiV1AdminPromos description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Promos' post: summary: '' operationId: postApiV1AdminPromos description: '' parameters: [] responses: { } tags: - 'Admin — Promos' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Must not be greater than 255 characters.' example: b code: type: string description: 'Must not be greater than 50 characters.' example: 'n' nullable: true description: type: string description: '' example: 'Eius et animi quos velit et.' nullable: true type: type: string description: '' example: fixed enum: - percentage - fixed value: type: integer description: 'Must be at least 1.' example: 16 min_order: type: integer description: 'Must be at least 0.' example: 42 max_discount: type: integer description: 'Must be at least 1.' example: 40 nullable: true starts_at: type: string description: 'Must be a valid date.' example: '2026-04-10T19:25:31' nullable: true ends_at: type: string description: 'Must be a valid date. Must be a date after or equal to starts_at.' example: '2052-05-03' nullable: true is_active: type: boolean description: '' example: true required: - name - type - value '/api/v1/admin/promos/{promo_id}': put: summary: '' operationId: putApiV1AdminPromosPromo_id description: '' parameters: [] responses: { } tags: - 'Admin — Promos' delete: summary: '' operationId: deleteApiV1AdminPromosPromo_id description: '' parameters: [] responses: { } tags: - 'Admin — Promos' parameters: - in: path name: promo_id description: 'The ID of the promo.' example: 16 required: true schema: type: integer /api/v1/admin/settings: get: summary: '' operationId: getApiV1AdminSettings description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Settings' put: summary: '' operationId: putApiV1AdminSettings description: '' parameters: [] responses: { } tags: - 'Admin — Settings' requestBody: required: true content: application/json: schema: type: object properties: settings: type: array description: '' example: - [] items: type: object properties: key: type: string description: '' example: architecto value: type: string description: '' example: architecto nullable: true required: - key required: - settings /api/v1/admin/orders: get: summary: 'Daftar semua pesanan (paginated)' operationId: daftarSemuaPesananpaginated description: '' parameters: - in: query name: status description: 'Filter status: pending, approved, in_progress, completed, cancelled.' example: pending required: false schema: type: string description: 'Filter status: pending, approved, in_progress, completed, cancelled.' example: pending - in: query name: search description: 'Cari berdasarkan nomor order, nama, atau telepon.' example: YKT-2026 required: false schema: type: string description: 'Cari berdasarkan nomor order, nama, atau telepon.' example: YKT-2026 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Orders' '/api/v1/admin/orders/{order_id}': get: summary: '' operationId: getApiV1AdminOrdersOrder_id description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Admin — Orders' parameters: - in: path name: order_id description: 'The ID of the order.' example: 16 required: true schema: type: integer '/api/v1/admin/orders/{order_id}/status': patch: summary: '' operationId: patchApiV1AdminOrdersOrder_idStatus description: '' parameters: [] responses: { } tags: - 'Admin — Orders' requestBody: required: true content: application/json: schema: type: object properties: status: type: string description: '' example: completed enum: - approved - in_progress - completed - cancelled admin_note: type: string description: 'Must not be greater than 1000 characters.' example: b nullable: true discount_amount: type: integer description: 'Override diskon — admin bisa sesuaikan saat approve. Must be at least 0.' example: 39 nullable: true required: - status parameters: - in: path name: order_id description: 'The ID of the order.' example: 16 required: true schema: type: integer