openapi: 3.1.0
info:
  title: SuccessGraph API
  version: 1.0.0
  description: |
    Knowledge graph API for Success IT's client ecosystem. Maps clients, tenants,
    products, corporate groups, and subscription statuses.

    ## Quick Start for AI Agents

    **"Which database does this client use?"**
    1. `GET /api/search?q=sinins` → find the client node
    2. `GET /api/node/client:S041` → see outbound edges including `owns → tenant:SINA`
    3. The tenant's metadata contains `compCode`, `allProducts`, `subscriptionStatus`

    **"What product is this client on?"**
    - `GET /api/blast/client:A054` → downstream shows `tenant:AUTOCLINIC → product:WS`

    **"Does this client have sister companies?"**
    - `GET /api/node/client:S041` → outbound `belongs_to → group:INSURE HUB`
    - `GET /api/search?q=INSURE HUB&type=client` → all clients in that group

    **"Is the subscription active?"**
    - Check `subscriptionStatus` in tenant metadata: `active`, `expiring_soon`, or `expired`

    **"Is this client mapped or legacy?"**
    - Check `platformStatus` in client metadata: `mapped` (has tenant DB) or `unmapped` (legacy/no platform)

    ## Node Key Format
    - `client:<TraderCode>` — SC customer (e.g., `client:A054`)
    - `tenant:<CompCode>` — Platform tenant (e.g., `tenant:AUTOCLINIC`)
    - `product:<Code>` — Product (e.g., `product:WS`)
    - `group:<Name>` — Sister company group (e.g., `group:ACCORD`)
    - `category:<Name>` — Product category (e.g., `category:SINS`)

    ## Edge Types
    - `owns` — client → tenant (client owns this tenant database)
    - `belongs_to` — client → group (sister company grouping)
    - `categorized_as` — client → category (product type)
    - `runs` — tenant → product (tenant runs this product)
    - `grouped_with` — tenant ↔ tenant (same corporate group)

    ## Products
    | Code | Name |
    |------|------|
    | SINS | Success Insurance |
    | WS | Success Workshop |
    | HP | Success Hire Purchase |
    | CAR | Success Car Sales |
    | SQL | Success Trading |
    | CAT | Success Catalyst |

  contact:
    name: Success IT Consultancy & Services
  license:
    name: Private
servers:
  - url: http://localhost:3000
    description: Local development server

