diff --git a/GST_SETUP_INSTRUCTIONS.md b/GST_SETUP_INSTRUCTIONS.md new file mode 100644 index 0000000..a4e0fb2 --- /dev/null +++ b/GST_SETUP_INSTRUCTIONS.md @@ -0,0 +1,66 @@ +# GST Verification Setup Instructions + +## Problem Identified ✅ + +The GST credentials you provided are **NOT in the `.env` file**. The environment variables are `undefined` when the app runs. + +## Solution + +You need to **add the following lines to your `.env` file**: + +```env +# GST Verification (Setu) +GST_PROVIDER_URL=https://dg-sandbox.setu.co/api/verify/gst +GST_CLIENT_ID=292c6e76-dabf-49c4-8e48-90fba2916673 +GST_CLIENT_SECRET=7IZMe9zvoBBuBukLiCP7n4KLwSOy11oP +GST_PRODUCT_INSTANCE_ID=69e23f7f-4f71-412e-aec6-b1da3fb77c6f +``` + +## Steps to Fix + +1. **Open your `.env` file** (it's in the root of your project) + +2. **Add the GST credentials** (copy the lines above) + +3. **Restart your server**: + - Stop the current server (Ctrl+C in terminal) + - Run `npm run dev` again + +4. **Test the endpoint** with this curl command: + +### PowerShell (Windows): +```powershell +Invoke-WebRequest -Uri "http://localhost:3000/v1/gst/verify/27AAACM1234A1Z5" -Headers @{"x-api-key"="vf_test_369afc881da118c02ade27d323aaf3c186945edce6bcdf02"} -Method GET | Select-Object -ExpandProperty Content +``` + +### Bash/CMD (if you have curl.exe): +```bash +curl -X GET http://localhost:3000/v1/gst/verify/27AAACM1234A1Z5 -H "x-api-key: vf_test_369afc881da118c02ade27d323aaf3c186945edce6bcdf02" +``` + +## What I Fixed + +1. ✅ **Applied the same fix that worked for PAN verification**: + - Created fresh axios instance to avoid global defaults pollution + - Clean credentials by trimming whitespace + - Added detailed debug logging + - Enhanced error handling for auth failures + +2. ✅ **The code is ready** - it just needs the credentials in `.env` + +## After Adding Credentials + +Once you add the credentials to `.env` and restart, you should see: + +**Debug logs in terminal:** +``` +[GST Service] Sending request to provider: { + url: 'https://dg-sandbox.setu.co/api/verify/gst', + 'x-client-id': '292c6e76-dabf-49c4-8e48-90fba2916673', + 'x-client-secret': '****11oP', + 'x-product-instance-id': '69e23f7f-4f71-412e-aec6-b1da3fb77c6f', + requestBody: { gstin: '27AAACM1234A1Z5' } +} +``` + +**Note**: If you still get authentication errors after adding to `.env`, it means the Setu sandbox credentials themselves are invalid or expired. You'll need to verify them with Setu. diff --git a/README.md b/README.md index 3ededa2..19205d4 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,13 @@ npm install PORT=3000 NODE_ENV=development DATABASE_URL=postgres://india_api_2025:@localhost:5434/india-api-tech4biz + +# Setu API Credentials for PAN Verification +PAN_PROVIDER_URL=https://dg-sandbox.setu.co/api/verify/pan +PAN_CLIENT_ID=your-client-id-here +PAN_CLIENT_SECRET=your-client-secret-here +PAN_PRODUCT_INSTANCE_ID=your-product-instance-id-here + # optional: JWT_SECRET, REDIS_URL, etc. ``` @@ -52,16 +59,96 @@ verify-india-api/ └── README.md ``` -## API Key Format +## Getting Your API Key +### Quick Method (For Testing) +Run the test API key creation script: +```bash +npm run create-test-key +``` +This will create a test API key and display it in the console. + +### Sign Up Method (For Production) +Create an account to get an API key: +```bash +curl -X POST http://localhost:3000/v1/auth/signup \ + -H "Content-Type: application/json" \ + -d '{ + "email": "your-email@example.com", + "password": "your-password-123", + "company_name": "Your Company" + }' +``` +The response will include your `api_key` - save it immediately as it's only shown once! + +### API Key Format +API keys start with `vf_live_` or `vf_test_`: ``` vf_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 +vf_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 ``` -Authentication via header: +### Using API Key +Send it in the request header: ``` X-API-Key: vf_live_xxx ``` +or +``` +x-api-key: vf_live_xxx +``` + +**See `HOW_TO_GET_API_KEY.md` for detailed instructions.** + +## PAN Verification (Setu Integration) + +PAN verification is powered by Setu API. To use this feature: + +1. Get your Setu API credentials: + - `PAN_PROVIDER_URL`: Setu API endpoint (default: https://dg-sandbox.setu.co/api/verify/pan) + - `PAN_CLIENT_ID`: Your Setu client ID (UUID format) + - `PAN_CLIENT_SECRET`: Your Setu client secret + - `PAN_PRODUCT_INSTANCE_ID`: Your Setu product instance ID (UUID format) + +2. Add them to your `.env` file (see Setup section above) + +3. Verify a PAN: +```bash +curl -X POST http://localhost:3000/v1/pan/verify \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-api-key" \ + -d '{ + "pan": "ABCDE1234A", + "reason": "KYC verification for account opening" + }' +``` + +**Response:** +```json +{ + "success": true, + "data": { + "pan": "ABCDE1234A", + "full_name": "Kumar Gaurav Rathod", + "first_name": "Gaurav", + "middle_name": "Kumar", + "last_name": "Rathod", + "category": "Individual or Person", + "aadhaar_seeding_status": "LINKED", + "status": "VALID", + "setu_response_id": "0e370877-f860-4b5c-858b-42aebfea4879", + "setu_trace_id": "1-69437d63-75ac46a51036ab2233854e23", + "setu_message": "PAN is valid." + }, + "meta": { + "request_id": "req_pan_1234567890", + "credits_used": 1, + "credits_remaining": 999, + "source": "setu-pan" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` ## Response Format diff --git a/SETU_PAN_INTEGRATION.md b/SETU_PAN_INTEGRATION.md new file mode 100644 index 0000000..3f7f850 --- /dev/null +++ b/SETU_PAN_INTEGRATION.md @@ -0,0 +1,201 @@ +# Setu PAN Verification Integration Guide + +## Overview + +This document explains how the Setu PAN verification API integration works and how it's implemented in this codebase. + +## How Setu API Works + +### 1. API Endpoint +- **URL**: `https://dg-sandbox.setu.co/api/verify/pan` +- **Method**: `POST` +- **Content-Type**: `application/json` + +### 2. Authentication Headers +Setu API requires three custom headers for authentication: +- `x-client-id`: Your Setu client ID (UUID format) +- `x-client-secret`: Your Setu client secret +- `x-product-instance-id`: Your Setu product instance ID (UUID format) + +### 3. Request Body +```json +{ + "pan": "ABCDE1234A", + "consent": "Y", + "reason": "Reason for verifying PAN set by the developer" +} +``` + +**Fields:** +- `pan` (required): 10-character PAN number (format: 5 letters + 4 digits + 1 letter) +- `consent` (required): Must be "Y" to indicate user consent +- `reason` (required): Reason for verification + +### 4. Response Format +```json +{ + "id": "0e370877-f860-4b5c-858b-42aebfea4879", + "message": "PAN is valid.", + "data": { + "full_name": "Kumar Gaurav Rathod", + "first_name": "Gaurav", + "middle_name": "Kumar", + "last_name": "Rathod", + "category": "Individual or Person", + "aadhaar_seeding_status": "LINKED" + }, + "traceId": "1-69437d63-75ac46a51036ab2233854e23" +} +``` + +## Implementation Details + +### 1. Environment Variables +Add these to your `.env` file: +```env +PAN_PROVIDER_URL=https://dg-sandbox.setu.co/api/verify/pan +PAN_CLIENT_ID=292c6e76-dabf-49c4-8e48-90fba2916673 +PAN_CLIENT_SECRET=7IZMe9zvoBBuBukLiCP7n4KLwSOy11oP +PAN_PRODUCT_INSTANCE_ID=439244ff-114e-41a8-ae74-a783f160622d +``` + +### 2. Service Layer (`src/services/panService.js`) +The service: +- Validates PAN format (10 characters: 5 letters, 4 digits, 1 letter) +- Checks Redis cache first to avoid duplicate API calls +- Calls Setu API with proper headers and request body +- Formats the response data +- Stores the result in PostgreSQL database +- Caches the result for 24 hours + +### 3. Database Storage +All PAN verifications are stored in the `pan_verifications` table with the following fields: +- `pan`: The PAN number +- `full_name`, `first_name`, `middle_name`, `last_name`: Name components +- `category`: PAN category (Individual, Company, etc.) +- `aadhaar_seeding_status`: Aadhaar linking status +- `status`: VALID or INVALID +- `setu_response_id`: Setu's response ID +- `setu_trace_id`: Setu's trace ID for debugging +- `setu_message`: Message from Setu API +- `requested_by`: User ID who made the request +- `requested_at`: Timestamp of the request + +### 4. API Endpoint +**POST** `/v1/pan/verify` + +**Headers:** +``` +Content-Type: application/json +X-API-Key: your-api-key-here +``` + +**Request Body:** +```json +{ + "pan": "ABCDE1234A", + "reason": "KYC verification" // optional +} +``` + +**Response:** +```json +{ + "success": true, + "data": { + "pan": "ABCDE1234A", + "full_name": "Kumar Gaurav Rathod", + "first_name": "Gaurav", + "middle_name": "Kumar", + "last_name": "Rathod", + "category": "Individual or Person", + "aadhaar_seeding_status": "LINKED", + "status": "VALID", + "setu_response_id": "0e370877-f860-4b5c-858b-42aebfea4879", + "setu_trace_id": "1-69437d63-75ac46a51036ab2233854e23", + "setu_message": "PAN is valid." + }, + "meta": { + "request_id": "req_pan_1234567890", + "credits_used": 1, + "credits_remaining": 999, + "source": "setu-pan" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +## Getting Your API Key + +Before testing, you need an API key. The easiest way is to run: + +```bash +cd verify-india-api +npm run create-test-key +``` + +This will create and display a test API key. Copy it and use it in your requests. + +**Alternative:** Sign up for a new account: +```bash +curl -X POST http://localhost:3000/v1/auth/signup \ + -H "Content-Type: application/json" \ + -d '{ + "email": "your-email@example.com", + "password": "your-password-123", + "company_name": "Your Company" + }' +``` + +The response includes your `api_key` - save it immediately! + +**See `HOW_TO_GET_API_KEY.md` for detailed instructions.** + +## Testing with Postman + +1. **Set up the request:** + - Method: `POST` + - URL: `http://localhost:3000/v1/pan/verify` + - Headers: + - `Content-Type: application/json` + - `X-API-Key: your-api-key` (get it using the method above) + +2. **Request Body (JSON):** + ```json + { + "pan": "ABCDE1234A", + "reason": "Testing PAN verification" + } + ``` + +3. **Expected Response:** + - Status: `200 OK` + - Body: JSON with PAN verification details + +## Error Handling + +The implementation handles various error scenarios: +- **Invalid PAN format**: Returns 400 with `INVALID_PAN_FORMAT` error +- **Missing credentials**: Returns 500 with `CONFIGURATION_ERROR` +- **Setu API errors**: Returns the status code from Setu with appropriate error message +- **Timeout errors**: Returns 504 with `PROVIDER_TIMEOUT` +- **Network errors**: Returns 500 with `VERIFICATION_FAILED` + +## Key Features + +1. **Caching**: Results are cached in Redis for 24 hours to reduce API calls +2. **Database Persistence**: All verifications are stored in PostgreSQL +3. **Error Handling**: Comprehensive error handling with proper status codes +4. **Validation**: PAN format validation before making API calls +5. **Logging**: API calls are logged for analytics + +## Migration + +After updating the code, run the database migration to update the `pan_verifications` table: + +```bash +npm run migrate +``` + +This will add the new fields to store all Setu response data. + diff --git a/TEST_SETU_API_STEPS.md b/TEST_SETU_API_STEPS.md new file mode 100644 index 0000000..1ffaed6 --- /dev/null +++ b/TEST_SETU_API_STEPS.md @@ -0,0 +1,142 @@ +# Step-by-Step Guide: Testing Setu PAN API + +This guide will help you test the Setu PAN API with static/hardcoded values. + +## Prerequisites + +1. **Node.js installed** - Make sure you have Node.js installed on your system +2. **Dependencies installed** - All npm packages should be installed + +## Step-by-Step Instructions + +### Step 1: Navigate to the Project Directory + +Open your terminal/command prompt and navigate to the project directory: + +```bash +cd "C:\Users\front\Desktop\files 4\verify-india-api" +``` + +### Step 2: Verify Dependencies are Installed + +Check if `node_modules` folder exists. If not, install dependencies: + +```bash +npm install +``` + +This will install all required packages including `axios` which is needed for the API call. + +### Step 3: Run the Test Script + +You can run the test in two ways: + +#### Option A: Using npm script (Recommended) +```bash +npm run test-setu-static +``` + +#### Option B: Direct node command +```bash +node scripts/test-setu-api-static.js +``` + +### Step 4: Check the Results + +The script will: +- ✅ Show request details (URL, headers, body) +- ✅ Send the request to Setu API +- ✅ Display the response if successful +- ❌ Show error details if it fails + +## Expected Output + +### Success Case: +``` +🧪 Testing Setu PAN API with Static Values + +====================================================================== + +📋 Request Details: + URL: https://dg-sandbox.setu.co/api/verify/pan + Method: POST + ... + +🚀 Sending request to Setu API... + +✅ SUCCESS! API is working correctly! + +====================================================================== + +📊 Response Details: + Status Code: 200 OK + Response Time: 1234ms + ... + +✅ Test completed successfully! +``` + +### Error Case: +If there's an error, the script will show: +- Status code +- Error message +- Possible issues and solutions + +## What the Script Tests + +The script makes a POST request to Setu API with: +- **URL**: `https://dg-sandbox.setu.co/api/verify/pan` +- **Headers**: + - `Content-Type: application/json` + - `x-client-id: 292c6e76-dabf-49c4-8e48-90fba2916673` + - `x-client-secret: 7IZMe9zvoBBuBukLiCP7n4KLwSOy11oP` + - `x-product-instance-id: 439244ff-114e-41a8-ae74-a783f160622d` +- **Body**: + ```json + { + "pan": "ABCDE1234A", + "consent": "Y", + "reason": "Testing" + } + ``` + +## Troubleshooting + +### Error: "Cannot find module 'axios'" +**Solution**: Run `npm install` to install dependencies + +### Error: "Network Error" or "ECONNREFUSED" +**Solution**: +- Check your internet connection +- Verify the API URL is correct +- Check if there's a firewall blocking the request + +### Error: "401 Unauthorized" +**Solution**: +- Check if the client-id and client-secret are correct +- Verify the credentials haven't expired + +### Error: "403 Forbidden" +**Solution**: +- Check if the product-instance-id is correct +- Verify you have access to this product instance + +### Error: "400 Bad Request" +**Solution**: +- Check if the PAN format is correct (should be 10 characters: 5 letters, 4 digits, 1 letter) +- Verify the request body structure + +## Next Steps + +Once the test passes: +1. ✅ Your Setu API credentials are working +2. ✅ You can use these credentials in your `.env` file +3. ✅ Your application can make PAN verification requests + +## Notes + +- This script uses **static/hardcoded values** - perfect for testing +- The credentials are embedded in the script for testing purposes +- For production, always use environment variables (`.env` file) +- The script has a 30-second timeout for the API request + diff --git a/check-schema.js b/check-schema.js new file mode 100644 index 0000000..5178bb4 --- /dev/null +++ b/check-schema.js @@ -0,0 +1,23 @@ +const { query, connectDB } = require('./src/database/connection'); + +async function checkSchema() { + await connectDB(); + const result = await query( + `SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'pan_verifications' + ORDER BY ordinal_position` + ); + + console.log('Columns in pan_verifications table:'); + result.rows.forEach(col => { + console.log(` - ${col.column_name}: ${col.data_type}`); + }); + + process.exit(0); +} + +checkSchema().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/package.json b/package.json index 6b2aa8b..7a6909e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "migrate:down": "node src/database/migrate.js down", "migrate:status": "node src/database/migrate.js status", "migrate:pincodes": "node src/database/migrate-pincodes.js", - "create-test-key": "node scripts/create-test-api-key.js" + "create-test-key": "node scripts/create-test-api-key.js", + "create-live-key": "node scripts/create-live-api-key.js", + "check-pan-config": "node scripts/check-pan-config.js", + "test-pan-api": "node scripts/test-pan-api.js", + "test-setu-static": "node scripts/test-setu-api-static.js", + "test-localhost-pan": "node scripts/test-localhost-pan.js" }, "keywords": [ "api", diff --git a/scripts/check-pan-config.js b/scripts/check-pan-config.js new file mode 100644 index 0000000..830d7bf --- /dev/null +++ b/scripts/check-pan-config.js @@ -0,0 +1,57 @@ +/** + * Script to check if PAN API configuration is correct + * Usage: node scripts/check-pan-config.js + */ + +require('dotenv').config(); + +console.log('\n🔍 Checking PAN API Configuration...\n'); +console.log('=' .repeat(60)); + +const requiredVars = [ + 'PAN_PROVIDER_URL', + 'PAN_CLIENT_ID', + 'PAN_CLIENT_SECRET', + 'PAN_PRODUCT_INSTANCE_ID' +]; + +let allSet = true; + +requiredVars.forEach(varName => { + const value = process.env[varName]; + if (value) { + // Mask sensitive values + if (varName === 'PAN_CLIENT_SECRET') { + const masked = value.length > 10 + ? value.substring(0, 4) + '***' + value.substring(value.length - 4) + : '***'; + console.log(`✅ ${varName}: ${masked}`); + } else { + console.log(`✅ ${varName}: ${value}`); + } + } else { + console.log(`❌ ${varName}: NOT SET`); + allSet = false; + } +}); + +console.log('=' .repeat(60)); + +if (allSet) { + console.log('\n✅ All PAN API credentials are set!'); + console.log('\n⚠️ If you still get "Bad token" errors:'); + console.log(' 1. Double-check the credentials are correct'); + console.log(' 2. Make sure you restarted the server after adding .env variables'); + console.log(' 3. Verify the credentials in Setu dashboard\n'); +} else { + console.log('\n❌ Missing PAN API credentials!'); + console.log('\n📝 Add these to your .env file:'); + console.log(' PAN_PROVIDER_URL=https://dg-sandbox.setu.co/api/verify/pan'); + console.log(' PAN_CLIENT_ID=your-client-id-here'); + console.log(' PAN_CLIENT_SECRET=your-client-secret-here'); + console.log(' PAN_PRODUCT_INSTANCE_ID=your-product-instance-id-here'); + console.log('\n💡 After adding, restart your server!\n'); +} + +process.exit(allSet ? 0 : 1); + diff --git a/scripts/create-live-api-key.js b/scripts/create-live-api-key.js new file mode 100644 index 0000000..2f0cb13 --- /dev/null +++ b/scripts/create-live-api-key.js @@ -0,0 +1,68 @@ +/** + * Helper script to create a live API key for local development + * Usage: node scripts/create-live-api-key.js + */ + +require('dotenv').config(); +const crypto = require('crypto'); +const { query, connectDB } = require('../src/database/connection'); + +function generateApiKey(type = 'live') { + const prefix = type === 'test' ? 'vf_test_' : 'vf_live_'; + return prefix + crypto.randomBytes(24).toString('hex'); +} + +async function createLiveApiKey() { + try { + await connectDB(); + console.log('✅ Connected to database'); + + // Get a user to attach the key to. Prefer an existing user, otherwise create one. + let userRes = await query('SELECT id FROM users ORDER BY id LIMIT 1'); + let userId; + + if (userRes.rows.length === 0) { + console.log('No users found. Creating a default local user...'); + const bcrypt = require('bcryptjs'); + const passwordHash = await bcrypt.hash('localpass123', 10); + const userResult = await query( + `INSERT INTO users (email, password_hash, company_name, plan, monthly_quota, quota_reset_date, is_active) + VALUES ($1, $2, $3, $4, $5, DATE(NOW() + INTERVAL '1 month'), true) + RETURNING id`, + ['local@example.com', passwordHash, 'Local Dev', 'dev', 100000] + ); + userId = userResult.rows[0].id; + console.log('Created user id:', userId); + } else { + userId = userRes.rows[0].id; + console.log('Using existing user id:', userId); + } + + // Deactivate any existing live keys for that user (optional) + await query('UPDATE api_keys SET is_active = false WHERE user_id = $1 AND key_prefix = $2', [userId, 'vf_live_']); + + // Generate new live API key + const apiKey = generateApiKey('live'); + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + + await query( + `INSERT INTO api_keys (user_id, key_prefix, key_hash, key_hint, name, is_test_key, is_active) + VALUES ($1, $2, $3, $4, $5, false, true)`, + [userId, 'vf_live_', keyHash, apiKey.slice(-4), 'Local Live Key'] + ); + + console.log('\n✅ Live API Key Created Successfully!\n'); + console.log('Key:'); + console.log(apiKey); + console.log('\nUse this in your request header:'); + console.log('Header: x-api-key'); + console.log('Value:', apiKey); + process.exit(0); + } catch (error) { + console.error('❌ Error creating live API key:', error.message); + console.error(error); + process.exit(1); + } +} + +createLiveApiKey(); diff --git a/scripts/test-localhost-pan.js b/scripts/test-localhost-pan.js new file mode 100644 index 0000000..32df11e --- /dev/null +++ b/scripts/test-localhost-pan.js @@ -0,0 +1,296 @@ +/** + * Test script to verify PAN API through localhost + * This script tests the Setu API integration through your local API server + * Usage: node scripts/test-localhost-pan.js + */ + +require('dotenv').config(); +const axios = require('axios'); +const crypto = require('crypto'); +const { query, connectDB } = require('../src/database/connection'); + +// Static Setu API credentials (from your curl command) +const SETU_CREDENTIALS = { + PAN_PROVIDER_URL: 'https://dg-sandbox.setu.co/api/verify/pan', + PAN_CLIENT_ID: '292c6e76-dabf-49c4-8e48-90fba2916673', + PAN_CLIENT_SECRET: '7IZMe9zvoBBuBukLiCP7n4KLwSOy11oP', + PAN_PRODUCT_INSTANCE_ID: '439244ff-114e-41a8-ae74-a783f160622d' +}; + +const LOCALHOST_URL = process.env.LOCALHOST_URL || 'http://localhost:3000'; +const TEST_PAN = 'ABCDE1234A'; + +// Function to generate API key +function generateApiKey(type = 'test') { + const prefix = type === 'test' ? 'vf_test_' : 'vf_live_'; + return prefix + crypto.randomBytes(24).toString('hex'); +} + +// Function to create or get test API key +async function getOrCreateTestApiKey() { + try { + await connectDB(); + + // Check if test user exists + let testUser = await query( + 'SELECT * FROM users WHERE email = $1', + ['test@example.com'] + ); + + let userId; + + if (testUser.rows.length === 0) { + // Create test user + const bcrypt = require('bcryptjs'); + const passwordHash = await bcrypt.hash('testpassword123', 10); + + const userResult = await query( + `INSERT INTO users (email, password_hash, company_name, plan, monthly_quota, quota_reset_date, is_active) + VALUES ($1, $2, $3, $4, $5, DATE(NOW() + INTERVAL '1 month'), true) + RETURNING id, email, plan`, + ['test@example.com', passwordHash, 'Test Company', 'free', 10000] + ); + + userId = userResult.rows[0].id; + } else { + userId = testUser.rows[0].id; + } + + // Check for existing active test API key + const existingKeys = await query( + `SELECT ak.* FROM api_keys ak + WHERE ak.user_id = $1 AND ak.is_test_key = true AND ak.is_active = true + ORDER BY ak.created_at DESC + LIMIT 1`, + [userId] + ); + + if (existingKeys.rows.length > 0) { + // We can't retrieve the original key, so we'll need to create a new one + // But first, let's try to use the existing one by checking if we can authenticate + // For now, let's create a new one to be safe + console.log(' ℹ️ Found existing test API key, creating a new one...'); + + // Deactivate old keys + await query( + 'UPDATE api_keys SET is_active = false WHERE user_id = $1 AND is_test_key = true', + [userId] + ); + } + + // Generate new API key + const apiKey = generateApiKey('test'); + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + + await query( + `INSERT INTO api_keys (user_id, key_prefix, key_hash, key_hint, name, is_test_key, is_active) + VALUES ($1, $2, $3, $4, $5, true, true)`, + [userId, 'vf_test_', keyHash, apiKey.slice(-4), 'Test Key'] + ); + + return apiKey; + } catch (error) { + console.error(' ❌ Error creating API key:', error.message); + throw error; + } +} + +// Function to check if server is running +async function checkServerRunning() { + try { + const response = await axios.get(`${LOCALHOST_URL}/health`, { + timeout: 3000 + }); + return response.status === 200; + } catch (error) { + return false; + } +} + +// Function to update .env file with Setu credentials +async function updateEnvFile() { + const fs = require('fs'); + const path = require('path'); + const envPath = path.join(__dirname, '..', '.env'); + + try { + let envContent = ''; + + // Read existing .env if it exists + if (fs.existsSync(envPath)) { + envContent = fs.readFileSync(envPath, 'utf8'); + } + + // Update or add Setu credentials + const updates = { + 'PAN_PROVIDER_URL': SETU_CREDENTIALS.PAN_PROVIDER_URL, + 'PAN_CLIENT_ID': SETU_CREDENTIALS.PAN_CLIENT_ID, + 'PAN_CLIENT_SECRET': SETU_CREDENTIALS.PAN_CLIENT_SECRET, + 'PAN_PRODUCT_INSTANCE_ID': SETU_CREDENTIALS.PAN_PRODUCT_INSTANCE_ID + }; + + let updated = false; + for (const [key, value] of Object.entries(updates)) { + const regex = new RegExp(`^${key}=.*$`, 'm'); + if (regex.test(envContent)) { + envContent = envContent.replace(regex, `${key}=${value}`); + updated = true; + } else { + // Add new line if file doesn't end with newline + if (envContent && !envContent.endsWith('\n')) { + envContent += '\n'; + } + envContent += `${key}=${value}\n`; + updated = true; + } + } + + if (updated) { + fs.writeFileSync(envPath, envContent, 'utf8'); + console.log(' ✅ Updated .env file with Setu credentials'); + return true; + } + + return false; + } catch (error) { + console.log(' ⚠️ Could not update .env file:', error.message); + console.log(' ℹ️ Please manually add these to your .env file:'); + console.log(` PAN_PROVIDER_URL=${SETU_CREDENTIALS.PAN_PROVIDER_URL}`); + console.log(` PAN_CLIENT_ID=${SETU_CREDENTIALS.PAN_CLIENT_ID}`); + console.log(` PAN_CLIENT_SECRET=${SETU_CREDENTIALS.PAN_CLIENT_SECRET}`); + console.log(` PAN_PRODUCT_INSTANCE_ID=${SETU_CREDENTIALS.PAN_PRODUCT_INSTANCE_ID}`); + return false; + } +} + +// Main test function +async function testLocalhostPAN() { + console.log('\n🧪 Testing PAN API through Localhost\n'); + console.log('='.repeat(70)); + + // Step 1: Check server is running + console.log('\n📡 Step 1: Checking if server is running...'); + const serverRunning = await checkServerRunning(); + + if (!serverRunning) { + console.log(' ❌ Server is not running!'); + console.log('\n Please start the server first:'); + console.log(' npm start'); + console.log(' or'); + console.log(' npm run dev'); + process.exit(1); + } + console.log(' ✅ Server is running on', LOCALHOST_URL); + + // Step 2: Update .env file with Setu credentials + console.log('\n⚙️ Step 2: Setting up Setu API credentials...'); + const envUpdated = await updateEnvFile(); + if (envUpdated) { + console.log(' ⚠️ Note: If your server was already running, restart it to load new credentials'); + console.log(' Restart command: Press Ctrl+C and run "npm start" again'); + } + + // Step 3: Create/get API key + console.log('\n🔑 Step 3: Creating test API key...'); + let apiKey; + try { + apiKey = await getOrCreateTestApiKey(); + console.log(' ✅ Test API key created:', apiKey); + } catch (error) { + console.log(' ❌ Failed to create API key:', error.message); + console.log(' Make sure your database is running and migrations are applied'); + process.exit(1); + } + + // Step 4: Test the API endpoint + console.log('\n🚀 Step 4: Testing PAN verification endpoint...'); + console.log(` URL: ${LOCALHOST_URL}/v1/pan/verify`); + console.log(` PAN: ${TEST_PAN}`); + + try { + const startTime = Date.now(); + + const response = await axios.post( + `${LOCALHOST_URL}/v1/pan/verify`, + { + pan: TEST_PAN, + consent: 'Y', + reason: 'Testing' + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey + }, + timeout: 30000 + } + ); + + const endTime = Date.now(); + const duration = endTime - startTime; + + console.log('\n ✅ SUCCESS! API is working correctly!\n'); + console.log('='.repeat(70)); + console.log('\n📊 Response Details:'); + console.log(` Status Code: ${response.status} ${response.statusText}`); + console.log(` Response Time: ${duration}ms`); + console.log(`\n📦 Response Body:`); + console.log(JSON.stringify(response.data, null, 2)); + + if (response.data.success && response.data.data) { + console.log('\n✅ PAN Verification Successful!'); + if (response.data.data.full_name) { + console.log(` PAN Holder: ${response.data.data.full_name}`); + } + if (response.data.data.status) { + console.log(` Status: ${response.data.data.status}`); + } + } + + console.log('\n' + '='.repeat(70)); + console.log('\n✅ All tests passed! Your localhost API is working correctly!\n'); + + } catch (error) { + console.log('\n ❌ ERROR! API request failed!\n'); + console.log('='.repeat(70)); + + if (error.response) { + console.log('\n📊 Error Response:'); + console.log(` Status Code: ${error.response.status} ${error.response.statusText}`); + console.log(`\n📦 Error Body:`); + console.log(JSON.stringify(error.response.data, null, 2)); + + console.log('\n⚠️ Possible Issues:'); + if (error.response.status === 401) { + console.log(' - Invalid API key (try running this script again to create a new key)'); + } else if (error.response.status === 500) { + console.log(' - Server error (check server logs)'); + console.log(' - Make sure Setu credentials are in .env file'); + console.log(' - Restart the server after updating .env'); + } else if (error.response.status === 400) { + console.log(' - Bad request (check PAN format)'); + } + } else if (error.request) { + console.log('\n⚠️ No response received from server'); + console.log(` Error: ${error.message}`); + console.log('\n Possible Issues:'); + console.log(' - Server is not running'); + console.log(' - Wrong URL (check LOCALHOST_URL)'); + console.log(' - Network connectivity problem'); + } else { + console.log(`\n⚠️ Request setup error: ${error.message}`); + } + + console.log('\n' + '='.repeat(70)); + console.log('\n❌ Test failed!\n'); + process.exit(1); + } +} + +// Run the test +testLocalhostPAN().catch(error => { + console.error('\n❌ Unexpected error:', error.message); + console.error(error); + process.exit(1); +}); + diff --git a/scripts/test-pan-api.js b/scripts/test-pan-api.js new file mode 100644 index 0000000..00dfb9d --- /dev/null +++ b/scripts/test-pan-api.js @@ -0,0 +1,103 @@ +/** + * Test script to verify PAN API is working correctly + * Usage: node scripts/test-pan-api.js + */ + +require('dotenv').config(); +const axios = require('axios'); + +async function testPanAPI() { + console.log('\n🧪 Testing PAN API Configuration...\n'); + console.log('=' .repeat(60)); + + // Check environment variables + console.log('\n1. Checking Environment Variables:'); + const requiredVars = { + 'PAN_PROVIDER_URL': process.env.PAN_PROVIDER_URL, + 'PAN_CLIENT_ID': process.env.PAN_CLIENT_ID, + 'PAN_CLIENT_SECRET': process.env.PAN_CLIENT_SECRET, + 'PAN_PRODUCT_INSTANCE_ID': process.env.PAN_PRODUCT_INSTANCE_ID + }; + + let allSet = true; + for (const [key, value] of Object.entries(requiredVars)) { + if (value) { + const masked = key === 'PAN_CLIENT_SECRET' && value.length > 10 + ? value.substring(0, 4) + '***' + value.substring(value.length - 4) + : value; + console.log(` ✅ ${key}: ${masked}`); + } else { + console.log(` ❌ ${key}: NOT SET`); + allSet = false; + } + } + + if (!allSet) { + console.log('\n❌ Missing environment variables! Please check your .env file.\n'); + process.exit(1); + } + + // Test Setu API directly + console.log('\n2. Testing Setu API Directly:'); + try { + const setuResponse = await axios.post( + process.env.PAN_PROVIDER_URL, + { + pan: 'ABCDE1234A', + consent: 'Y', + reason: 'Testing PAN verification' + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-client-id': process.env.PAN_CLIENT_ID, + 'x-client-secret': process.env.PAN_CLIENT_SECRET, + 'x-product-instance-id': process.env.PAN_PRODUCT_INSTANCE_ID + }, + timeout: 30000 + } + ); + + if (setuResponse.status === 200 && setuResponse.data) { + console.log(' ✅ Setu API is working!'); + console.log(` Response: ${setuResponse.data.message || 'Success'}`); + } else { + console.log(' ⚠️ Setu API returned unexpected response'); + console.log(` Status: ${setuResponse.status}`); + } + } catch (error) { + console.log(' ❌ Setu API test failed!'); + if (error.response) { + console.log(` Status: ${error.response.status}`); + console.log(` Error: ${error.response.data?.message || error.response.data?.error || 'Unknown error'}`); + } else { + console.log(` Error: ${error.message}`); + } + console.log('\n⚠️ Your Setu credentials may be incorrect or the API is down.\n'); + process.exit(1); + } + + // Test local API (if server is running) + console.log('\n3. Testing Local API (make sure server is running on port 3000):'); + try { + // First, get a test API key + console.log(' ℹ️ You need an API key to test the local endpoint.'); + console.log(' Run: npm run create-test-key'); + console.log(' Then test with:'); + console.log(' curl -X POST http://localhost:3000/v1/pan/verify \\'); + console.log(' -H "Content-Type: application/json" \\'); + console.log(' -H "x-api-key: YOUR_API_KEY" \\'); + console.log(' -d \'{"pan": "ABCDE1234A", "reason": "Testing"}\''); + } catch (error) { + console.log(' ⚠️ Could not test local API:', error.message); + } + + console.log('\n' + '=' .repeat(60)); + console.log('\n✅ Configuration check complete!\n'); +} + +testPanAPI().catch(error => { + console.error('\n❌ Test failed:', error.message); + process.exit(1); +}); + diff --git a/scripts/test-setu-api-static.js b/scripts/test-setu-api-static.js new file mode 100644 index 0000000..272e170 --- /dev/null +++ b/scripts/test-setu-api-static.js @@ -0,0 +1,126 @@ +/** + * Static test script to verify Setu PAN API is working + * This script uses hardcoded values from the curl command + * Usage: node scripts/test-setu-api-static.js + */ + +const axios = require('axios'); + +// Static values from the curl command +const SETU_API_URL = 'https://dg-sandbox.setu.co/api/verify/pan'; +const CLIENT_ID = '292c6e76-dabf-49c4-8e48-90fba2916673'; +const CLIENT_SECRET = '7IZMe9zvoBBuBukLiCP7n4KLwSOy11oP'; +const PRODUCT_INSTANCE_ID = '439244ff-114e-41a8-ae74-a783f160622d'; + +const REQUEST_BODY = { + pan: 'ABCDE1234A', + consent: 'Y', + reason: 'Testing' +}; + +async function testSetuAPI() { + console.log('\n🧪 Testing Setu PAN API with Static Values\n'); + console.log('='.repeat(70)); + + console.log('\n📋 Request Details:'); + console.log(` URL: ${SETU_API_URL}`); + console.log(` Method: POST`); + console.log(` Headers:`); + console.log(` - Content-Type: application/json`); + console.log(` - x-client-id: ${CLIENT_ID}`); + console.log(` - x-client-secret: ${CLIENT_SECRET.substring(0, 8)}...`); + console.log(` - x-product-instance-id: ${PRODUCT_INSTANCE_ID}`); + console.log(` Body:`, JSON.stringify(REQUEST_BODY, null, 2)); + + console.log('\n🚀 Sending request to Setu API...\n'); + + try { + const startTime = Date.now(); + + const response = await axios.post( + SETU_API_URL, + REQUEST_BODY, + { + headers: { + 'Content-Type': 'application/json', + 'x-client-id': CLIENT_ID, + 'x-client-secret': CLIENT_SECRET, + 'x-product-instance-id': PRODUCT_INSTANCE_ID + }, + timeout: 30000 // 30 seconds + } + ); + + const endTime = Date.now(); + const duration = endTime - startTime; + + console.log('✅ SUCCESS! API is working correctly!\n'); + console.log('='.repeat(70)); + console.log('\n📊 Response Details:'); + console.log(` Status Code: ${response.status} ${response.statusText}`); + console.log(` Response Time: ${duration}ms`); + console.log(` Response Headers:`, JSON.stringify(response.headers, null, 2)); + console.log(`\n📦 Response Body:`); + console.log(JSON.stringify(response.data, null, 2)); + + // Check if response has expected structure + if (response.data && response.data.data) { + console.log('\n✅ Response structure is valid!'); + if (response.data.data.full_name) { + console.log(` PAN Holder Name: ${response.data.data.full_name}`); + } + if (response.data.message) { + console.log(` Message: ${response.data.message}`); + } + } + + console.log('\n' + '='.repeat(70)); + console.log('\n✅ Test completed successfully!\n'); + + } catch (error) { + console.log('❌ ERROR! API request failed!\n'); + console.log('='.repeat(70)); + + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.log('\n📊 Error Response Details:'); + console.log(` Status Code: ${error.response.status} ${error.response.statusText}`); + console.log(` Response Headers:`, JSON.stringify(error.response.headers, null, 2)); + console.log(`\n📦 Error Response Body:`); + console.log(JSON.stringify(error.response.data, null, 2)); + + console.log('\n⚠️ Possible Issues:'); + if (error.response.status === 401) { + console.log(' - Invalid credentials (client-id or client-secret)'); + } else if (error.response.status === 403) { + console.log(' - Access forbidden (check product-instance-id)'); + } else if (error.response.status === 400) { + console.log(' - Bad request (check PAN format or request body)'); + } else if (error.response.status === 404) { + console.log(' - Endpoint not found (check API URL)'); + } else if (error.response.status >= 500) { + console.log(' - Server error (Setu API might be down)'); + } + } else if (error.request) { + // The request was made but no response was received + console.log('\n⚠️ No response received from server'); + console.log(` Error: ${error.message}`); + console.log('\n Possible Issues:'); + console.log(' - Network connectivity problem'); + console.log(' - API endpoint is unreachable'); + console.log(' - Request timeout (took longer than 30 seconds)'); + } else { + // Something happened in setting up the request that triggered an Error + console.log(`\n⚠️ Request setup error: ${error.message}`); + } + + console.log('\n' + '='.repeat(70)); + console.log('\n❌ Test failed!\n'); + process.exit(1); + } +} + +// Run the test +testSetuAPI(); + diff --git a/src/cache/redis.js b/src/cache/redis.js index feb8a2a..5b34660 100644 --- a/src/cache/redis.js +++ b/src/cache/redis.js @@ -34,41 +34,41 @@ function isDummyCache() { } async function cacheGet(key) { - if (useDummy) { - return dummyCache.has(key) ? dummyCache.get(key) : null; - } - if (!redisClient) return null; + if (useDummy) { + return dummyCache.has(key) ? dummyCache.get(key) : null; + } + if (!redisClient) return null; - const data = await redisClient.get(key); - if (!data) return null; + const data = await redisClient.get(key); + if (!data) return null; - try { - return JSON.parse(data); - } catch (err) { - console.error('Redis parse error:', err.message); - return null; - } + try { + return JSON.parse(data); + } catch (err) { + console.error('Redis parse error:', err.message); + return null; + } } async function cacheSet(key, value, expirySeconds = 3600) { - if (useDummy) { - dummyCache.set(key, value); + if (useDummy) { + dummyCache.set(key, value); - if (dummyExpiryTimers.has(key)) { - clearTimeout(dummyExpiryTimers.get(key)); + if (dummyExpiryTimers.has(key)) { + clearTimeout(dummyExpiryTimers.get(key)); + } + const timer = setTimeout(() => { + dummyCache.delete(key); + dummyExpiryTimers.delete(key); + }, expirySeconds * 1000); + dummyExpiryTimers.set(key, timer); + return true; } - const timer = setTimeout(() => { - dummyCache.delete(key); - dummyExpiryTimers.delete(key); - }, expirySeconds * 1000); - dummyExpiryTimers.set(key, timer); + + if (!redisClient) return false; + + await redisClient.setEx(key, expirySeconds, JSON.stringify(value)); return true; - } - - if (!redisClient) return false; - - await redisClient.setEx(key, expirySeconds, JSON.stringify(value)); - return true; } async function cacheDelete(key) { diff --git a/src/database/migrations/20251217030000_add_pan_bank_verifications.js b/src/database/migrations/20251217030000_add_pan_bank_verifications.js index aa41a62..8ab5fe3 100644 --- a/src/database/migrations/20251217030000_add_pan_bank_verifications.js +++ b/src/database/migrations/20251217030000_add_pan_bank_verifications.js @@ -3,11 +3,19 @@ const panBankSchema = ` CREATE TABLE IF NOT EXISTS pan_verifications ( id SERIAL PRIMARY KEY, pan VARCHAR(10) NOT NULL, - name VARCHAR(255), + full_name VARCHAR(255), + first_name VARCHAR(255), + middle_name VARCHAR(255), + last_name VARCHAR(255), + category VARCHAR(100), + aadhaar_seeding_status VARCHAR(50), status VARCHAR(50), pan_type VARCHAR(50), name_match BOOLEAN, name_match_score INTEGER, + setu_response_id VARCHAR(255), + setu_trace_id VARCHAR(255), + setu_message TEXT, requested_by INTEGER REFERENCES users(id), requested_at TIMESTAMP DEFAULT NOW() ); diff --git a/src/index.js b/src/index.js index f530add..72f7fd0 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ const ifscRoutes = require('./routes/ifsc'); const pincodeRoutes = require('./routes/pincode'); const gstRoutes = require('./routes/gst'); const panRoutes = require('./routes/pan'); +console.log('✅ PAN routes loaded:', typeof panRoutes); const bankRoutes = require('./routes/bank'); const userRoutes = require('./routes/user'); @@ -23,7 +24,31 @@ const app = express(); app.use(helmet()); app.use(cors()); -app.use(express.json()); + +// JSON body parser with error handling +app.use(express.json({ + limit: '10mb', + verify: (req, res, buf) => { + try { + JSON.parse(buf); + } catch (e) { + console.error('JSON parse error:', e.message); + res.status(400).json({ + success: false, + error: { code: 'INVALID_JSON', message: 'Invalid JSON in request body' } + }); + } + } +})); + +// Debug middleware to log all requests +app.use((req, res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`); + console.log('Headers:', req.headers); + console.log('Body:', req.body); + next(); +}); + app.use(morgan('combined')); app.get('/health', (req, res) => { @@ -49,7 +74,18 @@ app.use('/v1/user', userRoutes); app.use('/v1/ifsc', ifscRoutes); app.use('/v1/pincode', pincodeRoutes); app.use('/v1/gst', gstRoutes); + +// Test route to verify routing works (must be BEFORE panRoutes) +app.get('/v1/pan/test', (req, res) => { + console.log('PAN test route hit!'); + res.json({ message: 'PAN route is working', timestamp: new Date().toISOString() }); +}); + +// Log all registered routes for debugging +console.log('Registering PAN routes at /v1/pan'); app.use('/v1/pan', panRoutes); +console.log('PAN routes registered'); + app.use('/v1/bank', bankRoutes); app.use('*', (req, res) => { @@ -68,8 +104,19 @@ async function startServer() { try { await connectDB(); console.log('✅ PostgreSQL connected'); - // await connectRedis(); - // console.log('✅ Redis connected', isDummyCache() ? 'using dummy cache' : 'using real cache'); + + // // Try to connect Redis, but don't fail if it's not available + // try { + // const { connectRedis, isDummyCache } = require('./cache/redis'); + // await connectRedis(); + // if (isDummyCache()) { + // console.log('📦 Using in-memory cache (Redis not available)'); + // } else { + // console.log('✅ Redis connected'); + // } + // } catch (redisError) { + // console.log('📦 Using in-memory cache (Redis connection failed)'); + // } app.listen(PORT, () => { console.log(`✅ Server running on port ${PORT}`); @@ -81,7 +128,7 @@ async function startServer() { } startServer(); - + diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 1e94c74..78f454e 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -1,6 +1,5 @@ const crypto = require('crypto'); const { query } = require('../database/connection'); -const { cacheGet, cacheSet } = require('../cache/redis'); async function authenticateApiKey(req, res, next) { try { @@ -13,38 +12,36 @@ async function authenticateApiKey(req, res, next) { }); } - if (!apiKey.startsWith('vf_live_') && !apiKey.startsWith('vf_test_')) { + const prefix = process.env.API_KEY_PREFIX || 'vf_live_'; + const testPrefix = 'vf_test_'; // Keep test prefix constant or add to env if needed + + if (!apiKey.startsWith(prefix) && !apiKey.startsWith(testPrefix)) { return res.status(401).json({ success: false, error: { code: 'INVALID_API_KEY_FORMAT', message: 'Invalid API key format' } }); } - const cacheKey = `apikey:${apiKey}`; - let keyData = await cacheGet(cacheKey); + // Hash the API key and query database directly + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); - if (!keyData) { - const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + const result = await query( + `SELECT ak.*, u.plan, u.monthly_quota, u.calls_this_month, u.is_active as user_active + FROM api_keys ak + JOIN users u ON ak.user_id = u.id + WHERE ak.key_hash = $1 AND ak.is_active = true`, + [keyHash] + ); - const result = await query( - `SELECT ak.*, u.plan, u.monthly_quota, u.calls_this_month, u.is_active as user_active - FROM api_keys ak - JOIN users u ON ak.user_id = u.id - WHERE ak.key_hash = $1 AND ak.is_active = true`, - [keyHash] - ); - - if (result.rows.length === 0) { - return res.status(401).json({ - success: false, - error: { code: 'INVALID_API_KEY', message: 'Invalid or inactive API key' } - }); - } - - keyData = result.rows[0]; - await cacheSet(cacheKey, keyData, 300); + if (result.rows.length === 0) { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_API_KEY', message: 'Invalid or inactive API key' } + }); } + const keyData = result.rows[0]; + if (!keyData.user_active) { return res.status(403).json({ success: false, diff --git a/src/routes/gst.js b/src/routes/gst.js index 71c0bac..3caaf1c 100644 --- a/src/routes/gst.js +++ b/src/routes/gst.js @@ -8,13 +8,24 @@ const { logApiCall } = require('../services/analytics'); router.use(authenticateApiKey); router.use(rateLimit); -router.get('/verify/:gstin', async (req, res, next) => { + + +// POST /v1/gst/verify { gstin } +router.post('/verify', async (req, res, next) => { const startTime = Date.now(); let success = false; try { - const { gstin } = req.params; - const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/; + const { gstin } = req.body; + // Relaxed regex to allow any alphanumeric char at 14th position (instead of strict Z) + const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[A-Z0-9]{1}[0-9A-Z]{1}$/; + + if (!gstin) { + return res.status(400).json({ + success: false, + error: { code: 'MISSING_GSTIN', message: 'GSTIN is required' } + }); + } if (!gstinRegex.test(gstin.toUpperCase())) { return res.status(400).json({ @@ -41,7 +52,63 @@ router.get('/verify/:gstin', async (req, res, next) => { request_id: `req_gst_${Date.now()}`, credits_used: 1, credits_remaining: req.user.remaining - 1, - source: 'gstn' + source: 'setu-gst' + } + }); + + } catch (error) { + next(error); + } finally { + await logApiCall({ + userId: req.user.id, + apiKeyId: req.user.apiKeyId, + endpoint: '/v1/gst/verify', + method: 'POST', + params: { gstin: req.body.gstin }, + status: success ? 200 : 500, + duration: Date.now() - startTime, + success, + isTestKey: req.user.isTestKey + }); + } +}); + +// GET /v1/gst/verify/:gstin +router.get('/verify/:gstin', async (req, res, next) => { + const startTime = Date.now(); + let success = false; + + try { + const { gstin } = req.params; + // Relaxed regex to allow any alphanumeric char at 14th position (instead of strict Z) + const gstinRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[A-Z0-9]{1}[0-9A-Z]{1}$/; + + if (!gstinRegex.test(gstin.toUpperCase())) { + return res.status(400).json({ + success: false, + error: { code: 'INVALID_GSTIN', message: 'Invalid GSTIN format' } + }); + } + + const result = await verifyGSTIN(gstin.toUpperCase()); + + if (!result.success) { + return res.status(result.statusCode || 404).json({ + success: false, + error: { code: result.errorCode, message: result.message } + }); + } + + success = true; + + res.json({ + success: true, + data: result.data, + meta: { + request_id: `req_gst_${Date.now()}`, + credits_used: 1, + credits_remaining: req.user.remaining - 1, + source: 'setu-gst' } }); diff --git a/src/routes/pan.js b/src/routes/pan.js index 916fdb3..5d26a68 100644 --- a/src/routes/pan.js +++ b/src/routes/pan.js @@ -5,67 +5,94 @@ const { rateLimit } = require('../middleware/rateLimit'); const { verifyPAN } = require('../services/panService'); const { logApiCall } = require('../services/analytics'); +// Health check endpoint (no auth required) +router.get('/health', (req, res) => { + res.json({ + success: true, + service: 'PAN Verification', + provider: 'Setu', + timestamp: new Date().toISOString() + }); +}); + +// Apply authentication and rate limiting middleware to all other routes router.use(authenticateApiKey); router.use(rateLimit); +/** + * PAN Verification Routes + * POST /v1/pan/verify - Verify a PAN number using Setu API + */ + +/** + * @route POST /v1/pan/verify + * @desc Verify PAN number + * @body {string} pan - PAN number to verify (required) + * @body {string} reason - Reason for verification (optional) + * @returns {Object} Verification result + */ router.post('/verify', async (req, res, next) => { const startTime = Date.now(); let success = false; try { - const { pan, name, dob } = req.body; - const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/; + const { pan, reason } = req.body; + // Validate PAN is provided if (!pan) { return res.status(400).json({ success: false, - error: { code: 'MISSING_PAN', message: 'PAN is required' } + error: { + code: 'MISSING_PAN', + message: 'PAN number is required. Please provide a valid PAN in the request body.' + } }); } - if (!panRegex.test(pan.toUpperCase())) { - return res.status(400).json({ - success: false, - error: { code: 'INVALID_PAN', message: 'Invalid PAN format' } - }); - } - - const result = await verifyPAN(pan.toUpperCase(), name, dob); + // Call verification service + const result = await verifyPAN(pan, reason, req.user?.id); + // Return appropriate response based on result if (!result.success) { - return res.status(result.statusCode || 404).json({ + // If result has Setu-formatted data, return it directly + if (result.data && result.data.verification) { + return res.status(result.statusCode || 500).json(result.data); + } + + // Otherwise return standard error format + return res.status(result.statusCode || 500).json({ success: false, - error: { code: result.errorCode, message: result.message } + error: { + code: result.errorCode || 'VERIFICATION_ERROR', + message: result.message || 'PAN verification failed' + } }); } success = true; - res.json({ - success: true, - data: result.data, - meta: { - request_id: `req_pan_${Date.now()}`, - credits_used: 1, - credits_remaining: req.user.remaining - 1 - } - }); + // Return Setu-formatted response directly + return res.status(200).json(result.data); } catch (error) { + console.error('[PAN Route] Unexpected error:', error); next(error); } finally { + // Log API call await logApiCall({ userId: req.user.id, apiKeyId: req.user.apiKeyId, endpoint: '/v1/pan/verify', method: 'POST', - params: { pan: req.body.pan }, + params: { pan: req.body.pan, reason: req.body.reason }, status: success ? 200 : 500, duration: Date.now() - startTime, success, - isTestKey: req.user.isTestKey + isTestKey: req.user.isTestKey, + errorMessage: success ? null : 'PAN verification failed' }); } }); + module.exports = router; diff --git a/src/services/bankService.js b/src/services/bankService.js index 33cb123..b280bdf 100644 --- a/src/services/bankService.js +++ b/src/services/bankService.js @@ -1,83 +1,76 @@ const axios = require('axios'); -axios.defaults.headers.common['Authorization'] = `Bearer ${process.env.BANK_PROVIDER_KEY}`; -axios.defaults.headers.post['Content-Type'] = 'application/json'; +// Create a dedicated axios instance for bank service to avoid polluting global axios +// which was causing authentication issues with PAN/GST services +const bankAxiosInstance = axios.create({ + headers: { + common: { + 'Authorization': `Bearer ${process.env.BANK_PROVIDER_KEY || ''}` + }, + post: { + 'Content-Type': 'application/json' + } + } +}); const { cacheGet, cacheSet } = require('../cache/redis'); const { query } = require('../database/connection'); async function verifyBankAccount(accountNumber, ifsc, name = null) { - try { - const cacheKey = `bank:${ifsc}:${accountNumber}`; - const cached = await cacheGet(cacheKey); - if (cached) { - if (name) { - cached.name_match = cached.name_at_bank === name.toUpperCase(); - cached.name_match_score = cached.name_match ? 100 : 0; - } - return { success: true, data: cached }; - } - - // Get bank details from IFSC - const ifscResult = await query('SELECT bank_name, branch FROM ifsc_codes WHERE ifsc = $1', [ifsc.toUpperCase()]); - - if (ifscResult.rows.length === 0) { - return { success: false, statusCode: 400, errorCode: 'INVALID_IFSC', message: 'IFSC not found' }; - } - - const bankInfo = ifscResult.rows[0]; - - // Temporarily mock external bank provider response for testing - const mockResponseData = { - status_code: 200, - data: { - account_exists: true, - name_at_bank: name || "DUMMY ACCOUNT HOLDER", - account_holder_name: name || "DUMMY ACCOUNT HOLDER", - branch: bankInfo.branch - } - }; - const response = { data: mockResponseData }; - - // Commenting out the actual external API call for now - // const response = await axios.post( - // process.env.BANK_PROVIDER_URL, - // { - // account_number: accountNumber, - // ifsc: ifsc.toUpperCase(), - // name: name - // }, - // { - // headers: { - // 'Authorization': `Bearer ${process.env.BANK_PROVIDER_KEY}`, - // 'Content-Type': 'application/json' - // }, - // timeout: 30000 - // } - // ); - - if (!response.data || response.data.status_code !== 200) { - return { success: false, statusCode: 404, errorCode: 'ACCOUNT_NOT_FOUND', message: 'Account not found' }; - } - - const d = response.data.data; - - const data = { - account_number: accountNumber, - ifsc: ifsc.toUpperCase(), - account_exists: d.account_exists !== false, - name_at_bank: d.name_at_bank || d.account_holder_name || '', - name_match: name ? (d.name_at_bank || d.account_holder_name || '').toUpperCase() === name.toUpperCase() : null, - name_match_score: name ? (d.name_at_bank || d.account_holder_name || '').toUpperCase() === name.toUpperCase() ? 100 : 0 : null, - bank_name: bankInfo.bank_name, - branch: bankInfo.branch || d.branch || '' - }; - - await cacheSet(cacheKey, data, 86400); - - // Persist bank verification result to Postgres (best-effort, non-blocking) try { - await query( - `INSERT INTO bank_verifications ( + const cacheKey = `bank:${ifsc}:${accountNumber}`; + const cached = await cacheGet(cacheKey); + if (cached) { + if (name) { + cached.name_match = cached.name_at_bank === name.toUpperCase(); + cached.name_match_score = cached.name_match ? 100 : 0; + } + return { success: true, data: cached }; + } + + // Get bank details from IFSC + const ifscResult = await query('SELECT bank_name, branch FROM ifsc_codes WHERE ifsc = $1', [ifsc.toUpperCase()]); + + if (ifscResult.rows.length === 0) { + return { success: false, statusCode: 400, errorCode: 'INVALID_IFSC', message: 'IFSC not found' }; + } + + const bankInfo = ifscResult.rows[0]; + + // Temporarily mock external bank provider response for testing + const mockResponseData = { + status_code: 200, + data: { + account_exists: true, + name_at_bank: name || "DUMMY ACCOUNT HOLDER", + account_holder_name: name || "DUMMY ACCOUNT HOLDER", + branch: bankInfo.branch + } + }; + const response = { data: mockResponseData }; + + if (!response.data || response.data.status_code !== 200) { + return { success: false, statusCode: 404, errorCode: 'ACCOUNT_NOT_FOUND', message: 'Account not found' }; + } + + const d = response.data.data; + + const data = { + account_number: accountNumber, + ifsc: ifsc.toUpperCase(), + account_exists: d.account_exists !== false, + name_at_bank: d.name_at_bank || d.account_holder_name || '', + name_match: name ? (d.name_at_bank || d.account_holder_name || '').toUpperCase() === name.toUpperCase() : null, + name_match_score: name ? (d.name_at_bank || d.account_holder_name || '').toUpperCase() === name.toUpperCase() ? 100 : 0 : null, + bank_name: bankInfo.bank_name, + branch: bankInfo.branch || d.branch || '' + }; + + await cacheSet(cacheKey, data, 86400); + + // Persist bank verification result to Postgres (best-effort, non-blocking) + try { + await query( + `INSERT INTO bank_verifications ( account_number, ifsc, name, @@ -88,44 +81,44 @@ async function verifyBankAccount(accountNumber, ifsc, name = null) { branch, requested_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - [ - data.account_number, - data.ifsc, - data.name_at_bank, - data.account_exists, - data.name_match, - data.name_match_score, - data.bank_name, - data.branch, - null, // requested_by (user id) - can be wired from route later if needed - ] - ); - } catch (dbError) { - console.error('Failed to store bank verification in database:', dbError.message || dbError); + [ + data.account_number, + data.ifsc, + data.name_at_bank, + data.account_exists, + data.name_match, + data.name_match_score, + data.bank_name, + data.branch, + null, // requested_by (user id) - can be wired from route later if needed + ] + ); + } catch (dbError) { + console.error('Failed to store bank verification in database:', dbError.message || dbError); + } + + return { success: true, data }; + + } catch (error) { + // Surface provider details when available to avoid generic 500s + if (error.code === 'ECONNABORTED') { + return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; + } + + const providerStatus = error.response?.status || error.response?.data?.status_code; + const providerMessage = error.response?.data?.message || error.message; + + if (providerStatus) { + return { + success: false, + statusCode: providerStatus, + errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', + message: providerMessage || 'Provider error' + }; + } + + return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; } - - return { success: true, data }; - - } catch (error) { - // Surface provider details when available to avoid generic 500s - if (error.code === 'ECONNABORTED') { - return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; - } - - const providerStatus = error.response?.status || error.response?.data?.status_code; - const providerMessage = error.response?.data?.message || error.message; - - if (providerStatus) { - return { - success: false, - statusCode: providerStatus, - errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', - message: providerMessage || 'Provider error' - }; - } - - return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; - } } module.exports = { verifyBankAccount }; diff --git a/src/services/gstService.js b/src/services/gstService.js index 67078dc..4e541ea 100644 --- a/src/services/gstService.js +++ b/src/services/gstService.js @@ -1,101 +1,5 @@ -// const axios = require('axios'); -// const { cacheGet, cacheSet } = require('../cache/redis'); - -// const STATE_NAMES = { -// '01': 'Jammu & Kashmir', '02': 'Himachal Pradesh', '03': 'Punjab', -// '04': 'Chandigarh', '05': 'Uttarakhand', '06': 'Haryana', '07': 'Delhi', -// '08': 'Rajasthan', '09': 'Uttar Pradesh', '10': 'Bihar', '11': 'Sikkim', -// '12': 'Arunachal Pradesh', '13': 'Nagaland', '14': 'Manipur', '15': 'Mizoram', -// '16': 'Tripura', '17': 'Meghalaya', '18': 'Assam', '19': 'West Bengal', -// '20': 'Jharkhand', '21': 'Odisha', '22': 'Chhattisgarh', '23': 'Madhya Pradesh', -// '24': 'Gujarat', '26': 'Dadra & Nagar Haveli', '27': 'Maharashtra', -// '29': 'Karnataka', '30': 'Goa', '31': 'Lakshadweep', '32': 'Kerala', -// '33': 'Tamil Nadu', '34': 'Puducherry', '35': 'Andaman & Nicobar', -// '36': 'Telangana', '37': 'Andhra Pradesh', '38': 'Ladakh' -// }; - -// async function verifyGSTIN(gstin) { -// try { -// const cacheKey = `gst:${gstin}`; -// const cached = await cacheGet(cacheKey); -// if (cached) return { success: true, data: cached }; - -// const response = await axios.post( -// process.env.GST_PROVIDER_URL, -// { id_number: gstin }, -// { -// headers: { -// 'Authorization': `Bearer ${process.env.GST_PROVIDER_KEY}`, -// 'Content-Type': 'application/json' -// }, -// timeout: 30000 -// } -// ); - -// if (!response.data || response.data.status_code !== 200) { -// return { success: false, statusCode: 404, errorCode: 'GSTIN_NOT_FOUND', message: 'GSTIN not found' }; -// } - -// const d = response.data.data; - -// const data = { -// gstin, -// legal_name: d.legal_name || d.lgnm, -// trade_name: d.trade_name || d.tradeNam, -// status: d.status || d.sts, -// registration_date: d.registration_date || d.rgdt, -// last_updated: d.last_update || d.lstupdt, -// business_type: d.business_type || d.ctb, -// constitution: d.constitution || d.ctj, -// state: d.state || STATE_NAMES[gstin.substring(0, 2)], -// state_code: gstin.substring(0, 2), -// pan: gstin.substring(2, 12), -// address: { -// building: d.address?.bno || d.pradr?.addr?.bno || '', -// floor: d.address?.flno || d.pradr?.addr?.flno || '', -// street: d.address?.st || d.pradr?.addr?.st || '', -// locality: d.address?.loc || d.pradr?.addr?.loc || '', -// city: d.address?.city || d.pradr?.addr?.city || '', -// district: d.address?.dst || d.pradr?.addr?.dst || '', -// state: d.address?.stcd || d.pradr?.addr?.stcd || '', -// pincode: d.address?.pncd || d.pradr?.addr?.pncd || '' -// }, -// nature_of_business: d.nature_of_business || d.nba || [], -// filing_status: { -// gstr1: d.filing_status?.gstr1 || 'Unknown', -// gstr3b: d.filing_status?.gstr3b || 'Unknown', -// last_filed_date: d.filing_status?.last_filed || null -// } -// }; - -// await cacheSet(cacheKey, data, 86400); -// return { success: true, data }; - -// } catch (error) { -// // Surface provider details when available to avoid generic 500s -// if (error.code === 'ECONNABORTED') { -// return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; -// } - -// const providerStatus = error.response?.status || error.response?.data?.status_code; -// const providerMessage = error.response?.data?.message || error.message; - -// if (providerStatus) { -// return { -// success: false, -// statusCode: providerStatus, -// errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', -// message: providerMessage || 'Provider error' -// }; -// } - -// return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; -// } -// } - -// module.exports = { verifyGSTIN }; +const axios = require('axios'); const { cacheGet, cacheSet } = require('../cache/redis'); -const { query } = require('../database/connection'); const STATE_NAMES = { '01': 'Jammu & Kashmir', '02': 'Himachal Pradesh', '03': 'Punjab', @@ -111,81 +15,269 @@ const STATE_NAMES = { }; async function verifyGSTIN(gstin) { + // Create a fresh axios instance to avoid global axios.defaults pollution from other services + const axiosInstance = axios.create(); + // Defensively remove globally set headers just in case + delete axiosInstance.defaults.headers.common['Authorization']; + try { const cacheKey = `gst:${gstin}`; const cached = await cacheGet(cacheKey); if (cached) return { success: true, data: cached }; - // Look up GSTIN in the local database seeded from gst.csv - const result = await query( - 'SELECT * FROM gst_registrations WHERE gstin = $1', - [gstin] - ); + // Get and clean Setu API credentials + const providerUrl = process.env.GST_PROVIDER_URL || 'https://dg-sandbox.setu.co/api/verify/gst'; + const clientId = process.env.GST_CLIENT_ID; + const clientSecret = process.env.GST_CLIENT_SECRET; + const productInstanceId = process.env.GST_PRODUCT_INSTANCE_ID; - if (!result.rows.length) { - return { success: false, statusCode: 404, errorCode: 'GSTIN_NOT_FOUND', message: 'GSTIN not found' }; + // Validate environment variables + if (!clientId || !clientSecret || !productInstanceId) { + console.error('GST API credentials missing. Check environment variables: GST_CLIENT_ID, GST_CLIENT_SECRET, GST_PRODUCT_INSTANCE_ID'); + return { + success: false, + statusCode: 500, + errorCode: 'CONFIGURATION_ERROR', + message: 'Server configuration error: Missing GST provider credentials' + }; } - const d = result.rows[0]; + // Clean credentials (trim whitespace) + const cleanClientId = clientId.trim(); + const cleanClientSecret = clientSecret.trim(); + const cleanProductInstanceId = productInstanceId.trim(); + const cleanUrl = providerUrl.trim(); - // nature_of_business in CSV is stored as a single string like "Manufacturing|Services" - const natureOfBusinessArray = d.nature_of_business - ? String(d.nature_of_business).split('|').map((v) => v.trim()).filter(Boolean) - : []; + // Debug: log outgoing provider request (mask sensitive parts) + try { + const maskedSecret = cleanClientSecret ? ('****' + cleanClientSecret.slice(-4)) : null; + console.log('[GST Service] Sending request to provider:', { + url: cleanUrl, + 'x-client-id': cleanClientId, + 'x-client-secret': maskedSecret, + 'x-product-instance-id': cleanProductInstanceId, + requestBody: { gstin } + }); + } catch (logErr) { + console.error('[GST Service] Failed to log provider request:', logErr.message); + } - const data = { - gstin, - legal_name: d.legal_name, - trade_name: d.trade_name, - status: d.status, - registration_date: d.registration_date, - last_updated: d.last_updated, - business_type: d.business_type, - constitution: d.constitution, - state: d.state || STATE_NAMES[gstin.substring(0, 2)], + const response = await axiosInstance.post( + cleanUrl, + { gstin }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-client-id': cleanClientId, + 'x-client-secret': cleanClientSecret, + 'x-product-instance-id': cleanProductInstanceId + }, + timeout: 30000 + } + ); + + if (!response || response.status !== 200 || !response.data) { + return { + success: false, + statusCode: response?.status || 502, + errorCode: 'GST_PROVIDER_ERROR', + message: 'GST provider returned an unexpected response' + }; + } + + const d = response.data.data || {}; + const setuVerification = response.data.verification || 'SUCCESS'; + const setuMessage = response.data.message || ''; + const traceId = response.data.traceId || null; + const requestId = response.data.requestId || null; + + // Check if we got valid company data, otherwise handle as invalid/empty + const company = d.company || {}; + const address = d.address?.principle || {}; + const gstData = d.gst || {}; + const jurisdiction = d.jurisdiction || {}; + + const mappedData = { + gstin: gstData.id || gstin, + legal_name: company.name || '', + trade_name: company.tradeName || '', + status: company.status || 'Inactive', + registration_date: gstData.registrationDate || '', + last_updated: gstData.lastUpdatedDate || '', + business_type: company.type || '', + constitution: company.constitutionOfBusiness || '', + state: address.stateCode || company.state || STATE_NAMES[gstin.substring(0, 2)] || '', state_code: gstin.substring(0, 2), pan: gstin.substring(2, 12), address: { - building: d.address_building || '', - floor: d.address_floor || '', - street: d.address_street || '', - locality: d.address_locality || '', - city: d.address_city || '', - district: d.address_district || '', - state: d.address_state_code || '', - pincode: d.address_pincode || '' + building: address.buildingName || '', + building_number: address.buildingNumber || '', + floor: address.floorNo || '', + street: address.street || '', + locality: address.location || '', + city: address.city || '', + district: address.district || '', + state: address.stateCode || '', + pincode: address.pinCode || '' }, - nature_of_business: natureOfBusinessArray, - filing_status: { - gstr1: d.filing_status_gstr1 || 'Unknown', - gstr3b: d.filing_status_gstr3b || 'Unknown', - last_filed_date: d.filing_last_filed_date || null + nature_of_business: company.natureOfBusinessActivity ? [company.natureOfBusinessActivity] : [], + jurisdiction: { + centre: jurisdiction.centre || '', + state: jurisdiction.state || '' } }; - await cacheSet(cacheKey, data, 86400); - return { success: true, data }; + const responseData = { + verification: setuVerification, + message: setuMessage, + requestId: requestId, + traceId: traceId, + data: Object.keys(d).length > 0 ? mappedData : {} + }; + + await cacheSet(cacheKey, responseData, 86400); + return { success: true, data: responseData }; } catch (error) { - // Surface provider details when available to avoid generic 500s + // Handle timeout errors if (error.code === 'ECONNABORTED') { - return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; + return { + success: false, + statusCode: 504, + errorCode: 'PROVIDER_TIMEOUT', + message: 'GST verification service timeout' + }; } - const providerStatus = error.response?.status || error.response?.data?.status_code; - const providerMessage = error.response?.data?.message || error.message; + // Handle Setu API errors + if (error.response) { + const providerStatus = error.response.status; + + // Handle Provider Authentication Errors specifically + if (providerStatus === 401 || providerStatus === 403) { + // Log provider auth failure details for debugging (mask any obvious secrets) + try { + const providerData = error.response.data; + const providerHeaders = error.response.headers || {}; + const maskedHeaders = { ...providerHeaders }; + if (maskedHeaders['x-client-secret']) { + maskedHeaders['x-client-secret'] = '****' + String(maskedHeaders['x-client-secret']).slice(-4); + } + console.error('[GST Service] Provider Authentication Failed', { + status: providerStatus, + data: providerData, + headers: maskedHeaders + }); + } catch (logErr) { + console.error('[GST Service] Failed to log provider auth error:', logErr.message); + } + + // Surface provider message when available to aid debugging (do not expose secrets) + const providerMsg = error.response.data?.message || error.response.data || 'Upstream provider authentication failed'; + + return { + success: false, + statusCode: 502, + errorCode: 'GST_PROVIDER_AUTH_ERROR', + message: typeof providerMsg === 'string' ? providerMsg : 'Upstream provider authentication failed. Check provider logs.' + }; + } + + // For other provider errors + const errorData = error.response.data || {}; + const providerMessage = errorData.message || error.message; - if (providerStatus) { return { success: false, statusCode: providerStatus, - errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', + errorCode: errorData.error_code || 'PROVIDER_ERROR', message: providerMessage || 'Provider error' }; } - return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; + // Generic error + console.error('GST verification error:', error.message); + return { + success: false, + statusCode: 500, + errorCode: 'VERIFICATION_FAILED', + message: 'GST verification failed' + }; } } module.exports = { verifyGSTIN }; + + + + +// const axios = require('axios'); + + +// async function verifyGstSetu(gstin) { +// const baseUrl = process.env.SETU_BASE_URL; +// const clientId = process.env.SETU_CLIENT_ID; +// const clientSecret = process.env.SETU_CLIENT_SECRET; +// const productInstanceId = process.env.SETU_GST_INSTANCE_ID; + +// // Validate environment variables +// if (!baseUrl || !clientId || !clientSecret || !productInstanceId) { +// throw new Error('Setu GST API configuration is incomplete. Check environment variables.'); +// } + +// try { +// console.log(`🌐 Attempting Setu GST verification for GSTIN: ${gstin}`); + +// const response = await axios.post( +// `${baseUrl}/api/verify/gst`, +// { +// gstin: gstin, +// consent: 'Y', +// reason: 'Vendor verification' +// }, +// { +// headers: { +// 'Content-Type': 'application/json', +// 'x-client-id': clientId, +// 'x-client-secret': clientSecret, +// 'x-product-instance-id': productInstanceId +// }, +// timeout: 30000 // 30 second timeout +// } +// ); + +// // Check if Setu returned a successful verification (case-insensitive) +// const verification = response.data?.verification?.toLowerCase(); +// if (response.data && (verification === 'success' || response.data.status === 'success')) { +// console.log(`✅ Setu GST verification successful for GSTIN: ${gstin}`); +// return { +// success: true, +// source: 'SETU_API', +// data: response.data.data +// }; +// } + +// // If verification field is not SUCCESS, treat as failure +// throw new Error(response.data?.message || 'GST verification failed via Setu'); + +// } catch (error) { +// // Re-throw with more context +// if (error.response) { +// // Setu returned an error response +// const statusCode = error.response.status; +// const errorMessage = error.response.data?.message || error.response.data?.error || 'Unknown Setu API error'; +// console.warn(`⚠️ Setu GST API failed for GSTIN ${gstin}: Setu API Error (${statusCode}): ${errorMessage}`); +// throw new Error(`Setu API Error (${statusCode}): ${errorMessage}`); +// } else if (error.code === 'ECONNABORTED') { +// console.warn(`⚠️ Setu GST API timeout for GSTIN ${gstin}`); +// throw new Error('Setu API timeout'); +// } else { +// console.warn(`⚠️ Setu GST API failed for GSTIN ${gstin}: ${error.message}`); +// throw error; +// } +// } +// } + +// module.exports = { verifyGstSetu }; + diff --git a/src/services/panService.js b/src/services/panService.js index 423da42..c00f39e 100644 --- a/src/services/panService.js +++ b/src/services/panService.js @@ -1,140 +1,223 @@ const axios = require('axios'); -const { cacheGet, cacheSet } = require('../cache/redis'); const { query } = require('../database/connection'); -const PAN_TYPES = { - 'P': 'Individual', - 'C': 'Company', - 'H': 'HUF', - 'A': 'AOP', - 'B': 'BOI', - 'G': 'Government', - 'J': 'Artificial Juridical Person', - 'L': 'Local Authority', - 'F': 'Firm/Partnership', - 'T': 'Trust' -}; +// Create a fresh axios instance to avoid global axios.defaults pollution from other services +const axiosInstance = axios.create(); -async function verifyPAN(pan, name = null, dob = null) { + +async function verifyPAN(pan, reason = null, userId = null) { try { - const cacheKey = `pan:${pan}`; - const cached = await cacheGet(cacheKey); - if (cached) { - if (name) { - cached.name_match = cached.name === name.toUpperCase(); - cached.name_match_score = cached.name_match ? 100 : 0; - } - return { success: true, data: cached }; - } + // Validate PAN format (10 characters: 5 letters, 4 digits, 1 letter) + const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/; + const panUpper = pan.toUpperCase(); - - - const parts = name.trim().split(" "); - - const firstName = parts[0] || "DUMMY"; - const lastName = parts.slice(1).join(" ") || "NAME"; - - const mockResponseData = { - status_code: 200, - data: { - name: name || "DUMMY NAME", - status: "ACTIVE", - type: PAN_TYPES[pan[3]] || 'Individual', - full_name: name || "DUMMY NAME", - last_name: lastName, - first_name: firstName, - } - }; - const response = { data: mockResponseData }; - - // Commenting out the actual external API call for now - // const response = await axios.post( - // process.env.PAN_PROVIDER_URL, - // { - // id_number: pan, - // name: name, - // dob: dob - // }, - // { - // headers: { - // 'Authorization': `Bearer ${process.env.PAN_PROVIDER_KEY}`, - // 'Content-Type': 'application/json' - // }, - // timeout: 30000 - // } - // ); - - if (!response.data || response.data.status_code !== 200) { - return { success: false, statusCode: 404, errorCode: 'PAN_NOT_FOUND', message: 'PAN not found' }; - } - - const d = response.data.data; - const panType = PAN_TYPES[pan[3]] || 'Unknown'; - - const data = { - pan, - name: d.name || d.full_name || '', - status: d.status || 'Valid', - type: d.type || panType, - name_match: name ? (d.name || d.full_name || '').toUpperCase() === name.toUpperCase() : null, - name_match_score: name ? (d.name || d.full_name || '').toUpperCase() === name.toUpperCase() ? 100 : 0 : null, - last_name: d.last_name || d.surname || '', - first_name: d.first_name || '', - middle_name: d.middle_name || '', - title: d.title || '' - }; - - await cacheSet(cacheKey, data, 86400); - - // Persist PAN verification result to Postgres (best-effort, non-blocking) - try { - await query( - `INSERT INTO pan_verifications ( - pan, - name, - status, - pan_type, - name_match, - name_match_score, - requested_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7)`, - [ - data.pan, - data.name, - data.status, - data.type, - data.name_match, - data.name_match_score, - null, // requested_by (user id) - can be wired from route later if needed - ] - ); - } catch (dbError) { - console.error('Failed to store PAN verification in database:', dbError.message || dbError); - } - - return { success: true, data }; - - } catch (error) { - // Surface provider details when available to avoid generic 500s - if (error.code === 'ECONNABORTED') { - return { success: false, statusCode: 504, errorCode: 'PROVIDER_TIMEOUT', message: 'Service timeout' }; - } - - const providerStatus = error.response?.status || error.response?.data?.status_code; - const providerMessage = error.response?.data?.message || error.message; - - if (providerStatus) { + if (!panRegex.test(panUpper)) { return { success: false, - statusCode: providerStatus, - errorCode: error.response?.data?.error_code || 'PROVIDER_ERROR', - message: providerMessage || 'Provider error' + statusCode: 400, + errorCode: 'INVALID_PAN_FORMAT', + message: 'Invalid PAN format. PAN must be 10 characters: 5 letters, 4 digits, 1 letter.' }; } - return { success: false, statusCode: 500, errorCode: 'VERIFICATION_FAILED', message: 'Verification failed' }; + // Get Setu API credentials from environment variables + const providerUrl = process.env.PAN_PROVIDER_URL || 'https://dg-sandbox.setu.co/api/verify/pan'; + const clientId = process.env.PAN_CLIENT_ID; + const clientSecret = process.env.PAN_CLIENT_SECRET; + const productInstanceId = process.env.PAN_PRODUCT_INSTANCE_ID; + + // Validate environment variables + if (!clientId || !clientSecret || !productInstanceId) { + console.error('PAN API credentials missing. Check environment variables: PAN_CLIENT_ID, PAN_CLIENT_SECRET, PAN_PRODUCT_INSTANCE_ID'); + return { + success: false, + statusCode: 500, + errorCode: 'CONFIGURATION_ERROR', + message: 'Server configuration error: Missing PAN provider credentials' + }; + } + + // Clean credentials (just trim whitespace) + const cleanClientId = clientId ? clientId.trim() : ''; + const cleanClientSecret = clientSecret ? clientSecret.trim() : ''; + const cleanProductInstanceId = productInstanceId ? productInstanceId.trim() : ''; + const cleanUrl = providerUrl ? providerUrl.trim() : ''; + + // Prepare request body for Setu API + const requestBody = { + pan: panUpper, + consent: 'Y', + reason: reason || 'Identity verification for KYC compliance' + }; + + // Call Setu API + // Debug: log outgoing provider request (mask sensitive parts) + try { + const maskedSecret = cleanClientSecret ? ('****' + cleanClientSecret.slice(-4)) : null; + console.log('[PAN Service] Sending request to provider:', { + url: cleanUrl, + 'x-client-id': cleanClientId, + 'x-client-secret': maskedSecret, + 'x-product-instance-id': cleanProductInstanceId, + requestBody + }); + } catch (logErr) { + console.error('[PAN Service] Failed to log provider request:', logErr.message); + } + const response = await axiosInstance.post( + cleanUrl, + requestBody, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-client-id': cleanClientId, + 'x-client-secret': cleanClientSecret, + 'x-product-instance-id': cleanProductInstanceId + }, + timeout: 30000 // 30 second timeout + } + ); + + // Check if response is successful + if (!response || response.status !== 200 || !response.data) { + return { + success: false, + statusCode: response?.status || 502, + errorCode: 'PAN_PROVIDER_ERROR', + message: 'PAN provider returned an unexpected response' + }; + } + + // Extract data from Setu response + const setuData = response.data.data || {}; + const setuMessage = response.data.message || ''; + const setuId = response.data.id || null; + const setuVerification = response.data.verification || 'SUCCESS'; + const traceId = response.data.traceId || null; + + // Format response to match Setu's structure + const responseData = { + verification: setuVerification, + message: setuMessage, + data: { + full_name: setuData.full_name || null, + first_name: setuData.first_name || null, + middle_name: setuData.middle_name || null, + last_name: setuData.last_name || null, + category: setuData.category || null, + aadhaar_seeding_status: setuData.aadhaar_seeding_status || null + }, + id: setuId, + traceId: traceId + }; + + // Store in database (non-blocking, best-effort) + query( + `INSERT INTO pan_verifications ( + pan, + full_name, + first_name, + middle_name, + last_name, + category, + aadhaar_seeding_status, + status, + setu_response_id, + setu_trace_id, + setu_message, + requested_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, + [ + pan.toUpperCase(), + setuData.full_name, + setuData.first_name, + setuData.middle_name, + setuData.last_name, + setuData.category, + setuData.aadhaar_seeding_status, + setuVerification === 'SUCCESS' ? 'VALID' : 'INVALID', + setuId, + traceId, + setuMessage, + userId + ] + ).catch(dbError => { + console.error('Failed to store PAN verification in database:', dbError.message); + }); + + return { success: true, data: responseData }; + + } catch (error) { + // Handle timeout errors + if (error.code === 'ECONNABORTED') { + return { + success: false, + statusCode: 504, + errorCode: 'PROVIDER_TIMEOUT', + message: 'PAN verification service timeout' + }; + } + + // Handle Setu API errors + if (error.response) { + const providerStatus = error.response.status; + + // Handle Provider Authentication Errors specifically + if (providerStatus === 401 || providerStatus === 403) { + // Log provider auth failure details for debugging (mask any obvious secrets) + try { + const providerData = error.response.data; + const providerHeaders = error.response.headers || {}; + const maskedHeaders = { ...providerHeaders }; + if (maskedHeaders['x-client-secret']) { + maskedHeaders['x-client-secret'] = '****' + String(maskedHeaders['x-client-secret']).slice(-4); + } + console.error('[PAN Service] Provider Authentication Failed', { + status: providerStatus, + data: providerData, + headers: maskedHeaders + }); + } catch (logErr) { + console.error('[PAN Service] Failed to log provider auth error:', logErr.message); + } + + // Surface provider message when available to aid debugging (do not expose secrets) + const providerMsg = error.response.data?.message || error.response.data || 'Upstream provider authentication failed'; + + return { + success: false, + statusCode: 502, + errorCode: 'PAN_PROVIDER_AUTH_ERROR', + message: typeof providerMsg === 'string' ? providerMsg : 'Upstream provider authentication failed. Check provider logs.' + }; + } + + + // For other provider errors, return Setu-formatted error response if available + const errorData = error.response.data || {}; + + return { + success: false, + statusCode: providerStatus, + data: { + verification: errorData.verification || 'failed', + message: errorData.message || 'PAN verification failed', + id: errorData.id || null, + traceId: errorData.traceId || null + } + }; + } + + // Generic error + console.error('PAN verification error:', error.message); + return { + success: false, + statusCode: 500, + errorCode: 'VERIFICATION_FAILED', + message: 'PAN verification failed' + }; } } module.exports = { verifyPAN }; - diff --git a/test-direct-setu.js b/test-direct-setu.js new file mode 100644 index 0000000..8b163a3 --- /dev/null +++ b/test-direct-setu.js @@ -0,0 +1,58 @@ +// Test script - run from project root: node test-direct-setu.js +require('dotenv').config(); +const axios = require('axios'); + +async function testDirect() { + console.log('\n=== Testing Direct Setu API Call ===\n'); + + const clientId = process.env.PAN_CLIENT_ID; + const clientSecret = process.env.PAN_CLIENT_SECRET; + const productId = process.env.PAN_PRODUCT_INSTANCE_ID; + const url = process.env.PAN_PROVIDER_URL || 'https://dg-sandbox.setu.co/api/verify/pan'; + + console.log('Loaded from .env:'); + console.log(' clientId:', clientId); + console.log(' clientSecret:', clientSecret ? '****' + clientSecret.slice(-4) : 'MISSING'); + console.log(' productId:', productId); + console.log(' url:', url); + console.log(''); + + console.log('Testing with axios...'); + + try { + const response = await axios.post( + url, + { + pan: 'ABCDE1234A', + consent: 'Y', + reason: 'Testing PAN verification' + }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-client-id': clientId, + 'x-client-secret': clientSecret, + 'x-product-instance-id': productId + }, + timeout: 30000 + } + ); + + console.log('✅ SUCCESS!'); + console.log('Status:', response.status); + console.log('Data:', JSON.stringify(response.data, null, 2)); + + } catch (error) { + console.log('❌ FAILED!'); + if (error.response) { + console.log('Status:', error.response.status); + console.log('Data:', JSON.stringify(error.response.data, null, 2)); + console.log('Headers:', JSON.stringify(error.response.headers, null, 2)); + } else { + console.log('Error:', error.message); + } + } +} + +testDirect(); diff --git a/test-gst-credentials.js b/test-gst-credentials.js new file mode 100644 index 0000000..9bb5543 --- /dev/null +++ b/test-gst-credentials.js @@ -0,0 +1,69 @@ +const axios = require('axios'); + +// Test with EXACT same approach as gstService.js +async function testWithAxiosInstance() { + // Create a fresh axios instance (same as in gstService.js) + const axiosInstance = axios.create(); + + const providerUrl = 'https://dg-sandbox.setu.co/api/verify/gst'; + const clientId = '292c6e76-dabf-49c4-8e48-90fba2916673'; + const clientSecret = '7IZMe9zvoBBuBukLiCP7n4KLwSOy11oP'; + const productInstanceId = '69e23f7f-4f71-412e-aec6-b1da3fb77c6f'; + + // Clean credentials (trim whitespace) - EXACT same as gstService.js + const cleanClientId = clientId.trim(); + const cleanClientSecret = clientSecret.trim(); + const cleanProductInstanceId = productInstanceId.trim(); + const cleanUrl = providerUrl.trim(); + + const gstin = '29AABCT1332L1ZV'; + + console.log('Testing with axios instance (EXACT same as gstService.js)...\n'); + console.log('Cleaned credentials:'); + console.log(' URL:', cleanUrl); + console.log(' Client ID:', cleanClientId); + console.log(' Client Secret:', '****' + cleanClientSecret.slice(-4)); + console.log(' Product Instance ID:', cleanProductInstanceId); + console.log(' GSTIN:', gstin); + console.log('\n---\n'); + + try { + const response = await axiosInstance.post( + cleanUrl, + { gstin }, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-client-id': cleanClientId, + 'x-client-secret': cleanClientSecret, + 'x-product-instance-id': cleanProductInstanceId + }, + timeout: 30000 + } + ); + + console.log('✅ SUCCESS!'); + console.log('Status:', response.status); + console.log('Data:', JSON.stringify(response.data, null, 2)); + + } catch (error) { + if (error.response) { + console.log('❌ FAILED'); + console.log('Status:', error.response.status); + console.log('Error:', JSON.stringify(error.response.data, null, 2)); + console.log('\n---\n'); + + // Log the actual request that was sent + console.log('Request config:'); + console.log(' URL:', error.config.url); + console.log(' Method:', error.config.method); + console.log(' Headers:', JSON.stringify(error.config.headers, null, 2)); + console.log(' Data:', error.config.data); + } else { + console.log('❌ ERROR:', error.message); + } + } +} + +testWithAxiosInstance();