openapi: "3.0.3"
info:
  title: The Plan Shop Agent API
  version: "1.0.0"
  description: |
    Public REST API for programmatic access to The Plan Shop catalog and checkout.
    Payments are auto-accepted (mock) — no real card required.
    All prices are in EUR cents (integer). Idempotency-Key header is required on
    all purchase endpoints. Rate limits: 120 reads/min per IP, 10 writes/hour per IP.

servers:
  - url: https://planshop.binary.tools

paths:
  /api/v1/templates:
    get:
      summary: List or search templates
      operationId: listTemplates
      parameters:
        - name: q
          in: query
          schema:
            type: string
          description: Full-text search query
        - name: category
          in: query
          schema:
            type: string
            enum: [money, business, home]
          description: Filter by category
        - name: maxPriceCents
          in: query
          schema:
            type: integer
          description: Maximum price in cents (inclusive)
      responses:
        "200":
          description: Array of template summaries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TemplateSummary"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/templates/{slug}:
    get:
      summary: Get template detail by slug
      operationId: getTemplate
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
          description: Template URL slug (e.g. crm-lite)
      responses:
        "200":
          description: Full template detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TemplateDetail"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/bundles:
    get:
      summary: List all bundles
      operationId: listBundles
      responses:
        "200":
          description: Array of bundle DTOs
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/BundleDto"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/catalog.json:
    get:
      summary: Full catalog feed (all templates + bundles)
      operationId: catalogFeed
      responses:
        "200":
          description: Complete catalog snapshot
          headers:
            Cache-Control:
              schema:
                type: string
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogFeed"
        "304":
          description: Not modified (ETag matched)
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/checkout:
    post:
      summary: Purchase one or more templates or bundles
      operationId: checkout
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{8,128}$"
          description: Caller-provided idempotency key to prevent duplicate purchases
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CheckoutRequest"
      responses:
        "201":
          description: New order created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutResponse"
        "200":
          description: Replayed — same idempotency key seen before
          headers:
            Idempotency-Replayed:
              schema:
                type: string
                enum: ["true"]
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutResponse"
        "400":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/orders/{id}:
    get:
      summary: Look up an order by ID and email
      operationId: lookupOrder
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
        - name: email
          in: query
          required: true
          schema:
            type: string
            format: email
      responses:
        "200":
          description: Order detail with download links if paid
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponse"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/orders/{id}/download-links:
    post:
      summary: Get fresh download links for a paid order
      operationId: downloadLinks
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
              required: [email]
      responses:
        "200":
          description: Order with download links
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponse"
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/template-requests:
    post:
      summary: Submit a request for a template that does not exist yet
      operationId: requestTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                text:
                  type: string
                  minLength: 5
                  maxLength: 500
                  description: Description of the desired template
                email:
                  type: string
                  format: email
                  description: Optional email for follow-up
              required: [text]
      responses:
        "201":
          description: Request recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  requestId:
                    type: integer
                    format: int64
        "400":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/notify-me:
    post:
      summary: Sign up for notification when a template becomes available
      operationId: notifyMe
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                slug:
                  type: string
                  description: Template slug to watch
                email:
                  type: string
                  format: email
              required: [slug, email]
      responses:
        "200":
          description: Subscription confirmed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  watching:
                    type: string
        "400":
          $ref: "#/components/responses/Error"
        "404":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  schemas:
    TemplateSummary:
      type: object
      properties:
        slug:
          type: string
        title:
          type: string
        shortDescription:
          type: string
        category:
          type: string
        complexity:
          type: string
          enum: [simple, intermediate, advanced]
        priceCents:
          type: integer
        currency:
          type: string
          example: EUR
        free:
          type: boolean
        ratingAvg:
          type: number
          format: double
        ratingCount:
          type: integer
        detailUrl:
          type: string
          format: uri
        coverUrl:
          type: string
          format: uri
        apiUrl:
          type: string
          format: uri

    TemplateDetail:
      allOf:
        - $ref: "#/components/schemas/TemplateSummary"
        - type: object
          properties:
            descriptionMd:
              type: string
            sheets:
              type: array
              items:
                type: string
            version:
              type: integer
            fileSizeBytes:
              type: integer
            sha256:
              type: string

    BundleDto:
      type: object
      properties:
        slug:
          type: string
        title:
          type: string
        priceCents:
          type: integer
        currency:
          type: string
          example: EUR
        description:
          type: string
        templates:
          type: array
          items:
            type: string
        savingsCents:
          type: integer

    ShopInfo:
      type: object
      properties:
        name:
          type: string
        baseUrl:
          type: string
        currency:
          type: string

    CatalogFeed:
      type: object
      properties:
        generatedAt:
          type: string
          format: date-time
        shop:
          $ref: "#/components/schemas/ShopInfo"
        templates:
          type: array
          items:
            $ref: "#/components/schemas/TemplateDetail"
        bundles:
          type: array
          items:
            $ref: "#/components/schemas/BundleDto"

    CheckoutRequest:
      type: object
      required: [email, items]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
        items:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: "#/components/schemas/ItemRef"

    ItemRef:
      type: object
      properties:
        slug:
          type: string
          description: Template slug (use this OR bundleSlug)
        bundleSlug:
          type: string
          description: Bundle slug (use this OR slug)

    DownloadLink:
      type: object
      properties:
        title:
          type: string
        url:
          type: string
          format: uri
        expiresAt:
          type: string
          format: date-time
        downloadsRemaining:
          type: integer

    CheckoutResponse:
      type: object
      properties:
        orderId:
          type: integer
          format: int64
        status:
          type: string
          example: PAID
        totalCents:
          type: integer
        currency:
          type: string
          example: EUR
        email:
          type: string
        downloads:
          type: array
          items:
            $ref: "#/components/schemas/DownloadLink"
        replayed:
          type: boolean

    OrderResponse:
      type: object
      properties:
        orderId:
          type: integer
          format: int64
        status:
          type: string
        totalCents:
          type: integer
        currency:
          type: string
        email:
          type: string
        downloads:
          type: array
          items:
            $ref: "#/components/schemas/DownloadLink"

    ErrorEnvelope:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              enum: [bad_request, not_found, conflict, rate_limited, internal]
            message:
              type: string

  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimited:
      description: Too many requests
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
