V2 Migration api for auxiliary_fingerprint_id
- 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
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