# Product change leads to a new model version

> A change to the product master in the ERP notifies the person responsible, who creates a new model version in BatteryPass; declared data is never written automatically.

When engineering changes a battery (another cell supplier, a new capacity, a different mass), the product master in
the ERP changes, and the battery passport needs a new **model version** with its own effective date. That change is
declared data with legal weight. In this workflow **nothing is written to BatteryPass automatically**: the
middleware notices the change and tells a person, who creates the new version in the portal and has its values
approved. Keys cannot write declared data, so the pattern is also what BatteryPass enforces.

```mermaid
sequenceDiagram
  participant ERP
  participant MW as Middleware
  participant BP as BatteryPass API
  participant P as Person responsible
  participant M as Member in the portal
  ERP->>MW: thin event: product number, kind of change
  MW->>ERP: read the changed product, check the event
  MW->>MW: passport data affected? otherwise drop
  MW->>BP: listBatteryModels (scope read)
  BP-->>MW: the model by GTIN or model identifier
  MW->>P: ticket or chat message with model and change
  P->>M: decides on a new model version
  M->>BP: create the version, enter values, have them approved
  M->>BP: check the change impact on existing units
```

**Trigger:** The ERP reports a change to the product or material master, for example as a business event or a webhook.

**Key scopes:** read

| # | Who                  | What                                                                                                                                                                       | Operation                                                                                                             |
| - | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 1 | ERP                  | Sends a thin event with the product number and the kind of change.                                                                                                         |                                                                                                                       |
| 2 | Middleware           | Reads the changed product from the ERP and decides whether passport data is affected (for example cell chemistry, capacity, mass); price or text changes are dropped.      |                                                                                                                       |
| 3 | Middleware           | Finds the battery model by GTIN or model identifier.                                                                                                                       | [`listBatteryModels`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-battery-models.md) (Scope: read) |
| 4 | Middleware           | Notifies the person responsible with the model and the change, for example by ticket or chat.                                                                              |                                                                                                                       |
| 5 | Member in the portal | Creates a new model version with its effective date in the portal, enters the changed values and has them approved; BatteryPass shows beforehand which units are affected. |                                                                                                                       |

**Security**

- The middleware needs the scope read only; keys cannot write declared data.
- The middleware checks the ERP's webhooks the way the vendor provides (signature, validation handshake or secret URL).

**Data minimisation**

- The notification names the model and the changed characteristics, no supplier or price data.

## The trigger

ERPs announce product changes in different ways; the [ERP pages](https://passport.kabin.sh/docs/en/integrations/erp/sap-s4hana-cloud) list
what each system offers. Examples: business events of SAP S/4HANA through SAP Event Mesh, webhook subscriptions of
Business Central, business events of Dynamics 365 Finance and Supply Chain Management, automation rules in Odoo.
Most of them are thin: they say _what_ changed, not _how_. The middleware therefore:

1. checks the event as the vendor provides (a signature, a validation handshake, a client state or a secret URL)
   and drops what it cannot verify;
2. reads the current product from the ERP's API;
3. compares the attributes that matter for the passport (for example chemistry, rated capacity or energy, mass,
   cell or module supplier) with what it saw last time, and drops changes of prices, texts or logistics data.

Without events, compare a nightly product export with the previous one: the result is the same.

## Find the battery model

[`listBatteryModels`](https://passport.kabin.sh/docs/en/api-reference/catalog/list-battery-models) returns your models with `gtin` and `sku`
(the model identifier). There is no filter by GTIN: page through the models once and keep a map in the middleware.

```bash title="cURL"
curl -sS "$BATTERYPASS_URL/api/v1/battery-models?limit=200" \
  -H "Authorization: Bearer $BATTERYPASS_READ_KEY" | jq '.items[] | select(.gtin == "04012345000023") | {id, name, sku}'
```

```ts title="TypeScript"
const BASE = `${process.env.BATTERYPASS_URL}/api/v1`;
const headers = { Authorization: `Bearer ${process.env.BATTERYPASS_READ_KEY}` };

type Model = { id: string; name: string; gtin?: string; sku?: string };

export async function modelsByGtin(): Promise<Map<string, Model>> {
  const out = new Map<string, Model>();
  let cursor: string | null = null;
  do {
    const query = new URLSearchParams({ limit: '200', ...(cursor ? { cursor } : {}) });
    const res = await fetch(`${BASE}/battery-models?${query}`, { headers });
    if (!res.ok) throw new Error(`listBatteryModels: ${res.status} ${(await res.json()).code}`);
    const page: { items: Model[]; next_cursor: string | null } = await res.json();
    for (const m of page.items) if (m.gtin) out.set(m.gtin, m);
    cursor = page.next_cursor;
  } while (cursor);
  return out;
}

// Placeholder: open a ticket for the product owner. Never write to BatteryPass here.
export function notify(model: Model, changed: string[]) {
  console.log(`New model version needed for ${model.name} (${model.id}): ${changed.join(', ')} changed in the ERP`);
}
```

```python title="Python"
import os

import requests

BASE = os.environ["BATTERYPASS_URL"] + "/api/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["BATTERYPASS_READ_KEY"]


def models_by_gtin():
    out, cursor = {}, None
    while True:
        params = {"limit": 200, **({"cursor": cursor} if cursor else {})}
        res = session.get(f"{BASE}/battery-models", params=params, timeout=30)
        res.raise_for_status()
        page = res.json()
        out.update({m["gtin"]: m for m in page["items"] if m.get("gtin")})
        cursor = page["next_cursor"]
        if not cursor:
            return out


def notify(model, changed):
    """Placeholder: open a ticket for the product owner. Never write to BatteryPass here."""
    print(f"New model version needed for {model['name']} ({model['id']}): {', '.join(changed)} changed in the ERP")
```

## The person decides

The notification names the model, what changed in the ERP and from which day it applies. In the portal a member then:

1. creates a new **model version** with its effective date, usually copying the values of the current version;
2. enters the changed values with their evidence, and has them approved (the four-eyes rule applies where your
   organisation has it switched on);
3. checks the **change impact**: which existing units would move to the new version by their manufacturing date,
   and which published passports would then need a new version;
4. approves the model version.

The [change feed](https://passport.kabin.sh/docs/en/integrations/workflows/passport-links-to-erp) reports `model_version.approved`, and later
`passport.republished` for units whose passports were published again, so the ERP can follow without asking.

## What never flows

The notification carries the model and the changed characteristics. Supplier names and conditions, prices and the
people who made the change stay in the ERP and the ticket system.

## Failure handling

| Answer                             | Meaning                                               | What to do                                       |
| ---------------------------------- | ----------------------------------------------------- | ------------------------------------------------ |
| `400 validation_error` on `cursor` | A stale cursor                                        | Start the listing again.                         |
| `401 unauthenticated`              | Key missing, unknown or revoked                       | Check the secret; alert.                         |
| `403 forbidden`                    | The key lacks the scope `read`                        | Use the read key.                                |
| No model with the GTIN             | The product is not a battery model in BatteryPass yet | Notify anyway: the model may need to be created. |
| `429 rate_limited`                 | Too many requests                                     | Wait `Retry-After` seconds.                      |
| `5xx`, timeout                     | Unknown outcome                                       | Retry with backoff; reads are safe to repeat.    |

An ERP event that arrives twice produces two notifications at most; de-duplicate them by product number and change
date in the ticket system.