paths:
  /api/search:
    get:
      operationId: searchNodes
      summary: Fuzzy search across all nodes
      description: |
        Search clients, tenants, products, groups, and categories by name, node key,
        or metadata. Supports acronym/initials matching (e.g., "SRG" finds "Specialist Risk Group").

        **Use this as the primary entry point** when you have a company name, code, or abbreviation.
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: |
            Search query. Supports:
            - Full or partial name: "autoclinic", "sinins"
            - TraderCode: "A054", "S041"
            - CompCode: "HLSUNTEK"
            - Acronym/initials: "SRG" (Specialist Risk Group), "ABA"
            - Contact info: "vicky@sinins"
          examples:
            byName:
              value: sinins
              summary: Search by company name
            byAcronym:
              value: SRG
              summary: Search by acronym
            byCode:
              value: A054
              summary: Search by trader code
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [client, tenant, product, group, category]
          description: Filter results to a specific node type
        - name: layer
          in: query
          required: false
          schema:
            type: string
            enum: [catalyst, platform]
          description: Filter results to a specific layer
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 25
          description: Maximum number of results to return
      responses:
        "200":
          description: Scored search results, sorted by relevance
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SearchResult"
              example:
                - score: 81.9
                  node_key: "client:S041"
                  name: "SININS AGENCY PTE. LTD."
                  node_type: "client"
                  layer: "catalyst"
                  metadata: '{"traderCode":"S041","email":"vicky@sinins.com.sg","platformStatus":"mapped"}'
        "400":
          description: Missing query parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/node/{key}:
    get:
      operationId: getNode
      summary: Get full node details with all edges
      description: |
        Returns a node's metadata plus all inbound and outbound edges.

        **Key endpoint for investigation**: After finding a node via search, use this to see
        what it connects to (tenants, products, groups).

        For clients: metadata includes `traderCode`, `email`, `phone`, `attn` (contact),
        `salesman`, `platformStatus` (mapped/unmapped).

        For tenants: metadata includes `compCode`, `subscriptionStatus`, `subscriptionExpiry`,
        `allProducts`, `isUat`.
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: |
            Node key. Format: `type:identifier`
            - `client:A054` (by TraderCode)
            - `tenant:AUTOCLINIC` (by CompCode)
            - `product:WS` (by product code)
            - `group:ACCORD` (by group name)
          examples:
            client:
              value: "client:A054"
              summary: Autoclinic client node
            tenant:
              value: "tenant:AUTOCLINIC"
              summary: Autoclinic tenant node
      responses:
        "200":
          description: Node with inbound and outbound edges
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NodeDetail"
              example:
                node:
                  node_key: "client:S041"
                  name: "SININS AGENCY PTE. LTD."
                  node_type: "client"
                  layer: "catalyst"
                  metadata:
                    traderCode: "S041"
                    email: "vicky@sinins.com.sg"
                    phone: "9182 9740 (vicky)"
                    attn: "Ms Vicky Tan"
                    platformStatus: "mapped"
                inbound: []
                outbound:
                  - target: "group:INSURE HUB"
                    target_name: "INSURE HUB"
                    target_type: "group"
                    edge_type: "belongs_to"
                  - target: "tenant:SINA"
                    target_name: "SININS AGENCY PTE. LTD."
                    target_type: "tenant"
                    edge_type: "owns"
        "404":
          description: Node not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/blast/{key}:
    get:
      operationId: blastRadius
      summary: Full impact analysis — upstream and downstream traversal
      description: |
        Recursively traverses the graph in both directions from the given node.
        Returns all connected nodes with their depth from the starting node.

        **Downstream** (what does this connect to): client → tenant → product
        **Upstream** (what connects to this): product → tenant → client

        Use this to answer questions like:
        - "How many companies does Sinins Agency have?" (blast client:S041)
        - "Which clients use Workshop?" (blast product:WS, look at upstream)
        - "What's the full picture for Autoclinic?" (blast client:A054)
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: Node key to analyze (e.g., `client:A054`)
        - name: depth
          in: query
          required: false
          schema:
            type: integer
            default: 10
          description: Maximum traversal depth
      responses:
        "200":
          description: Upstream and downstream node lists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlastResult"
              example:
                upstream: []
                downstream:
                  - node_key: "client:A054"
                    name: "AUTOCLINIC PTE LTD"
                    node_type: "client"
                    layer: "catalyst"
                    depth: 0
                  - node_key: "tenant:AUTOCLINIC"
                    name: "AUTOCLINIC PTE LTD"
                    node_type: "tenant"
                    layer: "platform"
                    depth: 1
                  - node_key: "product:WS"
                    name: "Success Workshop"
                    node_type: "product"
                    layer: "platform"
                    depth: 2

  /api/stats:
    get:
      operationId: getStats
      summary: Graph statistics summary
      description: Returns node and edge counts grouped by type and layer.
      responses:
        "200":
          description: Graph statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Stats"

  /api/graph:
    get:
      operationId: getFullGraph
      summary: Full graph data for visualization
      description: |
        Returns all nodes and edges in the graph. Primarily used by the 3D web UI.
        **Warning**: Large response (~911 nodes, ~1168 edges). For targeted queries,
        use `/api/search`, `/api/node/{key}`, or `/api/blast/{key}` instead.
      responses:
        "200":
          description: Complete graph data
          content:
            application/json:
              schema:
                type: object
                properties:
                  nodes:
                    type: array
                    items:
                      $ref: "#/components/schemas/Node"
                  edges:
                    type: array
                    items:
                      $ref: "#/components/schemas/Edge"

