Passer au contenu principal
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 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.
{
  "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.
{
  "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.
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).