juspay / juspay/hyperswitch

V2 Migration api for auxiliary_fingerprint_id

Open
#12,232 0 comments 0 reactions 0 assignees View on GitHub
T-Payment Methods & Routing
Dominant language
Rust
Stars
43.7k
Forks
5.1k
Avg merge
2d 20h
Merged PRs (30d)
205

Description

# Auxiliary Fingerprint Migration API - Implementation Plan

## Overview

### Purpose
Populate the `auxiliary_fingerprint_id` column in the `payment_methods` table for records created by v2. This enables card deduplication based on the raw card number/network token.

### Why This Migration is Needed
- **Historical Context**: Payment methods added by v2 don't have `auxiliary_fingerprint_id`
- **Future Need**: Required for the deduplication logic in `list_customer_payment_methods_core()`
- **Solution**: Retrieve raw payment method data from locker, generate fingerprint, update DB record

### Supported Payment Method Types
The migration supports all types that can have auxiliary fingerprints:
1. **Card** - Credit/Debit cards
2. **NetworkToken** - Network tokens (Visa Token Service, etc.)
3. **CardNumber** - Direct card numbers
4. **BankDebit** - ACH bank debits

---

## Architecture

### High-Level Flow
```
Client Request (POST /v2/payment-methods/migrate)
|
v
Step 1: Authentication & Validation
- Validate Admin API Key
- Parse request body
- Validate payment_method_ids format
|
v
Step 2: Process Each Payment Method
For each payment_method_id:
- Fetch PM from DB (check if already has fingerprint)
- Retrieve raw PM data from locker
- Generate auxiliary fingerprint
- Update DB record
|
v
Step 3: Return aggregated response
```

---

## API Specification

### Endpoint
```
POST /v2/payment-methods/migrate
```

### Authentication
- **Type**: Admin API Key
- **Header**: `api-key: `
- **Rust Auth Type**: `V2AdminApiAuth`

### Request
```json
{
"payment_method_ids": [
"pm_gpay_abc123",
"pm_gpay_def456"
]
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| payment_method_ids | `string[]` | Yes | List of v2 payment method IDs to migrate. Max 100 per request (recommended). |

### Response (Success)
```json
{
"total_requested": 2,
"successful": 2,
"failed": 0,
"skipped": 0,
"results": [
{
"payment_method_id": "pm_gpay_abc123",
"status": "success",
"auxiliary_fingerprint_id": "afp_card_xyz789"
}
]
}
```

### Response (Partial Success)
```json
{
"total_requested": 3,
"successful": 2,
"failed": 1,
"results": [
{
"payment_method_id": "pm_gpay_fail999",
"status": "error",
"error": "Locker retrieval failed: Card not found"
}
]
}
```

---

## Implementation Files

### 1. API Models (`crates/api_models/src/payment_methods.rs`)

```rust
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct MigrateAuxiliaryFingerprintRequest {
pub payment_method_ids: Vec,
}

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(tag = "status")]
pub enum AuxiliaryFingerprintMigrationResult {
#[serde(rename = "success")]
Success {
payment_method_id: id_type::GlobalPaymentMethodId,
auxiliary_fingerprint_id: String,
},
#[serde(rename = "error")]
Error {
payment_method_id: id_type::GlobalPaymentMethodId,
error: String,
},
#[serde(rename = "skipped")]
Skipped {
payment_method_id: id_type::GlobalPaymentMethodId,
reason: String,
},
}

#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct MigrateAuxiliaryFingerprintResponse {
pub total_requested: usize,
pub successful: usize,
pub failed: usize,
pub skipped: usize,
pub results: Vec,
}
```

### 2. Core Module (`crates/router/src/core/payment_methods/migration_auxiliary_fingerprint.rs`)

New file for migration logic.

### 3. Route Handler (`crates/router/src/routes/payment_methods.rs`)

Add handler function with `#[cfg(all(feature = "v2", feature = "olap"))]`.

### 4. Route Registration (`crates/router/src/routes/app.rs`)

Add under v2 payment-methods scope.

---

## Pseudocode

### Main Migration Function

