# Merchant PIN Verification

API reference for verifying the merchant's PIN before performing a void or refund on the mobile POS app.

---

## Base URL

```
https://uat.auxcore.net
```

---

## Endpoint

```
POST /api/v1/merchants/verify-pin
```

---

## When to Call

Call this endpoint immediately before submitting a **void** or **refund** request. The merchant enters their 6-digit PIN on the app; the app calls this endpoint to verify it before proceeding. Do not cache the result — call it fresh each time a void or refund is initiated.

---

## Authentication

| Header            | Required | Description                                                                 |
|-------------------|----------|-----------------------------------------------------------------------------|
| `Authorization`   | Yes      | `Bearer <user_jwt>` — the merchant's user JWT from login. The PIN checked is that of the merchant the JWT is scoped to |
| `X-Tenant-ID`     | Yes      | Tenant identifier (same value used on all other requests)                  |
| `Content-Type`    | Yes      | `application/json`                                                          |
| `X-Device-Token`  | No       | Device JWT from registration. If provided and valid, it is validated. If provided and invalid or revoked, the request is rejected with 401. If omitted, the request proceeds on user JWT alone. |

> **Note:** The device token is optional. When sent, it is only checked for validity (live session, device not suspended) — it does not change which merchant's PIN is verified, and it is not compared with the merchant in the user JWT. When calling from the portal (web), omit it.

---

## Request

### Headers

```
POST /api/v1/merchants/verify-pin HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Tenant-ID: tenant_abc123
X-Device-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

### Body

```json
{
  "pin": "123456"
}
```

| Field | Type   | Required | Rules                                              |
|-------|--------|----------|----------------------------------------------------|
| `pin` | string | Yes      | Exactly 6 digits, numeric only. No letters, spaces, or symbols. |

#### Valid examples

```json
{ "pin": "123456" }
{ "pin": "000000" }
{ "pin": "999999" }
```

#### Invalid examples

```json
{ "pin": "12345" }      ← too short
{ "pin": "1234567" }    ← too long
{ "pin": "12345a" }     ← contains letter
{ "pin": " 12345" }     ← leading space
{}                      ← missing field
```

---

## Responses

### 200 OK — PIN verified

The PIN is correct. Proceed with the void or refund.

```json
{
  "success": true,
  "data": {
    "verified": true
  }
}
```

### 400 Bad Request — Validation error

`pin` is missing or does not meet the format rules. Fix the input before retrying.

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "pin is required and must be exactly 6 digits"
  }
}
```

### 401 Unauthorized — PIN not configured

The merchant has not set a PIN yet. Show a message directing them to set a PIN in the portal.

```json
{
  "success": false,
  "error": {
    "code": "PIN_NOT_CONFIGURED",
    "message": "Merchant PIN is not configured"
  }
}
```

### 401 Unauthorized — Wrong PIN

The PIN format is valid but does not match the merchant's PIN. Show an error to the merchant and allow retry.

```json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid PIN"
  }
}
```

### 401 Unauthorized — Auth error

The user JWT is missing, expired, or invalid. Re-authenticate the merchant. Through the API gateway an expired token returns `TOKEN_EXPIRED` (`"Access token has expired. Please refresh your token."`) — refresh it and retry.

```json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired token"
  }
}
```

> A wrong PIN and an auth failure both return `401 UNAUTHORIZED`; tell them apart by `error.message` (`"Invalid PIN"`) or by checking the JWT first.

### 401 Unauthorized — Device token invalid or revoked

`X-Device-Token` was provided but is invalid or revoked. `message` is one of `"Invalid device token"`, `"Invalid token type"`, `"Device session not found"`, `"Device token has been revoked"` or `"Device not found"`. Re-register the device.

```json
{
  "success": false,
  "error": {
    "code": "INVALID_DEVICE_TOKEN",
    "message": "Device token has been revoked"
  }
}
```

### 403 Forbidden — Device suspended

The device has been suspended by an admin. Contact the merchant portal admin.

```json
{
  "success": false,
  "error": {
    "code": "DEVICE_SUSPENDED",
    "message": "This device has been suspended"
  }
}
```

### 404 Not Found — No merchant

The user JWT is not scoped to a merchant (for example a tenant-admin session), or the merchant no longer exists. Select or switch merchant first.

```json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Merchant not found"
  }
}
```

