openapi: 3.0.3
info:
  title: Ecom & FileManager API Documentation
  version: 1.0.0
  license:
    name: MIT
    url: https://github.com/thaihadefi/Ecom/blob/main/LICENSE
  description: |-
    Documentation of every API endpoint of the Ecom web application (port 3000) and the FileManager microservice (port 4000). Server-rendered HTML pages are out of scope, apart from the few read-only ones described under Response conventions.

    ### Base URLs
    - Web application: `http://localhost:3000`
    - FileManager microservice: `http://localhost:4000` (operations under `/files` and `/folders` are served only there)

    ### Authentication
    - **Customer** and **admin** JSON endpoints (`/api`, `/admin/api`) accept either the HTTP-only cookie (`tokenUser`, `tokenAdmin`), which browsers use, or `Authorization: Bearer <accessToken>`, which API clients use. The login endpoints return `accessToken` in the body and set the cookie; a request that carries an `Authorization` header is authenticated by it alone and its cookies are ignored. The token lasts one day (`expiresIn` says how long); there is no refresh for Bearer clients, so log in again on 401.
    - **FileManager** `/files` and `/folders` endpoints use `Authorization: Bearer <FILE_MANAGER_SECRET>`. `/media` is public. Use a random secret of at least 32 characters; repeated wrong secrets from one address are answered with HTTP 429.
    - Admin endpoints also check a permission (shown on each operation). Every admin write is recorded in the admin activity log.

    ### Trying requests
    The Web application serves this documentation itself at `/api-docs/`, which is the place to try customer and admin endpoints: log in through the login endpoint (the cookie is then sent automatically), or paste the returned `accessToken` under "Authorize". The Web application sends CORS headers on non-admin routes, so the Vercel copy can reach storefront endpoints with a Bearer token; admin endpoints skip CORS on purpose, so try those from this same-origin copy instead. The FileManager bearer endpoints also work from the Vercel copy when FileManager lists its origin in `FILE_MANAGER_CORS_ORIGINS`.

    ### Response conventions
    - A successful JSON response has HTTP 200 and `{ "code": "success", "message": "...", ...data }`.
    - An error has the matching HTTP status and `{ "code": "error", "message": "..." }`: **400** invalid request, **401** not logged in, **403** no permission or not allowed, **404** record not found, **409** conflict with the current state (duplicate value, missing stock, a change that is not allowed), **429** too many attempts (see `Retry-After`), **500** unexpected failure, **502** a service the application depends on (FileManager, email, the shipping carrier) failed, and **503** the store is not fully configured yet for the request (for example, checkout without a warehouse location set).
    - JSON endpoints (`/api`, `/admin/api`) never redirect: without a valid login they answer 401, and without the permission 403, whatever the method. Server-rendered page requests are redirected (302) to the login page, unless they send an `Authorization` header, `Accept: application/json` without `text/html`, or `X-Requested-With: XMLHttpRequest`, which get the same 401 or 403 as the JSON endpoints. FileManager answers 401 when the bearer secret is missing or wrong.
    - The storefront is server-rendered. The few page routes that act as read endpoints (catalog, articles, cart, flagged orders, payment and OAuth redirects) are documented with `text/html` or redirect responses; the other pages are not part of the API.
    - JSON endpoints are plural resources under `/api` (storefront) and `/admin/api` (admin). The HTTP method states the action: `POST` creates, `PUT` and `PATCH` change, `DELETE` removes, and `GET` never changes data. Payment-gateway and OAuth callbacks keep the URLs their providers call.
    - Trash lifecycle of admin resources: `DELETE /{id}` moves a record to the trash, `POST /{id}/restore` restores it and `DELETE /{id}?permanent=true` deletes it for good. The bulk variants take `{ "ids": [...] }` on the collection: `POST /trash`, `POST /restore` and `DELETE /`.

    ### Optional modules
    Each store switches optional modules on or off with `FEATURE_*` environment variables (see the Ecom README). An endpoint of a switched-off module answers **404** like a route that does not exist: chat (`/api/chat-rooms`, `/admin/api/chat-rooms`), AI chat helpers, flagged orders, recommendation jobs, blog (`/api/articles`, `/api/article-categories`, `/admin/api/articles`, `/admin/api/article-categories`), wishlist, compare, reviews (`/api/reviews`, `/admin/api/reviews`), coupons (`/api/coupon-checks`, `/admin/api/coupons`; checkout then refuses a coupon with 400) and loyalty points (checkout ignores `usePoint`).

    ### Real-time chat (Socket.IO, same origin as the Web app)
    Connect with the auth cookie and `auth: { role: "user" | "admin", roomId? }`.
    - Client to server: `CLIENT_SEND_MESSAGE`, `CLIENT_TYPING`, `CLIENT_OPEN_CHAT`, `CLIENT_DELETE_MESSAGE`, `ADMIN_OPEN_CHAT`, `ADMIN_TYPING`, `ADMIN_DELETE_ROOM`.
    - Server to client: `SERVER_SEND_MESSAGE`, `SERVER_SEND_STATUS`, `SERVER_MESSAGE_DELIVERED`, `SERVER_DELETE_MESSAGE`, `SERVER_DELETE_ROOM`, `SERVER_ROOM_ASSIGNED`, `SERVER_SEND_CLIENT_TYPING`, `SERVER_SEND_ADMIN_TYPING`, `SERVER_CLIENT_READ`, `SERVER_ADMIN_READ`, `SERVER_ADMIN_STATUS`, `USER_STATUS_ONLINE`, `LIST_USER_ONLINE`.
servers:
  - url: http://localhost:3000
    description: Ecom Web application (local)
tags:
  - name: Payment & Logistics (VNPAY, ZaloPay, GoShip)
    description: Payment gateway integrations (VNPAY, ZaloPay), webhooks, and GoShip automated courier rates
  - name: Client Auth
    description: Customer registration, login, logout, password recovery & social OAuth
  - name: Client Products
    description: Product catalog, search suggestions, reviews, and reports
  - name: Client Articles
    description: Blog and editorial articles
  - name: Client Cart & Checkout
    description: Cart quotes, GoShip logistics, coupon validation, and order creation
  - name: Client Wishlist & Compare
    description: Product comparison and customer wishlist resolution
  - name: Client Dashboard
    description: Profile, avatar upload, address book CRUD, and email change verification
  - name: Client Chat
    description: Real-time customer support chat messages, file upload & rating
  - name: Client Contact & Pages
    description: Customer contact inquiry submissions
  - name: Admin Auth
    description: Administrative login and session termination
  - name: Admin Products
    description: Product lifecycle, multi-status, SEO, recommendations ML, CSV import/export
  - name: Admin Categories & Attributes
    description: Catalog taxonomy and variant attributes
  - name: Admin Orders
    description: Fulfillment status, CSV export, and Isolation Forest ML bot detection queue
  - name: Admin Coupons
    description: Coupon codes, discount types, validity ranges and usage limits
  - name: Admin Content & CMS
    description: Articles, dynamic visual blocks, and page layout templates
  - name: Admin Access Control
    description: Administrator accounts, customer accounts, and role permissions
  - name: Admin Settings & Ops
    description: Third-party API keys (shipping, payment, email, OAuth) and cache flushing
  - name: Admin Contact Inquiries
    description: Contact inquiry processing, status changes, and trash lifecycle
  - name: Admin Reviews
    description: Review moderation (approve, reject, clear reports, delete)
  - name: Admin Chat
    description: 'Admin side of the support chat: history, status, uploads and AI helpers (live messages use Socket.IO)'
  - name: Admin File Manager
    description: 'Admin proxy to the FileManager microservice: browse, upload, rename, move and delete files and folders'
  - name: Admin Dashboard
    description: Chart data for the admin dashboard
  - name: Site Endpoints
    description: Sitemap, robots.txt and the PWA manifest
  - name: FileManager Microservice
    description: The standalone media service (port 4000). Every /files and /folders call needs the shared bearer secret; /media is public
