# Example Data System

The Example Data system provides realistic sample data for all Blesta models, used for testing, validation, and API documentation.

## Architecture: Pure Schema-Driven

The system follows a **pure schema-driven approach** where:
1. **Example files contain ONLY native table fields** (no embedded relationships)
2. **Relationships are loaded dynamically** via schema definitions
3. **Each model has its own example file** (clean separation)

## Directory Structure

```
examples/
  ├── Clients.json           # Client records (table fields only)
  ├── Invoices.json          # Invoice records (table fields only)
  ├── InvoiceLineItems.json  # Line items (array of 3 items)
  ├── Packages.json          # Package records (no embedded groups/pricing)
  └── ...                    # 37 total models
```

## Example File Format

### Single Item (belongs_to relationships)
Most models use a single object:

```json
{
  "id": 76,
  "module_id": 3,
  "status": "active",
  "company_id": 1
}
```

### Multiple Items (has_many collections)
Collection models use an array with multiple items:

```json
[
  {
    "id": 178,
    "invoice_id": 121,
    "description": "Service A",
    "amount": "10.0000"
  },
  {
    "id": 179,
    "invoice_id": 121,
    "description": "Setup Fee",
    "amount": "25.0000"
  }
]
```

## Rules for Example Files

### ✅ DO Include:
- **Native table fields** - Fields that exist in the database table
- **Primary keys and foreign keys** - id, client_id, invoice_id, etc.
- **All required fields** - Fields that cannot be null
- **Representative optional fields** - Common optional fields with realistic values

### ❌ DO NOT Include:
- **Virtual/computed fields** - These are generated at runtime (id_code, totals, etc.)
- **Relationship data** - No embedded objects or arrays from other models
- **Merged fields** - Fields that come from belongs_to merges (client name from contact)

### Examples of What to Remove:

**Packages.json - Before (WRONG):**
```json
{
  "id": 76,
  "name": "Basic Monthly",           // ← REMOVE: from PackageNames
  "groups": [{...}],                 // ← REMOVE: relationship to PackageGroups
  "pricing": [{...}],                // ← REMOVE: relationship to PackagePricing
  "description": "Basic hosting"     // ← REMOVE: from PackageDescriptions
}
```

**Packages.json - After (CORRECT):**
```json
{
  "id": 76,
  "module_id": 3,
  "status": "active",
  "company_id": 1
}
```

## Using ExampleDataLoader

### Load a Simple Example
```php
$loader = new ExampleDataLoader();
$package = $loader->loadExample('Packages');
// Returns: {id: 76, module_id: 3, status: "active", ...}
```

### Load with Relationships
```php
$loader = new ExampleDataLoader();
$package = $loader->getContext('Packages', [
    'preset' => 'get',
    'depth' => 2
]);
// Returns package with dynamically loaded:
// - names (from PackageNames)
// - descriptions (from PackageDescriptions)
// - pricing (from PackagePricing)
// - groups (from PackageGroups)
```

### Load with Specific Relationships
```php
$invoice = $loader->getContext('Invoices', [
    'relationships' => ['client', 'line_items'],
    'depth' => 1
]);
// Returns invoice with only client and line_items loaded
```

### Multiple Items in Collections
When a collection example is an array, all items are loaded:

```php
// InvoiceLineItems.json contains 3 items
$invoice = $loader->getContext('Invoices', [
    'load_collections' => true,
    'collections' => ['line_items']
]);

// $invoice->line_items will have all 3 items from the array
```

## Collection Examples (Multiple Items)

These models should have **arrays** in their example files to demonstrate realistic collections:

- `InvoiceLineItems.json` - Multiple line items per invoice (2-4 items)
- `PackageNames.json` - Multiple language variants (en_us, es_es)
- `PackageDescriptions.json` - Multiple language variants
- `PackagePricing.json` - Multiple pricing tiers (monthly, yearly)
- `ServiceOptions.json` - Multiple configurable options
- `ClientSettings.json` - Multiple settings per client