### 500 Internal Server Error

Unexpected server error. Retry once; if it persists, contact backend team.

```json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "PIN verification failed"
  }
}
```

---

## Error Handling Guide

| HTTP | `error.code`          | App action                                                       |
|------|-----------------------|------------------------------------------------------------------|
| 400  | `VALIDATION_ERROR`    | Show inline field error — invalid PIN format                     |
| 401  | `UNAUTHORIZED`        | `"Invalid PIN"` → allow merchant to retry; otherwise the user JWT is invalid → re-authenticate |
| 401  | `PIN_NOT_CONFIGURED`  | Show "PIN not set" — direct merchant to set PIN in portal        |
| 401  | `INVALID_DEVICE_TOKEN`| Device token invalid — trigger device re-registration            |
| 401  | `TOKEN_EXPIRED`       | Refresh the user JWT and retry once                              |
| 403  | `DEVICE_SUSPENDED`    | Show suspension message — do not retry automatically             |
| 404  | `NOT_FOUND`           | Session has no merchant — select or switch merchant              |
| 500  | `INTERNAL_ERROR`      | Show generic error — retry once after short delay                |

> Branch your error-handling logic on `error.code`. The one exception is `401 UNAUTHORIZED`, which covers both a wrong PIN and an invalid user JWT.

---

## Retry Policy

- **Wrong PIN (401 UNAUTHORIZED)**: Allow the merchant to retry. The server does not limit or lock out failed attempts, so implement a UI counter to warn after 3 failed attempts. Do not auto-retry.
- **PIN not configured (401 PIN_NOT_CONFIGURED)**: Do not retry — show a message directing the merchant to set a PIN via the portal.
- **Validation error (400)**: Do not retry — fix the input first.
- **Server error (500)**: Retry once after 2–3 seconds.
- **Auth errors (401 token issues, 403)**: Do not retry automatically — require user action.

---

## Integration Example (Dart / Flutter)

```dart
Future<bool> verifyMerchantPin(String pin) async {
  final response = await http.post(
    Uri.parse('$baseUrl/api/v1/merchants/verify-pin'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer $userJwt',
      'X-Tenant-ID': tenantId,
      if (deviceToken != null) 'X-Device-Token': deviceToken!,
    },
    body: jsonEncode({'pin': pin}),
  );

  if (response.statusCode == 200) {
    return true;
  }

  final body = jsonDecode(response.body);
  final code = body['error']?['code'];

  if (response.statusCode == 401 && code == 'UNAUTHORIZED') {
    throw InvalidPinException();
  }

  if (response.statusCode == 401 && code == 'PIN_NOT_CONFIGURED') {
    throw PinNotConfiguredException();
  }

  if (response.statusCode == 403 && code == 'DEVICE_SUSPENDED') {
    throw DeviceSuspendedException();
  }

  throw PinVerificationException(body['error']?['message']);
}
```

---

## Integration Example (Kotlin / Android)

```kotlin
suspend fun verifyMerchantPin(pin: String): Boolean {
    val request = Request.Builder()
        .url("$baseUrl/api/v1/merchants/verify-pin")
        .addHeader("Content-Type", "application/json")
        .addHeader("Authorization", "Bearer $userJwt")
        .addHeader("X-Tenant-ID", tenantId)
        .apply { deviceToken?.let { addHeader("X-Device-Token", it) } }
        .post("""{"pin":"$pin"}""".toRequestBody("application/json".toMediaType()))
        .build()

    val response = client.newCall(request).await()
    val body = response.body?.string()?.let { Json.parseToJsonElement(it).jsonObject }

    val errorCode = body?.get("error")?.jsonObject?.get("code")?.jsonPrimitive?.content
    return when {
        response.code == 200 -> true
        response.code == 401 && errorCode == "PIN_NOT_CONFIGURED" -> throw PinNotConfiguredException()
        response.code == 401 -> throw InvalidPinException(body?.errorMessage())
        response.code == 403 -> throw DeviceSuspendedException()
        else -> throw PinVerificationException(body?.errorMessage())
    }
}
```

---

## Changelog

| Date       | Change                                                                 |
|------------|------------------------------------------------------------------------|
| 2026-04-24 | Full implementation shipped — real DB + bcrypt; added PIN_NOT_CONFIGURED case |
| 2026-04-23 | Initial stub implementation shipped                                    |