```rust
async fn migrate_auxiliary_fingerprints(
state: &SessionState,
platform: &Platform,
profile: &Profile,
payment_method_ids: Vec,
) -> Result {

let mut results: Vec = Vec::new();
let mut successful_count = 0;
let mut failed_count = 0;
let mut skipped_count = 0;

// Process each payment method ID sequentially
for pm_id in payment_method_ids {
match process_single_payment_method(state, platform, profile, &pm_id).await {
Ok(result) => {
match &result {
AuxiliaryFingerprintMigrationResult::Success { .. } => successful_count += 1,
AuxiliaryFingerprintMigrationResult::Skipped { .. } => skipped_count += 1,
AuxiliaryFingerprintMigrationResult::Error { .. } => failed_count += 1,
}
results.push(result);
}
Err(e) => {
failed_count += 1;
results.push(AuxiliaryFingerprintMigrationResult::Error {
payment_method_id: pm_id,
error: e.to_string(),
});
}
}
}

Ok(MigrateAuxiliaryFingerprintResponse {
total_requested: results.len(),
successful: successful_count,
failed: failed_count,
skipped: skipped_count,
results,
})
}
```

### Process Single Payment Method

```rust
async fn process_single_payment_method(
state: &SessionState,
platform: &Platform,
profile: &Profile,
payment_method_id: &GlobalPaymentMethodId,
) -> Result {

// Step 1: Fetch PM from database
let payment_method = db
.find_payment_method_by_id(platform.get_provider().get_key_store(), payment_method_id)
.await?;

// Step 2: Check if already has auxiliary fingerprint (idempotent)
if payment_method.auxiliary_fingerprint_id.is_some() {
return Ok(AuxiliaryFingerprintMigrationResult::Skipped {
payment_method_id: payment_method_id.clone(),
reason: "Auxiliary fingerprint already set".to_string(),
});
}

// Step 3: Check locker_id exists
let locker_id = payment_method.locker_id.as_ref()
.ok_or(ApiErrorResponse::InternalServerError)?;

// Step 4: Retrieve raw PM data from locker
let retrieve_response = payment_methods::retrieve_payment_method(
state.clone(),
api::PaymentMethodId { payment_method_id: payment_method_id.clone() },
profile.clone(),
platform.clone(),
enums::ApiKeyType::Internal,
true, // fetch_raw_detail = true - CRUCIAL!
).await?;

// Step 5: Extract raw payment method data
let raw_pm_data = match retrieve_response {
ApplicationResponse::Json(response) => response.raw_payment_method_data,
_ => None,
}.ok_or(ApiErrorResponse::InternalServerError)?;

// Step 6: Convert to vaulting data format
let pm_vaulting_data = convert_raw_to_vaulting_data(raw_pm_data)?;

// Step 7: Generate auxiliary fingerprint
let customer_id = payment_method.customer_id
.map(|c| c.to_string())
.unwrap_or_default();

let auxiliary_fingerprint_id = generate_auxiliary_fingerprint(
state,
&pm_vaulting_data,
customer_id,
).await?;

// Step 8: Update payment method in database
update_payment_method_with_fingerprint(
state,
platform,
payment_method_id,
auxiliary_fingerprint_id.clone(),
).await?;

// Step 9: Return success
Ok(AuxiliaryFingerprintMigrationResult::Success {
payment_method_id: payment_method_id.clone(),
auxiliary_fingerprint_id,
})
}
```

### Generate Auxiliary Fingerprint

```rust
async fn generate_auxiliary_fingerprint(
state: &SessionState,
payment_method_data: &PaymentMethodVaultingData,
customer_id: String,
) -> Result {

// Convert payment method data to fingerprint data
let fingerprint_data = payment_method_data.to_auxiliary_fingerprint_data();

// Call vault to generate fingerprint
let fingerprint_id = vault::get_fingerprint_id_from_vault(
state,
&fingerprint_data,
customer_id,
).await?;

Ok(fingerprint_id)
}
```

### Convert Raw Payment Method Data

```rust
fn convert_raw_to_vaulting_data(
raw_data: RawPaymentMethodData,
) -> Result {

match raw_data {
RawPaymentMethodData::Card(card_details) => {
Ok(PaymentMethodVaultingData::CardNumber(card_details.card_number))
}
RawPaymentMethodData::CardWithNT(card_with_nt) => {
if let Some(token) = card_with_nt.network_token_details {
Ok(PaymentMethodVaultingData::NetworkToken(token))
} else {
Ok(PaymentMethodVaultingData::CardNumber(card_with_nt.card_details.card_number))
}
}
RawPaymentMethodData::BankDebit(bank_debit) => {
Ok(PaymentMethodVaultingData::BankDebit(bank_debit))
}
_ => Err(ApiErrorResponse::InternalServerError)
.attach_printable("Unsupported payment method type for fingerprint migration")
}
}
```