## Updating Example Files

When cleaning up an example file:

1. **Find the database schema**: `DatabaseSchemas/tables/{table_name}.json`
2. **Identify native columns**: Only keep fields listed in the schema
3. **Remove relationships**: Delete any nested objects or arrays
4. **Remove virtual fields**: Delete computed fields (check ModelSchemas)
5. **Keep realistic values**: Use plausible data, not Lorem Ipsum

## Schema Integration

Example files work with the schema system:

```
ExampleDataLoader + SchemaLoader
         ↓
  Complete Object Graph
```

- `DatabaseSchemas/` - Table structure (155 tables)
- `ModelSchemas/` - Virtual fields, relationships, presets (80+ models)
- `examples/` - Sample data (37 models)

The loader merges these automatically to build complete, realistic object graphs.

## Benefits of Pure Schema-Driven Approach

✅ **Clear Separation** - Each model's data lives in one place
✅ **No Duplication** - Relationship data isn't repeated across files
✅ **Easy Maintenance** - Update PackageNames.json once, used everywhere
✅ **Flexible Loading** - Choose which relationships to load at runtime
✅ **Realistic Collections** - Arrays demonstrate real-world data shapes
✅ **Schema Accuracy** - Examples match actual database structure

## Plugin & Module Integration

Plugins and modules can expose custom template variables to the AI by implementing `ExampleDataProviderInterface`. The system auto-discovers all installed plugins and modules and registers any that implement the interface before building AI context.

### How It Works

```
Plugin/Module implements ExampleDataProviderInterface
              ↓
  EmailTagContextBuilderFactory (auto-discovery per company)
       ↓                    ↓
ExampleDataLoader     EmailTagContextBuilder
registerProvider()    registerTagMapping()
       ↓                    ↓
  loadExample()        getModelName()
       ↓                    ↓
        AI context generation
```

The factory (`core/Util/AI/EmailTagContextBuilderFactory.php`) is called automatically in the email generation controllers. No controller changes are needed when a plugin or module implements the interface.

---

### The Interface

**Location:** `core/Util/ExampleData/Common/ExampleDataProviderInterface.php`

```php
use Blesta\Core\Util\ExampleData\Common\ExampleDataProviderInterface;

interface ExampleDataProviderInterface
{
    // Add fields to an existing core model example (e.g. service.cpanel_username)
    public function getExampleExtensions(): array;

    // Provide example data for a new model with no core JSON file
    public function getExampleModels(): array;

    // Map tag names to model names for new root-level template tags
    public function getTagMappings(): array;
}
```

All three methods must be implemented. Return an empty array from any method that isn't needed.

---

### Use Case 1: Adding Fields to an Existing Tag

Use this when your plugin/module stores extra data on a core object — for example, a cPanel module that stores `cpanel_username` on a service.

Implement `getExampleExtensions()` with the model name as key and a flat `field => value` array as the value. Return empty arrays for the other two methods.

```php
use Blesta\Core\Util\ExampleData\Common\ExampleDataProviderInterface;

class Cpanel extends Module implements ExampleDataProviderInterface
{
    public function getExampleExtensions(): array
    {
        return [
            'Services' => [
                'cpanel_username' => 'johndoe',
                'cpanel_domain'   => 'johndoe.example.com',
            ],
        ];
    }

    public function getExampleModels(): array
    {
        return [];
    }

    public function getTagMappings(): array
    {
        return [];
    }
}
```

The AI will now see `service.cpanel_username` and `service.cpanel_domain` as valid tags with example values.

---

### Use Case 2: Adding a New Root-Level Tag

Use this when your plugin introduces an entirely new template tag (e.g., `{{ domain.name }}`). You need to provide both example data for the model and a tag-to-model mapping.

