# Cashback Postback Integration Guide

This guide documents the full cashback postback integration flow for affiliate networks.

## 1) API Endpoint

- Method: `POST`
- URL: `/api/v1/cashback/postback/{provider?}`
- Example:
  - `https://your-domain.com/api/v1/cashback/postback/default`
  - `https://your-domain.com/api/v1/cashback/postback/impact`
  - `https://your-domain.com/api/v1/cashback/postback/admitad`

`{provider}` is optional. If omitted, provider is treated as `default`.

## 2) Required Payload

Minimum required fields:

```json
{
  "external_transaction_id": "txn-10001",
  "status": "paid"
}
```

Recommended full payload:

```json
{
  "external_transaction_id": "txn-10001",
  "order_reference": "ACT-25",
  "user_id": 5,
  "store_id": 12,
  "cashback_offer_id": 9,
  "amount": 25.00,
  "currency_code": "SAR",
  "status": "paid",
  "occurred_at": "2026-04-17T10:30:00Z",
  "signature": "hex_hmac_sha256",
  "meta": {
    "network": "impact",
    "campaign_id": "CAMP-44"
  }
}
```

Allowed `status` values:

- `pending`
- `approved`
- `rejected`
- `paid`

## 3) Signature (HMAC-SHA256)

### 3.1 String-to-sign (exact order)

The backend signs/verifies this exact concatenation:

```text
external_transaction_id|order_reference|status|amount
```

Example:

```text
txn-10001|ACT-25|paid|25
```

### 3.2 Secret resolution

The backend checks secret in this order:

1. `cashback_postback_secret_{provider}`
2. `cashback_postback_secret`

Examples:

- `cashback_postback_secret_impact`
- `cashback_postback_secret_admitad`
- fallback: `cashback_postback_secret`

### 3.3 Signature required behavior

- If `cashback_postback_require_signature = 1`, invalid/missing signature is rejected.
- If setting is missing, production defaults to requiring signature.

## 4) Admin Settings Checklist

Configure these keys in Admin Settings (`group=cashback`):

- `cashback_postback_enabled = 1`
- `cashback_postback_require_signature = 1` (recommended for production)
- `cashback_postback_secret` or per-provider secrets:
  - `cashback_postback_secret_impact`
  - `cashback_postback_secret_admitad`
  - `cashback_postback_secret_cj`
  - `cashback_postback_secret_webgains`

## 5) Lifecycle in Backend

1. Validate payload.
2. Validate postback enabled setting.
3. Verify signature.
4. Log raw postback in `cashback_postbacks`.
5. Resolve transaction by:
   - (`provider`, `external_transaction_id`) first
   - then `order_reference`
   - then infer from `ACT-{activation_id}` if available
6. Create/update `cashback_transactions`.
7. Sync wallet balance when status transitions to/from `paid`.
8. Mark postback row as processed.

## 6) Idempotency and Deduplication

- Transaction uniqueness is enforced on:
  - (`provider`, `external_transaction_id`)
- Repeating same postback does not create duplicate transaction rows.
- Wallet sync is delta-based, so repeated same state/amount does not double-credit.

## 7) Affiliate Network Mapping (Practical)

Different networks use different field names. Normalize them before calling this endpoint.

### Impact-style mapping example

- `action_id` -> `external_transaction_id`
- `sub_id_1` (or click id) -> `order_reference`
- `payout` -> `amount`
- `currency` -> `currency_code`
- `event_date` -> `occurred_at`
- network status mapping:
  - `PENDING` -> `pending`
  - `APPROVED` -> `approved`
  - `REJECTED` -> `rejected`
  - `PAID` -> `paid`

### Admitad-style mapping example

- `action_id` -> `external_transaction_id`
- `subid` -> `order_reference`
- `payment` -> `amount`
- `currency` -> `currency_code`
- `status` mapped to app statuses

### Generic rule

Always normalize to the platform contract before sending to `/cashback/postback/{provider}`.

## 8) cURL Examples

Assume:

- Base URL: `http://localhost/ReactCouponApp/laravel-backend/public/api/v1`
- Provider: `default`
- Secret: `my-secret`

