openapi: 3.0.3
info:
  title: Medicine API
  description: |
    Medicine management API — Express, DynamoDB (AWS), JWT, and role-based access (`employee`, `admin`, `superadmin`).

    **Auth:** Most routes require `Authorization: Bearer <jwt>`. Login returns a JWT directly after email and password verification.

    **Provisioned accounts:** If `mustChangePassword` is true on the user, protected routes (medicines, users, audit) return **403** with `code: MUST_CHANGE_PASSWORD` until the password is changed via `/api/auth/change-password`.

    **Validation errors (422):** Many write routes return `{ "message": "Validation failed", "errors": [{ "field", "message" }] }`.

    **Rate limits:** Auth endpoints and general `/api/*` traffic are rate-limited in production (see deployment config).
  version: 1.0.0
  contact:
    name: Manya Shukla

servers:
  - url: http://localhost:5050
    description: Local development

tags:
  - name: Health
  - name: Auth
  - name: Medicines
  - name: Users
  - name: Audit

paths:
  /api/health:
    get:
      tags: [Health]
      summary: Health check
      operationId: getHealth
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /api/auth/register:
    post:
      tags: [Auth]
      summary: Register employee
      operationId: authRegister
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Created — returns JWT and user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokenResponse'
        '409':
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageError'

  /api/auth/admin-signup:
    post:
      tags: [Auth]
      summary: Admin self-registration (pending superadmin approval)
      operationId: authAdminSignup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdminSignupRequest'
      responses:
        '201':
          description: Submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Registration submitted. You will receive an email once approved.
        '409':
          description: Duplicate email or phone
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageError'

  /api/auth/login:
    post:
      tags: [Auth]
      summary: Login with mobile number and password
      operationId: authLogin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: Authenticated — returns JWT and user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokenResponse'
        '401':
          description: Invalid credentials
        '403':
          description: Inactive or unapproved admin
        '404':
          description: User not found

  /api/auth/change-password:
    post:
      tags: [Auth]
      summary: Change password (JWT + new password)
      operationId: authChangePassword
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
      responses:
        '200':
          description: Success

  /api/auth/logout:
    post:
      tags: [Auth]
      summary: Log out (audit event)
      operationId: authLogout
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Logged out

  /api/auth/me:
    get:
      tags: [Auth]
      summary: Current user (from JWT)
      operationId: authMe
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/UserPublic'

  /api/medicines/search:
    get:
      tags: [Medicines]
      summary: Text search (scoped by role)
      description: |
        **JWT required.** Blocked with 403 `MUST_CHANGE_PASSWORD` until initial password is changed.

        Without `field`, searches tag, nomenclature, vendorName, and companyName. With `field`, searches only that attribute.
      operationId: medicinesSearch
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: q
          required: true
          schema:
            type: string
            maxLength: 100
        - in: query
          name: field
          schema:
            type: string
            enum: [tag, companyName, nomenclature]
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MedicineSearchResponse'
        '401':
          description: Missing or invalid token
        '403':
          description: Forbidden or must change password
        '422':
          description: Validation error

  /api/medicines/office-stats:
    get:
      tags: [Medicines]
      summary: Medicine counts per member for caller's office (admins with officeName)
      operationId: medicinesOfficeStats
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OfficeStatsResponse'

  /api/medicines/tag/{tag}:
    get:
      tags: [Medicines]
      summary: Get medicine by internal tag
      operationId: medicinesGetByTag
      security:
        - bearerAuth: []
      parameters:
        - name: tag
          in: path
          required: true
          schema:
            type: string
            pattern: '^\d{7,}$'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MedicineOneResponse'
        '404':
          description: Not found or not visible

  /api/medicines:
    get:
      tags: [Medicines]
      summary: Paginated list (employees see only their own medicines)
      operationId: medicinesList
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MedicineListResponse'
    post:
      tags: [Medicines]
      summary: Create medicine (server assigns tag)
      operationId: medicinesCreate
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MedicineWriteBody'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  data:
                    $ref: '#/components/schemas/Medicine'
        '403':
          description: Role not allowed or must change password
        '422':
          description: Validation failed

  /api/medicines/scan-image:
    post:
      tags: [Medicines]
      summary: Upload image — store file and extract fields (Gemini)
      operationId: medicinesScanImage
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
      responses:
        '200':
          description: Extracted fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanImageResponse'
        '400':
          description: Missing file
        '409':
          description: Duplicate — same image already saved, or extracted supply order matches an existing article
        '502':
          description: Scan failed
        '503':
          description: AI rate limit / quota

  /api/medicines/{id}:
    get:
      tags: [Medicines]
      summary: Get medicine by id
      operationId: medicinesGetOne
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/MedicineId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MedicineOneResponse'
        '404':
          description: Not found
    put:
      tags: [Medicines]
      summary: Update medicine (edit windows apply per role)
      operationId: medicinesUpdate
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/MedicineId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MedicineWriteBody'
      responses:
        '200':
          description: Updated
        '403':
          description: Edit not allowed (window / unit)
        '404':
          description: Not found
        '422':
          description: Validation failed
    delete:
      tags: [Medicines]
      summary: Soft-delete medicine
      description: "Superadmin only."
      operationId: medicinesDelete
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/MedicineId'
      responses:
        '200':
          description: Soft-deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '403':
          description: Not superadmin
        '404':
          description: Not found

  /api/users:
    get:
      tags: [Users]
      summary: List users (admin sees own employees; superadmin sees all)
      operationId: usersList
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK — pagination fields are top-level (`page`, `limit`, `total`, `pages`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
    post:
      tags: [Users]
      summary: Create user (admin creates employee; superadmin creates admin or employee)
      description: Server generates a temporary password, emails credentials, and sets mustChangePassword until first password change.
      operationId: usersCreate
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: Created
        '400':
          description: Validation / bad admin assignment
        '403':
          description: Not allowed for role
        '409':
          description: Duplicate email or phone

  /api/users/pending-approvals:
    get:
      tags: [Users]
      summary: Pending admin signups
      description: "Superadmin only."
      operationId: usersPendingApprovals
      security:
        - bearerAuth: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PendingAdminRow'

  /api/users/admins:
    get:
      tags: [Users]
      summary: List approved admins (summary)
      description: "Superadmin only."
      operationId: usersListAdmins
      security:
        - bearerAuth: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AdminSummary'

  /api/users/{id}/approve:
    post:
      tags: [Users]
      summary: Approve pending admin
      description: "Superadmin only."
      operationId: usersApprove
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Approved
        '400':
          description: Not a pending admin
        '404':
          description: User not found

  /api/users/{id}:
    put:
      tags: [Users]
      summary: Update user (name, email, optional password, isActive)
      description: "Superadmin only."
      operationId: usersUpdate
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: Updated
        '404':
          description: Not found
        '409':
          description: Email taken
    delete:
      tags: [Users]
      summary: Delete user (admin may delete own employees; superadmin broader rules)
      operationId: usersDelete
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  id:
                    type: string
                    format: uuid
        '400':
          description: Cannot delete self or last superadmin
        '403':
          description: Not allowed

  /api/users/{id}/role:
    put:
      tags: [Users]
      summary: Change user role
      description: "Superadmin only. Target role admin or employee only."
      operationId: usersChangeRolePut
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangeRoleRequest'
      responses:
        '200':
          description: Role updated
    patch:
      tags: [Users]
      summary: Change user role (same as PUT)
      operationId: usersChangeRolePatch
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangeRoleRequest'
      responses:
        '200':
          description: Role updated

  /api/audit:
    get:
      tags: [Audit]
      summary: Audit log (superadmin only — all entries)
      operationId: auditList
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/Page'
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
          description: Default 30
        - in: query
          name: action
          schema:
            type: string
            enum:
              - CREATE_USER
              - UPDATE_USER
              - DELETE_USER
              - CHANGE_USER_ROLE
              - APPROVE_USER
              - ADD_MEDICINE
              - UPDATE_MEDICINE
              - DELETE_MEDICINE
              - SCAN_MEDICINE_IMAGE
              - LOGIN
              - LOGOUT
              - PASSWORD_RESET
        - in: query
          name: resource
          schema:
            type: string
            enum: [User, Medicine]
        - in: query
          name: performedBy
          schema:
            type: string
            format: uuid
        - in: query
          name: from
          schema:
            type: string
            format: date-time
          description: ISO 8601 — inclusive filter on `createdAt`
        - in: query
          name: to
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: OK — pagination at top level
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditListResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    MedicineId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    UserId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid

  schemas:
    HealthResponse:
      type: object
      properties:
        ok:
          type: boolean
          example: true
        status:
          type: string
          example: ok
        service:
          type: string
          example: medicine-api
        env:
          type: string
          example: production

    MessageError:
      type: object
      properties:
        message:
          type: string

    ValidationErrorResponse:
      type: object
      properties:
        message:
          type: string
          example: Validation failed
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string

    PaginationNested:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        pages:
          type: integer

    PaginationFlat:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        pages:
          type: integer

    RegisterRequest:
      type: object
      required: [name, email, password]
      properties:
        name:
          type: string
        email:
          type: string
          format: email
        password:
          type: string
          description: Min 6 chars; at least one letter, one digit, and one special (!@#$%^&*()_+-=[]{};':"\|,.<>/?`~)

    AdminSignupRequest:
      type: object
      required: [officeName, address, location, state, pincode, phone, email, password]
      properties:
        officeName:
          type: string
        address:
          type: string
        location:
          type: string
        state:
          type: string
        pincode:
          type: string
          pattern: '^\d{6}$'
        phone:
          type: string
          description: 10-digit Indian mobile (optional +91/91 prefix accepted; stored as E.164 +91…).
        email:
          type: string
          format: email
        password:
          type: string
          description: Min 6 chars; letter, digit, and special character required

    LoginRequest:
      type: object
      required: [phone, password]
      properties:
        phone:
          type: string
          description: 10-digit Indian mobile (optional +91/91 prefix accepted)
        password:
          type: string

    ChangePasswordRequest:
      type: object
      required: [newPassword]
      properties:
        newPassword:
          type: string
          description: Min 6 chars; letter, digit, and special character required

    AuthTokenResponse:
      type: object
      properties:
        token:
          type: string
        user:
          $ref: '#/components/schemas/UserPublic'

    UserPublic:
      type: object
      description: Public user shape (no password). May include both `_id` and `id`.
      properties:
        _id:
          type: string
          format: uuid
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          enum: [employee, admin, superadmin]
        phone:
          type: string
          nullable: true
        designation:
          type: string
          nullable: true
        officeName:
          type: string
          nullable: true
        address:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        state:
          type: string
          nullable: true
        pincode:
          type: string
          nullable: true
        isActive:
          type: boolean
        isApproved:
          type: boolean
        mustChangePassword:
          type: boolean
        createdBy:
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/UserPublic'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    MedicineLineItem:
      type: object
      required:
        - nomenclature
      properties:
        nomenclature:
          type: string
          maxLength: 500
        lineTotal:
          type: number
          minimum: 0
          description: Per-line basic amount in INR when present on the document
        quantity:
          type: string
          maxLength: 100
          description: Ordered quantity for this line when shown per row

    MedicineWriteBody:
      type: object
      required:
        - nomenclature
        - quantity
        - quantityUnit
        - totalValue
        - companyName
        - location
        - organisation
        - supplyOrder
        - supplyDate
      properties:
        lineItems:
          type: array
          maxItems: 20
          description: |
            Optional line-level nomenclatures and amounts. When every element includes `lineTotal`,
            the server sets `totalValue` to the sum of line totals. When every element has a numeric
            `quantity`, the server sets top-level `quantity` to the sum. Primary `nomenclature` is
            always the first line's name.
          items:
            $ref: '#/components/schemas/MedicineLineItem'
        nomenclature:
          type: string
        quantity:
          type: number
          minimum: 0
        quantityUnit:
          type: string
          maxLength: 100
        totalValue:
          type: number
          minimum: 0
        totalValueWords:
          type: string
          maxLength: 500
        companyName:
          type: string
          maxLength: 200
        location:
          type: string
          maxLength: 300
        organisation:
          type: string
          enum: ['R&R', 'Dental Centre', 'Bure Hospital', 'Other']
        organisationCustom:
          type: string
          maxLength: 200
          description: Required when organisation is `Other`
        supplyOrder:
          type: string
        supplyDate:
          type: string
          format: date-time
          description: ISO 8601
        uoNumber:
          type: string
          description: If set, 12 digits (non-digits stripped for validation)
        imageUrl:
          type: string
          format: uri
          maxLength: 2048
        gstExclusive:
          type: number
          minimum: 0
        gstInclusive:
          type: number
          minimum: 0
        grandTotal:
          type: number
          minimum: 0
        narration:
          type: string
          maxLength: 1000

    Medicine:
      allOf:
        - $ref: '#/components/schemas/MedicineWriteBody'
        - type: object
          properties:
            _id:
              type: string
              format: uuid
            tag:
              type: string
              description: Assigned by server (e.g. 202600001 pattern)
            vendorName:
              type: string
            perUnitPrice:
              type: number
            isActive:
              type: boolean
            createdBy:
              oneOf:
                - type: string
                - $ref: '#/components/schemas/UserPublic'
            updatedBy:
              oneOf:
                - type: string
                - $ref: '#/components/schemas/UserPublic'
            createdAt:
              type: string
              format: date-time
            updatedAt:
              type: string
              format: date-time

    MedicineListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Medicine'
        pagination:
          $ref: '#/components/schemas/PaginationNested'

    MedicineSearchResponse:
      type: object
      properties:
        query:
          type: string
        data:
          type: array
          items:
            $ref: '#/components/schemas/Medicine'
        pagination:
          $ref: '#/components/schemas/PaginationNested'

    MedicineOneResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Medicine'

    OfficeStatsResponse:
      type: object
      properties:
        officeName:
          type: string
          nullable: true
        total:
          type: integer
        breakdown:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              count:
                type: integer

    ScanImageResponse:
      type: object
      properties:
        imageUrl:
          type: string
          format: uri
        extractedFields:
          type: object
          additionalProperties: true
          description: Key/value fields from vision OCR (shape varies)

    CreateUserRequest:
      type: object
      description: |
        **Admin** creates an employee: required `name`, `email`, `phone`; optional `designation`. `role` must not be set to anything other than employee (ignored as employee). The server generates a temporary password, emails it to the user, and sets `mustChangePassword` until they change it after first sign-in.

        **Superadmin** creating an **employee**: same as above plus required `assignedAdminId` (UUID of an active approved admin).

        **Superadmin** creating an **admin**: set `role` to `admin` and send `officeName`, `address`, `location`, `state`, `pincode`, `email`, `phone`; optional `designation`. Display name is taken from `officeName` on the server. Temporary password is generated and emailed the same way.

        Do not send `password` on this endpoint; it is ignored if present.
      properties:
        role:
          type: string
          enum: [employee, admin]
        name:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
          description: 10-digit Indian mobile (optional +91/91 prefix accepted; stored as E.164 +91…).
        designation:
          type: string
        assignedAdminId:
          type: string
          format: uuid
        officeName:
          type: string
        address:
          type: string
        location:
          type: string
        state:
          type: string
        pincode:
          type: string
          pattern: '^\d{6}$'

    UpdateUserRequest:
      type: object
      properties:
        name:
          type: string
        email:
          type: string
        password:
          type: string
        isActive:
          type: boolean

    ChangeRoleRequest:
      type: object
      required: [role]
      properties:
        role:
          type: string
          enum: [admin, employee]

    UserListResponse:
      allOf:
        - $ref: '#/components/schemas/PaginationFlat'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/UserPublic'

    PendingAdminRow:
      type: object
      properties:
        _id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
        phone:
          type: string
        officeName:
          type: string
        address:
          type: string
        location:
          type: string
        state:
          type: string
        pincode:
          type: string
        createdAt:
          type: string
          format: date-time

    AdminSummary:
      type: object
      properties:
        _id:
          type: string
          format: uuid
        name:
          type: string
        officeName:
          type: string
        email:
          type: string

    AuditLog:
      type: object
      properties:
        _id:
          type: string
          format: uuid
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        user:
          oneOf:
            - type: string
              format: uuid
            - $ref: '#/components/schemas/UserPublic'
        action:
          type: string
        resource:
          type: string
        resourceId:
          type: string
          nullable: true
        details:
          nullable: true
        ip:
          type: string
          nullable: true

    AuditListResponse:
      allOf:
        - $ref: '#/components/schemas/PaginationFlat'
        - type: object
          properties:
            data:
              type: array
              items:
                $ref: '#/components/schemas/AuditLog'
