# Consumer-driven Contracts

Abhängigkeiten mit einem Consumer-driven Contract explizit machen.

Source: https://learn.datacontract.com/de/consumer-driven/

Deine View aus der [vorigen Übung](https://learn.datacontract.com/de/implement/) liest direkt die Tabellen des Produzenten.
Sie hängt implizit vom *gesamten* `orders_v2`-Kontrakt ab, braucht aber nur fünf Felder.
Das Orders-Team kann nicht erkennen, ob eine Änderung dich bricht.

Mach die Abhängigkeit explizit: ein **Consumer-driven Contract** für genau die Felder, die du brauchst, und Views, die nur diese Felder bereitstellen.

**You will learn:**
- Consumer-driven Data Contracts verstehen
- Einen Kontrakt schreiben, der nur enthält, was ein Konsument nutzt
- Dein Datenprodukt mit Zugriffs-Views von den Tabellen des Produzenten entkoppeln
- Prüfen, dass deine eigenen Konsumenten nicht betroffen sind

> **Consumer-driven Contracts**
>
> Der **Konsument** legt fest, welchen Ausschnitt der Daten er braucht und welche Qualität er erwartet.
> Die Idee stammt aus dem API-Testing (z. B. Pact): Der Produzent führt die Kontrakte aller Konsumenten in seiner eigenen Pipeline aus.
> So weiß er, welche Felder genutzt werden, und kann alles andere frei ändern.

_Scenario: the Orders data product (output ports orders_v1 and orders_v2) is consumed by the SKU Sales data product, which the purchasing team uses._

## Festlegen, was du brauchst

### Kontrakt des Produzenten kopieren

Kopiere den `orders_v2`-Kontrakt und öffne die Kopie im Editor:

macOS / Linux:

```bash
cp orders_v2.odcs.yaml orders_v2.consumer_sku_sales.odcs.yaml
datacontract edit orders_v2.consumer_sku_sales.odcs.yaml
```

Windows (PowerShell):

```powershell
Copy-Item orders_v2.odcs.yaml orders_v2.consumer_sku_sales.odcs.yaml
datacontract edit orders_v2.consumer_sku_sales.odcs.yaml
```

Output:

```text
Editing: /Users/you/learn.datacontract.com/orders_v2.consumer_sku_sales.odcs.yaml
Data Contract Editor running at http://localhost:4243
Press Ctrl+C to stop
```

Setze die **ID** auf `orders_v2_consumer_sku_sales`. Lass die **Version** bei `2.0.0`, da der Kontrakt auf den v2-Daten basiert.

### Auf das Genutzte reduzieren

Entferne alle Properties, die deine View nicht nutzt, samt ihren Quality Checks. Behalte nur:

- `orders`: `order_id`, `order_timestamp`
- `line_items`: `order_id`, `sku`, `quantity`

Behalte den Check `quantity > 0`. Dein Datenprodukt verlässt sich darauf: Sonst könnte `total_quantity` kleiner als `order_count` sein.

Die Kopie enthält auch `context` und `synonyms` des Produzenten (ODCS 3.2). Entferne den `context` auf Kontrakt-Ebene: Seine Verified Statements fragen `order_total` und die Tabellen des Produzenten ab, und sein Constraint betrifft ein Feld, das du entfernt hast. Kontext und Synonyme auf Schema-Ebene für behaltene Felder dürfen bleiben.

### Mach ihn zu deinem Kontrakt

Das ist jetzt *dein* Kontrakt, nicht der des Orders-Teams:

- setze `team` und `support` auf das Purchasing-Analytics-Team (wie in deinem `sku_sales_per_year`-Kontrakt),
- formuliere `description.purpose` neu, z. B. „The fields the SKU Sales data product actually needs from orders_v2“.

### Auf die Zugriffs-Views zeigen

Deine Zugriffs-Views liegen in einem neuen Schema `sku_sales_input`:

- ändere das **Schema** des Servers auf `sku_sales_input`,
- setze den `physicalType` beider Schema-Objekte auf `VIEW`,
- falls der Check `quantity > 0` eine SQL-Query ist, ändere darin `orders_v2.` in `sku_sales_input.`. Sonst testet er weiter die alten Tabellen.

### Tests ausführen: wieder rot

macOS / Linux:

```bash
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Output:

```text
Testing orders_v2.consumer_sku_sales.odcs.yaml
Server: postgres (type=postgres, host=localhost, port=5433, database=workshop, 
schema=sku_sales_input)
╭────────┬───────────────────────────────┬────────────────────────┬────────────────────────────────╮
│ Result │ Check                         │ Field                  │ Details                        │
├────────┼───────────────────────────────┼────────────────────────┼────────────────────────────────┤
│ failed │ Check that field 'order_id'   │ line_items.order_id    │ Could not read model           │
│        │ is present                    │                        │ 'line_items': line_items       │
…
🔴 data contract is invalid, found the following errors:
1) 7 checks on orders.order_id, orders.order_timestamp: Could not read model 'orders': orders
2) 9 checks on line_items.order_id, line_items.sku, line_items.quantity: Could not read model 
'line_items': line_items
```

Sie schlagen fehl, weil es die Views noch nicht gibt. Contract-first, schon wieder.

**Solution: orders_v2.consumer_sku_sales.odcs.yaml**

```yaml title=orders_v2.consumer_sku_sales.odcs.yaml
version: 2.0.0
kind: DataContract
apiVersion: v3.2.0
id: orders_v2_consumer_sku_sales
name: Orders (SKU Sales)
status: active
domain: ecommerce
description:
  purpose: Consumer-driven contract - the fields the SKU Sales data product actually needs from orders_v2