### Update Payment Method Record

```rust
async fn update_payment_method_with_fingerprint(
state: &SessionState,
platform: &Platform,
payment_method_id: &GlobalPaymentMethodId,
auxiliary_fingerprint_id: String,
) -> Result<(), ApiErrorResponse> {

let pm_update = PaymentMethodUpdate::AuxiliaryFingerprintUpdate {
auxiliary_fingerprint_id: Some(auxiliary_fingerprint_id),
};

db.update_payment_method(
platform.get_provider().get_key_store(),
payment_method_id,
pm_update,
).await?;

Ok(())
}
```

---

## Data Flow

### Detailed Step-by-Step Flow

**Input:** Payment method ID (e.g., `pm_gpay_abc123`)

1. **Fetch from DB**
```sql
SELECT * FROM payment_methods
WHERE payment_method_id = 'pm_gpay_abc123';
```
- Check if `auxiliary_fingerprint_id IS NOT NULL`
- If already set: return SKIPPED

2. **Call Locker** (via `retrieve_payment_method_core`)
- Internally makes HTTP call to locker service
- Passes `fetch_raw_detail=true` to get raw card data
- Returns encrypted card information

3. **Generate Fingerprint**
- Extract raw card number/network token
- Create `AuxiliaryFingerprintData`
- Call vault service to hash the data
- Returns: `afp_xyz789`

4. **Update DB**
```sql
UPDATE payment_methods
SET auxiliary_fingerprint_id = 'afp_xyz789'
WHERE payment_method_id = 'pm_gpay_abc123';
```

5. **Return SUCCESS**

---

## Error Handling

| Scenario | Error Type | HTTP Status | Result Entry |
|----------|-----------|-------------|--------------|
| Invalid PM ID format | `InvalidRequestData` | 400 | Request rejected |
| PM not found in DB | `PaymentMethodNotFound` | 404 | Error in results |
| PM already has fingerprint | Skipped | 200 | Skipped in results |
| PM missing locker_id | `InternalServerError` | 500 | Error in results |
| Locker retrieval fails | `InternalServerError` | 500 | Error in results |
| Vault fingerprint gen fails | `InternalServerError` | 500 | Error in results |
| DB update fails | `InternalServerError` | 500 | Error in results |

**Strategy:** Continue processing remaining records on individual failures.

---

### Manual Testing

```bash
# Call migration API
curl -X POST "https://api.example.com/v2/payment-methods/migrate" \
-H "Content-Type: application/json" \
-H "api-key: " \
-d '{
"payment_method_ids": ["pm_gpay_test_001"]
}'

# Verify in database
psql -c "SELECT payment_method_id, auxiliary_fingerprint_id
FROM payment_methods
WHERE payment_method_id = 'pm_gpay_test_001';"
```

---

## Reused Components

### Existing Functions

1. **retrieve_payment_method** (`crates/router/src/core/payment_methods.rs`)
- Fetches PM data from locker with raw data flag

2. **get_auxiliary_fingerprint_id_for_payment_method** (`crates/router/src/core/payment_methods/vault.rs`)
- Generates fingerprint from payment method data

3. **to_auxiliary_fingerprint_data** (`crates/hyperswitch_domain_models/src/vault.rs`)
- Converts vaulting data to fingerprint format

---

## Summary

This migration API enables:
1. **Backfilling** auxiliary fingerprints for v2 payment methods
3. **Batch processing** for efficiency
4. **Per-record error handling** with detailed results
5. **Support for all PM types** (Card, NetworkToken, CardNumber, BankDebit)

The implementation maximizes reuse of existing infrastructure (locker, vault, DB queries).

Contributor guide

Open the contributing guide

Research direction

Start by reading crates/api_models/src/payment_methods.rs, crates/router/src/core/payment_methods/migration_auxiliary_fingerprint.rs, crates/router/src/routes/payment_methods.rs, and crates/router/src/routes/app.rs. Trace the existing v2 payment-method route and the retrieve_payment_method_core flow before implementing the migration; done means the authenticated endpoint processes each requested ID, returns aggregated results, and persists auxiliary fingerprints as specified.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, backend, databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.