# Webhook Flow

The webhook flow handles real-time payment status updates from Clip's API to the WooCommerce store.

## Overview

When a payment status changes (completed, cancelled, expired), Clip sends a webhook notification to the store. The webhook listener validates the payload, finds the corresponding order, and updates its status accordingly.

## Webhook Flow Diagram

```mermaid
sequenceDiagram
    participant ClipAPI as Clip API
    participant Webhook as Webhook Endpoint<br/>/wc-api/wc-clip
    participant Listener as Webhooks::listener()
    participant Processor as Webhooks::process_webhook()
    participant Helper as Helper::handle_payment()
    participant SDK as ClipRedirectSdk
    participant Order as WooCommerce Order

    Note over ClipAPI,Order: Payment Status Change Event
    ClipAPI->>ClipAPI: Payment status changes
    Note right of ClipAPI: Possible events:<br/>- Payment completed<br/>- Payment cancelled<br/>- Payment expired<br/>- Payment pending

    ClipAPI->>Webhook: POST /wc-api/wc-clip
    Note right of ClipAPI: Webhook payload (JSON):<br/>{<br/>  "id" or "payment_request_id": "pr_123",<br/>  "event_type" or "resource_status": "COMPLETED",<br/>  ...additional payment data<br/>}

    Webhook->>Listener: Receive webhook
    Listener->>Listener: Read raw POST data (php://input)
    Listener->>Listener: Log webhook receipt
    
    Listener->>Processor: process_webhook(json)
    Processor->>Processor: json_decode(json)
    
    Note over Processor,Order: Validation Phase
    Processor->>Processor: validate_input(data)
    
    alt Validation: Check Payment ID
        Processor->>Processor: Check for 'id' or 'payment_request_id'
        alt ID Missing
            Processor->>Processor: Log error: Missing payment ID
            Processor-->>Listener: Return false
            Listener->>ClipAPI: Return HTTP 500 ERROR
        end
    end
    
    alt Validation: Check Status
        Processor->>Processor: Check for 'event_type' or 'resource_status'
        alt Status Missing
            Processor->>Processor: Log error: Missing status
            Processor-->>Listener: Return false
            Listener->>ClipAPI: Return HTTP 500 ERROR
        end
    end
    
    Note over Processor,Order: Order Lookup Phase
    Processor->>Processor: get_order_id(data)
    Processor->>Order: Find order by meta key<br/>_CLIPREDIRECT_PAYMENT_ID
    
    alt Order Found
        Order-->>Processor: Return order_id
    else Order Not Found
        Processor-->>Listener: Return false
        Listener->>ClipAPI: Return HTTP 500 ERROR
    end
    
    Note over Processor,Order: Payment Processing Phase
    Processor->>Helper: handle_payment(order_id, status)
    Helper->>Order: wc_get_order(order_id)
    Order-->>Helper: Return order object
    
    Helper->>Order: get_meta('_CLIPREDIRECT_PAYMENT_ID')
    Order-->>Helper: Return payment_request_id
    
    Helper->>SDK: Initialize SDK with credentials
    Helper->>SDK: get_payment_data(payment_request_id)
    SDK->>ClipAPI: GET /v2/checkout/{payment_request_id}
    Note right of SDK: Fetch latest payment details
    ClipAPI-->>SDK: Return payment data
    Note right of ClipAPI: Response includes:<br/>- status<br/>- receipt_no<br/>- payment details
    SDK-->>Helper: Return payment data
    
    Helper->>Order: get_meta('_CLIPREDIRECT_PAYMENT_STATUS')
    Order-->>Helper: Return previous status
    
    alt Status Changed
        Helper->>Helper: Compare previous vs new status
        
        alt Status: CHECKOUT_COMPLETED
            Helper->>Order: payment_complete()
            Note right of Order: Triggers WooCommerce<br/>order completion:<br/>- Change status to Processing/Completed<br/>- Send email<br/>- Reduce stock<br/>- Clear cart
            Helper->>Order: add_order_note("Approved Payment")
            Helper->>Order: Save receipt_no to meta
        else Status: CHECKOUT_CANCELLED
            Helper->>Order: add_order_note("Cancelled Payment")
        else Status: CHECKOUT_EXPIRED
            Helper->>Order: add_order_note("Expired Payment")
        else Status: CHECKOUT_PENDING
            Helper->>Order: add_order_note("Pending Payment")
        end
        
        Helper->>Order: update_meta_data('_CLIPREDIRECT_PAYMENT_STATUS')
        Helper->>Order: update_meta_data('_CLIPREDIRECT_RECEIPT_NO')
        Helper->>Order: save()
        
        Helper-->>Processor: Return true
    else Status Unchanged
        Helper-->>Processor: Return false
        Note right of Helper: No action needed,<br/>webhook already processed
    end
    
    Processor-->>Listener: Return processing result
    
    alt Processing Successful
        Listener->>ClipAPI: Return HTTP 200 OK
    else Processing Failed
        Listener->>ClipAPI: Return HTTP 500 ERROR
    end
```

