---
name: tes-integration
description: |
  TES (Thing Event System) GraphQL API integration guide. Use this skill when:
  (1) Creating or managing Things (physical items, products, returns)
  (2) Creating or managing Products (catalog items with value profiles)
  (3) Creating or managing Holders (customers, warehouses, stores)
  (4) Creating or managing Locations (warehouses, stores, processing centers)
  (5) Implementing product lifecycle tracking (manufacture → sale → return → recycle)
  (6) Building return/takeback flows
  (7) Querying entity events and history
  (8) Working with AI enrichment (vision, pricing, valuation)
  (9) Configuring pricing (AI-powered or rules-based)
  (10) Searching things/products by vector similarity
---

# TES Integration Guide

The Thing Event System (TES) is an event-sourced GraphQL API for tracking physical items through their lifecycle. All mutations emit events that are processed asynchronously by consumers (enrichment, pricing, embedding).

## Quick Reference

| Entity | Create | Update | Query | Events |
|--------|--------|--------|-------|--------|
| Thing | `createThing` | `updateThing`, `addThingStatus`, `transferThing`, `changeThingLocation` | `thing`, `things` | `thingEvents` |
| Product | `createProduct` | `updateProduct` | `product`, `products` | `productEvents` |
| Holder | `createHolder` | `updateHolder` | `holder`, `holders` | `holderEvents` |
| Location | `createLocation` | `updateLocation` | `location`, `locations` | - |
| Search | - | - | `searchThings`, `searchProducts` | - |
| Pricing Config | - | `updatePricingConfig` | `pricingConfig` | - |
| Media | `uploadImage` | - | - | - |

## API Endpoint

```
POST /api/graphql
Headers:
  Content-Type: application/json
  Authorization: Bearer <token>
  X-Client-Id: <client-id>
```

## Core Entities

### Things
Physical items tracked through their lifecycle. Key fields:
- `id`: Unique identifier
- `name`: Human-readable name
- `current_stage`: Lifecycle stage (manufactured, sold, returned, etc.)
- `holder_id`: Current holder (customer, warehouse, etc.)
- `location_id`: Current location
- `product_id`: Linked product (auto-matched by AI enrichment or set manually)
- `status_history`: Array of status changes with timestamps
- `vision`: AI vision analysis (brand, model, colorway, category, condition)
- `pricing`: Market pricing data (market_low, market_mid, market_high)
- `valuation`: AI valuation estimate (low/mid/high with confidence)
- `media`: Aggregated images from status history

### Products
Catalog items that things are instances of. Key fields:
- `id`: Unique identifier
- `name`, `brand`, `model`, `manufacturer`: Product identity
- `sku`: Stock keeping unit
- `category`: Product category
- `value_profile`: Pricing reference data (msrp, rrp, wholesale) — used by rules-based pricing
- `tags`, `features`: Classification and attributes
- `things`: Resolved list of thing instances (via graph)

### Holders
Entities that can hold things (people, organizations, facilities):
- `id`: Unique identifier
- `name`, `last_name`: Name fields
- `email`, `phone`: Contact info
- `type`: customer | warehouse | store | carrier | processor | manufacturer
- `address`: Location details

### Locations
Physical locations where things can be:
- `id`: Unique identifier
- `name`: Location name
- `type`: warehouse | store | processing_center
- `address`: Full address details

## Lifecycle Stages

Things progress through these stages:

```
Production:     manufactured → sourced
Inventory:      in_stock → in_transit → delivered
Customer:       sold → in_use
Capture:        captured → identified → valued
Return:         returned → received
Processing:     processing → inspecting → refurbishing
Outcomes:       processed → certified → listed → resold
End States:     recycled → donated → disposed
Problems:       issue → rejected → lost
```

## AI Enrichment Pipeline

When a thing is created or updated with an image, the enrichment pipeline runs automatically:

1. **Vision** — AI analyzes the image: brand, model, colorway, category, condition (grade + score)
2. **Pricing** — Market pricing estimates: `market_low`, `market_mid`, `market_high` with confidence
3. **Valuation** — Fair market value estimate: low/mid/high range with currency
4. **Product Matching** — Auto-matches to existing product or creates a new one
5. **Embedding** — Generates vector embeddings for similarity search

Pipeline latency: ~2.5s warm, ~4-6s with cold starts.

Results are stored on the thing's `enrichment` JSON field and projected to typed fields (`vision`, `pricing`, `valuation`).

### Pricing Modes

TES supports two pricing modes per client:

- **AI-powered** (default) — Enrichment consumer calls AI for market price estimates
- **Rules-based** — Dedicated pricing consumer evaluates configurable rules against thing/product attributes. When active, the enrichment consumer skips the AI pricing stage.

Configure via `pricingConfig` query and `updatePricingConfig` mutation. See [references/pricing.md](references/pricing.md).

## Common Workflows

### 1. Create a Customer and Product

