# Unified Webhook Gateway Integration

## What's This About?

We share the same PayPal and Telr merchant accounts between our jobseeker subscription system and the employer recruitment platform. The problem? Payment providers only let us configure one webhook URL per account. So when PayPal or Telr sends a renewal notification, how do we know if it's for a jobseeker or an employer?

This integration solves that by turning `drjob-subscription-api` into a webhook gateway that inspects incoming webhooks and routes them to the right system.

## How It Works

The gateway receives all payment webhooks, looks up the subscription ID in the jobseeker database, and decides where to send it:

- **Found locally?** Process it as a jobseeker subscription
- **Not found?** Forward it to `drjob-employer-api` (that's us)

```mermaid
flowchart TD
    subgraph external [Payment Providers]
        PayPal[PayPal Webhooks]
        Telr[Telr Webhooks]
    end
    
    subgraph gateway [drjob-subscription-api - Webhook Gateway]
        WebhookEndpoint["/v1/webhook/pg/{gateway}"]
        Router[WebhookRouter]
        JobseekerHandler[Jobseeker Handler]
        EmployerForwarder[EmployerWebhookForwarder]
    end
    
    subgraph employer [drjob-employer-api - This Project]
        InternalEndpoint["/api/internal/{gateway}/webhook"]
        VerifyMiddleware[VerifyInternalWebhook]
        InternalController[InternalWebhookController]
        WebhookController[SubscriptionWebHookController / TelrWebHookController]
    end
    
    PayPal --> WebhookEndpoint
    Telr --> WebhookEndpoint
    WebhookEndpoint --> Router
    Router -->|"Found in jobseeker_subscriptions"| JobseekerHandler
    Router -->|"Not found locally"| EmployerForwarder
    EmployerForwarder -->|"HTTP POST + API Key"| InternalEndpoint
    InternalEndpoint --> VerifyMiddleware
    VerifyMiddleware -->|"Valid API Key"| InternalController
    InternalController --> WebhookController
```

## What Changed in drjob-employer-api

### New Files

**`app/Http/Controllers/InternalWebhookController.php`**

Entry point for webhooks forwarded from the gateway. It receives the request and hands it off to the existing webhook controllers.

```php
class InternalWebhookController extends Controller
{
    public function handlePayPalWebhook(Request $request): JsonResponse
    public function handleTelrWebhook(Request $request): JsonResponse
}
```

Note: We skip PayPal signature verification here since the gateway already did that.

**`app/Http/Middleware/VerifyInternalWebhook.php`**

Guards the internal endpoints with API key authentication. Uses `hash_equals` for timing-safe comparison and logs all auth attempts.

```php
class VerifyInternalWebhook
{
    public function handle(Request $request, Closure $next): Response
}
```

### Modified Files

**`bootstrap/app.php`** — Registered the new middleware alias:

```php
'verify.internal.webhook' => \App\Http\Middleware\VerifyInternalWebhook::class,
```

**`routes/api.php`** — Added the internal webhook routes:

```php
Route::prefix('internal')->middleware('verify.internal.webhook')->group(function () {
    Route::post('/paypal/webhook', [InternalWebhookController::class, 'handlePayPalWebhook']);
    Route::post('/telr/webhook', [InternalWebhookController::class, 'handleTelrWebhook']);
});
```

**`config/services.php`** — Added config for the API key:

```php
'internal_webhook' => [
    'api_key' => env('INTERNAL_WEBHOOK_API_KEY'),
],
```

**`SubscriptionWebHookController.php` & `TelrWebHookController.php`** — Refactored to share logic between public and internal handlers. Extracted `processWebhookEvent()` to avoid duplicating the switch statement.

## Request Flow

### PayPal

```mermaid
sequenceDiagram
    participant PayPal
    participant Gateway as drjob-subscription-api
    participant Employer as drjob-employer-api
    participant DB as Employer Database

    PayPal->>Gateway: POST /v1/webhook/pg/paypal
    Gateway->>Gateway: Extract subscription_id
    Gateway->>Gateway: Look up in jobseeker_subscriptions
    
    alt Not found - it's an employer subscription
        Gateway->>Employer: POST /api/internal/paypal/webhook
        Note over Gateway,Employer: X-Internal-Webhook-Key header
        Employer->>Employer: Verify API key
        Employer->>Employer: Process event
        Employer->>DB: Update payment record
        Employer-->>Gateway: 200 OK
    else Found - it's a jobseeker subscription
        Gateway->>Gateway: Handle locally
    end
    
    Gateway-->>PayPal: 200 OK
```

### Telr

```mermaid
sequenceDiagram
    participant Telr
    participant Gateway as drjob-subscription-api
    participant Employer as drjob-employer-api
    participant TelrAPI as Telr API
    participant DB as Employer Database

    Telr->>Gateway: POST /v1/webhook/pg/telr
    Gateway->>Gateway: Extract agreement_id
    Gateway->>Gateway: Look up in jobseeker_subscriptions
    
    alt Not found - it's an employer subscription
        Gateway->>Employer: POST /api/internal/telr/webhook
        Note over Gateway,Employer: X-Internal-Webhook-Key header
        Employer->>Employer: Verify API key
        Employer->>TelrAPI: Verify transaction status
        TelrAPI-->>Employer: Status response
        Employer->>Employer: Process based on status
        Employer->>DB: Update payment record
        Employer-->>Gateway: 200 OK
    else Found - it's a jobseeker subscription
        Gateway->>Gateway: Handle locally
    end
    
    Gateway-->>Telr: 200 OK
```

## Setup

### Environment Variables

Add to your `.env`:

```env
INTERNAL_WEBHOOK_API_KEY=your-secure-random-key-here
```

This key must match exactly with what's configured in `drjob-subscription-api`.

### Generate a Key

```bash
openssl rand -hex 32
```

Or with PHP:

```bash
php -r "echo bin2hex(random_bytes(32));"
```

## API Reference

### POST /api/v1/internal/paypal/webhook

Receives forwarded PayPal webhooks.

**Headers:**

| Header | Required | Description |
|--------|----------|-------------|
| `X-Internal-Webhook-Key` | Yes | Shared secret for auth |
| `X-Forwarded-Gateway` | No | Identifies the source gateway |
| `Content-Type` | Yes | `application/json` |

**Body:** Standard PayPal webhook payload

**Response:**
```json
{
    "status": 200,
    "message": "Webhook processed successfully"
}
```

### POST /api/v1/internal/telr/webhook

Same structure as PayPal, but with Telr webhook payload.

## Security

- **API Key Auth:** Every request needs a valid `X-Internal-Webhook-Key`
- **HTTPS:** Always use HTTPS in production
- **IP Whitelisting:** Consider restricting these endpoints to the gateway server's IP
- **Signature Verification:** The gateway handles PayPal signature verification; we verify Telr transactions via their API
- **Logging:** All auth attempts are logged for audit purposes

## Error Handling

**Gateway can't reach us:**
1. Stores the webhook in `webhook_forward_failures`
2. Returns 200 to PayPal/Telr (so they don't flood us with retries)
3. Retries later via `webhook:retry-failed` command

**Invalid API key:**
```json
{
    "status": 401,
    "message": "Unauthorized - Invalid API key"
}
```

**Processing error:**
```json
{
    "status": 500,
    "message": "Error processing webhook",
    "error": "Details here"
}
```

## Deployment Checklist

- [ ] Set `INTERNAL_WEBHOOK_API_KEY` in `.env` (must match gateway)
- [ ] Test the internal endpoints respond correctly
- [ ] Check logs are capturing auth attempts
- [ ] Update PayPal/Telr webhook URLs in gateway (SubscriptionAPI Service)

## Related Docs

- Gateway implementation details: check `drjob-subscription-api` docs
- [PayPal Webhooks Guide](https://developer.paypal.com/docs/api-basics/notifications/webhooks/)
- [Telr Developer Portal](https://telr.com/support/)