components:
  schemas:
    Node:
      type: object
      properties:
        node_key:
          type: string
          description: "Unique key. Format: type:identifier (e.g., client:A054)"
        name:
          type: string
          description: Human-readable display name
        node_type:
          type: string
          enum: [client, tenant, product, group, category]
          description: |
            - `client`: SC customer (from tblCustomer)
            - `tenant`: Platform tenant database (from tblWebPlatformCompany)
            - `product`: Software product (WS, SINS, HP, CAR, SQL, CAT)
            - `group`: Sister company group (from tblCustomer.Sex field)
            - `category`: Product category (from tblCustomer.Group field)
        layer:
          type: string
          enum: [catalyst, platform]
        metadata:
          type: string
          description: JSON string with type-specific fields (parsed as object in /api/node responses)

    SearchResult:
      allOf:
        - $ref: "#/components/schemas/Node"
        - type: object
          properties:
            score:
              type: number
              description: |
                Relevance score (0-100+):
                - 100: Exact name match
                - 80: Name starts with query
                - 70: Acronym/initials match
                - 60: Node key contains query
                - 50: Name contains query
                - 10: Metadata contains query
                - +0-10: Trigram similarity bonus

    Edge:
      type: object
      properties:
        source:
          type: string
          description: Source node_key
        target:
          type: string
          description: Target node_key
        edge_type:
          type: string
          enum: [owns, belongs_to, categorized_as, runs, grouped_with]
          description: |
            - `owns`: client → tenant (this client owns this tenant DB)
            - `belongs_to`: client → group (sister company grouping via Sex field)
            - `categorized_as`: client → category (product type via Group field)
            - `runs`: tenant → product (tenant runs this product)
            - `grouped_with`: tenant ↔ tenant (same GrpCustCode corporate group)
        confidence:
          type: number
          description: "Match confidence: 1.0 = exact CustCode match, 0.8 = name match"

    NodeDetail:
      type: object
      properties:
        node:
          type: object
          properties:
            node_key:
              type: string
            name:
              type: string
            node_type:
              type: string
            layer:
              type: string
            metadata:
              type: object
              description: |
                Parsed JSON. Fields vary by node_type:

                **client metadata:**
                - `traderCode`: SC customer code (e.g., "A054")
                - `email`, `phone`, `tel`: Contact details
                - `attn`: Contact person name
                - `salesman`: Account manager
                - `sex`: Sister company group name
                - `group`: Product category (SINS/WS/HP/CAR)
                - `subGroup`: Sub-category (e.g., "With MC")
                - `platformStatus`: "mapped" (has tenant) or "unmapped" (legacy)

                **tenant metadata:**
                - `compCode`: Platform company code (e.g., "AUTOCLINIC")
                - `custCode`: Linked TraderCode (may be null)
                - `allProducts`: Comma-separated product codes
                - `subscriptionStatus`: "active", "expiring_soon", or "expired"
                - `subscriptionExpiry`: ISO date string
                - `isUat`: Boolean — true for UAT/dev tenants
                - `grpCustCode`: Corporate group code
                - `internalNote`: Internal deployment notes
        inbound:
          type: array
          description: Edges pointing TO this node
          items:
            type: object
            properties:
              source:
                type: string
              source_name:
                type: string
              source_type:
                type: string
              source_layer:
                type: string
              edge_type:
                type: string
        outbound:
          type: array
          description: Edges pointing FROM this node
          items:
            type: object
            properties:
              target:
                type: string
              target_name:
                type: string
              target_type:
                type: string
              target_layer:
                type: string
              edge_type:
                type: string

    BlastResult:
      type: object
      properties:
        upstream:
          type: array
          description: Nodes that connect TO the starting node (what depends on this?)
          items:
            $ref: "#/components/schemas/TraversalNode"
        downstream:
          type: array
          description: Nodes that the starting node connects TO (what does this depend on?)
          items:
            $ref: "#/components/schemas/TraversalNode"

    TraversalNode:
      type: object
      properties:
        node_key:
          type: string
        name:
          type: string
        node_type:
          type: string
        layer:
          type: string
        depth:
          type: integer
          description: Distance from the starting node (0 = the node itself)

    Stats:
      type: object
      properties:
        nodesByType:
          type: array
          items:
            type: object
            properties:
              node_type:
                type: string
              count:
                type: integer
        nodesByLayer:
          type: array
          items:
            type: object
            properties:
              layer:
                type: string
              count:
                type: integer
        total_nodes:
          type: integer
        total_edges:
          type: integer

    Error:
      type: object
      properties:
        error:
          type: string