```php
use Blesta\Core\Util\ExampleData\Common\ExampleDataProviderInterface;

class DomainsPlugin extends Plugin implements ExampleDataProviderInterface
{
    public function getExampleExtensions(): array
    {
        return [];
    }

    public function getExampleModels(): array
    {
        return [
            'DomainInfo' => [
                'id'         => 1,
                'domain'     => 'example.com',
                'tld'        => '.com',
                'expires'    => '2027-01-01 00:00:00',
                'auto_renew' => 1,
            ],
        ];
    }

    public function getTagMappings(): array
    {
        // Maps the template tag name to the model name above
        return [
            'domain' => 'DomainInfo',
        ];
    }
}
```

The AI will now recognise `{{ domain.name }}`, `{{ domain.tld }}`, etc. as valid tags.

---

### Use Case 3: New Tag with a Collection Model

For models that return multiple records (e.g., nameservers), use an indexed array in `getExampleModels()`:

```php
public function getExampleModels(): array
{
    return [
        'DomainNameservers' => [
            ['id' => 1, 'domain_id' => 1, 'host' => 'ns1.example.com'],
            ['id' => 2, 'domain_id' => 1, 'host' => 'ns2.example.com'],
        ],
    ];
}

public function getTagMappings(): array
{
    return [
        'nameservers' => 'DomainNameservers',
    ];
}
```

---

### Method Summary

| Method | What it does | When to use |
|---|---|---|
| `getExampleExtensions()` | Merges fields into an existing core model example | Adding plugin-specific fields to `service`, `client`, `invoice`, etc. |
| `getExampleModels()` | Provides example data for a brand-new model | Introducing a new template tag with its own data shape |
| `getTagMappings()` | Maps lowercase tag names to PascalCase model names | Always required when using `getExampleModels()` |

---

### Auto-Discovery

No manual registration is required. `EmailTagContextBuilderFactory` automatically handles both discovery strategies:

**1. Filesystem discovery** (`discoverPluginExamples()`)
The factory calls `ExampleDataLoader::discoverPluginExamples()` which scans all subdirectories of `PLUGINDIR` and `COMPONENTDIR/modules/` for an `examples/` folder. Any found are registered as plugin paths. This covers plugins/modules that ship JSON example files.

**2. Interface discovery**
The factory then calls `PluginManager::getAll($company_id)` and `ModuleManager::getAll($company_id)`, instantiates each plugin/module, and checks for `instanceof ExampleDataProviderInterface`. Those that implement it are registered as providers.

Both mechanisms run before any `loadExample()` call, so the cache is always populated with the full merged data.

Plugins and modules that implement neither approach (no `examples/` directory and no interface) are silently skipped.

> **Note:** Provider data is merged before caching. If you register a provider after a model has already been loaded, call `$loader->clearCache($modelName)` to force a fresh merge.

---

### Combined Schemas for Validation

`ExampleDataLoader::getSchema($modelName)` returns the structure of the fully merged example — core fields plus all plugin-provided extensions — with all values set to `null`. This is the combined schema for validation.

```php
// After factory builds the loader with all providers registered:
$schema = $loader->getSchema('Services');
// Returns: { id: null, status: null, ..., cpanel_username: null, cpanel_domain: null }
```

For tag-level validation (checking whether a template tag path is valid), use `EmailTagContextBuilder::getTagSchemas()` through a factory-built builder:

```php
Loader::loadModels($this, ['PluginManager', 'ModuleManager']);
$builder = (new \Blesta\Core\Util\AI\EmailTagContextBuilderFactory(
    $this->PluginManager,
    $this->ModuleManager,
    $this->company_id
))->build();

$schemas = $builder->getTagSchemas(['service.cpanel_username', 'service.status']);
// Returns schema info for both core and plugin-provided fields
// Plugin fields include: ['type' => 'string', 'nullable' => true, 'plugin_provided' => true]
```

Plugin-provided fields that have no entry in a core schema file are flagged with `'plugin_provided' => true` so callers can distinguish them.

## Migration Status

✅ Cleaned up: Packages, PackageGroups, Clients, InvoiceLineItems
⏳ To review: ~33 remaining example files

When adding new models, follow the pure schema-driven approach from the start.
