openapi: 3.1.0
info:
  title: MyInvois Compliance Rules Feed
  version: "0.1.0"
  description: >
    A stateless compliance-rules service for Malaysian LHDN e-invoicing (MyInvois).
    Returns the current, structured ruleset: scope (am I in, by when?), field rules,
    invoice validation, and change-diffs. Invoice data is never stored.
    Every response carries asOf, rulesetVersion, source, and a disclaimer.
    Informational, not tax advice. Verify against LHDN.
  contact:
    name: MyInvois Compliance Rules Feed
servers:
  - url: https://myinvois-compliance-rules-feed-anhq280.vercel.app
    description: Production (Singapore)
tags:
  - name: scope
  - name: fields
  - name: validate
  - name: changes
  - name: health
paths:
  /v1/scope:
    get:
      tags: [scope]
      summary: Check e-invoicing scope for a business
      description: Given annual turnover (RM), returns the LHDN phase and whether e-invoicing is mandatory by the given date.
      parameters:
        - name: turnover
          in: query
          required: true
          schema: { type: number, minimum: 0 }
          example: 2000000
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
          example: "2026-06-16"
        - name: entityType
          in: query
          required: false
          schema: { type: string }
          example: sdn_bhd
      responses:
        "200":
          description: Scope result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScopeResult" }
              example:
                asOf: "2026-06-16T16:29:12.913Z"
                rulesetVersion: "2025-12 LHDN e-Invoice timeline (4-phase)"
                source: "https://www.hasil.gov.my/en/e-invoice/implementation-of-e-invoicing-in-malaysia/e-invoice-implementation-timeline/"
                disclaimer: "Informational, not tax advice. Verify against LHDN."
                inScope: true
                phase: "Phase 4"
                mandatoryFrom: "2026-01-01"
                relaxationUntil: "2026-06-30"
                turnover: 2000000
                date: "2026-06-16"
                note: "In scope, but within the relaxation window until 2026-06-30. Annual turnover RM1m-RM5m. Mandatory 1 Jan 2026 per official LHDN timeline; 6-month relaxation to 30 Jun 2026."
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/fields:
    get:
      tags: [fields]
      summary: Get applicable field rules for a scenario
      parameters:
        - name: scenario
          in: query
          required: true
          schema: { type: string, enum: [b2b, b2c] }
        - name: amount
          in: query
          required: false
          schema: { type: number, minimum: 0 }
          example: 15000
      responses:
        "200":
          description: Field rules
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FieldRulesResult" }
              example:
                asOf: "2026-06-16T16:29:12.913Z"
                rulesetVersion: "2025-12 LHDN e-Invoice timeline (4-phase)"
                source: "https://www.hasil.gov.my/en/e-invoice/implementation-of-e-invoicing-in-malaysia/e-invoice-implementation-timeline/"
                disclaimer: "Informational, not tax advice. Verify against LHDN."
                scenario: "b2b"
                amount: 15000
                fields:
                  - fieldCode: "BuyerTIN"
                    fieldGroup: "Buyer"
                    required: true
                    condition: { when: "b2b" }
                    formatRegex: "^[A-Z]{1,2}[0-9]{6,12}$"
                    allowedValues: null
                    description: "Buyer TIN. Required for B2B. When no specific TIN is available, use a General TIN (EI00000000010 general public, EI00000000020 foreign buyer, EI00000000040 government)."
                  - fieldCode: "IndividualEInvoice"
                    fieldGroup: "Consolidation"
                    required: true
                    condition: { when: "amount>10000" }
                    formatRegex: null
                    allowedValues: null
                    description: "No-consolidation rule: for transactions exceeding RM10,000, an individual e-invoice must be issued, effective 1 Jan 2026."
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/validate:
    post:
      tags: [validate]
      summary: Validate an invoice against the current field rules
      description: >
        Stateless — the invoice is validated in memory and never stored. Include `scenario`
        (b2b|b2c) and `amount` (RM) in the invoice to select applicable rules; field codes are
        read as top-level keys.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invoice]
              properties:
                invoice:
                  $ref: "#/components/schemas/Invoice"
            example:
              invoice:
                scenario: b2b
                amount: 15000
                SellerTIN: "C1234567890"
                SellerName: "Acme Sdn Bhd"
                BuyerTIN: "C9876543210"
                BuyerName: "Buyer Bhd"
                InvoiceNumber: "INV-001"
                InvoiceDate: "2026-06-16"
                Currency: "MYR"
                TotalExcludingTax: 15000
                TaxAmount: 0
                TotalPayableAmount: 15000
                LineItemDescription: "Consulting"
                IndividualEInvoice: true
      responses:
        "200":
          description: Validation result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ValidationResult" }
              example:
                asOf: "2026-06-16T16:29:12.913Z"
                rulesetVersion: "2025-12 LHDN e-Invoice timeline (4-phase)"
                source: "https://www.hasil.gov.my/en/e-invoice/implementation-of-e-invoicing-in-malaysia/e-invoice-implementation-timeline/"
                disclaimer: "Informational, not tax advice. Verify against LHDN."
                valid: false
                errors:
                  - fieldCode: "BuyerTIN"
                    rule: "required"
                    message: "Required field \"BuyerTIN\" is missing."
                  - fieldCode: "InvoiceNumber"
                    rule: "required"
                    message: "Required field \"InvoiceNumber\" is missing."
                warnings: []
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/changes:
    get:
      tags: [changes]
      summary: Rule changes effective on/after a date
      parameters:
        - name: since
          in: query
          required: true
          schema: { type: string, format: date }
          example: "2026-01-01"
      responses:
        "200":
          description: Change feed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChangesResult" }
              example:
                asOf: "2026-06-16T16:29:12.913Z"
                rulesetVersion: "2025-12 LHDN e-Invoice timeline (4-phase)"
                source: "https://www.hasil.gov.my/en/e-invoice/implementation-of-e-invoicing-in-malaysia/e-invoice-implementation-timeline/"
                disclaimer: "Informational, not tax advice. Verify against LHDN."
                since: "2026-01-01"
                changes:
                  - changeType: "threshold"
                    summary: "Exemption floor raised RM500k -> RM1m (taxpayers below RM1m exempt)."
                    affectedEntity: "Exemption"
                    effectiveDate: "2026-01-01"
                    createdAt: "2026-06-16T10:42:28.554Z"
                  - changeType: "deadline"
                    summary: "Phase 4 (RM1m-RM5m): mandatory 1 Jan 2026 per official LHDN timeline, with a 6-month relaxation to 30 Jun 2026."
                    affectedEntity: "Phase 4"
                    effectiveDate: "2026-01-01"
                    createdAt: "2026-06-16T10:42:28.554Z"
        "400": { $ref: "#/components/responses/BadRequest" }
  /api/health:
    get:
      tags: [health]
      summary: Health + current ruleset version
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  dbReachable: { type: boolean }
                  rulesetVersion: { type: [string, "null"] }
                  currentRulesetLoaded: { type: boolean }
              example:
                ok: true
                dbReachable: true
                rulesetVersion: "2025-12 LHDN e-Invoice timeline (4-phase)"
                currentRulesetLoaded: true
