# PRA Refund Decline Queue

Track and retry declined refunds (including PRA — Protective Refund Amount — declines).

---

## Overview

When a refund is declined by the gateway, AuxCORE adds it to the Refund Decline Queue so it can be retried or resolved later:

- 📥 **Automatically queued** - Every declined refund is added (one open entry per refund)
- 🔄 **Retry with backoff** - Up to 3 retries, 5 → 10 → 20 minutes apart — but only when something calls `process-retries` (nothing schedules it today)
- ✅ **Auto-resolution** - Marked `resolved` when the retry's refund call returns successfully (see the caveat under Manual Retry)
- 🛠️ **Manual control** - Retry, resolve, or cancel anytime

All endpoints are admin API routes: `Authorization: Bearer <JWT>` (or an API key in `x-api-key`).

---

## How It Works

```
1. Refund Attempted → 2. Declined → 3. Added to Queue (pending)
                                            ↓
4. Resolved ← Retry (process-retries or manual) → retry → … → failed after 3 failed retries
```

---

## List Queue Entries

### Endpoint

```http
GET /api/v1/refund-decline-queue
```

### Query Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `status` | string | Filter by status (see below) |
| `merchantId` | string | Filter by merchant |
| `fromDate` | date | Created on/after |
| `toDate` | date | Created on/before |
| `page` | integer | Page number (default `1`) |
| `limit` | integer | Items per page (default `50`) |

```bash
curl "https://uat.auxcore.net/api/v1/refund-decline-queue?status=pending" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "entries": [
      {
        "id": "2c4e6a8b-1d3f-4a5b-9c7d-8e9f0a1b2c3d",
        "tenantId": "your-tenant-id",
        "merchantId": "3f2a9c1e-7b4d-4e8a-9f10-2c5d6e7f8a90",
        "transactionId": "TXN-REF-000789",
        "originalTransactionId": "TXN-000456",
        "amount": 150.75,
        "gatewayType": "NMI",
        "status": "retry",
        "declineCode": "300",
        "declineReason": "Refund declined",
        "gatewayResponse": { "responsetext": "Refund declined" },
        "retryCount": 1,
        "maxRetries": 3,
        "scheduledRetryAt": "2026-01-28T15:15:00Z",
        "lastRetryAt": "2026-01-28T15:05:00Z",
        "lastRetryError": "Refund declined",
        "resolvedAt": null,
        "resolvedBy": null,
        "resolutionNotes": null,
        "createdAt": "2026-01-28T15:00:00Z",
        "updatedAt": "2026-01-28T15:05:00Z"
      }
    ],
    "total": 15,
    "page": 1,
    "limit": 50
  }
}
```

Entries are ordered newest first. There is no single-entry `GET /:id` endpoint — filter the list instead.

---

## Queue Entry Statuses

| Status | Description |
|--------|-------------|
| `pending` | Queued, waiting for its first retry |
| `processing` | A retry is in progress |
| `retry` | A retry failed; waiting for the next one |
| `resolved` | Refunded by a retry, or resolved manually |
| `failed` | All retries used up (`retryCount` reached `maxRetries`) |
| `cancelled` | Manually cancelled |

---

## Manual Retry

Retry one entry now. The refund is re-sent for the original transaction through the same gateway.

### Endpoint