## Webhook Registration

The webhook endpoint is registered in WordPress:

**File**: `hooks.php`
```php
add_action(
    'woocommerce_api_wc-clip',
    array( '\Conexa\ClipRedirect\Orders\Webhooks', 'listener' )
);
```

**Webhook URL**: `https://your-store.com/wc-api/wc-clip`

This URL is automatically included in every checkout creation request to Clip API.

## Webhook Listener

**File**: `src/orders/class-webhooks.php`

### Main Listener Method

```php
public static function listener( string $data = null ) {
    // Read raw POST data
    if ( is_null( $data ) || empty( $data ) ) {
        $json = file_get_contents( 'php://input' );
    } else {
        $json = $data; // For testing
    }
    
    Helper::log( 'Webhook received' );
    Helper::log( $json );
    
    // Process webhook
    $process = self::process_webhook( $json );
    
    // Return HTTP response
    if ( $process ) {
        header( 'HTTP/1.1 200 OK' );
    } else {
        header( 'HTTP/1.1 500 ERROR' );
        wp_die(
            __( 'WooCommerce ClipRedirectSdk Webhook not valid.', 'clip-for-woocommerce' ),
            'ClipRedirectSdk Webhook',
            array( 'response' => 500 )
        );
    }
}
```

### Webhook Processing

```php
public static function process_webhook( $json ) {
    // Decode JSON
    $data = json_decode( $json, true );
    
    // Validate payload
    if ( self::validate_input( $data ) ) {
        // Find order
        $order_id = self::get_order_id( $data );
        
        // Handle payment
        return Helper::handle_payment( $order_id );
    } else {
        return false;
    }
}
```

### Webhook Validation

```php
private static function validate_input( array $data ) {
    $return = true;
    $data = wp_unslash( $data );
    
    // Check for payment ID (supports both old and new formats)
    if ( ( ! isset( $data['id'] ) || empty( $data['id'] ) ) && 
         ( ! isset( $data['payment_request_id'] ) || empty( $data['payment_request_id'] ) ) ) {
        Helper::log( 'Webhook received without payment ID' );
        $return = false;
    }
    
    // Check for status (supports both old and new formats)
    if ( ( ! isset( $data['event_type'] ) || empty( $data['event_type'] ) ) && 
         ( ! isset( $data['resource_status'] ) || empty( $data['resource_status'] ) ) ) {
        Helper::log( 'Webhook received without status' );
        $return = false;
    }
    
    return $return;
}
```

### Order Lookup

```php
private static function get_order_id( array $data ) {
    $clip_id = null;
    
    // Get payment request ID (supports both formats)
    if ( isset( $data['id'] ) ) {
        $clip_id = filter_var( $data['id'], FILTER_SANITIZE_STRING );
    } elseif ( isset( $data['payment_request_id'] ) ) {
        $clip_id = filter_var( $data['payment_request_id'], FILTER_SANITIZE_STRING );
    }
    
    if ( $clip_id ) {
        // Find order by payment request ID stored in meta
        return Helper::find_order_by_itemmeta_value(
            \ClipRedirect::META_ORDER_PAYMENT_ID,
            $clip_id
        );
    }
    
    return null;
}
```

## Payment Status Handling

**File**: `src/helper/class-handle-payment-trait.php`