paths:
  /admin/api/admin-accounts:
    post:
      tags:
        - Admin Access Control
      summary: Create admin account
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `account-admin-create`.

        The `email` must not belong to another admin account, and the selected roles may only contain permissions the acting admin holds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminAccountAdminCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminAccountAdminCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiAdminAccounts
    delete:
      tags:
        - Admin Access Control
      summary: Permanently delete selected admin accounts
      description: |-
        Requires the admin permission `account-admin-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiAdminAccounts
  /admin/api/admin-accounts/restore:
    post:
      tags:
        - Admin Access Control
      summary: Restore selected admin accounts from trash
      description: |-
        Requires the admin permission `account-admin-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiAdminAccountsRestore
  /admin/api/admin-accounts/trash:
    post:
      tags:
        - Admin Access Control
      summary: Move selected admin accounts to trash
      description: |-
        Requires the admin permission `account-admin-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiAdminAccountsTrash
  /admin/api/admin-accounts/{id}:
    patch:
      tags:
        - Admin Access Control
      summary: Edit admin account
      description: |-
        Requires the admin permission `account-admin-edit`.

        A superadmin account can only be edited by that superadmin, and you cannot deactivate your own account. The `email` must not belong to another admin account, and the selected roles may only contain permissions the acting admin holds.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminAccountAdminEditByIdBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminAccountAdminEditByIdBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiAdminAccountsById
    delete:
      tags:
        - Admin Access Control
      summary: Move admin account to trash
      description: Requires the admin permission `account-admin-delete`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiAdminAccountsById
  /admin/api/admin-accounts/{id}/password:
    put:
      tags:
        - Admin Access Control
      summary: Change admin account password
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Password changed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/StandardResponse'
                  - type: object
                    properties:
                      accessToken:
                        type: string
                        description: 'Present only when the admin changed their own password: the tokens issued before the change stop working, so this is the new one.'
                      tokenType:
                        type: string
                        enum:
                          - Bearer
                      expiresIn:
                        type: integer
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `account-admin-change-password`. Rate limited (HTTP 429 when exceeded).

        The password of a superadmin account can only be changed by that superadmin.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminAccountAdminChangePasswordByIdBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminAccountAdminChangePasswordByIdBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: putAdminApiAdminAccountsByIdPassword
  /admin/api/admin-accounts/{id}/restore:
    post:
      tags:
        - Admin Access Control
      summary: Restore admin account from trash
      description: Requires the admin permission `account-admin-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiAdminAccountsByIdRestore
  /admin/api/article-categories:
    post:
      tags:
        - Admin Content & CMS
      summary: Create article category
      description: |-
        Requires the admin permission `article-category-create`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticleCategories
    delete:
      tags:
        - Admin Content & CMS
      summary: Permanently delete selected article categories
      description: |-
        Requires the admin permission `article-category-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiArticleCategories
  /admin/api/article-categories/restore:
    post:
      tags:
        - Admin Content & CMS
      summary: Restore selected article categories from trash
      description: |-
        Requires the admin permission `article-category-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticleCategoriesRestore
  /admin/api/article-categories/trash:
    post:
      tags:
        - Admin Content & CMS
      summary: Move selected article categories to trash
      description: |-
        Requires the admin permission `article-category-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticleCategoriesTrash
  /admin/api/article-categories/{id}:
    patch:
      tags:
        - Admin Content & CMS
      summary: Edit article category
      description: |-
        Requires the admin permission `article-category-edit`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiArticleCategoriesById
    delete:
      tags:
        - Admin Content & CMS
      summary: Move article category to trash
      description: Requires the admin permission `article-category-delete`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiArticleCategoriesById
  /admin/api/article-categories/{id}/restore:
    post:
      tags:
        - Admin Content & CMS
      summary: Restore article category from trash
      description: Requires the admin permission `article-category-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticleCategoriesByIdRestore
  /admin/api/articles:
    post:
      tags:
        - Admin Content & CMS
      summary: Create article
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `article-create`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticles
    delete:
      tags:
        - Admin Content & CMS
      summary: Permanently delete selected articles
      description: |-
        Requires the admin permission `article-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiArticles
  /admin/api/articles/restore:
    post:
      tags:
        - Admin Content & CMS
      summary: Restore selected articles from trash
      description: |-
        Requires the admin permission `article-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticlesRestore
  /admin/api/articles/trash:
    post:
      tags:
        - Admin Content & CMS
      summary: Move selected articles to trash
      description: |-
        Requires the admin permission `article-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticlesTrash
  /admin/api/articles/{id}:
    patch:
      tags:
        - Admin Content & CMS
      summary: Edit article
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `article-edit`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiArticlesById
    delete:
      tags:
        - Admin Content & CMS
      summary: Delete article
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `article-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiArticlesById
  /admin/api/articles/{id}/restore:
    post:
      tags:
        - Admin Content & CMS
      summary: Restore article from trash
      description: Requires the admin permission `article-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiArticlesByIdRestore
  /admin/api/articles/{id}/seo:
    patch:
      tags:
        - Admin Content & CMS
      summary: Update article SEO and OpenGraph attributes
      description: Requires the admin permission `article-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SeoBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/SeoBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiArticlesByIdSeo
  /admin/api/blocks:
    post:
      tags:
        - Admin Content & CMS
      summary: Create visual CMS block with JSON editor payload
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `block-create`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminBlockCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiBlocks
  /admin/api/blocks/{id}:
    patch:
      tags:
        - Admin Content & CMS
      summary: Edit visual CMS block
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `block-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminBlockCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiBlocksById
    delete:
      tags:
        - Admin Content & CMS
      summary: Delete visual CMS block
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `block-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiBlocksById
  /admin/api/cache:
    delete:
      tags:
        - Admin Settings & Ops
      summary: Clear the application caches (settings, catalog, dashboard)
      responses:
        '200':
          description: Cache cleared.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiCache
  /admin/api/chat-rooms/{id}/customer-emotion:
    get:
      tags:
        - Admin Chat
      summary: Analyze the customer's emotion with the AI assistant
      description: Requires the admin permission `chat-list`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiChatRoomsByIdCustomerEmotion
  /admin/api/chat-rooms/{id}/reply-refinements:
    post:
      tags:
        - Admin Chat
      summary: Refine a draft reply with the AI assistant
      description: Requires the admin permission `chat-reply`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminChatEditReplyByIdBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminChatEditReplyByIdBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiChatRoomsByIdReplyRefinements
  /admin/api/chat-rooms/{id}/reply-suggestion:
    get:
      tags:
        - Admin Chat
      summary: Suggest a reply with the AI assistant
      description: Requires the admin permission `chat-reply`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiChatRoomsByIdReplySuggestion
  /admin/api/chat-rooms/{id}/summary:
    get:
      tags:
        - Admin Chat
      summary: Summarize the conversation with the AI assistant
      description: Requires the admin permission `chat-list`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiChatRoomsByIdSummary
  /admin/api/chat-rooms/{roomId}:
    patch:
      tags:
        - Admin Chat
      summary: Open or lock a chat room
      description: Requires the admin permission `chat-reply`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - status
              properties:
                status:
                  type: string
                  enum:
                    - open
                    - locked
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiChatRoomsByRoomId
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
  /admin/api/chat-rooms/{roomId}/attachments:
    post:
      tags:
        - Admin Chat
      summary: Upload attachments to a chat room
      description: Requires the admin permission `chat-reply`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  maxItems: 10
                  items:
                    type: string
                    format: binary
                  description: Up to 10 files, 10 MB each
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/UploadRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiChatRoomsByRoomIdAttachments
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
  /admin/api/chat-rooms/{roomId}/messages:
    get:
      tags:
        - Admin Chat
      summary: Load a page of messages of a chat room
      description: Requires the admin permission `chat-list`.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: lastMessageId
          in: query
          schema:
            $ref: '#/components/schemas/ObjectId'
          description: Return messages older than this one
      responses:
        '200':
          description: Result of the action.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  messages:
                    type: array
                    items:
                      type: object
                  userUnreadCount:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiChatRoomsByRoomIdMessages
  /admin/api/contact-inquiries:
    delete:
      tags:
        - Admin Contact Inquiries
      summary: Permanently delete selected contact inquiries
      description: |-
        Requires the admin permission `contact-inquiry-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiContactInquiries
  /admin/api/contact-inquiries/restore:
    post:
      tags:
        - Admin Contact Inquiries
      summary: Restore selected contact inquiries from trash
      description: |-
        Requires the admin permission `contact-inquiry-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiContactInquiriesRestore
  /admin/api/contact-inquiries/trash:
    post:
      tags:
        - Admin Contact Inquiries
      summary: Move selected contact inquiries to trash
      description: |-
        Requires the admin permission `contact-inquiry-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiContactInquiriesTrash
  /admin/api/contact-inquiries/{id}:
    delete:
      tags:
        - Admin Contact Inquiries
      summary: Soft delete inquiry
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `contact-inquiry-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiContactInquiriesById
  /admin/api/contact-inquiries/{id}/restore:
    post:
      tags:
        - Admin Contact Inquiries
      summary: Restore inquiry from trash
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Restored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `contact-inquiry-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiContactInquiriesByIdRestore
  /admin/api/contact-inquiries/{id}/status/{status}:
    put:
      tags:
        - Admin Contact Inquiries
      summary: Update inquiry review status
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: status
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Status updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `contact-inquiry-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: putAdminApiContactInquiriesByIdStatusByStatus
  /admin/api/coupons:
    post:
      tags:
        - Admin Coupons
      summary: Create discount coupon
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `coupon-create`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminCouponCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminCouponCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCoupons
    delete:
      tags:
        - Admin Coupons
      summary: Permanently delete selected coupons
      description: |-
        Requires the admin permission `coupon-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiCoupons
  /admin/api/coupons/restore:
    post:
      tags:
        - Admin Coupons
      summary: Restore selected coupons from trash
      description: |-
        Requires the admin permission `coupon-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCouponsRestore
  /admin/api/coupons/trash:
    post:
      tags:
        - Admin Coupons
      summary: Move selected coupons to trash
      description: |-
        Requires the admin permission `coupon-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCouponsTrash
  /admin/api/coupons/{id}:
    patch:
      tags:
        - Admin Coupons
      summary: Edit coupon
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `coupon-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminCouponCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminCouponCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiCouponsById
    delete:
      tags:
        - Admin Coupons
      summary: Delete coupon
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `coupon-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiCouponsById
  /admin/api/coupons/{id}/restore:
    post:
      tags:
        - Admin Coupons
      summary: Restore coupon from trash
      description: Requires the admin permission `coupon-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCouponsByIdRestore
  /admin/api/customer-accounts:
    delete:
      tags:
        - Admin Access Control
      summary: Permanently delete selected customer accounts
      description: |-
        Requires the admin permission `account-user-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiCustomerAccounts
  /admin/api/customer-accounts/restore:
    post:
      tags:
        - Admin Access Control
      summary: Restore selected customer accounts from trash
      description: |-
        Requires the admin permission `account-user-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCustomerAccountsRestore
  /admin/api/customer-accounts/trash:
    post:
      tags:
        - Admin Access Control
      summary: Move selected customer accounts to trash
      description: |-
        Requires the admin permission `account-user-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCustomerAccountsTrash
  /admin/api/customer-accounts/{id}:
    delete:
      tags:
        - Admin Access Control
      summary: Move customer account to trash
      description: Requires the admin permission `account-user-delete`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiCustomerAccountsById
  /admin/api/customer-accounts/{id}/restore:
    post:
      tags:
        - Admin Access Control
      summary: Restore customer account from trash
      description: Requires the admin permission `account-user-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiCustomerAccountsByIdRestore
  /admin/api/files:
    post:
      tags:
        - Admin File Manager
      summary: Upload files to the media store
      description: Requires the admin permission `file-manager`.
      parameters:
        - name: folderPath
          in: query
          schema:
            type: string
          description: 'Target folder (default: root)'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  maxItems: 20
                  items:
                    type: string
                    format: binary
                  description: Up to 20 files, 10 MB each
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/UploadRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiFiles
    delete:
      tags:
        - Admin File Manager
      summary: Delete a file
      description: Requires the admin permission `file-manager`.
      parameters:
        - name: folder
          in: query
          required: true
          schema:
            type: string
        - name: fileName
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiFiles
  /admin/api/files/location:
    patch:
      tags:
        - Admin File Manager
      summary: Move a file to another folder
      description: |-
        Requires the admin permission `file-manager`.

        Moving a file into the folder it is already in is rejected.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerMoveFileBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerMoveFileBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiFilesLocation
  /admin/api/files/name:
    patch:
      tags:
        - Admin File Manager
      summary: Rename a file
      description: Requires the admin permission `file-manager`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerChangeFileNameBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerChangeFileNameBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiFilesName
  /admin/api/flagged-orders/retraining:
    post:
      tags:
        - Admin Orders
      summary: Trigger manual retrain of anomaly detection model
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `order-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiFlaggedOrdersRetraining
    get:
      tags:
        - Admin Orders
      summary: Progress of the anomaly model retrain job
      description: Requires the admin permission `order-edit`.
      responses:
        '200':
          description: Result of the action.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  status:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiFlaggedOrdersRetraining
  /admin/api/flagged-orders/{id}:
    delete:
      tags:
        - Admin Orders
      summary: Dismiss anomaly flag from order
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Flag dismissed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `order-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiFlaggedOrdersById
  /admin/api/folders:
    post:
      tags:
        - Admin File Manager
      summary: Create a folder
      description: Requires the admin permission `file-manager`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminFileManagerFolderCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminFileManagerFolderCreateBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiFolders
    delete:
      tags:
        - Admin File Manager
      summary: Delete a folder
      description: Requires the admin permission `file-manager`.
      parameters:
        - name: folderPath
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiFolders
  /admin/api/folders/location:
    patch:
      tags:
        - Admin File Manager
      summary: Move a folder
      description: Requires the admin permission `file-manager`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerFolderMoveBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerFolderMoveBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiFoldersLocation
  /admin/api/folders/name:
    patch:
      tags:
        - Admin File Manager
      summary: Rename a folder
      description: Requires the admin permission `file-manager`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerFolderRenameBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerFolderRenameBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiFoldersName
  /admin/api/orders:
    delete:
      tags:
        - Admin Orders
      summary: Permanently delete selected orders
      description: |-
        Requires the admin permission `order-delete`.

        `ids` must be a non-empty array of ids.

        Only orders whose status is `cancelled` or `returned` can be deleted; other orders are rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiOrders
  /admin/api/orders/csv:
    get:
      tags:
        - Admin Orders
      summary: Export orders to CSV with formula protection
      responses:
        '200':
          description: The CSV file, sent as an attachment named `orders.csv`.
          headers:
            Content-Disposition:
              description: attachment; filename="orders.csv"
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `order-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiOrdersCsv
  /admin/api/orders/restore:
    post:
      tags:
        - Admin Orders
      summary: Restore selected orders from trash
      description: |-
        Requires the admin permission `order-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiOrdersRestore
  /admin/api/orders/trash:
    post:
      tags:
        - Admin Orders
      summary: Move selected orders to trash
      description: |-
        Requires the admin permission `order-delete`.

        `ids` must be a non-empty array of ids.

        Only orders whose status is `cancelled` or `returned` can be deleted; other orders are rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiOrdersTrash
  /admin/api/orders/{id}:
    patch:
      tags:
        - Admin Orders
      summary: Update order and payment status
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Cancelling or returning an order releases its stock, coupon usage and loyalty points exactly once. Completing a paid order grants loyalty points.

        Requires the admin permission `order-edit`.

        An order that is `completed`, `cancelled` or `returned` can no longer change its status, and a `paid` order cannot be set back to `unpaid`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminOrderEditByIdBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchAdminOrderEditByIdBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiOrdersById
    delete:
      tags:
        - Admin Orders
      summary: Move order to trash
      description: |-
        Requires the admin permission `order-delete`.

        Only orders whose status is `cancelled` or `returned` can be deleted; other orders are rejected with `code: "error"`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiOrdersById
  /admin/api/orders/{id}/restore:
    post:
      tags:
        - Admin Orders
      summary: Restore order from trash
      description: Requires the admin permission `order-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiOrdersByIdRestore
  /admin/api/product-attributes:
    post:
      tags:
        - Admin Categories & Attributes
      summary: Create product variant attribute
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-create`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminProductAttributeCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminProductAttributeCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductAttributes
  /admin/api/product-attributes/{id}:
    patch:
      tags:
        - Admin Categories & Attributes
      summary: Edit product attribute
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminProductAttributeCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminProductAttributeCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiProductAttributesById
    delete:
      tags:
        - Admin Categories & Attributes
      summary: Delete product attribute
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiProductAttributesById
  /admin/api/product-attributes/{id}/restore:
    post:
      tags:
        - Admin Categories & Attributes
      summary: Restore product attribute from trash
      description: Requires the admin permission `product-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductAttributesByIdRestore
  /admin/api/product-categories:
    post:
      tags:
        - Admin Categories & Attributes
      summary: Create category
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `product-create`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductCategories
    delete:
      tags:
        - Admin Categories & Attributes
      summary: Permanently delete selected product categories
      description: |-
        Requires the admin permission `product-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiProductCategories
  /admin/api/product-categories/restore:
    post:
      tags:
        - Admin Categories & Attributes
      summary: Restore selected product categories from trash
      description: |-
        Requires the admin permission `product-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductCategoriesRestore
  /admin/api/product-categories/trash:
    post:
      tags:
        - Admin Categories & Attributes
      summary: Move selected product categories to trash
      description: |-
        Requires the admin permission `product-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductCategoriesTrash
  /admin/api/product-categories/{id}:
    patch:
      tags:
        - Admin Categories & Attributes
      summary: Edit category
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `product-edit`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminArticleCategoryCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiProductCategoriesById
    delete:
      tags:
        - Admin Categories & Attributes
      summary: Soft delete category
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiProductCategoriesById
  /admin/api/product-categories/{id}/restore:
    post:
      tags:
        - Admin Categories & Attributes
      summary: Restore category
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Restored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductCategoriesByIdRestore
  /admin/api/products:
    post:
      tags:
        - Admin Products
      summary: Create new product
      responses:
        '200':
          description: Product created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `product-create`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminProductCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminProductCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProducts
    delete:
      tags:
        - Admin Products
      summary: Permanently delete selected products
      description: |-
        Requires the admin permission `product-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiProducts
  /admin/api/products/csv:
    post:
      tags:
        - Admin Products
      summary: Import products from CSV file upload
      responses:
        '200':
          description: Import status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/UploadRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `product-create`.

        Send the file in the `file` field as `multipart/form-data`. It must be a `text/csv` file of at most 5 MB.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: The CSV file (`text/csv`), at most 5 MB.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductsCsv
    get:
      tags:
        - Admin Products
      summary: Export product catalog to CSV with formula injection neutralization
      responses:
        '200':
          description: The CSV file, sent as an attachment named `products.csv`.
          headers:
            Content-Disposition:
              description: attachment; filename="products.csv"
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiProductsCsv
  /admin/api/products/restore:
    post:
      tags:
        - Admin Products
      summary: Restore selected products from trash
      description: |-
        Requires the admin permission `product-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductsRestore
  /admin/api/products/trash:
    post:
      tags:
        - Admin Products
      summary: Move selected products to trash
      description: |-
        Requires the admin permission `product-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductsTrash
  /admin/api/products/{id}:
    patch:
      tags:
        - Admin Products
      summary: Update product
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Product updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Requires the admin permission `product-edit`.

        The `slug` must be unique; a slug already used by another record is rejected with `code: "error"`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminProductCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminProductCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiProductsById
    delete:
      tags:
        - Admin Products
      summary: Move product to trash
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Soft deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiProductsById
  /admin/api/products/{id}/restore:
    post:
      tags:
        - Admin Products
      summary: Restore product from trash
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Restored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiProductsByIdRestore
  /admin/api/products/{id}/seo:
    patch:
      tags:
        - Admin Products
      summary: Update product SEO and OpenGraph attributes
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: SEO updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `product-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductSeoBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ProductSeoBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiProductsByIdSeo
  /admin/api/recommendation-jobs:
    post:
      tags:
        - Admin Products
      summary: Trigger ML Item-based Collaborative Filtering recompute
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `product-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiRecommendationJobs
  /admin/api/recommendation-jobs/latest:
    get:
      tags:
        - Admin Products
      summary: Progress of the recommendation recompute job
      description: Requires the admin permission `product-edit`.
      responses:
        '200':
          description: Result of the action.
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  status:
                    type: object
                    additionalProperties: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiRecommendationJobsLatest
  /admin/api/reviews:
    delete:
      tags:
        - Admin Reviews
      summary: Permanently delete selected reviews
      description: |-
        Requires the admin permission `review-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiReviews
  /admin/api/reviews/{id}:
    delete:
      tags:
        - Admin Reviews
      summary: Permanently delete a review
      description: Requires the admin permission `review-delete`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiReviewsById
  /admin/api/reviews/{id}/reports:
    delete:
      tags:
        - Admin Reviews
      summary: Clear the customer reports on a review
      description: Requires the admin permission `review-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiReviewsByIdReports
  /admin/api/reviews/{id}/status/{status}:
    put:
      tags:
        - Admin Reviews
      summary: Approve or reject a review
      description: Requires the admin permission `review-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: status
          in: path
          required: true
          schema:
            type: string
            enum:
              - approved
              - rejected
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: putAdminApiReviewsByIdStatusByStatus
  /admin/api/roles:
    post:
      tags:
        - Admin Access Control
      summary: Create administrative role
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `role-create`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminRoleCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminRoleCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiRoles
    delete:
      tags:
        - Admin Access Control
      summary: Permanently delete selected roles
      description: |-
        Requires the admin permission `role-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiRoles
  /admin/api/roles/restore:
    post:
      tags:
        - Admin Access Control
      summary: Restore selected roles from trash
      description: |-
        Requires the admin permission `role-edit`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiRolesRestore
  /admin/api/roles/trash:
    post:
      tags:
        - Admin Access Control
      summary: Move selected roles to trash
      description: |-
        Requires the admin permission `role-delete`.

        `ids` must be a non-empty array of ids.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdsBody'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiRolesTrash
  /admin/api/roles/{id}:
    patch:
      tags:
        - Admin Access Control
      summary: Edit administrative role
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `role-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminRoleCreateBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PostAdminRoleCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiRolesById
    delete:
      tags:
        - Admin Access Control
      summary: Move role to trash
      description: Requires the admin permission `role-delete`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
        - name: permanent
          in: query
          required: false
          description: '`true` deletes the record permanently instead of moving it to the trash.'
          schema:
            type: boolean
            default: false
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiRolesById
  /admin/api/roles/{id}/restore:
    post:
      tags:
        - Admin Access Control
      summary: Restore role from trash
      description: Requires the admin permission `role-edit`.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiRolesByIdRestore
  /admin/api/sessions:
    post:
      tags:
        - Admin Auth
      summary: Admin credentials login
      responses:
        '200':
          description: Admin authenticated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '429':
          $ref: '#/components/responses/RateLimited'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginBody'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/LoginBody'
      security: []
      operationId: postAdminApiSessions
  /admin/api/sessions/current:
    delete:
      tags:
        - Admin Auth
      summary: Admin sign out
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiSessionsCurrent
  /admin/api/settings/email:
    patch:
      tags:
        - Admin Settings & Ops
      summary: Configure Gmail app password for OTP delivery
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminSettingApiAppPasswordBody'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsEmail
  /admin/api/settings/general:
    patch:
      tags:
        - Admin Settings & Ops
      summary: Update website branding, logo, and title
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminSettingGeneralBody'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsGeneral
  /admin/api/settings/payment:
    patch:
      tags:
        - Admin Settings & Ops
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: Choose checkout payment methods and configure VNPAY and ZaloPay credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminSettingApiPaymentBody'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsPayment
  /admin/api/settings/shipping:
    patch:
      tags:
        - Admin Settings & Ops
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: Configure GoShip and the default parcel weight
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                defaultItemWeight:
                  oneOf:
                    - type: number
                      minimum: 0
                      maximum: 10000000
                    - type: string
                      enum:
                        - ''
                  description: Grams per unit for products without their own weight; an empty string clears it.
                goshipApiUrl:
                  type: string
                  format: uri
                  maxLength: 500
                tokenGoShip:
                  type: string
                  maxLength: 500
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsShipping
  /admin/api/settings/storefront:
    patch:
      tags:
        - Admin Settings & Ops
      summary: Set the theme, currency, locale, time zone and loyalty point rates
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminSettingStorefrontBody'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`. The store currency is what prices are entered in and orders are charged in; changing it does not convert existing prices.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsStorefront
  /admin/api/settings/pages:
    patch:
      tags:
        - Admin Settings & Ops
      summary: Set the text of the About, FAQ and policy pages
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminSettingPagesBody'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`. The HTML is sanitized when shown; an empty page keeps its default text.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsPages
  /admin/api/settings/social-login:
    patch:
      tags:
        - Admin Settings & Ops
      summary: Configure Google & Facebook OAuth app credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminSettingApiLoginSocialBody'
      responses:
        '200':
          description: Saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `setting-edit`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiSettingsSocialLogin
  /admin/api/slugs:
    post:
      tags:
        - Admin Settings & Ops
      summary: Generate unique SEO slug for entity title
      description: Requires an admin session; no specific permission is checked.
      responses:
        '200':
          description: Generated slug.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                modalName:
                  type: string
                string:
                  type: string
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiSlugs
  /admin/api/statistics/orders:
    get:
      tags:
        - Admin Dashboard
      summary: Order statistics for a custom date range
      description: Requires the admin permission `dashboard`.
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
          description: YYYY-MM-DD
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
          description: YYYY-MM-DD
      responses:
        '200':
          description: 'Chart data (`code: error` for an invalid range)'
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiStatisticsOrders
  /admin/api/statistics/revenue-by-time:
    get:
      tags:
        - Admin Dashboard
      summary: Revenue series for a custom date range
      description: Requires the admin permission `dashboard`.
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date
          description: YYYY-MM-DD (year 2000 or later, not after next year)
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date
          description: YYYY-MM-DD, not before `from`
      responses:
        '200':
          description: 'Chart data (`code: error` for an invalid range)'
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  from:
                    type: string
                  to:
                    type: string
                  granularity:
                    type: string
                    enum:
                      - hour
                      - day
                      - month
                additionalProperties: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: getAdminApiStatisticsRevenueByTime
  /admin/api/templates:
    post:
      tags:
        - Admin Content & CMS
      summary: Create page layout template with ordered block sequences
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `template-create`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminTemplateCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: postAdminApiTemplates
  /admin/api/templates/{id}:
    patch:
      tags:
        - Admin Content & CMS
      summary: Edit page layout template
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `template-edit`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminTemplateCreateBody'
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: patchAdminApiTemplatesById
    delete:
      tags:
        - Admin Content & CMS
      summary: Delete page layout template
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Requires the admin permission `template-delete`.
      security:
        - AdminCookie: []
        - AdminBearer: []
      operationId: deleteAdminApiTemplatesById
  /admin/order/flagged:
    get:
      tags:
        - Admin Orders
      summary: List orders flagged by Isolation Forest fraud detection
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          $ref: '#/components/responses/RedirectToLogin'
        '403':
          $ref: '#/components/responses/PermissionDenied'
      description: Requires the admin permission `order-edit`.
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      security:
        - AdminCookie: []
      operationId: getAdminOrderFlagged
  /api/article-categories/{slug}/views:
    post:
      tags:
        - Client Articles
      summary: Count a view of an article category
      description: Called by the page once it has loaded, because opening a page (GET) never changes data. A browser is counted once per 30 minutes.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      operationId: postApiArticleCategoriesBySlugViews
      security: []
  /api/articles/{slug}/views:
    post:
      tags:
        - Client Articles
      summary: Count a view of an article
      description: Called by the page once it has loaded, because opening a page (GET) never changes data. A browser is counted once per 30 minutes.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      operationId: postApiArticlesBySlugViews
      security: []
  /api/cart/quote:
    post:
      tags:
        - Client Cart & Checkout
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: Get verified cart lines and shipping options
      responses:
        '200':
          description: Cart quote with verified prices and shipping options.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CartQuoteResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - cart
              properties:
                cart:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    required:
                      - productId
                      - quantity
                    properties:
                      productId:
                        type: string
                      quantity:
                        type: integer
                        minimum: 0
                      variant:
                        type: array
                        items:
                          type: object
                userAddress:
                  type: object
                  properties:
                    latitude:
                      type: number
                      minimum: -90
                      maximum: 90
                    longitude:
                      type: number
                      minimum: -180
                      maximum: 180
      operationId: postApiCartQuote
      security: []
  /api/chat-rooms/current/attachments:
    post:
      tags:
        - Client Chat
      summary: Upload images or files to chat room
      responses:
        '200':
          description: Uploaded chat attachments.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/UploadRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  maxItems: 10
                  items:
                    type: string
                    format: binary
                  description: Up to 10 files, 10 MB each; only allowed file types.
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: postApiChatRoomsCurrentAttachments
  /api/chat-rooms/current/messages:
    get:
      tags:
        - Client Chat
      summary: Fetch paginated chat messages for current session
      responses:
        '200':
          description: Chat message history.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      parameters:
        - name: lastMessageId
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
      operationId: getApiChatRoomsCurrentMessages
      security: []
  /api/chat-rooms/current/rating:
    put:
      tags:
        - Client Chat
      summary: Rate the support conversation
      responses:
        '200':
          description: Rating saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - stars
              properties:
                stars:
                  type: integer
                  minimum: 1
                  maximum: 5
                comment:
                  type: string
                  maxLength: 500
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: putApiChatRoomsCurrentRating
  /api/compare/lookup:
    post:
      tags:
        - Client Wishlist & Compare
      summary: Fetch multi-product comparison specs by IDs
      responses:
        '200':
          description: Resolved comparison matrix.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostCompareListBody'
      operationId: postApiCompareLookup
      security: []
  /api/contact-inquiries:
    post:
      tags:
        - Client Contact & Pages
      summary: Submit contact inquiry message (rate-limited)
      responses:
        '200':
          description: Inquiry saved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 5
                  maxLength: 50
                email:
                  type: string
                  format: email
                subject:
                  type: string
                  minLength: 5
                  maxLength: 100
                  description: Empty string allowed.
                message:
                  type: string
                  minLength: 10
                  maxLength: 500
              required:
                - name
                - email
                - message
      operationId: postApiContactInquiries
      security: []
  /api/coupon-checks:
    post:
      tags:
        - Client Cart & Checkout
      summary: Verify discount coupon code and return discount amount
      responses:
        '200':
          description: Coupon validity and discount value.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                coupon:
                  type: string
              required:
                - coupon
      operationId: postApiCouponChecks
      security: []
  /api/customers:
    post:
      tags:
        - Client Auth
      summary: Register customer account
      responses:
        '200':
          description: Registration response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                fullName:
                  type: string
                  minLength: 5
                  maxLength: 50
                email:
                  type: string
                  format: email
                phone:
                  type: string
                password:
                  type: string
                  minLength: 8
              required:
                - fullName
                - email
                - phone
                - password
      operationId: postApiCustomers
      security: []
  /api/customers/me:
    patch:
      tags:
        - Client Dashboard
      summary: Update customer profile information
      responses:
        '200':
          description: Profile updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                fullName:
                  type: string
                  minLength: 5
                  maxLength: 50
                phone:
                  type: string
                  description: Empty string allowed.
              required:
                - fullName
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: patchApiCustomersMe
  /api/customers/me/addresses:
    post:
      tags:
        - Client Dashboard
      summary: Add delivery address
      responses:
        '200':
          description: Address created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostDashboardAddressCreateBody'
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: postApiCustomersMeAddresses
  /api/customers/me/addresses/{id}:
    patch:
      tags:
        - Client Dashboard
      summary: Edit delivery address
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Address updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostDashboardAddressCreateBody'
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: patchApiCustomersMeAddressesById
    delete:
      tags:
        - Client Dashboard
      summary: Delete delivery address
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Address deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: deleteApiCustomersMeAddressesById
  /api/customers/me/addresses/{id}/default:
    put:
      tags:
        - Client Dashboard
      summary: Set address as default shipping address
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Default address updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: putApiCustomersMeAddressesByIdDefault
  /api/customers/me/avatar:
    put:
      tags:
        - Client Dashboard
      summary: Upload and update customer profile avatar
      responses:
        '200':
          description: Avatar updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/UploadRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - avatar
              properties:
                avatar:
                  type: string
                  format: binary
                  description: A JPG, PNG, GIF or WEBP image, at most 5 MB.
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: putApiCustomersMeAvatar
  /api/customers/me/email:
    put:
      tags:
        - Client Dashboard
      summary: Verify OTP and confirm email change
      description: The code is valid for 10 minutes and accepts at most 5 attempts.
      responses:
        '200':
          description: Email updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                otp:
                  type: string
                  maxLength: 6
                  minLength: 6
              required:
                - otp
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: putApiCustomersMeEmail
  /api/customers/me/email-changes:
    post:
      tags:
        - Client Dashboard
      summary: Request email change OTP code
      responses:
        '200':
          description: OTP sent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      description: |-
        Rate limited (HTTP 429 when exceeded).

        The new email must differ from the current one and must not belong to another account. The code sent to it is valid for 10 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                newEmail:
                  type: string
                  format: email
              required:
                - newEmail
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: postApiCustomersMeEmailChanges
  /api/customers/me/password:
    put:
      tags:
        - Client Auth
      summary: Set new password after OTP verification
      responses:
        '200':
          description: Password reset successful.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminAccountAdminChangePasswordByIdBody'
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: putApiCustomersMePassword
  /api/orders:
    post:
      tags:
        - Client Cart & Checkout
      summary: Create and place order (COD, VNPAY, ZaloPay) with atomic loyalty point deduction
      description: The order is rejected when an item is no longer available or when the coupon has reached its usage limit.
      responses:
        '200':
          description: Order created; `redirectUrl` says where the customer goes next.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderCreatedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                fullName:
                  type: string
                  minLength: 5
                  maxLength: 50
                phone:
                  type: string
                address:
                  type: string
                longitude:
                  type: number
                latitude:
                  type: number
                note:
                  type: string
                  description: Empty string allowed.
                items:
                  type: array
                  items:
                    type: object
                    properties:
                      productId:
                        type: string
                      quantity:
                        type: integer
                        minimum: 1
                        maximum: 999
                      checked:
                        type: boolean
                      variant:
                        type: array
                        items:
                          type: object
                          properties:
                            attrId:
                              type: string
                            value:
                              type: string
                            label:
                              type: string
                            attrType:
                              type: string
                              description: Empty string allowed.
                          required:
                            - attrId
                            - value
                            - label
                    required:
                      - productId
                      - quantity
                  minItems: 1
                coupon:
                  type: string
                  description: Empty string allowed.
                paymentMethod:
                  type: string
                  enum:
                    - money
                    - vnpay
                    - zalopay
                shippingMethod:
                  type: string
                  description: The `id` of a rate from the cart quote's `shippingOptions`.
                usePoint:
                  type: boolean
                usedPoint:
                  type: integer
                  minimum: 0
                  description: Number of loyalty points to redeem.
              required:
                - fullName
                - phone
                - address
                - longitude
                - latitude
                - items
                - paymentMethod
                - shippingMethod
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: postApiOrders
  /api/password-resets:
    post:
      tags:
        - Client Auth
      summary: Request OTP password reset code
      responses:
        '200':
          description: Generic confirmation message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      description: |-
        Rate limited (HTTP 429 when exceeded).

        The code is valid for 5 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
              required:
                - email
      operationId: postApiPasswordResets
      security: []
  /api/password-resets/verification:
    post:
      tags:
        - Client Auth
      summary: Verify password reset OTP
      responses:
        '200':
          description: OTP verified.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: |-
        Rate limited (HTTP 429 when exceeded).

        The code is valid for 5 minutes and accepts at most 5 attempts.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                otp:
                  type: string
              required:
                - email
                - otp
      operationId: postApiPasswordResetsVerification
      security: []
  /api/product-categories/{slug}/views:
    post:
      tags:
        - Client Products
      summary: Count a view of a product category
      description: Called by the page once it has loaded, because opening a page (GET) never changes data. A browser is counted once per 30 minutes.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      operationId: postApiProductCategoriesBySlugViews
      security: []
  /api/product-suggestions:
    get:
      tags:
        - Client Products
      summary: Realtime auto-complete search suggestions
      parameters:
        - name: keyword
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: List of matching product suggestions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/ServerError'
      operationId: getApiProductSuggestions
      security: []
  /api/products/{slug}/views:
    post:
      tags:
        - Client Products
      summary: Count a view of a product
      description: Called by the page once it has loaded, because opening a page (GET) never changes data. A browser is counted once per 30 minutes. It also adds the product to the recently viewed list kept in a cookie.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
      operationId: postApiProductsBySlugViews
      security: []
  /api/reviews:
    post:
      tags:
        - Client Dashboard
      summary: Submit product review with photo uploads from order history
      description: Only products from `completed` orders can be reviewed. To attach photos, send the request as `multipart/form-data` with up to 5 JPG, PNG, GIF or WEBP images (5 MB each) in the `images` field.
      responses:
        '200':
          description: Review submitted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/UploadRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                orderId:
                  type: string
                orderItemId:
                  type: string
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
                comment:
                  type: string
                  maxLength: 300
              required:
                - orderId
                - orderItemId
                - rating
                - comment
          multipart/form-data:
            schema:
              type: object
              properties:
                orderId:
                  type: string
                orderItemId:
                  type: string
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
                comment:
                  type: string
                  maxLength: 300
                images:
                  type: array
                  maxItems: 5
                  items:
                    type: string
                    format: binary
                  description: Up to 5 JPG, PNG, GIF or WEBP images, at most 5 MB each.
              required:
                - orderId
                - orderItemId
                - rating
                - comment
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: postApiReviews
  /api/reviews/{id}/reports:
    post:
      tags:
        - Client Products
      summary: Report an inappropriate product review
      parameters:
        - name: id
          in: path
          required: true
          schema:
            $ref: '#/components/schemas/ObjectId'
      responses:
        '200':
          description: Report registered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - reason
              properties:
                reason:
                  type: string
      security:
        - UserCookie: []
        - UserBearer: []
      operationId: postApiReviewsByIdReports
  /api/sessions:
    post:
      tags:
        - Client Auth
      summary: Customer login
      responses:
        '200':
          description: Login response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PermissionDenied'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      description: Rate limited (HTTP 429 when exceeded).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginBody'
      operationId: postApiSessions
      security: []
  /api/sessions/current:
    delete:
      tags:
        - Client Auth
      summary: Customer sign out
      description: Ends the session, clears the auth cookies and revokes the refresh token. State-changing, so it is POST only.
      responses:
        '200':
          $ref: '#/components/responses/StandardResult'
      operationId: deleteApiSessionsCurrent
      security: []
    get:
      tags:
        - Client Chat
      summary: Verify or create support chat session
      responses:
        '200':
          description: Session data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
      operationId: getApiSessionsCurrent
      security: []
  /api/wishlist/lookup:
    post:
      tags:
        - Client Wishlist & Compare
      summary: Fetch product details for wishlist item IDs
      responses:
        '200':
          description: Resolved wishlist items.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/ServerError'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostCompareListBody'
      operationId: postApiWishlistLookup
      security: []
  /article:
    get:
      tags:
        - Client Articles
      summary: List blog and news articles
      parameters:
        - name: page
          in: query
          schema:
            type: integer
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          description: Redirect (record not found, not signed in, or wrong state)
      operationId: getArticle
      security: []
  /article/detail/{slug}:
    get:
      tags:
        - Client Articles
      summary: Read article detail by slug
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          description: Redirect (record not found, not signed in, or wrong state)
      operationId: getArticleDetailBySlug
      security: []
  /auth/facebook:
    get:
      tags:
        - Client Auth
      summary: Initiate Facebook OAuth2 authentication flow
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
      operationId: getAuthFacebook
      security: []
  /auth/facebook/callback:
    get:
      tags:
        - Client Auth
      summary: Facebook OAuth callback
      parameters:
        - name: code
          in: query
          schema:
            type: string
        - name: state
          in: query
          schema:
            type: string
      responses:
        '302':
          description: Redirects to the storefront after sign-in, or to /auth/login on failure
      operationId: getAuthFacebookCallback
      security: []
  /auth/google:
    get:
      tags:
        - Client Auth
      summary: Initiate Google OAuth2 authentication flow
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
      operationId: getAuthGoogle
      security: []
  /auth/google/callback:
    get:
      tags:
        - Client Auth
      summary: Google OAuth callback
      parameters:
        - name: code
          in: query
          schema:
            type: string
        - name: state
          in: query
          schema:
            type: string
      responses:
        '302':
          description: Redirects to the storefront after sign-in, or to /auth/login on failure
      operationId: getAuthGoogleCallback
      security: []
  /cart:
    get:
      tags:
        - Client Cart & Checkout
      summary: View shopping cart
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
      operationId: getCart
      security: []
  /files:
    post:
      tags:
        - FileManager Microservice
      summary: Upload files to the media store
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Upload result
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  saveLinks:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  maxItems: 20
                  items:
                    type: string
                    format: binary
                  description: Up to 20 files, 10 MB each; only allowed file types
                folderPath:
                  type: string
                  description: 'Target folder (default: root)'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: postFmFiles
    get:
      tags:
        - FileManager Microservice
      summary: List files of a folder (paginated)
      security:
        - FileManagerBearer: []
      parameters:
        - name: folderPath
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: File list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: getFmFiles
    delete:
      tags:
        - FileManager Microservice
      summary: Delete a file
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: deleteFmFiles
      parameters:
        - name: folder
          in: query
          required: true
          schema:
            type: string
        - name: fileName
          in: query
          required: true
          schema:
            type: string
  /files/location:
    patch:
      tags:
        - FileManager Microservice
      summary: Move a file to another folder
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Moved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerMoveFileBody'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: patchFmFilesLocation
  /files/name:
    patch:
      tags:
        - FileManager Microservice
      summary: Rename a file
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Renamed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchFileManagerChangeFileNameBody'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: patchFmFilesName
  /folders:
    post:
      tags:
        - FileManager Microservice
      summary: Create a folder
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Folder created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PostAdminFileManagerFolderCreateBody'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: postFmFolders
    get:
      tags:
        - FileManager Microservice
      summary: List sub-folders
      security:
        - FileManagerBearer: []
      parameters:
        - name: folderPath
          in: query
          schema:
            type: string
          description: 'Folder to list (default: root)'
      responses:
        '200':
          description: Folder list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: getFmFolders
    delete:
      tags:
        - FileManager Microservice
      summary: Delete a folder
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Folder deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: deleteFmFolders
      parameters:
        - name: folderPath
          in: query
          required: true
          schema:
            type: string
  /folders/location:
    patch:
      tags:
        - FileManager Microservice
      summary: Move a folder
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Folder moved.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerFolderMoveBody'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: patchFmFoldersLocation
  /folders/name:
    patch:
      tags:
        - FileManager Microservice
      summary: Rename a folder
      security:
        - FileManagerBearer: []
      responses:
        '200':
          description: Folder renamed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '401':
          $ref: '#/components/responses/InvalidBearerSecret'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
        '409':
          $ref: '#/components/responses/FileManagerConflict'
        '429':
          $ref: '#/components/responses/TooManyFailedAttempts'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchAdminFileManagerFolderRenameBody'
      servers:
        - url: http://localhost:4000
          description: FileManager Microservice
      operationId: patchFmFoldersName
  /manifest.webmanifest:
    get:
      tags:
        - Site Endpoints
      summary: PWA web app manifest generated from the store settings
      responses:
        '200':
          description: Web App Manifest
          content:
            application/manifest+json:
              schema:
                type: object
                additionalProperties: true
      operationId: getManifestWebmanifest
      security: []
  /media/{subPath}:
    get:
      tags:
        - FileManager Microservice
      summary: Stream media asset directly from disk
      parameters:
        - name: subPath
          in: path
          required: true
          description: File path below the media root, for example `users/abc/photo.png`. Slashes may be sent as `/` or `%2F`. Paths with a `temp` folder or that leave the media root are rejected.
          schema:
            type: string
        - name: type
          in: query
          description: '`download` forces a file download. Without it, images, video, audio and PDF are shown inline and every other type is sent as an attachment.'
          schema:
            type: string
            enum:
              - download
      responses:
        '200':
          description: 'The file. Errors are real HTTP statuses (400, 403, 404) with a JSON body `{ "code": "error", "message": "..." }`.'
          headers:
            Content-Disposition:
              description: Present when the file is sent as an attachment.
              schema:
                type: string
          content:
            image/*:
              schema:
                type: string
                format: binary
            video/*:
              schema:
                type: string
                format: binary
            audio/*:
              schema:
                type: string
                format: binary
            application/pdf:
              schema:
                type: string
                format: binary
            application/octet-stream:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/FileManagerBadRequest'
        '403':
          $ref: '#/components/responses/FileManagerForbidden'
        '404':
          $ref: '#/components/responses/FileManagerNotFound'
      description: Served by the Web app (proxy to the media store); FileManager serves the same path on port 4000.
      operationId: getMediaBySubPath
      security: []
  /order/payment-{method}:
    get:
      tags:
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: Start (or retry) the payment of an order on an online gateway
      parameters:
        - name: method
          in: path
          required: true
          schema:
            type: string
            enum:
              - vnpay
              - zalopay
          description: An online payment method that is enabled at checkout
        - name: orderCode
          in: query
          required: true
          schema:
            type: string
            example: AB123456
          description: Order tracking code
        - name: phone
          in: query
          required: true
          schema:
            type: string
            example: '0901234567'
          description: Customer checkout phone number
      responses:
        '302':
          description: Redirect to the gateway, to the order summary when it is already paid, or home when the order or method is not available
        '429':
          $ref: '#/components/responses/RateLimited'
      description: Rate limited (HTTP 429 when exceeded). The checkout response's `redirectUrl` points here for online methods.
      operationId: getOrderPaymentByMethod
      security: []
  /order/payment-vnpay-ipn:
    get:
      tags:
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: VNPay IPN (server-to-server payment notification)
      description: Called by VNPay, not by browsers. Verifies the HMAC-SHA512 signature and the amount, then marks the order paid. VNPay retries until it receives `RspCode` 00 or 02. The IPN URL must be registered in the VNPay merchant portal and reachable from the internet.
      parameters:
        - name: vnp_Amount
          in: query
          schema:
            type: string
        - name: vnp_BankCode
          in: query
          schema:
            type: string
        - name: vnp_ResponseCode
          in: query
          schema:
            type: string
        - name: vnp_TransactionNo
          in: query
          schema:
            type: string
        - name: vnp_TransactionStatus
          in: query
          schema:
            type: string
        - name: vnp_TxnRef
          in: query
          schema:
            type: string
        - name: vnp_SecureHash
          in: query
          schema:
            type: string
      responses:
        '200':
          description: VNPay acknowledgement
          content:
            application/json:
              schema:
                type: object
                properties:
                  RspCode:
                    type: string
                    enum:
                      - '00'
                      - '01'
                      - '02'
                      - '04'
                      - '97'
                      - '99'
                    description: 00 confirmed, 01 order not found, 02 already confirmed, 04 invalid amount, 97 invalid signature, 99 unknown error
                  Message:
                    type: string
      operationId: getOrderPaymentVnpayIpn
      security: []
  /order/payment-vnpay-result:
    get:
      tags:
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: 'VNPAY return callback: verifies the HMAC-SHA512 signature (vnp_SecureHash) and the amount, then marks the order paid and redirects to the order page'
      parameters:
        - name: vnp_ResponseCode
          in: query
          schema:
            type: string
            example: '00'
          description: 00 indicates successful transaction
        - name: vnp_TxnRef
          in: query
          schema:
            type: string
            example: ORD-123456
          description: Order code
        - name: vnp_Amount
          in: query
          schema:
            type: string
            example: '10000000'
          description: Amount in VND x 100
        - name: vnp_SecureHash
          in: query
          schema:
            type: string
          description: HMAC-SHA512 signature
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          description: Redirect (record not found, not signed in, or wrong state)
      operationId: getOrderPaymentVnpayResult
      security: []
  /order/payment-zalopay-callback:
    post:
      tags:
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: ZaloPay secondary merchant callback endpoint
      responses:
        '200':
          description: Callback received.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: string
                mac:
                  type: string
      operationId: postOrderPaymentZalopayCallback
      security: []
  /order/payment-zalopay-result:
    post:
      tags:
        - Payment & Logistics (VNPAY, ZaloPay, GoShip)
      summary: ZaloPay Webhook / IPN notification callback
      responses:
        '200':
          description: Webhook acknowledgment to ZaloPay server
          content:
            application/json:
              schema:
                type: object
                properties:
                  return_code:
                    type: integer
                    example: 1
                  return_message:
                    type: string
                    example: success
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - data
                - mac
              properties:
                data:
                  type: string
                  description: JSON string containing app_id, app_trans_id, amount, embed_data
                mac:
                  type: string
                  description: HMAC-SHA256 verification hash
                type:
                  type: integer
      operationId: postOrderPaymentZalopayResult
      security: []
  /product:
    get:
      tags:
        - Client Products
      summary: Browse and search product catalog
      parameters:
        - name: keyword
          in: query
          schema:
            type: string
        - name: category
          in: query
          schema:
            type: string
        - name: sortKey
          in: query
          schema:
            type: string
        - name: sortValue
          in: query
          schema:
            type: string
            enum:
              - asc
              - desc
        - name: minPrice
          in: query
          schema:
            type: number
        - name: maxPrice
          in: query
          schema:
            type: number
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          description: Redirect (record not found, not signed in, or wrong state)
      operationId: getProduct
      security: []
  /product/category/{slug}:
    get:
      tags:
        - Client Products
      summary: Filter products by category slug
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          description: Redirect (record not found, not signed in, or wrong state)
      operationId: getProductCategoryBySlug
      security: []
  /product/detail/{slug}:
    get:
      tags:
        - Client Products
      summary: View product details by slug
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          $ref: '#/components/responses/HtmlPage'
        '302':
          description: Redirect (record not found, not signed in, or wrong state)
      operationId: getProductDetailBySlug
      security: []
  /robots.txt:
    get:
      tags:
        - Site Endpoints
      summary: Robots.txt
      responses:
        '200':
          description: Robots rules
          content:
            text/plain:
              schema:
                type: string
      operationId: getRobotsTxt
      security: []
  /sitemap.xml:
    get:
      tags:
        - Site Endpoints
      summary: XML sitemap (cached for one hour)
      responses:
        '200':
          description: Sitemap
          content:
            application/xml:
              schema:
                type: string
        '500':
          $ref: '#/components/responses/ServerError'
      operationId: getSitemapXml
      security: []
components:
  securitySchemes:
    UserCookie:
      type: apiKey
      in: cookie
      name: tokenUser
      description: Customer access token cookie (browsers). Set by the login endpoints; HTTP-only.
    AdminCookie:
      type: apiKey
      in: cookie
      name: tokenAdmin
      description: Admin access token cookie (browsers). Set by the login endpoint; HTTP-only.
    FileManagerBearer:
      type: http
      scheme: bearer
      description: The shared FILE_MANAGER_SECRET
    UserBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Customer access token from `accessToken` in the login response, sent as `Authorization: Bearer <token>`. When this header is present the cookies are ignored.'
    AdminBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Admin access token from `accessToken` in the login response, sent as `Authorization: Bearer <token>`. When this header is present the cookies are ignored.'
  schemas:
    StandardResponse:
      type: object
      properties:
        code:
          type: string
          enum:
            - success
        message:
          type: string
          example: Action completed successfully!
    ErrorResponse:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          enum:
            - error
        message:
          type: string
          example: Invalid data!
    ObjectId:
      type: string
      pattern: ^[0-9a-fA-F]{24}$
      description: MongoDB ObjectId
    IdsBody:
      type: object
      required:
        - ids
      properties:
        ids:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ObjectId'
          description: Ids of the records to act on
    SeoBody:
      type: object
      properties:
        seoTitle:
          type: string
          description: Meta title
        seoDescription:
          type: string
          description: Meta description
        seoKeywords:
          type: string
          description: JSON array string or single keyword
        seoRobotsIndex:
          type: string
          enum:
            - 'true'
            - 'false'
        seoRobotsFollow:
          type: string
          enum:
            - 'true'
            - 'false'
        seoOgTitle:
          type: string
        seoOgDescription:
          type: string
        seoOgImage:
          type: string
          description: Image path
    ProductSeoBody:
      type: object
      properties:
        seoTitle:
          type: string
          maxLength: 60
          description: Meta title
        seoDescription:
          type: string
          maxLength: 160
          description: Meta description
        seoKeywords:
          type: string
          description: JSON array string or single keyword
        seoRobotsIndex:
          type: string
          enum:
            - 'true'
            - 'false'
        seoRobotsFollow:
          type: string
          enum:
            - 'true'
            - 'false'
        seoOgTitle:
          type: string
          maxLength: 95
        seoOgDescription:
          type: string
          maxLength: 200
        seoOgImage:
          type: string
          description: Image path
    PatchAdminAccountAdminChangePasswordByIdBody:
      type: object
      properties:
        password:
          type: string
          minLength: 8
      required:
        - password
    PostAdminAccountAdminCreateBody:
      type: object
      properties:
        fullName:
          type: string
          minLength: 5
          maxLength: 50
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 8
        status:
          type: string
          enum:
            - initial
            - active
            - inactive
            - ''
        avatar:
          type: string
          description: Empty string allowed.
        roles:
          type: string
          description: Empty string allowed.
      required:
        - fullName
        - email
        - password
    PatchAdminAccountAdminEditByIdBody:
      type: object
      properties:
        fullName:
          type: string
          minLength: 5
          maxLength: 50
        email:
          type: string
          format: email
        status:
          type: string
          enum:
            - initial
            - active
            - inactive
            - ''
        avatar:
          type: string
          description: Empty string allowed.
        roles:
          type: string
          description: Empty string allowed.
      required:
        - fullName
        - email
    LoginBody:
      type: object
      properties:
        email:
          type: string
          format: email
          example: customer@example.com
        password:
          type: string
          example: MyPassw0rd!
        rememberPassword:
          type: boolean
          default: false
      required:
        - email
        - password
    PostAdminArticleCategoryCreateBody:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
          maxLength: 200
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        parent:
          type: string
          description: Empty string allowed.
        status:
          type: string
          enum:
            - active
            - inactive
            - ''
        avatar:
          type: string
          description: Empty string allowed.
        description:
          type: string
          description: Empty string allowed.
      required:
        - name
        - slug
    PostAdminArticleCreateBody:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
          maxLength: 200
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        category:
          type: string
          description: Empty string allowed.
        status:
          type: string
          enum:
            - draft
            - published
            - archived
            - ''
        avatar:
          type: string
          description: Empty string allowed.
        description:
          type: string
          description: Empty string allowed.
        content:
          type: string
          description: Empty string allowed.
      required:
        - name
        - slug
    PostAdminBlockCreateBody:
      type: object
      properties:
        name:
          type: string
          maxLength: 200
        fileName:
          type: string
        status:
          type: string
          enum:
            - active
            - inactive
            - ''
        data:
          type: object
      required:
        - name
        - fileName
    PostAdminChatEditReplyByIdBody:
      type: object
      required:
        - content
      properties:
        content:
          type: string
          description: Draft reply
    PostAdminCouponCreateBody:
      type: object
      properties:
        code:
          type: string
        name:
          type: string
        typeDiscount:
          type: string
          enum:
            - percentage
            - fixed
            - ''
        value:
          type: number
          minimum: 0
          description: Empty string allowed.
        minOrderValue:
          type: number
          minimum: 0
          description: Empty string allowed.
        maxDiscountValue:
          type: number
          minimum: 0
          description: Empty string allowed.
        usageLimit:
          type: integer
          minimum: 0
          description: Empty string allowed.
        typeDisplay:
          type: string
          enum:
            - public
            - private
            - ''
        status:
          type: string
          enum:
            - active
            - inactive
            - ''
        startDate:
          type: string
          pattern: ^(\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4})$
          description: ISO 8601 date (YYYY-MM-DD) preferred. DD/MM/YYYY also accepted via form submission.
        endDate:
          type: string
          pattern: ^(\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4})$
          description: ISO 8601 date (YYYY-MM-DD) preferred. DD/MM/YYYY also accepted via form submission.
        description:
          type: string
          description: Empty string allowed.
      required:
        - code
        - name
    PatchAdminFileManagerChangeFileNameBody:
      type: object
      required:
        - folder
        - oldFileName
        - newFileName
      properties:
        folder:
          type: string
        oldFileName:
          type: string
        newFileName:
          type: string
          maxLength: 255
          pattern: ^[^/\\:*?"<>|]+$
          description: 'A file name without any of `/ \ : * ? " < > |`.'
    PatchFileManagerChangeFileNameBody:
      type: object
      required:
        - folder
        - oldFileName
        - newFileName
      properties:
        folder:
          type: string
        oldFileName:
          type: string
        newFileName:
          type: string
          description: A plain file name without a path. Its extension must be one the media store allows.
    PostAdminFileManagerFolderCreateBody:
      type: object
      required:
        - folderName
      properties:
        folderName:
          type: string
          pattern: ^(?!.*\.\.)[^/\\:*?"<>|]+$
          description: 'Cannot contain `/ \ : * ? " < > |` or `..`. The name `temp` is reserved.'
        folderPath:
          type: string
          description: Parent folder
    PatchAdminFileManagerFolderMoveBody:
      type: object
      required:
        - folderPath
      properties:
        folderPath:
          type: string
        targetFolder:
          type: string
          description: Destination folder. Omit it to use the media root.
    PatchAdminFileManagerFolderRenameBody:
      type: object
      required:
        - folderPath
        - newFolderName
      properties:
        folderPath:
          type: string
        newFolderName:
          type: string
          pattern: ^(?!.*\.\.)[^/\\:*?"<>|]+$
          description: 'Cannot contain `/ \ : * ? " < > |` or `..`.'
    PatchAdminFileManagerMoveFileBody:
      type: object
      required:
        - folder
        - fileName
      properties:
        folder:
          type: string
        fileName:
          type: string
        targetFolder:
          type: string
          description: Destination folder. Omit it to use the media root.
    PatchAdminOrderEditByIdBody:
      type: object
      required:
        - orderStatus
        - paymentStatus
      properties:
        orderStatus:
          type: string
          enum:
            - pending
            - confirmed
            - shipping
            - completed
            - cancelled
            - returned
        paymentStatus:
          type: string
          enum:
            - unpaid
            - paid
            - refunded
        note:
          type: string
          description: Internal note
    PostAdminProductAttributeCreateBody:
      type: object
      properties:
        name:
          type: string
        type:
          type: string
          enum:
            - text
            - select
            - color
            - ''
        options:
          type: string
          description: Empty string allowed.
      required:
        - name
    PostAdminProductCreateBody:
      type: object
      properties:
        name:
          type: string
        slug:
          type: string
          maxLength: 200
          pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
        position:
          type: integer
          minimum: 0
          description: Accepts both integer and string-encoded integer via multipart.
        status:
          type: string
          enum:
            - draft
            - active
            - inactive
            - ''
        category:
          type: string
          description: Empty string allowed.
        description:
          type: string
          description: Empty string allowed.
        content:
          type: string
          description: Empty string allowed.
        images:
          type: string
          description: Empty string allowed.
        priceOld:
          type: number
          minimum: 0
          description: In the store currency, rounded to its decimals. Accepts a number or a string-encoded number via multipart.
        priceNew:
          type: number
          minimum: 0
          description: In the store currency, rounded to its decimals. Accepts a number or a string-encoded number via multipart.
        attributes:
          type: string
          description: Empty string allowed.
        variants:
          type: string
          description: Empty string allowed.
        stock:
          type: integer
          minimum: 0
          description: Accepts both integer and string-encoded integer via multipart.
        weight:
          type: integer
          minimum: 0
          maximum: 10000000
          description: Shipping weight of one unit in grams; empty uses the store default.
        tags:
          type: string
          description: Empty string allowed.
        boughtTogether:
          type: string
          description: Empty string allowed.
      required:
        - name
        - slug
    PostAdminRoleCreateBody:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
          description: Empty string allowed.
        permissions:
          type: string
          description: Empty string allowed.
        status:
          type: string
          enum:
            - active
            - inactive
            - ''
      required:
        - name
    PostAdminTemplateCreateBody:
      type: object
      properties:
        name:
          type: string
          maxLength: 200
        slug:
          type: string
          maxLength: 200
        status:
          type: string
          enum:
            - active
            - inactive
            - ''
        blocks:
          type: array
          items:
            type: object
            properties:
              blockId:
                type: string
                maxLength: 24
                minLength: 24
              position:
                type: integer
                minimum: 0
            required:
              - blockId
              - position
      required:
        - name
        - slug
    PostCompareListBody:
      type: object
      properties:
        ids:
          type: array
          items:
            type: string
    PostDashboardAddressCreateBody:
      type: object
      properties:
        fullName:
          type: string
          minLength: 5
          maxLength: 50
        phone:
          type: string
        address:
          type: string
        longitude:
          type: number
        latitude:
          type: number
        isDefault:
          type: boolean
      required:
        - fullName
        - phone
        - address
        - longitude
        - latitude
        - isDefault
    PatchAdminSettingGeneralBody:
      type: object
      properties:
        websiteName:
          type: string
          maxLength: 100
        domainWebsite:
          type: string
          format: uri
          maxLength: 500
        logo:
          type: string
          maxLength: 500
        favicon:
          type: string
          maxLength: 500
        shopSenderName:
          type: string
          maxLength: 100
        shopSenderPhone:
          type: string
          maxLength: 30
        shopSenderAddress:
          type: string
          maxLength: 300
        contactEmail:
          type: string
          format: email
          maxLength: 254
        shopLat:
          oneOf:
            - type: number
              minimum: -90
              maximum: 90
            - type: string
              enum:
                - ''
          description: A number, or an empty string to clear it.
        shopLng:
          oneOf:
            - type: number
              minimum: -180
              maximum: 180
            - type: string
              enum:
                - ''
          description: A number, or an empty string to clear it.
        storeDescription:
          type: string
          maxLength: 300
          description: Short text under the footer logo.
        facebookUrl:
          type: string
          format: uri
          maxLength: 500
        instagramUrl:
          type: string
          format: uri
          maxLength: 500
        youtubeUrl:
          type: string
          format: uri
          maxLength: 500
        tiktokUrl:
          type: string
          format: uri
          maxLength: 500
        linkedinUrl:
          type: string
          format: uri
          maxLength: 500
        xUrl:
          type: string
          format: uri
          maxLength: 500
    PatchAdminSettingApiPaymentBody:
      type: object
      properties:
        zaloPayAppId:
          type: string
          maxLength: 100
        zaloPayKey1:
          type: string
          maxLength: 500
          format: password
        zaloPayKey2:
          type: string
          maxLength: 500
          format: password
        zaloPayDomain:
          type: string
          format: uri
          maxLength: 500
        zaloPayEndpoint:
          type: string
          format: uri
          maxLength: 500
        vnPayTmnCode:
          type: string
          maxLength: 100
        vnPayHashSecret:
          type: string
          maxLength: 500
          format: password
        vnPayURL:
          type: string
          format: uri
          maxLength: 500
        enabledMethods:
          type: array
          uniqueItems: true
          description: Payment methods offered at checkout. Unset means every method whose keys are filled in; a method whose keys are missing, or that cannot charge the store currency, is never offered.
          items:
            type: string
            enum:
              - money
              - vnpay
              - zalopay
    PatchAdminSettingStorefrontBody:
      type: object
      properties:
        primaryColor:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
        secondaryColor:
          type: string
          pattern: ^#[0-9a-fA-F]{6}$
        headingFont:
          type: string
          maxLength: 60
          pattern: ^[A-Za-z0-9 ]*$
          description: A Google Fonts family name; empty keeps the theme font.
        bodyFont:
          type: string
          maxLength: 60
          pattern: ^[A-Za-z0-9 ]*$
          description: A Google Fonts family name; empty keeps the theme font.
        currency:
          type: string
          description: ISO 4217 code, e.g. VND.
        displayCurrencies:
          oneOf:
            - type: array
              items:
                type: string
            - type: string
          description: ISO 4217 codes shoppers may view prices in, as a list or comma-separated text.
        locale:
          type: string
          maxLength: 20
          description: BCP 47 locale for numbers and dates, e.g. vi-VN.
        timezone:
          type: string
          maxLength: 60
          description: IANA time zone, e.g. Asia/Ho_Chi_Minh.
        language:
          type: string
          pattern: ^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})?$
          description: The storefront html lang attribute.
        moneyPerPoint:
          type: number
          exclusiveMinimum: true
          minimum: 0
          maximum: 1000000000000
          description: Order value that earns one loyalty point.
        pointValue:
          type: number
          exclusiveMinimum: true
          minimum: 0
          maximum: 1000000000000
          description: Discount one loyalty point is worth.
    PatchAdminSettingPagesBody:
      type: object
      properties:
        about:
          type: string
          maxLength: 60000
        faq:
          type: string
          maxLength: 60000
        privacyPolicy:
          type: string
          maxLength: 60000
        termsAndConditions:
          type: string
          maxLength: 60000
        returnPolicy:
          type: string
          maxLength: 60000
    ShippingRate:
      type: object
      properties:
        id:
          type: string
          description: Pass it back as `shippingMethod` when placing the order ("<provider>:<rate>").
          example: goship:12345
        provider:
          type: string
          example: goship
        carrierName:
          type: string
          example: J&T Express
        service:
          type: string
        expected:
          type: string
          description: Expected delivery time as the carrier words it.
        fee:
          type: number
    CartQuoteResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            cart:
              type: array
              items:
                type: object
              description: The cart lines with current product details and quantities capped at the stock.
            shippingOptions:
              type: array
              items:
                $ref: '#/components/schemas/ShippingRate'
              description: Cheapest first. Carriers that price by address (GoShip) appear only once `userAddress` is sent.
            point:
              type: object
              properties:
                canUsePoint:
                  type: integer
                POINT_TO_MONEY:
                  type: number
    OrderCreatedResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            orderCode:
              type: string
            phone:
              type: string
            total:
              type: number
            paymentStatus:
              type: string
              enum:
                - unpaid
                - paid
            redirectUrl:
              type: string
              description: Where to send the customer next, the order summary or the payment gateway start page.
              example: /order/payment-vnpay?orderCode=AB123456&phone=0901234567
    PatchAdminSettingApiLoginSocialBody:
      type: object
      properties:
        googleClientId:
          type: string
          maxLength: 500
        googleClientSecret:
          type: string
          maxLength: 500
          format: password
        googleCallbackUrl:
          type: string
          format: uri
          maxLength: 500
        facebookAppId:
          type: string
          maxLength: 500
        facebookAppSecret:
          type: string
          maxLength: 500
          format: password
        facebookCallbackUrl:
          type: string
          format: uri
          maxLength: 500
    PatchAdminSettingApiAppPasswordBody:
      type: object
      properties:
        gmailUser:
          type: string
          maxLength: 200
        gmailPassword:
          type: string
          maxLength: 200
          format: password
    SessionResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          required:
            - accessToken
            - tokenType
          properties:
            accessToken:
              type: string
              description: 'JWT for `Authorization: Bearer <token>`. It is also set in the HttpOnly cookie, which is what browsers use.'
              example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
            tokenType:
              type: string
              enum:
                - Bearer
            expiresIn:
              type: integer
              description: 'Seconds until the access token expires. There is no refresh for Bearer clients: log in again on 401.'
  responses:
    StandardResult:
      description: Result of the action.
      content:
        application/json:
          schema:
            type: object
            properties:
              code:
                type: string
                enum:
                  - success
              message:
                type: string
              content:
                type: string
    BadRequest:
      description: 'Invalid request: validation failed or the input is malformed.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: Not logged in. Page requests are redirected to the login page instead.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: The record does not exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: The request conflicts with the current state, for example a duplicate value, missing stock or a change that is not allowed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServerError:
      description: Unexpected failure.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    BadGateway:
      description: A service the application depends on (FileManager, email, GoShip) failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RedirectToLogin:
      description: Not logged in; redirected to the login page.
      headers:
        Location:
          description: The login page.
          schema:
            type: string
    PermissionDenied:
      description: 'The admin lacks the permission, or the action is not allowed for this account. API calls get `{ "code": "error", "message": "..." }`; page requests get an HTML 403 page.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: 'Too many attempts. The `Retry-After` header holds the seconds to wait; the body is `{ "code": "error", "message": "..." }`.'
      headers:
        Retry-After:
          description: Seconds to wait before trying again.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    UploadRejected:
      description: Invalid request, or the upload was rejected (type, size or file count).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    HtmlPage:
      description: HTML page
      content:
        text/html:
          schema:
            type: string
    FileManagerBadRequest:
      description: Invalid request or the operation failed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    InvalidBearerSecret:
      description: Missing or wrong bearer secret.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    TooManyFailedAttempts:
      description: Too many failed bearer attempts from this address; retry after the number of seconds in the `Retry-After` header. Requests with the correct secret are never blocked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    FileManagerForbidden:
      description: Path outside the media root or not allowed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    FileManagerNotFound:
      description: File or folder not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    FileManagerConflict:
      description: A file or folder with that name already exists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServiceUnavailable:
      description: The store is not fully configured yet (warehouse location, sender phone or address missing in Settings > General), so the request cannot be completed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
