> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solya.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Spécifications d'ingestion — exemples travaillés

> Deux spécifications complètes et annotées : une archive SAV Polaris et une export de tickets Ginkoia.

Voici des spécifications réelles (légèrement réduites) montrant comment la détection, l'analyse et les étapes de promotion s'assemblent. Reportez-vous au [catalogue d'étapes de promotion](/fr/developers/ingestion-specs/promotion-steps) pour chaque étape.

## Exemple 1 — Archive SAV Polaris

Une archive `.sav` est un dump SQL Polaris. La spécification extrait les tables SQL, puis exécute une promotion par dataset de sortie. Deux sont montrées : `brands` et `inventory_items`.

```json theme={null}
{
  "spec_id": "polaris_sav",
  "spec_version": 1,
  "name": "Polaris SAV Archive",
  "pos_system": "polaris",
  "status": "ACTIVE",
  "scope": "GLOBAL",
  "is_default": true,
  "priority": 0,

  "detection": {
    "match_mode": "composite",
    "rules": [
      { "type": "data_source", "codes": ["pos_polaris"] },
      { "type": "filename_matches", "pattern": "(?i)\\.sav$|\\.polaris\\.sav$" }
    ]
  },

  "parsing": {
    "parser": {
      "type": "polaris_sav",
      "sql_filename": "0-full.sql",
      "extract_media": true,
      "tables": ["marque", "libelleproduit", "article", "detvte", "libtaille"]
    },
    "extra_fields": { "mode": "store_json", "target_column": "_extra" }
  },

  "tags": { "systems": ["polaris"], "formats": ["sav"] },

  "promotions": [
    {
      "dataset_id": "brands",
      "domain": "catalog",
      "source_table": "marque",
      "conformed_table": "brands",
      "merge_keys": ["organization_id", "code"],
      "steps": [
        { "type": "deduplicate", "columns": ["no_marque"] },
        { "type": "add_column", "column": "is_active", "expression": "true" },
        { "type": "inject_value", "column": "organization_id", "value_key": "organization_id" },
        { "type": "add_column", "column": "created_at", "expression": "current_timestamp()" },
        { "type": "rename_columns", "columns": { "no_marque": "code", "nom": "name", "no_pays": "country_id" } },
        { "type": "generate_id", "output_column": "id", "key_columns": ["code"] },
        { "type": "id_mapping_output", "mapping_type": "brands", "code_column": "code", "id_column": "id" }
      ]
    },
    {
      "dataset_id": "inventory_items",
      "domain": "inventory",
      "source_table": "libelleproduit",
      "conformed_table": "inventory_items",
      "merge_keys": ["organization_id", "code"],
      "source_tables": ["detvte", "article", "libtaille"],
      "steps": [
        { "type": "deduplicate", "columns": ["no_libelleproduit"] },
        { "type": "sequential_join", "target_dataset": "detvte", "join_column": "no_libelleproduit",
          "select_columns": ["no_libelleproduit", "no_critmod", "no_article"], "deduplicate_on": ["no_libelleproduit"] },
        { "type": "sequential_join", "target_dataset": "article", "join_column": "no_article",
          "select_columns": ["no_article", "no_libtaille", "no_magasin"], "deduplicate_on": ["no_article"] },
        { "type": "id_mapping_join", "mapping_type": "variant", "source_column": "no_critmod", "output_column": "variant_uuid" },
        { "type": "sequential_join", "target_dataset": "libtaille", "join_column": "no_libtaille",
          "select_columns": [], "select_expressions": { "_size_label": "libelle" }, "deduplicate_on": ["no_libtaille"] },
        { "type": "taxonomy_mapping", "taxonomy_type": "size", "source_column": "_size_label",
          "source_id_column": "no_libtaille", "output_column": "taxonomy_id" },
        { "type": "id_mapping_join", "mapping_type": "shop", "source_column": "no_magasin", "output_column": "shop_uuid" },
        { "type": "inject_value", "column": "organization_id", "value_key": "organization_id" },
        { "type": "rename_columns", "columns": { "no_libelleproduit": "code", "variant_uuid": "variant_id",
          "taxonomy_id": "taxonomy_size_id", "shop_uuid": "shop_id" } },
        { "type": "generate_id", "output_column": "id", "key_columns": ["code"] },
        { "type": "id_mapping_output", "mapping_type": "inventory_items", "code_column": "code", "id_column": "id" }
      ]
    }
  ]
}
```

**À remarquer**

* La **détection** se conditionne sur la source de données Polaris **et** un nom de fichier `.sav`.
* La promotion **brands** se termine par `generate_id` puis `id_mapping_output` — publiant un mapping code→id `brands` que d'autres promotions peuvent résoudre.
* La promotion **inventory\_items** enchaîne les `sequential_join` pour rassembler des champs sur les tables, résout la variante et le magasin via `id_mapping_join`, mappe le label de taille en id de taxonomie, et finalement génère son propre id.

## Exemple 2 — Tickets Ginkoia (Excel)

Une export Excel de lignes de tender-paiement. Notez l'option de parser `section_context` et la promotion d'enrichissement `update_columns`.

```json theme={null}
{
  "spec_id": "ginkoia_tickets",
  "spec_version": 2,
  "name": "Ginkoia — Tickets (Payment Tender Lines)",
  "pos_system": "ginkoia",
  "status": "ACTIVE",

  "detection": {
    "match_mode": "composite",
    "rules": [
      { "type": "data_source", "codes": ["pos_ginkoia", "user_uploads"] },
      { "type": "filename_matches", "pattern": "(?i)tickets" },
      { "type": "header_contains", "columns": ["Numéro", "Séquence", "Montant", "Mode paiement"] }
    ]
  },

  "parsing": {
    "parser": {
      "type": "excel",
      "header_row": 0,
      "strip_columns": [0, 1],
      "section_context": [
        { "label": "Magasin", "output_column": "_section_magasin" },
        { "label": "Poste",   "output_column": "_section_poste" }
      ]
    },
    "mapping": {
      "columns": {
        "Numéro":       { "target": "ticket_number",  "type": "STRING" },
        "Montant":      { "target": "amount",         "type": "DOUBLE" },
        "Mode paiement":{ "target": "payment_method", "type": "STRING" },
        "_section_magasin": { "target": "shop_name",  "type": "STRING" }
      }
    },
    "extra_fields": { "mode": "store_json", "target_column": "_extra_fields" },
    "validation": { "required_columns": ["ticket_number", "payment_method"], "min_rows": 1 }
  },

  "promotions": [
    {
      "dataset_id": "customers",
      "domain": "catalog",
      "conformed_table": "customers",
      "merge_keys": ["organization_id", "code"],
      "update_columns": ["postal_code", "updated_at"],
      "source_table": "Tickets",
      "steps": [
        { "type": "filter", "condition": "customer_postal_code IS NOT NULL AND customer_postal_code != ''" },
        { "type": "deduplicate", "columns": ["customer_name"] },
        { "type": "id_mapping_join", "mapping_type": "customers_by_name", "source_column": "customer_name", "output_column": "code" },
        { "type": "filter", "condition": "code IS NOT NULL" },
        { "type": "rename_columns", "columns": { "customer_postal_code": "postal_code" } },
        { "type": "inject_value", "column": "organization_id", "value_key": "organization_id" },
        { "type": "add_column", "column": "updated_at", "expression": "current_timestamp()" },
        { "type": "generate_id", "output_column": "id", "key_columns": ["organization_id", "code"] }
      ]
    }
  ]
}
```

**À remarquer**

* La **détection** combine un filtre par source de données, un regex de nom de fichier, et une vérification `header_contains` sur les colonnes attendues.
* Le **`section_context`** porte les en-têtes de section « Magasin » / « Poste » vers le bas sur chaque ligne.
* La promotion **customers** est un **enrichissement** ciblé : `update_columns` limite l'upsert à `postal_code` (et `updated_at`), donc elle remplit les codes postaux sans écraser le reste d'une ligne client existante. Les deux étapes `filter` garantissent que seules les lignes valides et résolubles sont écrites.

<Note>
  Les vrais specs de seed vivent sous `scripts/db/seeds/seed-data/ingestion-specs/<system>/`. Les nouvelles
  spécifications sont validées au moment du seed (incluant les codes `data_source`).
</Note>
