# WooCommerce Sync Flow

This document explains how and when WooCommerce orders are synced to MiniCRM.

## Table of Contents

- [Automatic Sync](#automatic-sync)
- [Manual Sync](#manual-sync)
- [Project ID Calculation](#project-id-calculation)
- [What Gets Synced](#what-gets-synced)
- [Security & Access Control](#security--access-control)
- [Sync Timing](#sync-timing)
- [Data Transformation](#data-transformation)

---

## Automatic Sync

Automatic sync happens whenever orders are created or updated in WooCommerce.

### Trigger Events

The sync is triggered by these WooCommerce hooks:

| Hook | When It Fires | Example |
|------|---------------|---------|
| `woocommerce_new_order` | New order created | Customer completes checkout |
| `woocommerce_update_order` | Order data updated | Admin edits order details |
| `woocommerce_order_status_changed` | Order status changes | Order marked "completed" |

### Complete Automatic Flow

```
┌─────────────────────────────────────────────────────────────┐
│ 1. WooCommerce Event (new order, update, status change)     │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. WooCommerce::queue_order_for_sync( $order_id )           │
│    ├─ Check if sync is enabled                             │
│    ├─ Calculate project ID (user ID or guest)              │
│    └─ Add to $queued_project_ids array (deduplicated)      │
└────────────────────────┬────────────────────────────────────┘
                         │
                         │ (multiple orders may queue during one request)
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Shutdown Hook (end of WordPress request)                 │
│    └─ WooCommerce::sync_queued_projects()                  │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Order_Sync_Manager::sync_projects( $project_ids )        │
│    ├─ Generate nonce (32-char random string)               │
│    ├─ Store nonce in transient (6 hours)                   │
│    └─ Build feed URL with nonce & project IDs              │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Call MiniCRM SyncFeed API                                 │
│    POST https://r3.minicrm.hu/Api/SyncFeed/{system_id}     │
│    ?Source={feed_url}                                       │
│    Authorization: Basic {system_id}:{api_key}               │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 6. MiniCRM Pulls Feed URL (within seconds)                  │
│    GET https://yoursite.com/wp-json/minicrm-bridge/v1/     │
│    woocommerce/feed?project_ids=42,100&nonce=abc123...     │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 7. Feed_Endpoint::handle_feed_request()                     │
│    ├─ Validate nonce (check transient exists)              │
│    ├─ Validate IP (check whitelist)                        │
│    └─ If valid, proceed to generate XML                    │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 8. Feed_Generator::generate_feed( $project_ids )            │
│    (See "XML Generation Flow" below)                       │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 9. Return XML to MiniCRM                                     │
│    <?xml version="1.0" encoding="UTF-8"?>                  │
│    <Projects>...</Projects>                                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 10. MiniCRM Imports Data                                     │
│     ├─ Parse XML                                           │
│     ├─ Create/update projects                              │
│     └─ Link orders to projects                             │
└─────────────────────────────────────────────────────────────┘
```

### Queue Deduplication

Multiple events may trigger for the same order (e.g., status change + update):

```php
// Example: Order #123 belongs to User #42

Event 1: woocommerce_new_order (123)
  → Queue project 42

Event 2: woocommerce_update_order (123)
  → Queue project 42 (already queued, skip)

Event 3: woocommerce_order_status_changed (123)
  → Queue project 42 (already queued, skip)

Shutdown: Sync project 42 once (not 3 times)
```

**Why?** Prevents duplicate API calls and reduces load.

---

## Manual Sync

Manual sync is triggered from the WooCommerce Sync settings page.

### Manual Sync Flow

```
┌─────────────────────────────────────────────────────────────┐
│ 1. Admin Opens WooCommerce Sync Page                         │
│    └─ Clicks "Load Projects" button                        │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. AJAX: minicrm_wc_get_projects                             │
│    ├─ Query all WooCommerce orders                         │
│    ├─ Group by project ID                                  │
│    ├─ Extract project name (customer name/email)           │
│    └─ Return project list with order counts                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Display Project List in Admin UI                         │
│    [☑] John Doe (42) - 5 orders                            │
│    [☑] Guest Order #100 (1000100) - 1 order                │
│    [☑] Jane Smith (87) - 12 orders                         │
│    [Sync Selected Projects]                                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         │ Admin selects projects
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. Admin Clicks "Sync Selected Projects"                    │
│    └─ JavaScript batches project IDs (150 per batch)       │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. AJAX: minicrm_wc_sync_projects (batched)                 │
│    Batch 1: IDs 1-150                                      │
│    Batch 2: IDs 151-300                                    │
│    Batch 3: IDs 301-450                                    │
│    └─ Each batch calls Order_Sync_Manager::sync_projects() │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│ 6. Same as Automatic Sync Steps 4-10                        │
│    (Nonce → MiniCRM API → Feed Pull → XML → Import)        │
└─────────────────────────────────────────────────────────────┘
```

### Batch Processing

**Why batch?** URL length limits and performance.

**Batch size**: 150 project IDs per request

**Example**:
- 500 projects selected
- Batch 1: Projects 1-150 (1 API call)
- Batch 2: Projects 151-300 (1 API call)
- Batch 3: Projects 301-450 (1 API call)
- Batch 4: Projects 451-500 (1 API call)
- **Total**: 4 API calls

Progress bar updates after each batch completes.

---

## Project ID Calculation

Project ID determines how orders are grouped in MiniCRM.

### Logic Flowchart

```
Order Created/Updated
        │
        ↓
   Get Customer ID
        │
        ├─────────┬─────────────┐
        │         │             │
   User ID > 0?   │             │
        │         │             │
       YES       NO             │
        │         │             │
        ↓         ↓             │
Registered User  Guest Order    │
        │         │             │
        ↓         ↓             │
 Project ID =   Project ID =    │
   User ID     Order ID +       │
              1,000,000         │
        │         │             │
        └─────────┴─────────────┘
                  │
                  ↓
         Validate ID
                  │
                  ↓
           Queue for Sync
```

### Registered Users

**Criteria**: Order has `customer_id > 0`

**Project ID**: User ID

**Example**:
```php
User ID: 42

Order #100 → Project 42
Order #101 → Project 42  ✓ Same project
Order #102 → Project 42  ✓ Same project

Result: All orders grouped under one project in MiniCRM
```

**Benefits**:
- Complete customer history in one project
- Easy to track repeat customers
- Order relationship visible

### Guest Orders

**Criteria**: Order has `customer_id = 0`

**Project ID**: `Order ID + 1,000,000`

**Example**:
```php
Guest orders:

Order #100 → Project 1,000,100
Order #101 → Project 1,000,101
Order #102 → Project 1,000,102

Result: Each order creates a separate project
```

**Why offset?** Prevents collision with user IDs:
```
User ID 100 → Project 100
Guest Order #100 → Project 1,000,100 ✓ Different IDs
```

**Trade-off**:
- ✅ Pro: No ID collisions
- ❌ Con: Can't group guest orders by customer
- ℹ️ Workaround: Encourage customers to register

### Multi-Shop Offsets

If you have multiple shops (Shop ID > 0):

**Project ID Calculation**:
```php
$base_id = ( $customer_id > 0 )
    ? $customer_id
    : $order_id + 1,000,000;

$project_id = $base_id + ( $shop_id * 100,000,000 );
```

**Example with Shop ID 2**:
```php
User ID 42, Shop 2:
  → Project ID = 42 + (2 × 100,000,000) = 200,000,042

Guest Order #100, Shop 2:
  → Project ID = 1,000,100 + (2 × 100,000,000) = 201,000,100
```

**Why?** Prevents project ID collisions between shops.

### ID Validation

Before queueing, the plugin validates IDs:

```php
// User IDs must be less than GUEST_OFFSET
if ( $customer_id > 0 && $customer_id >= 1,000,000 ) {
    // ERROR: User ID collision risk!
    // This user ID would overlap with guest offset
}
```

**Safe ranges**:
- User IDs: 1 - 999,999
- Guest Orders: 1,000,000+
- Shop 1 Users: 100,000,001 - 100,999,999
- Shop 1 Guests: 101,000,000+

---

## What Gets Synced

### Project-Level Data

Each project includes:

| Data | Source | Notes |
|------|--------|-------|
| **Project ID** | User ID or Order ID + offset | See [Project ID Calculation](#project-id-calculation) |
| **Name** | Billing name or email | Falls back to "Guest #{order_id}" |
| **Billing Address** | First order billing address | Country, postal code, city, street |
| **Shipping Address** | First order shipping address | Optional, if different from billing |
| **Custom Fields** | WooCommerce field mapping | Configured in settings |

### Order-Level Data

Each order includes:

| Data | Source | Notes |
|------|--------|-------|
| **Order ID** | WooCommerce order ID | Unique identifier |
| **Order Date** | Order creation date | Format: `YYYY-MM-DD HH:MM:SS` |
| **Status** | Order status | `pending`, `processing`, `completed`, etc. |
| **Total** | Order total amount | Decimal |
| **Currency** | Order currency | `USD`, `EUR`, `GBP`, etc. |
| **Note** | Customer order notes | Optional |
| **Products** | Order items | See below |

### Product-Level Data

Each product includes:

| Data | Source | Notes |
|------|--------|-------|
| **Product ID** | WooCommerce product ID | With shop offset if applicable |
| **Name** | Product name | From product or order item |
| **SKU** | Product SKU | Optional |
| **Quantity** | Item quantity | Aggregated if duplicates |
| **Net Unit Price** | Price excluding tax | Calculated |
| **Tax Percentage** | Tax rate | Calculated from tax amount |
| **Description** | Product description | Optional, max 1024 chars |
| **Unit** | Measurement unit | `piece`, `kg`, `hour`, etc. |
| **EPO Fields** | Extra Product Options | If configured |

### Product Aggregation

**Before aggregation** (order items):
```
Order #123:
  2× Product #100 "Widget" ($10 each)
  3× Product #100 "Widget" ($10 each)
  1× Product #200 "Gadget" ($20 each)
```

**After aggregation** (synced to MiniCRM):
```
Order #123:
  5× Product #100 "Widget" ($10 each)  ← Aggregated
  1× Product #200 "Gadget" ($20 each)
```

**EPO-aware aggregation**:
```
Order items:
  2× Product #100 (Color: Red)
  3× Product #100 (Color: Blue)

Synced:
  2× Product #100 (Color: Red)   ← Separate
  3× Product #100 (Color: Blue)  ← Separate
```

**Why?** Same product with different EPO values = different items.

### Special Item Types

| Item Type | How It's Synced | Product ID Range |
|-----------|-----------------|------------------|
| **Products** | Standard product data | Original ID + shop offset |
| **Fees** | As products with synthetic ID | 50,000,000+ |
| **Shipping** | As product with locale name | 60,000,000+ |
| **Coupons** | As negative-price product | 70,000,000+ |

**Example**:
```
Order #123:
  1× Product #100 "Widget" ($50.00)
  1× Shipping ($10.00) → Product #60,000,001 "Shipping"
  1× Fee "Rush Processing" ($5.00) → Product #50,000,001 "Rush Processing"
  1× Coupon "SAVE10" (-$5.00) → Product #70,000,001 "SAVE10"

Total: $60.00
```

---

## Security & Access Control

### Multi-Layer Security

```
MiniCRM Request
       │
       ↓
┌─────────────────────┐
│ 1. Nonce Check      │  ← Is nonce valid and not expired?
│    (6-hour limit)   │
└──────┬──────────────┘
       │ ✓ Valid
       ↓
┌─────────────────────┐
│ 2. IP Check         │  ← Is IP in whitelist?
│    (CIDR support)   │
└──────┬──────────────┘
       │ ✓ Allowed
       ↓
┌─────────────────────┐
│ 3. Generate XML     │
│    Return to        │
│    MiniCRM          │
└─────────────────────┘
```

### Nonce System

**Generation**:
```php
$nonce = bin2hex( random_bytes( 16 ) ); // 32-character string
set_transient( 'minicrm_wc_feed_nonce_' . $nonce, true, 6 * HOUR_IN_SECONDS );
```

**Validation**:
```php
$valid = get_transient( 'minicrm_wc_feed_nonce_' . $nonce );
if ( ! $valid ) {
    return new WP_Error( 'invalid_nonce', 'Nonce expired or invalid' );
}
```

**Properties**:
- **Length**: 32 characters
- **Character set**: Hexadecimal (0-9, a-f)
- **Entropy**: 128 bits (2^128 possible values)
- **Lifetime**: 6 hours
- **Storage**: WordPress transients
- **Reusable**: Yes (within 6 hours, for retries)

**Cleanup**:
Daily cron job deletes expired nonces.

### IP Whitelist

**Format Support**:

1. **Single IP**:
   ```
   203.0.113.50
   ```

2. **CIDR Notation**:
   ```
   203.0.113.0/24     → 203.0.113.0 - 203.0.113.255
   198.51.100.0/28    → 198.51.100.0 - 198.51.100.15
   ```

**Validation Algorithm**:
```php
foreach ( $whitelist_ips as $allowed_ip ) {
    if ( contains( '/', $allowed_ip ) ) {
        // CIDR range check
        if ( ip_in_cidr_range( $client_ip, $allowed_ip ) ) {
            return true; // Allowed
        }
    } else {
        // Exact match
        if ( $client_ip === $allowed_ip ) {
            return true; // Allowed
        }
    }
}

return false; // Denied
```

---

## Sync Timing

### Automatic Sync Timing

| Event | Timing | Notes |
|-------|--------|-------|
| **Queue** | Immediate (during event) | < 1ms |
| **Shutdown Hook** | End of request | After page fully loaded |
| **MiniCRM API Call** | During shutdown | ~200-500ms |
| **MiniCRM Pull** | 1-5 seconds after API call | MiniCRM server latency |
| **Total** | ~2-10 seconds | From order creation to import |

**Example Timeline**:
```
00:00.000 - Customer clicks "Place Order"
00:00.500 - Order created in WooCommerce
00:00.501 - woocommerce_new_order hook fires
00:00.502 - Project queued for sync
00:01.200 - Page fully rendered
00:01.201 - shutdown hook fires
00:01.202 - Nonce generated, API called
00:01.450 - MiniCRM API returns success
00:03.000 - MiniCRM pulls feed URL
00:03.500 - XML generated and returned
00:04.000 - MiniCRM imports project
```

### Manual Sync Timing

**Per batch (150 projects)**:
- Generate nonce: ~5ms
- Call MiniCRM API: ~200-500ms
- MiniCRM pull (async): 1-5 seconds
- **Total per batch**: ~1-6 seconds

**Large sync (1000 projects)**:
- Batches: 7 (ceil(1000/150))
- Time: 7-42 seconds
- Progress updates after each batch

---

## Data Transformation

### XML Generation Flow

```
Feed_Generator::generate_feed( $project_ids )
        │
        ↓
┌─────────────────────────────────────────┐
│ 1. Validate Settings                     │
│    ├─ Locale valid (EN/HU/RO)?          │
│    └─ Credentials configured?           │
└──────────┬──────────────────────────────┘
           │
           ↓
┌─────────────────────────────────────────┐
│ 2. Query Orders                          │
│    ├─ Order_Query_Service::             │
│    │   get_orders_for_project()         │
│    └─ Handle user IDs vs guest IDs      │
└──────────┬──────────────────────────────┘
           │
           ↓
┌─────────────────────────────────────────┐
│ 3. Group Orders by Project               │
│    └─ Order_Grouping_Service::          │
│        group_by_project()                │
└──────────┬──────────────────────────────┘
           │
           ↓
┌─────────────────────────────────────────┐
│ 4. Create XML Root                       │
│    └─ <Projects>                        │
└──────────┬──────────────────────────────┘
           │
           ↓ For each project
┌─────────────────────────────────────────┐
│ 5. Build Project XML                     │
│    ├─ Project_XML_Builder               │
│    ├─ Extract addresses                 │
│    │   (Address_Extractor)              │
│    └─ Map custom fields                 │
│       (Custom_Field_Mapper)             │
└──────────┬──────────────────────────────┘
           │
           ↓ For each order in project
┌─────────────────────────────────────────┐
│ 6. Build Order XML                       │
│    ├─ Order_XML_Builder                 │
│    └─ Create Product_Aggregator         │
└──────────┬──────────────────────────────┘
           │
           ↓ For each item in order
┌─────────────────────────────────────────┐
│ 7. Process Order Items (Strategy)       │
│    ├─ Try Product_Item_Handler          │
│    ├─ Try Fee_Item_Handler              │
│    ├─ Try Shipping_Item_Handler         │
│    └─ Try Coupon_Item_Handler           │
│        └─ Extract Product_Data          │
└──────────┬──────────────────────────────┘
           │
           ↓
┌─────────────────────────────────────────┐
│ 8. Aggregate Products                    │
│    └─ Product_Aggregator::add_product() │
│        ├─ Generate aggregation key      │
│        │   (ID + EPO fields hash)       │
│        └─ Merge or add new              │
└──────────┬──────────────────────────────┘
           │
           ↓ For each aggregated product
┌─────────────────────────────────────────┐
│ 9. Build Product XML                     │
│    ├─ Product_XML_Builder               │
│    ├─ Apply EPO mappings                │
│    └─ Add to order                      │
└──────────┬──────────────────────────────┘
           │
           ↓
┌─────────────────────────────────────────┐
│ 10. Finalize Structure                   │
│     ├─ Add order to project             │
│     ├─ Add project to root              │
│     └─ Return XML string                │
└─────────────────────────────────────────┘
```

### Address Extraction

```php
$order = wc_get_order( $order_id );

// Billing Address
$billing = new Address(
    $order->get_billing_country(),
    $order->get_billing_postcode(),
    $order->get_billing_city(),
    $order->get_billing_address_1() . ' ' . $order->get_billing_address_2()
);

// Shipping Address (if different)
if ( $order->has_shipping_address() ) {
    $shipping = new Address(
        $order->get_shipping_country(),
        $order->get_shipping_postcode(),
        $order->get_shipping_city(),
        $order->get_shipping_address_1() . ' ' . $order->get_shipping_address_2()
    );
}
```

### Field Mapping

**Input (Settings)**:
```
billing_phone:Phone
billing_company:CompanyName
customer_note:OrderNote
```

**Processing**:
```php
$mappings = [
    'billing_phone' => 'Phone',
    'billing_company' => 'CompanyName',
    'customer_note' => 'OrderNote',
];

foreach ( $mappings as $wc_field => $minicrm_field ) {
    $method = 'get_' . $wc_field; // e.g., get_billing_phone()
    $value = $order->$method();

    if ( $value ) {
        $custom_fields[ $minicrm_field ] = $value;
    }
}
```

**Output (XML)**:
```xml
<Project Id="42">
    <Name>John Doe</Name>
    <Phone>+1-555-0123</Phone>
    <CompanyName>Acme Corp</CompanyName>
    <OrderNote>Please gift wrap</OrderNote>
    ...
</Project>
```

### Tax Calculation

**From Order Item**:
```php
$subtotal = $item->get_subtotal();      // e.g., 100.00
$tax = $item->get_subtotal_tax();        // e.g., 20.00

$percentage = ( $tax / $subtotal ) * 100;  // = 20.0%
```

**Rounding**:
```php
// WooCommerce may have rounding errors
$percentage = round( $percentage, 2 );  // 19.999... → 20.00
```

---

## Summary

### Key Takeaways

1. **Automatic sync** triggers on order create/update/status change
2. **Shutdown hook** batches multiple order events into one sync
3. **Project IDs** group registered user orders, separate guest orders
4. **Security** uses nonce + IP whitelist
5. **XML generation** uses builders, services, and strategy pattern
6. **Product aggregation** deduplicates items (EPO-aware)
7. **Field mapping** syncs custom WooCommerce data to MiniCRM

### Sync Guarantees

✅ **Eventually consistent** - sync happens within ~10 seconds
✅ **Deduplicated** - same project only synced once per request
✅ **Secure** - multi-layer validation (nonce + IP)
✅ **Complete** - all order data, products, and addresses
✅ **Reliable** - retries via manual sync if automatic fails

### What's NOT Guaranteed

❌ **Real-time** - small delay (2-10 seconds)
❌ **Instant updates** - status changes require new sync
❌ **Guest grouping** - each guest order = separate project
❌ **Infinite history** - max 1000 orders per project (configurable)

---

## Next Steps

- **Setup**: [SETUP.md](SETUP.md) - Configure your integration
- **Troubleshooting**: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Fix common issues
- **Architecture**: [../README.md](../README.md) - Developer documentation

---

**Last Updated**: 2025-11-09