```http
POST /api/v1/refund-decline-queue/:id/retry
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/refund-decline-queue/2c4e6a8b-1d3f-4a5b-9c7d-8e9f0a1b2c3d/retry \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response (refund call returned successfully):**
```json
{
  "success": true,
  "data": { "success": true, "message": "Refund processed successfully" },
  "message": "Refund retry initiated successfully"
}
```

A manual retry counts toward `maxRetries` like an automatic one — it does **not** reset the counter.

> **Caveat:** the entry is marked `resolved` whenever the internal refund call returns `success: true`. That call also returns `success: true` when it creates a new refund that the gateway **declines** again, so a `resolved` entry with `resolutionNotes: "Automatic retry successful"` does not prove the money was refunded. Check the original transaction's refunds before closing the case with the customer.

| Status | When |
|--------|------|
| `400` | `VALIDATION_ERROR` — entry already `resolved`, already `failed`, or max retries reached (a `cancelled` entry is not blocked) |
| `404` | Entry not found |
| `500` | `INTERNAL_ERROR` — the refund call errored (non-2xx or timeout); the entry moves to `retry` (or `failed` on the last attempt) |

---

## Manual Resolution

Mark an entry as resolved (e.g., refunded through a different method).

### Endpoint

```http
POST /api/v1/refund-decline-queue/:id/resolve
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/refund-decline-queue/2c4e6a8b-1d3f-4a5b-9c7d-8e9f0a1b2c3d/resolve \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "notes": "Refunded via check" }'
```

`notes` is required (`400` `VALIDATION_ERROR` otherwise). The response `data` is the updated entry with `status: "resolved"`, `resolvedAt`, `resolvedBy` (your user id) and `resolutionNotes`, plus `"message": "Refund resolved successfully"`.

---

## Cancel Entry

Cancel a queue entry (e.g., customer no longer wants the refund).

### Endpoint

```http
POST /api/v1/refund-decline-queue/:id/cancel
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/refund-decline-queue/2c4e6a8b-1d3f-4a5b-9c7d-8e9f0a1b2c3d/cancel \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer request" }'
```

`reason` is required (`400` `VALIDATION_ERROR` otherwise). The response `data` is the updated entry with `status: "cancelled"`; the reason is stored in `resolutionNotes`.

---

## Retry Schedule

Each entry allows **3** retries (`maxRetries`). The next retry is scheduled at `5 minutes × 2^retryCount`, capped at 24 hours:

| Retry | Scheduled after |
|-------|-----------------|
| 1 | 5 minutes after the decline |
| 2 | 10 minutes after retry 1 fails |
| 3 | 20 minutes after retry 2 fails |

These are only the `scheduledRetryAt` times; nothing runs a retry at that time unless `process-retries` is called (see below). After the third failed retry the entry becomes `failed` and needs manual action (resolve or cancel).

---

## Process Retries

Runs every due retry: entries in `pending` or `retry` whose `scheduledRetryAt` has passed and that still have retries left.

### Endpoint

```http
POST /api/v1/refund-decline-queue/process-retries
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/refund-decline-queue/process-retries \
  -H "Authorization: Bearer YOUR_TOKEN"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "processed": 5,
    "successful": 2,
    "failed": 3,
    "errors": [
      { "id": "2c4e6a8b-1d3f-4a5b-9c7d-8e9f0a1b2c3d", "error": "Refund declined" }
    ]
  },
  "message": "Processed 5 refunds: 2 successful, 3 failed"
}
```

> **Note:** No scheduler calls this endpoint automatically today. Scheduled retries only run when `process-retries` (or a manual retry) is called.

---

## Webhooks and Analytics

> **Note:** There are no `refund_decline_queue.*` webhook events and no `/refund-decline-queue/stats` endpoint. A declined refund raises the standard `transaction.refund_failed` webhook (see [Advanced Webhooks](./webhooks-advanced.md)); use the list endpoint with `status` filters for counts.

---

## Best Practices

### ✅ DO:
- **Monitor the queue daily** - Check for `failed` entries
- **Subscribe to `transaction.refund_failed`** - Get notified of declines
- **Document resolutions** - Notes are required and kept on the entry

### ❌ DON'T:
- **Don't ignore failed entries** - Follow up with customers
- **Don't cancel prematurely** - Give retries time to run

---

## Troubleshooting

### Queue Not Processing

If scheduled retries aren't happening, make sure something calls `POST /process-retries` (see the note above), then check the entry's `scheduledRetryAt` and `retryCount`.

### Stuck Entries

If an entry stays in `processing`, the retry call did not complete. Use **Manual Resolution** or **Cancel** once you have confirmed the refund state with the gateway.

---

## Next Steps

- [Process refunds →](../core/refunds.md)
- [Set up webhooks →](./webhooks-advanced.md)
- [View transaction details →](../core/transactions.md)

---

**Need help?** Contact support@auxcore.net