### 8.1 Generate signature (PowerShell)

```powershell
$string = "txn-10001|ACT-25|paid|25"
$secret = "my-secret"
$hmac = New-Object System.Security.Cryptography.HMACSHA256
$hmac.Key = [Text.Encoding]::UTF8.GetBytes($secret)
$signatureBytes = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($string))
$signature = -join ($signatureBytes | ForEach-Object { $_.ToString("x2") })
$signature
```

### 8.2 Send signed postback

```bash
curl -X POST "http://localhost/ReactCouponApp/laravel-backend/public/api/v1/cashback/postback/default" \
  -H "Content-Type: application/json" \
  -d '{
    "external_transaction_id": "txn-10001",
    "order_reference": "ACT-25",
    "user_id": 5,
    "store_id": 12,
    "cashback_offer_id": 9,
    "amount": 25,
    "currency_code": "SAR",
    "status": "paid",
    "occurred_at": "2026-04-17T10:30:00Z",
    "signature": "PUT_GENERATED_SIGNATURE_HERE",
    "meta": { "network": "default", "raw_status": "paid" }
  }'
```

### 8.3 Unsigned postback (expected to fail if require_signature=1)

```bash
curl -X POST "http://localhost/ReactCouponApp/laravel-backend/public/api/v1/cashback/postback/default" \
  -H "Content-Type: application/json" \
  -d '{
    "external_transaction_id": "txn-10002",
    "order_reference": "ACT-26",
    "amount": 10,
    "status": "paid"
  }'
```

## 9) QA End-to-End Scenario

### Step A: Activate cashback

1. Login user.
2. Call `POST /api/v1/cashback/activate/{storeId}`.
3. Verify response contains:
   - `activation_id`
   - `redirect_url` containing `click_id=ACT-{activation_id}`.

Expected DB:

- New row in `cashback_activations`.
- New pending row in `cashback_transactions` with `order_reference=ACT-{activation_id}`.

### Step B: Postback arrives

1. Send postback with same `order_reference` and a unique `external_transaction_id`.
2. Status `paid`, amount `25`.

Expected DB:

- `cashback_postbacks` row logged and processed.
- Existing transaction updated to `paid` (or created if missing).
- `users.wallet_balance` increased by `25`.

### Step C: Replay same postback

1. Send identical payload again.

Expected:

- No duplicate transaction row.
- Wallet balance unchanged (no double-credit).

### Step D: Status rollback test

1. Update same transaction to `rejected` via admin API.

Expected:

- Wallet balance decreases by previous paid amount.

### Step E: Status back to paid with changed amount

1. Update to `paid` with amount `30`.

Expected:

- Wallet applies delta (`+30` from non-paid, or `+5` if already paid at `25`).

## 10) Monitoring Queries (SQL)

Check latest postbacks:

```sql
SELECT id, provider, external_transaction_id, status, signature_valid, processed, processing_note, created_at
FROM cashback_postbacks
ORDER BY id DESC
LIMIT 20;
```

Check cashback transactions:

```sql
SELECT id, user_id, store_id, provider, external_transaction_id, order_reference, amount, status, paid_at, updated_at
FROM cashback_transactions
ORDER BY id DESC
LIMIT 20;
```

Check wallet balances:

```sql
SELECT id, name, email, wallet_balance
FROM users
ORDER BY id DESC
LIMIT 20;
```

## 11) Common Failure Cases

- `403 Cashback postback is disabled.` -> set `cashback_postback_enabled=1`.
- `401 Invalid postback signature.` -> wrong/empty signature with signature required.
- `422 Unable to resolve transaction user/store.` -> send `order_reference=ACT-{id}` or include `user_id` and `store_id`.

## 12) Production Recommendations

1. Always set `cashback_postback_require_signature=1`.
2. Use provider-specific secrets.
3. Keep network source fields inside `meta`.
4. Monitor `cashback_postbacks.signature_valid=0`.
5. Keep clocks synced (NTP) for accurate `occurred_at` auditing.