tags: ['orders', 'sku', 'consumer-driven']
servers:
- server: postgres
  type: postgres
  host: localhost
  port: 5433
  database: workshop
  schema: sku_sales_input
schema:
- name: orders
  physicalType: view
  logicalType: object
  physicalName: orders
  properties:
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
    primaryKey: true
  - name: order_timestamp
    physicalType: timestamptz
    logicalType: date
    required: true
- name: line_items
  physicalType: view
  logicalType: object
  physicalName: line_items
  properties:
  - name: order_id
    physicalType: text
    logicalType: string
    required: true
  - name: sku
    physicalType: text
    logicalType: string
    required: true
  - name: quantity
    physicalType: bigint
    logicalType: integer
    quality:
    - type: sql
      description: Ensure quantity is positive
      query: SELECT COUNT(*) FROM sku_sales_input.line_items WHERE quantity <= 0;
      mustBe: 0
team:
  name: purchasing_analytics_team
  members:
  - username: purchasing@example.com
    role: Owner
support:
- channel: "#purchasing-analytics"
  url: https://example.slack.com/archives/purchasing-analytics
  tool: slack
```

## Zugriffs-Views anlegen

### Views anlegen

Lege `sql/sku_sales_input.sql` mit Views an, die nur die vereinbarten Felder bereitstellen:

```sql title=sku_sales_input.sql
CREATE SCHEMA IF NOT EXISTS sku_sales_input;

CREATE OR REPLACE VIEW sku_sales_input.orders AS
SELECT order_id, order_timestamp FROM orders_v2.orders;

CREATE OR REPLACE VIEW sku_sales_input.line_items AS
SELECT order_id, sku, quantity FROM orders_v2.line_items;
```

Wende sie an:

macOS / Linux:

```bash
docker compose exec -T postgres psql -U workshop -d workshop < sql/sku_sales_input.sql
```

Windows (PowerShell):

```powershell
Get-Content sql/sku_sales_input.sql | docker compose exec -T postgres psql -U workshop -d workshop
```

Output:

```text
CREATE SCHEMA
CREATE VIEW
CREATE VIEW
```

### Tests ausführen: grün

macOS / Linux:

```bash
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test orders_v2.consumer_sku_sales.odcs.yaml
```

Output:

```text
Testing orders_v2.consumer_sku_sales.odcs.yaml
Server: postgres (type=postgres, host=localhost, port=5433, database=workshop, 
schema=sku_sales_input)
╭────────┬──────────────────────────────────────────────────────┬────────────────────────┬─────────╮
│ Result │ Check                                                │ Field                  │ Details │
├────────┼──────────────────────────────────────────────────────┼────────────────────────┼─────────┤
│ passed │ Check that field 'order_id' is present               │ line_items.order_id    │         │
│ passed │ Check that field order_id has physical type text     │ line_items.order_id    │         │
│ passed │ Check that field order_id has no missing values      │ line_items.order_id    │         │
│ passed │ Check that field 'quantity' is present               │ line_items.quantity    │         │
│ passed │ Check that field quantity has physical type bigint   │ line_items.quantity    │         │
│ passed │ Ensure quantity is positive                          │ line_items.quantity    │         │
…
│ passed │ Check that field 'order_timestamp' is present        │ orders.order_timestamp │         │
╰────────┴──────────────────────────────────────────────────────┴────────────────────────┴─────────╯
🟢 data contract is valid. Run 16 checks. Took 0.545395 seconds.
```

## Dein Datenprodukt umstellen

### Aus den Zugriffs-Views lesen

Ändere `sql/sku_sales_per_year.sql`, sodass die View aus `sku_sales_input.orders` und `sku_sales_input.line_items` liest statt aus den `orders_v2`-Tabellen. Wende sie erneut an:

macOS / Linux:

```bash
docker compose exec -T postgres psql -U workshop -d workshop < sql/sku_sales_per_year.sql
```

Windows (PowerShell):

```powershell
Get-Content sql/sku_sales_per_year.sql | docker compose exec -T postgres psql -U workshop -d workshop
```

Output:

```text
NOTICE:  schema "analytics" already exists, skipping
CREATE SCHEMA
CREATE VIEW
```

> **Note**
>
> `sql/sku_sales_per_year.sql` hängt jetzt von `sql/sku_sales_input.sql` ab, wende also zuerst die Input-Views an. Die CI-Pipeline in [Teil C](https://learn.datacontract.com/de/ci-cd/) macht genau das.

**Solution: sql/sku_sales_per_year.sql**

```sql title=sku_sales_per_year.sql
-- Rebased onto the consumer-driven input views (exercise 6)
CREATE SCHEMA IF NOT EXISTS analytics;

