> For the complete documentation index, see [llms.txt](https://docs.thewalletcrew.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.thewalletcrew.io/connectors/pos/cegid/customer-data-and-loyalty/customise-the-mapping.md).

# Customise the mapping

Customise Cegid Retail Y2 field mapping for wallet installation, languages, consents, preferences, and brand-specific customer data.

The default mapping covers identity and contact fields. A script maps other Cegid data, including preferences, language, wallet installation state, and fiscal identifiers. This page defines the script contract, practical recipes, and safe mapping patterns.

{% hint style="info" %}
Nearly every brand running this connector deploys a mapping script. It should be treated as part of the initial implementation rather than as an optional extra.
{% endhint %}

### The contract

A mapping script registers two functions. `MapToY2` receives the wallet account and the Cegid customer structure, and runs immediately before the record is written. `MapFromY2` receives the Cegid record and the wallet account, and runs immediately after a record is read. Both run **after** the default mapping, so either can override it.

Both functions are required. Registering only one causes the other to fail when it is called.

```javascript
// server/scripts/y2.mapping.js

const mapToY2 = function (account, data) { /* wallet -> Cegid */ };
const mapFromY2 = function (data, account) { /* Cegid -> wallet */ };

export default function (context) {
  context.register('extensions.cegid.y2.mapper', {
    MapToY2: mapToY2,
    MapFromY2: mapFromY2
  });
}
```

The script is edited in the **Mapping** tab of the Cegid configuration page. The shape of the Cegid customer object it receives is documented in the [Scripting reference](/connectors/pos/cegid/developer-reference/scripting-reference.md).

```mermaid
flowchart LR
  subgraph write["Write path"]
    W1["Form data<br/>wallet account"] --> W2["Default mapping"] --> W3["MapToY2"] --> W4["Write to Cegid Retail Y2"]
  end
  subgraph read["Read path"]
    R1["Cegid record"] --> R2["Default mapping"] --> R3["MapFromY2"] --> R4["Wallet account"]
  end
  W4 -->|"Record read back"| R1
```

{% hint style="warning" %}
All scripts in a tenant are loaded together. A syntax error in any one file prevents every script endpoint from loading, not only the file containing the error, so a mistake in an unrelated script disables the Cegid mapping too.
{% endhint %}

#### Writing against real data

While a mapping is being written, the raw Cegid response is available on the account under `debug.y2.customer`, with equivalents for loyalty, sales documents, and cash operations. These make it possible to see exactly what Cegid returned for a given customer. They are a development aid available to the script, **not** fields that can be used in a pass template.

### Cegid's extensible fields

Cegid provides several containers for brand-specific data, and choosing the right one is the first decision in any mapping.

| Container             | Shape                                                                         | Slots                           |
| --------------------- | ----------------------------------------------------------------------------- | ------------------------------- |
| `UserDefinedData`     | An object with properties `UserDefinedTable0Value` … `UserDefinedTable9Value` | 10                              |
| `UserDefinedTexts`    | An array of `{ Id, Value }`, value is text                                    | **3**                           |
| `UserDefinedBooleans` | An array of `{ Id, Value }`, value is true or false                           | **3**                           |
| `UserDefinedDates`    | An array of `{ Id, Value }`, value is a date                                  | **3**                           |
| `UserDefinedValues`   | An array of `{ Id, Value }`, value is a number                                | **3**                           |
| `UserFields`          | An array of typed entries with a numeric identifier                           | Defined per Cegid configuration |

{% hint style="danger" %}
**Identifiers start at zero; Cegid labels start at one.** In every array container, `Id: 0` is the field a Cegid user sees labelled as the first one, `Id: 1` the second, and `Id: 2` the third. The same offset applies to `UserDefinedTable0Value`, which is the first free table. This is the single most common cause of a value arriving in the wrong Cegid field, and it is worth a comment on every constant.
{% endhint %}

### Recipes

#### Wallet installation state

Recording whether a customer installed the pass, and on which platform, is the most frequently implemented customisation. It is what lets a brand build campaign selections in Cegid around wallet adoption.

The pattern writes a code into a free table. Two details determine whether the resulting data is usable. The platform states must be compared to the literal value rather than tested for truthiness, because the state of a removed pass is a non-empty string and would otherwise read as installed. And the negative case must be written: a mapping that records installation but never records its absence produces a segment that only ever grows.

```javascript
// Free table 3 holds wallet installation state.
// Confirm the slot with the brand: the same ordinal points at a
// different business field in every Cegid database.
const INSTALL_SLOT = "UserDefinedTable2Value";

const NOT_INSTALLED = "000";
const APPLE_ONLY    = "001";
const GOOGLE_ONLY   = "002";
const BOTH          = "003";

function installCode(account) {
  // Compare to the literal state. "Uninstalled" is a truthy string,
  // so a truthiness test reports a removed pass as installed.
  const apple  = account["neo.pass.Apple.state"]  === "Installed";
  const google = account["neo.pass.Google.state"] === "Installed";
  if (apple && google) return BOTH;
  if (apple)           return APPLE_ONLY;
  if (google)          return GOOGLE_ONLY;
  return NOT_INSTALLED;   // explicit, not empty
}

const mapToY2 = function (account, data) {
  data.UserDefinedData = { ...data.UserDefinedData, [INSTALL_SLOT]: installCode(account) };
};

const mapFromY2 = function (data, account) {
  account.installState = data.UserDefinedData?.[INSTALL_SLOT] || NOT_INSTALLED;
};
```

An explicit `"000"` is preferable to an empty value, because a blank cannot be distinguished from a field that was never written, which makes it impossible to build a selection of customers who removed the pass.

This recipe depends on the connector writing back to Cegid when a pass is installed or removed, which is the **Update customer on pass installation change** setting described in [Advanced configuration](/connectors/pos/cegid/setup-and-configuration/advanced-configuration.md).

#### Language

Cegid carries a proprietary language identifier, and there is **no common code table across Cegid databases**: the codes are defined per database. A language table taken from another implementation is a guess, and the correct codes have to come from the brand's own Cegid configuration.

The reverse table should be derived from the forward one rather than written out separately. Independently written tables drift, and the resulting defect is subtle: a customer saved in one language reads back in another.

```javascript
// Cegid-side codes for THIS Cegid database.
const LANGUAGES = { fr: "FRA", en: "ANG", es: "ESP", de: "ALL" };
const DEFAULT_LANGUAGE = "ANG";

// Derived, so the two directions cannot drift apart.
const LANGUAGES_REVERSE = Object.fromEntries(
  Object.entries(LANGUAGES).map(([locale, code]) => [code, locale])
);

const mapToY2 = function (account, data) {
  const locale = (account.neo?.lng || "").toLowerCase().split("-")[0];
  data.LanguageId = LANGUAGES[locale] || DEFAULT_LANGUAGE;
};

const mapFromY2 = function (data, account) {
  const locale = LANGUAGES_REVERSE[data.LanguageId];
  if (locale) account.language = locale;  // unknown: leave untouched
};
```

Normalising the locale before lookup, by lowercasing it and dropping the regional part, prevents regional variants from silently missing. An unknown language should fall back to a documented default rather than to an empty value.

#### Consents in free booleans

Cegid offers two places to record marketing consent. The `Optin` fields are a four-value classification rather than a yes-or-no, listed in the [Scripting reference](/connectors/pos/cegid/developer-reference/scripting-reference.md). Which of the middle values counts as consent is a business decision that has to be agreed with the brand; it is not self-evident.

Free booleans are the more common choice, and are unambiguous. The pattern below shows the general form for writing into any array container: **filter out the existing entry, then append**.

```javascript
const CONSENT_EMAIL_ID = 0;   // free boolean 1

function upsert(list, id, entry) {
  return [...(list || []).filter((e) => e?.Id != id), { Id: id, ...entry }];
}

const mapToY2 = function (account, data) {
  // Skip only when the form collected no answer at all.
  // When it did, write false as readily as true.
  if (typeof account.consents_email === "boolean") {
    data.UserDefinedBooleans = upsert(data.UserDefinedBooleans, CONSENT_EMAIL_ID, {
      Value: account.consents_email
    });
  }
};

const mapFromY2 = function (data, account) {
  // find, not filter: filter returns an array, whose .Value is undefined
  const entry = (data.UserDefinedBooleans || []).find((e) => e?.Id == CONSENT_EMAIL_ID);
  if (entry) account.consents_email = !!entry.Value;
};
```

#### Free texts

Free texts follow the same array pattern, and the same offset applies: `Id: 2` is the field labelled as the third.

```javascript
const LOYALTY_NOTE_ID = 2;   // free text 3

const mapToY2 = function (account, data) {
  if (account.note) {
    data.UserDefinedTexts = upsert(data.UserDefinedTexts, LOYALTY_NOTE_ID, { Value: account.note });
  }
};

const mapFromY2 = function (data, account) {
  const v = (data.UserDefinedTexts || []).find((e) => e?.Id == LOYALTY_NOTE_ID)?.Value;
  if (v) account.note = v;
};
```

#### Preference lists

`UserFields` hold typed entries against identifiers defined in the brand's Cegid configuration. A list-element field carries a value from a Cegid-defined list, and the value type identifies which kind of entry it is.

```javascript
const PREFERENCE_IDS = [1, 2, 3];   // agreed with the brand
const LIST_ELEMENT = 3;             // UserField value type

const mapToY2 = function (account, data) {
  for (const id of PREFERENCE_IDS) {
    const answered = account["preference" + id];
    if (answered === undefined) continue;
    // Write NO as well as YES, or a withdrawn preference never reaches Cegid.
    data.UserFields = upsert(data.UserFields, id, {
      ValueType: LIST_ELEMENT,
      ListElementValue: answered ? "YES" : "NO"
    });
  }
};
```

#### Standard fields the default mapping leaves alone

Several standard Cegid fields are not part of the default mapping but are commonly needed. The address block may be absent on a new record, so it has to be created before anything is written into it.

```javascript
const mapToY2 = function (account, data) {
  // AddressData can be absent. Writing into it unguarded throws,
  // and the whole save fails.
  if (!data.AddressData) data.AddressData = {};
  data.AddressData.RegionId     = account.state || "";
  data.AddressData.AddressLine3 = account.address3;

  data.PassportNumber = account.passport;
  if (account.fiscalCode) data.FiscalId = account.fiscalCode.toUpperCase();
  if (account.comment)    data.Notepad  = account.comment;

  // Alternate names carry a second script, for example a Latin form
  // alongside a local one.
  data.AlternateFirstName = account.firstNameLatin;
  data.AlternateLastName  = account.lastNameLatin;
};
```

### Practices worth following

#### Replace entries, never append to them

The array containers round-trip whatever is already on the record. Appending without first removing the existing entry adds a duplicate on *every* save, and which duplicate Cegid honours is arbitrary.

```javascript
// Wrong: grows on every save.
data.UserFields = [...(data.UserFields || []), { Id: id, /* ... */ }];

// Right: remove, then add.
data.UserFields = [...(data.UserFields || []).filter((e) => e?.Id != id), { Id: id, /* ... */ }];
```

The opposite error is as damaging: assigning a fresh array replaces the whole container, discarding free fields the brand maintains by hand in Cegid.

#### Keep the two directions symmetric

Any table used in `MapToY2` must be used in `MapFromY2`, and the reverse direction should be derived rather than retyped. Two independently written tables eventually disagree, and the symptom, a value that changes into a different value across a save, is hard to attribute.

The same applies to codes: a read path that recognises a code the write path never produces is dead, usually because the script was copied from another implementation.

#### Write the negative case

A field that the wallet can set must also be one the wallet can clear. Mappings that record only the positive case produce data that can never be corrected: a preference that cannot be withdrawn, an adoption segment that only grows.

#### Guard reads and writes

Nested objects may be absent. Writing into `data.AddressData` without creating it first fails the entire save, and reading `data.UserDefinedData.X` without the optional-chaining form fails on a customer who has never had a free field set.

#### Never log the account

The account object carries names, email addresses, telephone numbers, and dates of birth. Logging it places personal data into platform logs. Where a trace is genuinely needed, a single derived value should be logged instead.

### Anti-patterns

| Anti-pattern                                       | What is observed                                                                                    |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Appending to an array without filtering            | Duplicate entries accumulate on each save; the effective value becomes unpredictable.               |
| Assigning a whole new array                        | Free fields maintained by hand in Cegid disappear the first time the wallet saves.                  |
| Using `filter` where `find` was meant              | The value reads back as undefined permanently. No error is raised; the form simply never pre-fills. |
| Separate forward and reverse tables                | Two values swap places; a record saved as one reads back as the other.                              |
| Testing a state string for truthiness              | A removed pass counts as installed; platform attribution freezes at its first value.                |
| Writing only the positive value                    | The segment grows and never shrinks; customers cannot be removed from it.                           |
| A guard that makes the negative branch unreachable | Opt-ins reach Cegid; opt-outs never do.                                                             |
| Unguarded write into a nested object               | The mapper throws for any customer lacking that block, and the whole save fails.                    |
| Logging the account object                         | Personal data in platform logs.                                                                     |
| A commented-out copy of the mapper left in place   | The next maintainer edits the dead copy.                                                            |

### Starter script

The script below combines the recipes above and can be used as a starting point.

```javascript
// server/scripts/y2.mapping.js
//
// Conventions used here:
//  - Free-field identifiers are zero-based; the Cegid label is one higher.
//    Every constant carries a comment naming the Cegid field.
//  - Array fields are written filter-then-append, never appended to.
//  - Mapping tables are declared once and reversed programmatically.
//  - Every field that can be set can also be unset.

// ------------------------------------------------------------ constants

const INSTALL_SLOT = "UserDefinedTable2Value";  // free table 3
const NOT_INSTALLED = "000", APPLE_ONLY = "001",
      GOOGLE_ONLY = "002", BOTH = "003";

const CONSENT_EMAIL_ID = 0;   // free boolean 1
const PREFERENCE_IDS   = [1, 2, 3];
const LIST_ELEMENT     = 3;

const LANGUAGES = { fr: "FRA", en: "ANG", es: "ESP" };
const DEFAULT_LANGUAGE = "ANG";
const LANGUAGES_REVERSE = Object.fromEntries(
  Object.entries(LANGUAGES).map(([l, c]) => [c, l])
);

// -------------------------------------------------------------- helpers

function upsert(list, id, entry) {
  return [...(list || []).filter((e) => e?.Id != id), { Id: id, ...entry }];
}

function installCode(account) {
  const apple  = account["neo.pass.Apple.state"]  === "Installed";
  const google = account["neo.pass.Google.state"] === "Installed";
  if (apple && google) return BOTH;
  if (apple)           return APPLE_ONLY;
  if (google)          return GOOGLE_ONLY;
  return NOT_INSTALLED;
}

// -------------------------------------------------------- wallet -> Cegid

const mapToY2 = function (account, data) {

  const locale = (account.neo?.lng || "").toLowerCase().split("-")[0];
  data.LanguageId = LANGUAGES[locale] || DEFAULT_LANGUAGE;

  if (!data.AddressData) data.AddressData = {};
  data.AddressData.RegionId = account.state || "";

  data.UserDefinedData = { ...data.UserDefinedData, [INSTALL_SLOT]: installCode(account) };

  if (typeof account.consents_email === "boolean") {
    data.UserDefinedBooleans =
      upsert(data.UserDefinedBooleans, CONSENT_EMAIL_ID, { Value: account.consents_email });
  }

  for (const id of PREFERENCE_IDS) {
    const answered = account["preference" + id];
    if (answered === undefined) continue;
    data.UserFields = upsert(data.UserFields, id, {
      ValueType: LIST_ELEMENT,
      ListElementValue: answered ? "YES" : "NO"
    });
  }
};

// -------------------------------------------------------- Cegid -> wallet

const mapFromY2 = function (data, account) {

  const locale = LANGUAGES_REVERSE[data.LanguageId];
  if (locale) account.language = locale;

  account.state = data.AddressData?.RegionId;
  account.installState = data.UserDefinedData?.[INSTALL_SLOT] || NOT_INSTALLED;

  const consent = (data.UserDefinedBooleans || []).find((e) => e?.Id == CONSENT_EMAIL_ID);
  if (consent) account.consents_email = !!consent.Value;

  for (const id of PREFERENCE_IDS) {
    const f = (data.UserFields || []).find((e) => e?.Id == id);
    if (f) account["preference" + id] = f.ListElementValue === "YES";
  }
};

// --------------------------------------------------------- registration

export default function (context) {
  context.register('extensions.cegid.y2.mapper', { MapToY2: mapToY2, MapFromY2: mapFromY2 });
}
```

### FAQ

<details>

<summary><strong>A value is being written into the wrong Cegid field. Why?</strong></summary>

Almost always the zero-based identifier. The field a Cegid user calls the third free text is `Id: 2` in a script. Confirming the intended field with the brand by its Cegid label, and commenting the constant accordingly, prevents it.

</details>

<details>

<summary><strong>A field saves but never reads back. Why?</strong></summary>

The most common cause is using `filter` where `find` was intended. `filter` returns an array, and reading a value from an array yields nothing, silently and with no error.

</details>

<details>

<summary><strong>Can a mapping script override the default mapping?</strong></summary>

Yes. The script runs after the default mapping on both paths, so any field it sets takes precedence.

</details>

<details>

<summary><strong>Can a language table be copied from another implementation?</strong></summary>

No. Cegid language identifiers are defined per database. The codes have to come from the brand's own Cegid configuration.

</details>

<details>

<summary><strong>No customisation appears to take effect at all. What should be checked?</strong></summary>

A syntax error in any script file, not necessarily the mapping file, prevents every script from loading for that tenant. Reviewing the other scripts is the first step.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.thewalletcrew.io/connectors/pos/cegid/customer-data-and-loyalty/customise-the-mapping.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
