# Payment Methods

User and location payment methods management

## PUT /api/v1/locations/{locationId}/payment-methods/prepaid

Add the prepaid payment method to a location

Enables the built-in prepaid payment method for a location. The operation is idempotent and does not alter prepaid type definitions or other PSP settings.

Use `POST /api/v1/prepaid-types` separately to add a selectable prepaid type such as Wolt or a house account.

**Permissions:** 🟠 `prepaid_payment_methods.update`

Path parameters:
- `locationId` (string, required) — Location ID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    paymentMethod: "prepaid"
    enabled: boolean
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## DELETE /api/v1/locations/{locationId}/payment-methods/prepaid

Remove the prepaid payment method from a location

Disables the built-in prepaid payment method for a location. The operation is idempotent and retains prepaid type definitions and historical payment data. Re-enable it with PUT on the same URL.

**Permissions:** 🟠 `prepaid_payment_methods.update`

Path parameters:
- `locationId` (string, required) — Location ID

Response 204 — Prepaid payment method disabled

```ts
unknown
```

## POST /api/v1/loyalty/adyen-card-session

Create Adyen zero-value auth session for self-service card linking

Creates an Adyen checkout session with amount=0 and storePaymentMethodMode=enabled.

The client initialises Adyen Drop-in with the returned `sessionId` and `sessionData`.
After the user enters their card details, Adyen tokenises the card and issues a webhook.
The resulting token is stored in `user_payment_methods` for future card-linked loyalty recognition.

**Requires authenticated user session.**

**Permissions:** 🟢 `payment-methods.create`

Request body:

```ts
{
  returnUrl: string
  channel?: "Web" | "iOS" | "Android"
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    sessionId: string
    sessionData: string
    clientKey?: string | null
    environment: string
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## POST /api/v1/loyalty/finalize-card-session

Finalize Adyen zero-auth session and store card token

Verifies the Adyen session result and stores the tokenized card in user_payment_methods.

Call this after Adyen Drop-in's onPaymentCompleted fires with resultCode 'Authorised'.
The sessionResult token (from the Drop-in callback) is verified server-side against Adyen's
Sessions API — the card is never stored based solely on client-supplied data.

If this is the user's first card, it is automatically set as their default.

**Requires authenticated user session.**

**Permissions:** 🟢 `payment-methods.create`

Request body:

```ts
{
  sessionId: string
  sessionResult: string
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    card: {
      id: string
      brand?: string | null
      last4?: string | null
      expiryMonth?: integer | null
      expiryYear?: integer | null
      isDefault: boolean
      isTokenized: boolean
      nickname?: string | null
    }
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## POST /api/v1/loyalty/link-payment-method

Link payment card to user account

Links a payment card (from a purchase) to a user account.

This endpoint:
1. Extracts card data from the purchase
2. Finds or creates a user by email/phone
3. Links the card to the user for future recognition
4. Optionally enrolls the user in the loyalty program
5. Awards retroactive points for the triggering purchase

**Use case**: After a guest makes a purchase and enters their email/phone,
their card is linked so future purchases automatically earn loyalty points.

**Permissions:** 🟢 `loyalty.create`

Request body:

```ts
{
  purchaseIntentId: string
  email?: string // email
  phone?: string
  enrollInProgram?: boolean
  locationId?: number
}
```

Response 201 — Default Response

```ts
{
  status: "success"
  data: {
    user: {
      id: number
      isNewUser: boolean
    }
    paymentMethod: {
      linked: boolean
      id: string | null // uuid
      brand: string | null
      last4: string | null
    }
    enrollment: {
      enrolled?: boolean
      pointsAwarded?: number | null
      newBalance?: number | null
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## POST /api/v1/loyalty/staff-link-payment-method

Staff-assisted card link with verbal consent

Links a payment card to a user account using verbal consent captured by a staff member.

This endpoint:
1. Extracts card data from the purchase
2. Links the card to the specified user with verbal consent
3. Records a consent event with the staff member's retailerUserId
4. Auto-enrolls the user in the location's loyalty program if not already enrolled
5. Awards retroactive points for the triggering purchase

**Use case**: When a guest doesn't scan the QR code, staff can look up the user
and link their card manually after verbal confirmation.

**Permissions:** 🟢 `loyalty.create`

Request body:

```ts
{
  purchaseIntentId: string
  userId: number
  locationId: number
  consentMethod: "verbal"
  retailerUserId: number
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    cardLinked: boolean
    enrolled: boolean
    pointsAwarded: number
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/loyalty/user-search

Search users by name, email, or phone (staff)

Search for users matching a query string. Used by staff during the fallback enrollment flow.

Query parameters:
- **q**: Search term (minimum 2 characters) — matched against email, name, and phone
- **purchaseIntentId**: Optional — if provided, the tab member for that purchase is auto-suggested

Results are sorted by relevance. Maximum 10 results returned.

**Permissions:** 🔵 `users.read`

Query parameters:
- `q` (string, required) — Search term (name, email, or phone)
- `purchaseIntentId` (string, optional) — Optional purchase intent ID for tab member auto-suggest

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    users: Array<{
        id: number
        email?: string | null
        firstName?: string | null
        lastName?: string | null
        phone?: string | null
      }>
    autoSuggested: {
      userId?: number
      firstName?: string | null
      lastName?: string | null
      email?: string | null
      autoSuggested?: true
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/purchases/{purchaseIntentId}/loyalty-status

Get loyalty status for a purchase

Returns the loyalty scenario after a purchase completes.

Used by the frontend to determine what modal/action to show:
- **POINTS_EARNED**: User is enrolled, points were awarded automatically
- **ENROLLMENT_AVAILABLE**: Card is linked to user, but not enrolled at this location
- **CARD_LINK_AVAILABLE**: Card is not linked to any user, prompt for email/phone
- **NO_LOYALTY_PROGRAM**: Location has no loyalty program
- **PROCESSING**: Still processing (retry later)

**Permissions:** 🔵 `loyalty.read`

Path parameters:
- `purchaseIntentId` (string, required) — Purchase intent ID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    loyaltyScenario: "POINTS_EARNED" | "ENROLLMENT_AVAILABLE" | "CARD_LINK_AVAILABLE" | "NO_LOYALTY_PROGRAM" | "PROCESSING"
    pointsEarned: {
      points?: number
      newBalance?: number
    } | null
    enrollmentOffer: {
      programId?: number
      programName?: string
      potentialPoints?: number
    } | null
    cardLinkOffer: {
      programId?: number
      programName?: string
      potentialPoints?: number
      brand?: string | null
      last4?: string | null
    } | null
    user: {
      id?: number
      email?: string | null
    } | null
    location: {
      id: number
      name: string
      hasLoyaltyProgram: boolean
    }
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/retailer/users/{userId}/payment-methods

List payment methods for a specific user (staff view)

Returns all saved payment methods for a specific user.

Used by staff to view linked payment cards on the loyalty member detail page.
This is a read-only view — staff cannot modify user payment methods through this endpoint.

**Permissions:** 🔵 `users.read`

Path parameters:
- `userId` (string, required) — User ID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    paymentMethods: Array<{
        id?: string // uuid
        nickname?: string | null
        brand?: string | null
        last4?: string | null
        expiryMonth?: number | null
        expiryYear?: number | null
        isDefault?: boolean
        isTokenized?: boolean
        lastUsedAt?: string | null // date-time
        createdAt?: string // date-time
      }>
    defaultStoredMethodId?: string | null
    lastUsedStoredMethodId?: string | null
    lastUsedKind?: "card" | "swish"
    savedSwish?: {
      uuid: string // uuid
      payerAlias: string
      lastUsedAt: string | null // date-time
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/users/me/payment-methods

List saved payment methods

Returns all saved payment methods for the authenticated user.

Payment methods are sorted by:
1. Default payment method first
2. Most recently used
3. Creation date

**Permissions:** 🔵 `payment-methods.read`

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    paymentMethods: Array<{
        id?: string // uuid
        nickname?: string | null
        brand?: string | null
        last4?: string | null
        expiryMonth?: number | null
        expiryYear?: number | null
        isDefault?: boolean
        isTokenized?: boolean
        lastUsedAt?: string | null // date-time
        createdAt?: string // date-time
      }>
    defaultStoredMethodId?: string | null
    lastUsedStoredMethodId?: string | null
    lastUsedKind?: "card" | "swish"
    savedSwish?: {
      uuid: string // uuid
      payerAlias: string
      lastUsedAt: string | null // date-time
    } | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## DELETE /api/v1/users/me/payment-methods/{id}

Remove saved payment method

Removes a saved payment method (soft delete). The card will no longer be recognized for loyalty purposes.

**Permissions:** 🔴 `payment-methods.delete`

Path parameters:
- `id` (string, required) — Payment method UUID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    message?: string
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## PATCH /api/v1/users/me/payment-methods/{id}

Update payment method

Updates a payment method's nickname.

**Permissions:** 🟠 `payment-methods.update`

Path parameters:
- `id` (string, required) — Payment method UUID

Request body:

```ts
{
  nickname: string
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    id?: string // uuid
    nickname?: string | null
    brand?: string | null
    last4?: string | null
    expiryMonth?: number | null
    expiryYear?: number | null
    isDefault?: boolean
    isTokenized?: boolean
    lastUsedAt?: string | null // date-time
    createdAt?: string // date-time
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## POST /api/v1/users/me/payment-methods/{id}/default

Set default payment method

Sets a payment method as the default for the authenticated user.

**Permissions:** 🟠 `payment-methods.update`

Path parameters:
- `id` (string, required) — Payment method UUID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    message?: string
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```