CREATE OR REPLACE VIEW analytics.sku_sales_per_year AS
SELECT
    li.sku,
    EXTRACT(YEAR FROM o.order_timestamp)::int AS year,
    COUNT(*)::bigint                          AS order_count,
    SUM(li.quantity)::bigint                  AS total_quantity
FROM sku_sales_input.line_items li
JOIN sku_sales_input.orders o ON li.order_id = o.order_id
GROUP BY li.sku, EXTRACT(YEAR FROM o.order_timestamp);
```

### Prüfen, dass deine Konsumenten nicht betroffen sind

Die Ausgabe deines Datenprodukts darf sich nicht ändern. Dein eigener Kontrakt beweist es:

macOS / Linux:

```bash
datacontract test sku_sales_per_year.odcs.yaml
```

Windows (PowerShell):

```powershell
datacontract test sku_sales_per_year.odcs.yaml
```

Output:

```text
Testing sku_sales_per_year.odcs.yaml
…
│ passed │ Ensure year is plausible                                 │ year           │         │
╰────────┴──────────────────────────────────────────────────────────┴────────────────┴─────────╯
🟢 data contract is valid. Run 15 checks. Took 0.606872 seconds.
```

Dein Datenprodukt berührt jetzt nur die Felder aus deinem Consumer-driven Contract.
Alles andere in `orders_v2` darf sich ändern, ohne dich zu brechen.
Noch besser: Das Orders-Team kann *deinen* Kontrakt in *seiner* CI-Pipeline ausführen und einen Breaking Change abfangen, bevor er live geht. So eine Pipeline baust du in der [nächsten Übung](https://learn.datacontract.com/de/ci-cd/).

**Quick check:** Das Orders-Team will customer_email_address aus orders_v2 entfernen. Bricht das das Datenprodukt SKU Sales?

- Ja, jede Änderung an orders_v2 ist breaking
- Nein, die Spalte steht nicht im Consumer-driven Contract, und die Zugriffs-Views selektieren sie nicht (correct)
- Nur wenn die Zugriffs-Views SELECT * nutzen
- Das weiß man erst nach dem Deployment

Richtig. Der Consumer-driven Contract dokumentiert, dass SKU Sales nur fünf Felder braucht. Läuft er in der Pipeline des Producers, ist belegt, dass das Entfernen von `customer_email_address` für diesen Consumer sicher ist.

## Bonus

- Vergleiche den Kontrakt des Produzenten mit dem des Konsumenten: `datacontract changelog orders_v2.odcs.yaml orders_v2.consumer_sku_sales.odcs.yaml`
- Lass den Input-Port in `sku_sales_per_year.odps.yaml` auf `orders_v2_consumer_sku_sales` zeigen. Sollte der Input-Port auf den Kontrakt des Produzenten verweisen oder auf deinen? Für beides gibt es gute Argumente.