```php
public static function handle_payment( $order_id, $status = '' ) {
    if ( null !== $order_id ) {
        $order = wc_get_order( $order_id );
        $order_data = $order->get_meta( '_CLIPREDIRECT_PAYMENT_ID' );
    }
    
    if ( isset( $order_data ) ) {
        // Initialize SDK and get payment data
        $options = Helper::get_options( \ClipRedirect::GATEWAY_ID );
        $sdk = new ClipRedirectSdk( $options['api_key'], $options['api_secret'] );
        $response = $sdk->get_payment_data( $order_data );
        
        $receipt_no = isset( $response['receipt_no'] ) ? $response['receipt_no'] : '';
        $prevStatus = $order->get_meta( '_CLIPREDIRECT_PAYMENT_STATUS' );
        
        // Only process if status changed
        if ( $prevStatus !== $response['status'] ) {
            
            // Handle completed payment
            if ( 'CHECKOUT_COMPLETED' === $response['status'] ) {
                $order->payment_complete();
                
                if ( ! empty( $receipt_no ) ) {
                    $order->add_order_note(
                        sprintf(
                            __( 'Clip - Approved Payment. Receipt: %1$s. ID https://receipt.clip.mx/%2$s', 'clip-for-woocommerce' ),
                            $receipt_no,
                            $order_data
                        )
                    );
                }
            }
            
            // Handle cancelled payment
            if ( 'CHECKOUT_CANCELLED' === $response['status'] ) {
                $order->add_order_note(
                    sprintf(
                        __( 'Clip - Cancelled Payment. ID %s', 'clip-for-woocommerce' ),
                        $order_data
                    )
                );
            }
            
            // Handle expired payment
            if ( 'CHECKOUT_EXPIRED' === $response['status'] ) {
                $order->add_order_note(
                    sprintf(
                        __( 'Clip - Expired Payment. ID %s', 'clip-for-woocommerce' ),
                        $order_data
                    )
                );
            }
            
            // Handle pending payment
            if ( 'CHECKOUT_PENDING' === $response['status'] ) {
                $order->add_order_note(
                    sprintf(
                        __( 'Clip - Pending Payment. ID %s', 'clip-for-woocommerce' ),
                        $order_data
                    )
                );
            }
            
            // Save updated status and receipt
            $order->update_meta_data(
                \ClipRedirect::META_CLIPREDIRECT_RECEIPT_NO,
                $receipt_no
            );
            $order->update_meta_data(
                \ClipRedirect::META_CLIPREDIRECT_PAYMENT_STATUS,
                $response['status']
            );
            $order->save();
            
            return true;
        }
    }
    return false;
}
```

## Webhook Payload Examples

### Completed Payment Webhook

```json
{
    "payment_request_id": "pr_abc123xyz",
    "event_type": "CHECKOUT_COMPLETED",
    "resource_status": "COMPLETED",
    "amount": 1250.00,
    "currency": "MXN",
    "receipt_no": "5mUV5Dt",
    "created_at": "2025-11-11T10:30:00Z",
    "completed_at": "2025-11-11T10:35:00Z",
    "payment_method": "CARD",
    "last_four_digits": "4242"
}
```

### Cancelled Payment Webhook

```json
{
    "payment_request_id": "pr_abc123xyz",
    "event_type": "CHECKOUT_CANCELLED",
    "resource_status": "CANCELLED",
    "amount": 1250.00,
    "currency": "MXN",
    "created_at": "2025-11-11T10:30:00Z",
    "cancelled_at": "2025-11-11T10:32:00Z"
}
```

### Expired Payment Webhook

```json
{
    "payment_request_id": "pr_abc123xyz",
    "event_type": "CHECKOUT_EXPIRED",
    "resource_status": "EXPIRED",
    "amount": 1250.00,
    "currency": "MXN",
    "created_at": "2025-11-11T10:30:00Z",
    "expires_at": "2025-11-14T10:30:00Z"
}
```

## Payment Status Transitions

```mermaid
stateDiagram-v2
    [*] --> CHECKOUT_CREATED: Checkout created
    CHECKOUT_CREATED --> CHECKOUT_PENDING: Customer initiates payment
    CHECKOUT_PENDING --> CHECKOUT_COMPLETED: Payment successful
    CHECKOUT_PENDING --> CHECKOUT_CANCELLED: Customer cancels
    CHECKOUT_CREATED --> CHECKOUT_EXPIRED: Time expires
    CHECKOUT_PENDING --> CHECKOUT_EXPIRED: Time expires
    CHECKOUT_COMPLETED --> [*]: Order complete
    CHECKOUT_CANCELLED --> [*]: Order pending
    CHECKOUT_EXPIRED --> [*]: Order pending
```

## Status Descriptions

| Status | Description | Order Impact | Can Retry |
|--------|-------------|--------------|-----------|
| `CHECKOUT_CREATED` | Payment link created | Order stays pending | Yes |
| `CHECKOUT_PENDING` | Payment initiated but not finalized | Order stays pending | Yes |
| `CHECKOUT_COMPLETED` | Payment successfully processed | Order marked complete | No |
| `CHECKOUT_CANCELLED` | User cancelled payment | Order stays pending | Yes |
| `CHECKOUT_EXPIRED` | Payment link expired (72h default) | Order stays pending | Yes |