```graphql
# Create holder (customer)
mutation CreateHolder($input: HolderInput!) {
  createHolder(input: $input) {
    success
    message
    holder_id
  }
}

# Variables
{
  "input": {
    "name": "John",
    "last_name": "Doe",
    "email": "john@example.com",
    "type": "customer"
  }
}

# Create thing (product)
mutation CreateThing($input: ThingInput!) {
  createThing(input: $input) {
    success
    message
    eventId
    queued
  }
}

# Variables
{
  "input": {
    "name": "Blue Widget",
    "holder_id": "holder_xxx",
    "holder_type": "customer",
    "statuses": [{ "parent_status": "sold", "status": "sold" }],
    "source": "my-app"
  }
}
```

### 2. Process a Return

```graphql
# Customer initiates return
mutation InitiateReturn($id: String!, $input: ThingStatusInput!) {
  addThingStatus(id: $id, input: $input) {
    success
    message
    eventId
  }
}

# Variables
{
  "id": "thing_xxx",
  "input": {
    "parent_status": "captured",
    "status": "return_initiated",
    "data": { "reason": "defective", "source": "customer-portal" }
  }
}
```

### 3. Transfer to Warehouse

Use the dedicated `transferThing` and `changeThingLocation` mutations to move things between holders and locations. These update the top-level FK and automatically trigger `edge_history` tracking.

```javascript
// Record receipt in status history (audit)
await graphql(ADD_THING_STATUS, {
  id: "thing_xxx",
  input: {
    parent_status: "received",
    status: "received",
    data: { method: "return_receipt" }
  }
});

// Transfer ownership (updates holder_id FK + edge_history)
await graphql(TRANSFER_THING, {
  id: "thing_xxx",
  input: {
    holder_id: "warehouse_xxx",
    holder_type: "warehouse"
  }
});

// Change location (updates location_id FK + edge_history)
await graphql(CHANGE_THING_LOCATION, {
  id: "thing_xxx",
  input: { location_id: "loc_xxx" }
});
```

> **Note:** `holder_id`/`location_id` inside `addThingStatus` only go to `status_history` (audit trail). Use `transferThing` or `changeThingLocation` to update the actual FK and trigger edge_history.

### 4. Query Things with Filters

```graphql
query ListThings($limit: Int, $offset: Int) {
  things(limit: $limit, offset: $offset) {
    items {
      id
      name
      current_stage
      holder_id
      created_at
    }
    totalCount
    hasMore
  }
}
```

### 5. Upload an Image (Triggers Enrichment)

```javascript
// Upload image via multipart form data
const formData = new FormData();
formData.append("operations", JSON.stringify({
  query: `mutation($file: File!, $entity_type: String!, $entity_id: String!) {
    uploadImage(file: $file, entity_type: $entity_type, entity_id: $entity_id) {
      success r2_key
    }
  }`,
  variables: { file: null, entity_type: "thing", entity_id: "thing_xxx" }
}));
formData.append("map", JSON.stringify({ "0": ["variables.file"] }));
formData.append("0", imageFile);

const result = await fetch("/api/graphql", {
  method: "POST",
  headers: { Authorization: `Bearer ${token}`, "X-Client-Id": clientId },
  body: formData
});

// Poll thing after ~3s to see enrichment results (vision, pricing, valuation)
```

### 6. Search by Similarity

```graphql
query SearchThings($input: VectorSearchInput!) {
  searchThings(input: $input) {
    items {
      score
      thing { id name current_stage pricing { market_mid currency } }
    }
    search_type
    total_candidates
  }
}

# Variables — text search
{ "input": { "query": "blue running shoes", "limit": 10 } }

# Variables — similar to existing thing
{ "input": { "similar_to_thing_id": "thing_xxx", "limit": 10 } }

# Variables — image search
{ "input": { "image_url": "https://...", "available_for_sale_only": true } }
```

## Detailed References

- **Things management**: See [references/things.md](references/things.md)
- **Products management**: See [references/products.md](references/products.md)
- **Holders management**: See [references/holders.md](references/holders.md)
- **Locations management**: See [references/locations.md](references/locations.md)
- **Pricing configuration**: See [references/pricing.md](references/pricing.md)
- **Return flow**: See [references/return-flow.md](references/return-flow.md)
- **Full GraphQL schema**: See [references/graphql-schema.md](references/graphql-schema.md)

## Response Format

All mutations return:
```json
{
  "success": true,
  "message": "Operation completed",
  "eventId": "evt_xxx",
  "queued": true
}
```

Events are processed asynchronously. Poll the entity after ~500ms to see updated state. For enrichment results (vision, pricing, valuation), poll after ~3s.

## Error Handling

```json
{
  "errors": [{
    "message": "Validation error: name is required",
    "path": ["createThing"],
    "extensions": { "code": "VALIDATION_ERROR" }
  }]
}
```

Common error codes:
- `VALIDATION_ERROR`: Invalid input
- `NOT_FOUND`: Entity doesn't exist
- `UNAUTHORIZED`: Invalid or missing token
- `FORBIDDEN`: Insufficient permissions
