# Use Case Patterns & Reference Implementations

This document captures recurring processing patterns across production ENTR adapters.
Use it as a starting point when building a new handler — find the closest use case,
then explore the referenced adapter code for implementation details.

> **For Claude Code**: When the user describes a new handler, match it to a pattern below
> and read the referenced files for concrete examples. The adapter repos are sibling
> directories at `../MessageBird-{Name}/`.

---

## Use Case Overview

| Use Case | Adapters | Target Systems |
|----------|----------|----------------|
| [Sales Orders](#sales-orders) | DeltaWines, Koemans, AntoonRijnbeek, Zuidberg, Schoonderwolf (Heering), Smurfit | Business Central, Exact Globe, Vision ERP, GraphQL ERP, SQL Server + XML/SFTP, SAP COBIS |
| [Purchase Invoices](#purchase-invoices) | NMQDigital, DeMors | Exact Online, Proteus API |
| [Packing Slips](#packing-slips) | Schoonderwolf (Galvano) | MKG REST API |
| [PO Confirmations](#po-confirmations) | Zuidberg | GraphQL ERP |

---

## Sales Orders

The most common use case. Nearly all sales order handlers follow the same 3-step core pattern:

### The 3-Step Pattern

```
1. Find the Company (customer/debtor resolution)
2. Find the Shipping Address (delivery location)
3. Find the Products (article/material line item resolution)
   → Then: Create the order in the target system
```

### Step 1: Customer Resolution

Every adapter resolves the extracted company name/number to an ERP customer record.
The resolution strategy varies by what identifiers are available, but follows a
consistent priority pattern: **exact ID first, then structured identifiers, then name-based fuzzy**.

| Strategy | Priority | Adapters Using It |
|----------|----------|-------------------|
| Customer/debtor number (exact) | Highest | Koemans, AntoonRijnbeek, Smurfit, Schoonderwolf |
| VAT number (BTW) | High | Koemans, DeltaWines, Schoonderwolf |
| Chamber of Commerce (KvK) | High | Koemans, Schoonderwolf |
| Address-based (postal code + street) | Medium | Smurfit, DeltaWines |
| Company name (fuzzy match) | Fallback | All adapters |

**Common patterns**:
- Try identifiers in priority order; stop at first confident match
- Multiple matches at any step → return `REVIEW_REQUIRED` with candidates
- Name matching typically normalizes legal suffixes (B.V., GmbH, Ltd, etc.)

**Reference implementations**:

| Adapter | File | Notes |
|---------|------|-------|
| Koemans | `MessageBird-Koemans/app/handlers/koemans_verkoop_handler.py` | Tiered: debtor ID → VAT → KvK → name. Cleanest example of the priority chain |
| DeltaWines | `MessageBird-DeltaWines/app/handlers/deltawines_verkoop_handler.py` | Address + ID resolution, handles 2 companies (DGS/GWO) |
| Smurfit | `MessageBird-Smurfit/app/handlers/smurfit_verkoop_handler.py` | Address-based (postal code primary key), multi-division |
| Schoonderwolf | `MessageBird-Schoonderwolf/app/handlers/galvano_pakbon_handler.py` | MKG debtor lookup with search-name normalization |

### Step 2: Shipping Address Resolution

Most sales order handlers resolve a delivery address that may differ from the billing customer.

**Common patterns**:
- Look up shipping partners/addresses linked to the resolved customer
- Match by postal code + street number (most reliable across ERPs)
- Some ERPs have dedicated partner/address tables; others embed addresses in the customer record
- If shipping address matches the customer address, skip separate resolution

**Reference implementations**:

| Adapter | File | Notes |
|---------|------|-------|
| DeltaWines | `MessageBird-DeltaWines/app/handlers/deltawines_verkoop_handler.py` | Dedicated shipping address resolution step |
| Smurfit | `MessageBird-Smurfit/app/handlers/smurfit_verkoop_handler.py` | Separate Partners table, postal code + street matching |
| Zuidberg | `MessageBird-Zuidberg/app/handlers/zuidberg_verkoop_handler.py` | Customer address via GraphQL, address-based matching |

### Step 3: Product/Article Resolution

The most complex step. Extracted line items must be matched to ERP article/material records.
Multiple resolution strategies are tried in sequence.

| Strategy | Priority | Adapters Using It |
|----------|----------|-------------------|
| Article/material number (exact) | Highest | All adapters |
| EAN/barcode | High | Schoonderwolf (Heering), Koemans |
| Short code lookup | Medium | Schoonderwolf (Heering) |
| Order history (previous orders) | Medium | Koemans |
| Fuzzy matching on description | Fallback | DeltaWines, Koemans, Schoonderwolf, Smurfit |

**Common patterns**:
- Exact match on article number is always preferred
- Fuzzy matching uses the shared matching engine (`app/matching/engine.py`)
- Multiple matches → `REVIEW_REQUIRED` with ranked candidates
- Some adapters allow "dummy article" placeholders for unresolved items (Schoonderwolf/Galvano)

**Reference implementations**:

| Adapter | File | Notes |
|---------|------|-------|
| DeltaWines | `MessageBird-DeltaWines/app/handlers/deltawines_verkoop_handler.py` | Fuzzy matching engine with review suggestions |
| Koemans | `MessageBird-Koemans/app/handlers/koemans_verkoop_handler.py` | Article code → order history → fuzzy. Good example of layered fallback |
| Schoonderwolf (Heering) | `MessageBird-Schoonderwolf/app/handlers/heering_verkoop_handler.py` | Exact → EAN → short code → fuzzy. Most strategies in one handler |
| AntoonRijnbeek | `MessageBird-AntoonRijnbeek/app/handlers/rijnbeek_verkoop_handler.py` | Plant code lookup with size variants, order splitting |
| Smurfit | `MessageBird-Smurfit/app/handlers/smurfit_verkoop_handler.py` | Contains-search then fuzzy, CSV masterdata, multi-division materials |

### Step 4: Order Creation

After resolution, the order is created in the target system. The method varies by ERP.

| Method | Adapters | Notes |
|--------|----------|-------|
| REST/OData API | DeltaWines (Business Central) | Standard HTTP POST with JSON payload |
| SQL Server direct | Koemans (Exact Globe) | pyODBC, validate-only MVP initially |
| PostgreSQL + FTP CSV | AntoonRijnbeek (Vision) | Insert to DB + generate CSV for import |
| GraphQL mutation | Zuidberg | GraphQL API with structured input types |
| XML file via SFTP | Schoonderwolf (Heering), Smurfit (SAP COBIS) | Generate XML, upload to FTP/SFTP drop folder |
| REST API | Schoonderwolf (Galvano/MKG) | Sales order + production order creation via REST |

**Reference implementations**:

| Adapter | File | Notes |
|---------|------|-------|
| Smurfit | `MessageBird-Smurfit/app/handlers/smurfit_verkoop_handler.py` | XML generation + FTP/SFTP upload, multi-division |
| Schoonderwolf (Heering) | `MessageBird-Schoonderwolf/app/handlers/heering_verkoop_handler.py` | XML generation + SFTP upload |
| DeltaWines | `MessageBird-DeltaWines/app/handlers/deltawines_verkoop_handler.py` | Business Central OData POST |

---

## Purchase Invoices

Invoice processing follows a different pattern from sales orders. The core steps are:

### The Invoice Pattern

```
1. Match the Supplier (creditor resolution)
2. Resolve VAT & Accounting Codes
3. Validate Line Items & Totals
4. Check for Duplicates
   → Then: Create the invoice in the target system
```

### Step 1: Supplier Resolution

Similar to customer resolution but against creditor/supplier master data.

| Strategy | Priority | Adapters Using It |
|----------|----------|-------------------|
| Supplier code/ID (exact) | Highest | NMQDigital, DeMors |
| VAT number (BTW) | High | NMQDigital, DeMors |
| KvK number | Medium | DeMors |
| Supplier name (fuzzy) | Fallback | NMQDigital, DeMors |

**Reference implementations**:

| Adapter | File | Notes |
|---------|------|-------|
| NMQDigital | `MessageBird-NMQDigital/app/handlers/nmq_invoice_handler.py` | Multi-division (7 divisions), Exact Online REST API |
| DeMors | `MessageBird-DeMors/app/handlers/demors_invoice_handler.py` | Proteus API, CSV-cached supplier data |

### Step 2: VAT & Accounting Resolution

| Adapter | Approach |
|---------|----------|
| NMQDigital | GL account + VAT code resolution against Exact Online reference data |
| DeMors | VAT status → VAT code mapping from cached reference tables, payment condition lookup |

### Step 3: Duplicate Detection

Both invoice adapters check for existing invoices before creation:

| Adapter | Method |
|---------|--------|
| NMQDigital | Invoice number + supplier combination check in Exact Online |
| DeMors | Pay reference + supplier search via Proteus API |

### Step 4: Invoice Creation

| Adapter | Method | Notes |
|---------|--------|-------|
| NMQDigital | Exact Online REST API | Multi-division, OAuth2 with AWS Secrets Manager |
| DeMors | Proteus API POST | Invoice creation + PDF attachment via SFTP + document linking |

**Unique to DeMors**: Attaches the original PDF to the created invoice via SFTP upload + document linking API. See `_attach_document_to_invoice()` in the handler.

---

## Packing Slips

Packing slips (pakbonnen) are a specialized use case where incoming delivery notes
trigger both sales orders AND production orders in the ERP.

### The Packing Slip Pattern

```
1. Identify the Customer (from known customer set)
2. Resolve Articles (with dummy placeholders for unknowns)
3. Create Sales Order
4. Create Production Order (linked to sales order)
```

**Key differences from standard sales orders**:
- Customer set is small and known (5 customers with pre-configured rules)
- Per-customer business rules (weekly vs individual orders, UITB splitting)
- Lead time calculation with business day awareness (Dutch holidays)
- Unresolved articles get dummy placeholders instead of blocking

**Reference implementation**:

| Adapter | File | Notes |
|---------|------|-------|
| Schoonderwolf (Galvano) | `MessageBird-Schoonderwolf/app/handlers/galvano_pakbon_handler.py` | Full packing slip workflow with MKG API |
| Schoonderwolf (config) | `MessageBird-Schoonderwolf/app/matching/galvano_config.py` | Per-customer rules, lead times, treatment mappings |

---

## PO Confirmations

Purchase order confirmation validates incoming supplier confirmations against
existing orders in the ERP.

### The PO Confirmation Pattern

```
1. Find the Original PO in the ERP
2. Compare Confirmed vs Expected (prices, quantities, dates)
3. Flag Discrepancies (tolerance-based)
   → Then: Update PO status or create review items
```

**Reference implementation**:

| Adapter | File | Notes |
|---------|------|-------|
| Zuidberg | `MessageBird-Zuidberg/app/handlers/zuidberg_inkoop_handler.py` | GraphQL ERP, price tolerance (+-20%), PLAAT dimension matching |

---

## Cross-Cutting Patterns

### Fuzzy Matching Engine

Most adapters that resolve products use the shared fuzzy matching engine.

**Core algorithm**:
1. Token-based candidate retrieval (progressively fewer tokens)
2. Numeric pattern matching (part number detection)
3. RapidFuzz scoring (WRatio for flexible string comparison)
4. Confidence thresholds: >85 auto-accept, 60-85 review, <40 reject

**Reference implementations**:

| Adapter | File | Notes |
|---------|------|-------|
| DeltaWines | `MessageBird-DeltaWines/app/matching/engine.py` | Clean standalone implementation |
| Schoonderwolf | `MessageBird-Schoonderwolf/app/matching/engine.py` | Enhanced with synonym normalization, article code boost |
| Smurfit | `MessageBird-Smurfit/app/matching/matcher.py` | Contains-search + fuzzy fallback |

### Review Suggestions

When resolution is ambiguous, adapters return `REVIEW_REQUIRED` with structured suggestions.
Core presents these to the user, who selects the correct option. The document is then
reprocessed with the user's selection applied via mappings.

**Pattern** (consistent across all adapters):
```python
ReviewSuggestion(
    field_path="order_lines.0.product_number",
    original_value="ABC123",
    proposed_values=[
        ProposedValue(value="ABC123-A", description="Widget Type A - Blue"),
        ProposedValue(value="ABC123-B", description="Widget Type B - Red"),
    ],
    reason="Multiple product variants match"
)
```

### Mapping Service

Tracks user corrections and generates mapping rules for future documents.
When a user edits a field during review, the adapter can create a mapping so the
same correction is applied automatically next time.

**Reference**: `app/processing_engine/mapping_service.py` (present in most adapters)

### Document Retrieval Methods

| Method | Adapters | Connector |
|--------|----------|-----------|
| Microsoft Exchange (Graph API) | DeltaWines, DeMors, Schoonderwolf (Galvano) | `microsoft_entra_retrieval_handler.py` |
| Gmail (OAuth2) | NMQDigital, Schoonderwolf (Heering), Smurfit | `entr_mailbox_retrieval_handler.py` |
| IMAP | Zuidberg | Standard IMAP connector |
| FTP/SFTP | AntoonRijnbeek | FTP connector for mailbox polling |

### ERP Connection Patterns

| Pattern | Adapters | Key Files |
|---------|----------|-----------|
| REST/OData API | DeltaWines (Business Central) | `business_central_connector.py` |
| REST API (custom) | DeMors (Proteus), Schoonderwolf (MKG) | `demors_api_connector.py`, `mkg_connector.py` |
| REST + OAuth2 | NMQDigital (Exact Online) | Exact Online connector with token refresh |
| SQL Server (pyODBC) | Koemans (Exact Globe), Schoonderwolf (Heering) | `sql_connector.py` |
| PostgreSQL | AntoonRijnbeek (Vision) | PostgreSQL connector |
| GraphQL | Zuidberg | GraphQL connector |
| CSV masterdata + FTP/SFTP upload | Smurfit (SAP COBIS) | `csv_connector.py`, `ftp_connector.py` |

### Caching Strategies

| Strategy | Adapters | Notes |
|----------|----------|-------|
| CSV file cache with TTL | DeMors | Caches supplier, VAT, payment data from API |
| CSV masterdata from FTP | Smurfit | Syncs 6 tables per division from FTP server |
| In-memory with periodic refresh | Schoonderwolf | MKG data cached in handler |
| No cache (direct queries) | Koemans, NMQDigital | Small data sets or real-time requirements |

---

## Quick Reference: Which Adapter to Study

| If you're building... | Start with | Why |
|----------------------|------------|-----|
| Sales order handler (REST API target) | DeltaWines | Clean 3-step pattern, Business Central OData |
| Sales order handler (SQL target) | Koemans | Tiered customer resolution, Exact Globe |
| Sales order handler (XML/file target) | Smurfit or Schoonderwolf (Heering) | XML generation + SFTP upload |
| Purchase invoice handler | DeMors or NMQDigital | Full invoice workflow with VAT/accounting |
| Multi-division adapter | Smurfit or NMQDigital | Division-specific config, shared infrastructure |
| Fuzzy matching for products | DeltaWines or Schoonderwolf | Matching engine with review suggestions |
| Email retrieval (Exchange) | DeMors | Dual-folder strategy, PDF filtering |
| Email retrieval (Gmail) | Smurfit | Label-based tenant isolation |
| Complex per-customer rules | Schoonderwolf (Galvano) | Rule-based config per known customer set |
| PO confirmation/validation | Zuidberg | Compare confirmed vs expected with tolerances |