## Receipt URL

When payment is completed, the plugin generates a receipt URL:

```
https://receipt.clip.mx/{payment_request_id}
```

This URL is included in the order note for easy access.

## Duplicate Webhook Handling

The webhook handler includes protection against duplicate webhooks:

1. **Status Comparison**: Before processing, the handler checks if the status has changed
2. **Previous Status**: Stored in order meta `_CLIPREDIRECT_PAYMENT_STATUS`
3. **Skip Processing**: If status is unchanged, webhook returns false (but still sends 200 OK)

```php
$prevStatus = $order->get_meta( '_CLIPREDIRECT_PAYMENT_STATUS' );

if ( $prevStatus !== $response['status'] ) {
    // Process status change
    return true;
}

return false; // Status unchanged, skip processing
```

## Error Handling

### Webhook Validation Errors

| Error | Cause | HTTP Response |
|-------|-------|---------------|
| Missing payment ID | Webhook missing `id` or `payment_request_id` | 500 ERROR |
| Missing status | Webhook missing `event_type` or `resource_status` | 500 ERROR |
| Order not found | No order with matching payment ID | 500 ERROR |

### Processing Errors

If webhook processing fails:
- HTTP 500 ERROR is returned to Clip
- Clip will retry the webhook (typical retry logic: exponential backoff)
- Error is logged in WooCommerce logs (if debug enabled)

## Webhook Security

### Current Implementation

The webhook endpoint is publicly accessible (as required for webhooks). Security measures include:

1. **Data Validation**: All webhook data is validated before processing
2. **Sanitization**: User input is sanitized using WordPress functions
3. **Status Verification**: Payment status is verified by querying Clip API
4. **Order Lookup**: Only orders with matching payment IDs are updated

### Recommended Enhancements

For production environments, consider adding:

1. **Signature Verification**: Validate webhook signature (if Clip provides one)
2. **IP Whitelisting**: Restrict webhook endpoint to Clip's IP addresses
3. **Rate Limiting**: Implement rate limiting to prevent abuse
4. **Timestamp Validation**: Reject old webhooks (replay attack prevention)

## Testing Webhooks

### Manual Testing

You can manually trigger webhook processing:

```php
// In a test file or plugin
$webhook_payload = json_encode([
    'payment_request_id' => 'pr_test123',
    'event_type' => 'CHECKOUT_COMPLETED',
    'receipt_no' => '5mUV5Dt'
]);

$result = \Conexa\ClipRedirect\Orders\Webhooks::listener( $webhook_payload );
```

### Using Tools

- **Webhook.site**: Capture and inspect webhook payloads
- **ngrok**: Expose local development to receive real webhooks
- **Postman**: Send test webhook requests

### Test Payload

```bash
curl -X POST https://your-store.com/wc-api/wc-clip \
  -H "Content-Type: application/json" \
  -d '{
    "payment_request_id": "pr_abc123xyz",
    "event_type": "CHECKOUT_COMPLETED",
    "resource_status": "COMPLETED",
    "amount": 100.00,
    "currency": "MXN",
    "receipt_no": "5mUV5Dt"
  }'
```

## Logging

If debug logging is enabled (`wc_clipredirect_log_enabled = yes`):

**File**: `wp-content/uploads/wc-logs/clip-for-woocommerce-{date}.log`

**Log entries**:
```
Webhook received
Webhook received from ClipRedirectSdk: {"payment_request_id":"pr_abc123xyz",...}
Webhook received without payment ID (id or payment_request_id).
Webhook received without status (event_type or resource_status).
```

## Related Files

- `src/orders/class-webhooks.php` - Main webhook listener and processor
- `src/helper/class-handle-payment-trait.php` - Payment status handler
- `src/sdk/class-clip-redirect-sdk.php` - API communication for status retrieval
- `src/helper/class-logger-trait.php` - Logging functionality
- `hooks.php` - Webhook endpoint registration

## API Reference

### Clip API: Get Payment Status

**Endpoint**: `GET /v2/checkout/{payment_request_id}`

**Headers**:
```
Authorization: Basic {base64(api_key:api_secret)}
Content-Type: application/json
Accept: application/json
```

**Response**:
```json
{
    "payment_request_id": "pr_abc123xyz",
    "status": "CHECKOUT_COMPLETED",
    "amount": 1250.00,
    "currency": "MXN",
    "receipt_no": "5mUV5Dt",
    "created_at": "2025-11-11T10:30:00Z",
    "completed_at": "2025-11-11T10:35:00Z"
}
```