components:
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
              issues: { type: array, items: { type: object } }
  schemas:
    Envelope:
      type: object
      description: Carried by every successful response.
      required: [asOf, rulesetVersion, source, disclaimer]
      properties:
        asOf: { type: string, format: date-time }
        rulesetVersion: { type: string, example: "2025-12 LHDN e-Invoice timeline (4-phase)" }
        source: { type: string, format: uri }
        disclaimer: { type: string, example: "Informational, not tax advice. Verify against LHDN." }
    ScopeResult:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            inScope: { type: boolean }
            phase: { type: [string, "null"], example: "Phase 4" }
            mandatoryFrom: { type: [string, "null"], format: date }
            relaxationUntil: { type: [string, "null"], format: date }
            turnover: { type: number }
            date: { type: string, format: date }
            note: { type: [string, "null"] }
    FieldRule:
      type: object
      properties:
        fieldCode: { type: string, example: BuyerTIN }
        fieldGroup: { type: [string, "null"], example: Buyer }
        required: { type: boolean }
        condition:
          type: [object, "null"]
          example: { when: b2b }
        formatRegex: { type: [string, "null"], example: "^[A-Z]{1,2}[0-9]{6,12}$" }
        allowedValues: { type: [array, "null"], items: {} }
        description: { type: [string, "null"] }
    FieldRulesResult:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            scenario: { type: string, enum: [b2b, b2c] }
            amount: { type: [number, "null"] }
            fields: { type: array, items: { $ref: "#/components/schemas/FieldRule" } }
    Invoice:
      type: object
      description: Flat object keyed by field code, plus optional scenario/amount.
      properties:
        scenario: { type: string, enum: [b2b, b2c] }
        amount: { type: number, minimum: 0 }
      additionalProperties: true
    ValidationIssue:
      type: object
      properties:
        fieldCode: { type: string }
        rule: { type: string, enum: [required, format, allowed_values] }
        message: { type: string }
    ValidationResult:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            valid: { type: boolean }
            errors: { type: array, items: { $ref: "#/components/schemas/ValidationIssue" } }
            warnings: { type: array, items: { $ref: "#/components/schemas/ValidationIssue" } }
    RuleChange:
      type: object
      properties:
        changeType: { type: string, example: threshold }
        summary: { type: string }
        affectedEntity: { type: [string, "null"] }
        effectiveDate: { type: [string, "null"], format: date }
        createdAt: { type: string, format: date-time }
    ChangesResult:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            since: { type: string, format: date }
            changes: { type: array, items: { $ref: "#/components/schemas/RuleChange" } }
