ABHA Address Creation Implementation Plan
Overview
This document provides a comprehensive, production-ready implementation plan for the ABHA Address creation flow, strictly following existing ABDM module conventions and patterns. The implementation follows a stateless pattern without database transaction storage, consistent with the current ABDM module approach.
Architecture Overview
The ABHA Address creation flow supports three enrollment methods:
- Mobile-based enrollment - For new ABHA creation using mobile OTP
- ABHA Number-based enrollment - For existing ABHA holders to create new address
- Aadhaar-based enrollment - For Aadhaar-linked ABHA creation
Key Implementation Pattern
Stateless Flow
- No database transactions: Transaction IDs are passed between API calls via frontend
- Direct service communication: Each API call directly communicates with ABDM services
- Audit logging only: Only
AbdmRequestentries are created for logging - Final storage: ABHA details stored in
clinic_patientsonly after successful enrollment
Existing Database Fields
The clinic_patients table already has ABDM-related fields (from migration 20220629084152):
health_id- Stores the ABHA addresshealth_id_number- Stores the ABHA number
These fields will be updated only after successful enrollment completion.
Services Layer
1. Mobile Enrollment - Request OTP Service
# parts/abdm/services/abdm/abha/enrollment/mobile/request_otp.rb
module Abdm
module Abha
module Enrollment
module Mobile
class RequestOtp
def self.perform(mobile, user_id)
token = Abdm::FetchGatewayTokenV3.perform
encrypted_mobile = Abdm::Abha::EncryptData.perform(mobile)
url = "#{Rails.application.credentials.abdm_phr_host}/v3/phr/app/enrollment/request/otp"
headers = {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}",
'REQUEST-ID' => SecureRandom.uuid,
'TIMESTAMP' => Time.now.utc.iso8601(3)
}
body = {
scope: ['abha-address-enroll', 'mobile-verify'],
loginHint: 'mobile-number',
loginId: encrypted_mobile,
otpSystem: 'abdm'
}
begin
result = HTTParty.post(url, body: body.to_json, headers: headers)
rescue StandardError => e
error = e.to_s
end
# Log Request
abdm_request = AbdmRequest.create!(
url: url,
payload: body,
requested_by_id: user_id,
requested_by_type: 'User'
)
# Prepare response
if result.present? && result.success?
code = result.code
success = true
response = result.parsed_response
elsif error.present?
success = false
response = error
else
success = false
response = result.parsed_response
code = result.code
end
abdm_request.update!(
response: response,
status_code: code,
success: success
)
result
end
end
end
end
end
end2. Mobile Enrollment - Verify OTP Service
# parts/abdm/services/abdm/abha/enrollment/mobile/verify_otp.rb
module Abdm
module Abha
module Enrollment
module Mobile
class VerifyOtp
def self.perform(otp, txn_id, user_id)
token = Abdm::FetchGatewayTokenV3.perform
encrypted_otp = Abdm::Abha::EncryptData.perform(otp)
url = "#{Rails.application.credentials.abdm_phr_host}/v3/phr/app/enrollment/verify"
headers = {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}",
'REQUEST-ID' => SecureRandom.uuid,
'TIMESTAMP' => Time.now.utc.iso8601(3)
}
body = {
scope: ['abha-address-enroll', 'mobile-verify'],
authData: {
authMethods: ['otp'],
otp: {
txnId: txn_id,
otpValue: encrypted_otp
}
}
}
begin
result = HTTParty.post(url, body: body.to_json, headers: headers)
rescue StandardError => e
error = e.to_s
end
# Log Request
abdm_request = AbdmRequest.create!(
url: url,
payload: body,
requested_by_id: user_id,
requested_by_type: 'User'
)
# Process response
if result.present? && result.success?
code = result.code
success = true
response = result.parsed_response
elsif error.present?
success = false
response = error
else
success = false
response = result.parsed_response
code = result.code
end
abdm_request.update!(
response: response,
status_code: code,
success: success
)
result
end
end
end
end
end
end3. ABHA Number Enrollment - Request OTP Service
# parts/abdm/services/abdm/abha/enrollment/abha_number/request_otp.rb
module Abdm
module Abha
module Enrollment
module AbhaNumber
class RequestOtp
def self.perform(abha_number, user_id)
token = Abdm::FetchGatewayTokenV3.perform
encrypted_abha = Abdm::Abha::EncryptData.perform(abha_number)
url = "#{Rails.application.credentials.abdm_phr_host}/v3/phr/app/enrollment/request/otp"
headers = {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}",
'REQUEST-ID' => SecureRandom.uuid,
'TIMESTAMP' => Time.now.utc.iso8601(3)
}
body = {
scope: ['abha-login', 'mobile-verify'],
loginHint: 'abha-number',
loginId: encrypted_abha,
otpSystem: 'abdm'
}
begin
result = HTTParty.post(url, body: body.to_json, headers: headers)
rescue StandardError => e
error = e.to_s
end
# Log Request
abdm_request = AbdmRequest.create!(
url: url,
payload: body,
requested_by_id: user_id,
requested_by_type: 'User'
)
# Process response
if result.present? && result.success?
code = result.code
success = true
response = result.parsed_response
elsif error.present?
success = false
response = error
else
success = false
response = result.parsed_response
code = result.code
end
abdm_request.update!(
response: response,
status_code: code,
success: success
)
result
end
end
end
end
end
end4. Suggest ABHA Address Service
# parts/abdm/services/abdm/abha/enrollment/suggest_abha_address.rb
module Abdm
module Abha
module Enrollment
class SuggestAbhaAddress
def self.perform(txn_id, first_name, middle_name, last_name, year_of_birth, user_id)
token = Abdm::FetchGatewayTokenV3.perform
url = "#{Rails.application.credentials.abdm_phr_host}/v3/phr/app/enrollment/suggestion"
headers = {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}",
'REQUEST-ID' => SecureRandom.uuid,
'TIMESTAMP' => Time.now.utc.iso8601(3)
}
body = {
txnId: txn_id,
firstName: first_name,
middleName: middle_name,
lastName: last_name,
yearOfBirth: year_of_birth
}
begin
result = HTTParty.post(url, body: body.to_json, headers: headers)
rescue StandardError => e
error = e.to_s
end
# Log Request
abdm_request = AbdmRequest.create!(
url: url,
payload: body,
requested_by_id: user_id,
requested_by_type: 'User'
)
# Process response
if result.present? && result.success?
response = result.parsed_response
abdm_request.update!(
response: response,
status_code: result.code,
success: true
)
{ success: true, data: response }
else
error_msg = error || result&.parsed_response
abdm_request.update!(
response: error_msg,
status_code: result&.code,
success: false
)
{ success: false, error: error_msg }
end
end
end
end
end
end5. Check ABHA Address Existence Service
# parts/abdm/services/abdm/abha/enrollment/check_exists.rb
module Abdm
module Abha
module Enrollment
class CheckExists
def self.perform(abha_address, user_id)
token = Abdm::FetchGatewayTokenV3.perform
url = "#{Rails.application.credentials.abdm_phr_host}/v3/phr/app/enrollment/isExists"
headers = {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}",
'REQUEST-ID' => SecureRandom.uuid,
'TIMESTAMP' => Time.now.utc.iso8601(3)
}
params = { abhaAddress: abha_address }
begin
result = HTTParty.get(url, headers: headers, query: params)
rescue StandardError => e
error = e.to_s
end
# Log Request
abdm_request = AbdmRequest.create!(
url: "#{url}?#{params.to_query}",
payload: params,
requested_by_id: user_id,
requested_by_type: 'User'
)
# Process response
if result.present? && result.success?
response = result.parsed_response
abdm_request.update!(
response: response,
status_code: result.code,
success: true
)
{ success: true, data: response }
else
error_msg = error || result&.parsed_response
abdm_request.update!(
response: error_msg,
status_code: result&.code,
success: false
)
{ success: false, error: error_msg }
end
end
end
end
end
end6. Complete Enrollment Service
# parts/abdm/services/abdm/abha/enrollment/enroll.rb
module Abdm
module Abha
module Enrollment
class Enroll
def self.perform(txn_id, phr_details, user_id)
token = Abdm::FetchGatewayTokenV3.perform
# Encrypt sensitive fields
encrypted_details = encrypt_phr_details(phr_details)
url = "#{Rails.application.credentials.abdm_phr_host}/v3/phr/app/enrollment/enrol"
headers = {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{token}",
'REQUEST-ID' => SecureRandom.uuid,
'TIMESTAMP' => Time.now.utc.iso8601(3)
}
body = {
txnId: txn_id,
phrDetails: encrypted_details
}
begin
result = HTTParty.post(url, body: body.to_json, headers: headers)
rescue StandardError => e
error = e.to_s
end
# Log Request (excluding sensitive data)
abdm_request = AbdmRequest.create!(
url: url,
payload: { txnId: txn_id, phrDetails: 'ENCRYPTED' },
requested_by_id: user_id,
requested_by_type: 'User'
)
# Process response
if result.present? && result.success?
response = result.parsed_response
abdm_request.update!(
response: response,
status_code: result.code,
success: true
)
{ success: true, data: response }
else
error_msg = error || result&.parsed_response
abdm_request.update!(
response: error_msg,
status_code: result&.code,
success: false
)
{ success: false, error: error_msg }
end
end
private
def self.encrypt_phr_details(details)
encrypted_details = details.dup
# Encrypt sensitive fields
%w[mobile email password].each do |field|
if encrypted_details[field].present?
encrypted_details[field] = Abdm::Abha::EncryptData.perform(details[field])
end
end
encrypted_details
end
end
end
end
endInteractors (Business Logic)
1. Mobile Enrollment - Send OTP Interactor
# parts/abdm/interactors/abdm/abha/enrollment/mobile/send_otp.rb
module Abdm
module Abha
module Enrollment
module Mobile
class SendOtp < BaseInteractor
def call
require_context(:mobile)
result = Services::Mobile::RequestOtp.perform(
context.mobile,
context.current_user&.id
)
fail_with_errors!(result['errors']) if result.key?('errors')
context.records = result
context.message = 'OTP sent successfully'
end
end
end
end
end
end2. Mobile Enrollment - Verify OTP Interactor
# parts/abdm/interactors/abdm/abha/enrollment/mobile/verify_otp.rb
module Abdm
module Abha
module Enrollment
module Mobile
class VerifyOtp < BaseInteractor
def call
require_context(:otp)
require_context(:txn_id)
result = Services::Mobile::VerifyOtp.perform(
context.otp,
context.txn_id,
context.current_user&.id
)
fail_with_errors!(result['errors']) if result.key?('errors')
context.records = result
context.message = 'OTP verified successfully'
end
end
end
end
end
end3. Suggest ABHA Address Interactor
# parts/abdm/interactors/abdm/abha/enrollment/suggest_address.rb
module Abdm
module Abha
module Enrollment
class SuggestAddress < BaseInteractor
def call
require_context(:txn_id)
require_context(:firstName)
require_context(:yearOfBirth)
result = Services::SuggestAbhaAddress.perform(
context.txn_id,
context.firstName,
context.middleName,
context.lastName,
context.dayOfBirth,
context.monthOfBirth,
context.yearOfBirth,
context.email,
context.current_user&.id
)
fail_with_errors!(result['errors']) if result.key?('errors')
context.records = result
context.message = 'ABHA address suggestions retrieved'
end
end
end
end
end4. Check ABHA Address Availability Interactor
# parts/abdm/interactors/abdm/abha/enrollment/check_availability.rb
module Abdm
module Abha
module Enrollment
class CheckAvailability < BaseInteractor
def call
require_context(:abhaAddress)
result = Services::CheckExists.perform(
context.abhaAddress,
context.current_user&.id
)
fail_with_errors!(result['errors']) if result.key?('errors')
context.records = result
context.message = result['status'] ? 'ABHA address already exists' : 'ABHA address is available'
end
end
end
end
end5. Complete Enrollment Interactor
# parts/abdm/interactors/abdm/abha/enrollment/create_enrollment.rb
module Abdm
module Abha
module Enrollment
class CreateEnrollment < BaseInteractor
def call
require_context(:txn_id)
require_context(:phrDetails)
result = Services::Enroll.perform(
context.txn_id,
context.phrDetails,
context.current_user&.id
)
fail_with_errors!(result['errors']) if result.key?('errors')
# Update clinic patient if provided
update_clinic_patient(result) if context.clinic_patient_id.present?
context.records = result
context.message = 'ABHA address created successfully'
end
private
def update_clinic_patient(response_data)
clinic_patient = ClinicPatient.find_by(id: context.clinic_patient_id)
return unless clinic_patient
clinic_patient.update!(
health_id: response_data['phrAddress'] || response_data['abhaAddress'],
health_id_number: response_data['ABHANumber'] || response_data['abhaNumber']
)
end
end
end
end
endControllers
ABHA Address Enrollments Controller
# parts/abdm/controllers/abdm/abha/abha_address_enrollments_controller.rb
module Abdm
module Abha
class AbhaAddressEnrollmentsController < ApiController
before_action :policy_check
# Mobile enrollment endpoints
# POST /abdm/abha/abha_address_enrollments/mobile/send_otp
def mobile_send_otp
result = Enrollment::Mobile::SendOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/mobile/verify_otp
def mobile_verify_otp
result = Enrollment::Mobile::VerifyOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/mobile/resend_otp
def mobile_resend_otp
result = Enrollment::Mobile::ResendOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# ABHA Number enrollment endpoints
# POST /abdm/abha/abha_address_enrollments/abha_number/send_otp
def abha_number_send_otp
result = Enrollment::AbhaNumber::SendOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/abha_number/verify_otp
def abha_number_verify_otp
result = Enrollment::AbhaNumber::VerifyOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/abha_number/resend_otp
def abha_number_resend_otp
result = Enrollment::AbhaNumber::ResendOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# Aadhaar enrollment endpoints
# POST /abdm/abha/abha_address_enrollments/aadhaar/send_otp
def aadhaar_send_otp
result = Enrollment::Aadhaar::SendOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/aadhaar/verify_otp
def aadhaar_verify_otp
result = Enrollment::Aadhaar::VerifyOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/aadhaar/resend_otp
def aadhaar_resend_otp
result = Enrollment::Aadhaar::ResendOtp.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# Common enrollment endpoints
# POST /abdm/abha/abha_address_enrollments/suggest_address
def suggest_address
result = Enrollment::SuggestAddress.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# GET /abdm/abha/abha_address_enrollments/check_availability
def check_availability
result = Enrollment::CheckAvailability.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
# POST /abdm/abha/abha_address_enrollments/complete_enrollment
def complete_enrollment
result = Enrollment::CreateEnrollment.call(params)
render_result(result, result.success? ? AbdmAbhaSerializer : nil)
end
private
def policy_check
authorize params, policy_class: AbhaAddressEnrollmentsControllerPolicy
end
end
end
endRoutes Configuration
# parts/abdm/routes.rb (Addition to existing routes in the ABHA namespace)
# Add this inside the existing 'namespace :abha do' block
resources :abha_address_enrollments, only: [] do
collection do
# Mobile enrollment
namespace :mobile do
post :send_otp
post :verify_otp
post :resend_otp
end
# ABHA Number enrollment
namespace :abha_number do
post :send_otp
post :verify_otp
post :resend_otp
end
# Aadhaar enrollment
namespace :aadhaar do
post :send_otp
post :verify_otp
post :resend_otp
end
# Common enrollment endpoints
post :suggest_address
get :check_availability
post :complete_enrollment
end
endAuthorization Policy
# parts/abdm/policies/abdm/abha/abha_address_enrollments_controller_policy.rb
module Abdm
module Abha
class AbhaAddressEnrollmentsControllerPolicy < BasePolicy
def mobile_send_otp?
user_signed_in?
end
def mobile_verify_otp?
user_signed_in?
end
def mobile_resend_otp?
user_signed_in?
end
def abha_number_send_otp?
user_signed_in?
end
def abha_number_verify_otp?
user_signed_in?
end
def abha_number_resend_otp?
user_signed_in?
end
def aadhaar_send_otp?
user_signed_in?
end
def aadhaar_verify_otp?
user_signed_in?
end
def aadhaar_resend_otp?
user_signed_in?
end
def suggest_address?
user_signed_in?
end
def check_availability?
user_signed_in?
end
def complete_enrollment?
user_signed_in?
end
end
end
endSerialization
The existing AbdmAbhaSerializer will be used for all enrollment API responses. This serializer passes through the ABDM response data directly:
# Existing serializer: parts/abdm/serializers/abdm_abha_serializer.rb
class AbdmAbhaSerializer < BaseSerializer
def attributes(data, type = nil)
data = super
data[:data] = object[:data]
data
end
endAPI Request/Response Examples
1. Mobile Enrollment - Send OTP
Request:
POST /abdm/abha/abha_address_enrollments/mobile/send_otp
{
"mobile": "9876543210"
}Response:
{
"success": true,
"message": "OTP sent successfully",
"data": {
"txnId": "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6",
"message": "OTP has been sent to your mobile number"
}
}2. Mobile Enrollment - Verify OTP
Request:
POST /abdm/abha/abha_address_enrollments/mobile/verify_otp
{
"otp": "123456",
"txn_id": "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6"
}Response:
{
"success": true,
"message": "OTP verified successfully",
"data": {
"txnId": "updated-transaction-id",
"mobileLinked": true
}
}3. Suggest ABHA Address
Request:
POST /abdm/abha/abha_address_enrollments/suggest_address
{
"txn_id": "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6",
"firstName": "John",
"middleName": "",
"lastName": "Doe",
"dayOfBirth": "15",
"monthOfBirth": "06",
"yearOfBirth": "1990",
"email": "john.doe@example.com"
}Response:
{
"success": true,
"message": "ABHA address suggestions retrieved",
"data": {
"txnId": "updated-transaction-id",
"abhaAddressList": [
"john.doe",
"johndoe90",
"john_1990"
]
}
}4. Check ABHA Address Availability
Request:
GET /abdm/abha/abha_address_enrollments/check_availability?abhaAddress=johndoe@abdmResponse:
{
"success": true,
"message": "ABHA address is available",
"data": {
"status": false
}
}5. Complete Enrollment
Request:
POST /abdm/abha/abha_address_enrollments/complete_enrollment
{
"txn_id": "a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6",
"phrDetails": {
"mobile": "encrypted_mobile_value",
"firstName": "John",
"middleName": "",
"lastName": "Doe",
"yearOfBirth": "1990",
"dayOfBirth": "15",
"monthOfBirth": "06",
"gender": "M",
"email": "encrypted_email_value",
"profilePhoto": "",
"address": "123 Main Street",
"stateName": "Maharashtra",
"stateCode": "27",
"districtName": "Mumbai",
"districtCode": "519",
"pinCode": "400001",
"abhaAddress": "johndoe@abdm",
"password": "encrypted_password_value"
},
"clinic_patient_id": 123
}Response:
{
"success": true,
"message": "ABHA address created successfully",
"data": {
"phrAddress": "johndoe@abdm",
"abhaNumber": "91-1234-5678-9012",
"tokens": {
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 1800
},
"refreshTokenExpiresIn": 432000
}
}Error Handling
All API endpoints follow the existing ABDM error handling pattern:
# Standard error response format
{
"success": false,
"error": {
"code": "ABDM-001",
"message": "Invalid OTP",
"details": {
"field": "otp",
"reason": "OTP has expired or is invalid"
}
}
}Testing Strategy
1. Unit Tests
# spec/services/abdm/abha/enrollment/mobile/request_otp_spec.rb
# spec/interactors/abdm/abha/enrollment/mobile/send_otp_spec.rb
# spec/controllers/abdm/abha/enrollments_controller_spec.rb2. Integration Tests
# spec/requests/abdm/abha/enrollment_flow_spec.rb3. Factory Setup
# spec/factories/abha_enrollment_transactions.rb
FactoryBot.define do
factory :abha_enrollment_transaction do
clinic_patient
transaction_id { SecureRandom.uuid }
enrollment_type { 'mobile' }
status { 'initiated' }
expires_at { 10.minutes.from_now }
end
endConfiguration Requirements
Add to Rails credentials:
abdm_phr_host: "https://abhasbx.abdm.gov.in/abha/api"
abdm_hid_host_v3: "https://abhasbx.abdm.gov.in/abha/api"
abdm_cm_id: "sbx"Implementation Steps
-
Phase 1: Service Layer
- Implement all service classes for ABDM API communication
- Follow existing patterns from current ABDM services
- Test with ABDM sandbox environment
-
Phase 2: Business Logic
- Create lightweight interactors following existing patterns
- Add input validation and error handling
- No database transaction management required
-
Phase 3: API Layer
- Implement controller with all enrollment endpoints
- Configure routes in existing ABDM routes file
- Create authorization policy
-
Phase 4: Integration & Testing
- Integration testing with ABDM sandbox
- Error scenario testing
- Update clinic_patient fields after successful enrollment
Security Considerations
- All sensitive data (mobile, email, passwords) encrypted using RSA-OAEP
- Transaction IDs managed by ABDM, expire after their defined timeout
- All API requests logged via AbdmRequest for audit trail
- PII data excluded from logs
- Authentication required for all endpoints via user_signed_in?
Monitoring & Observability
- Track enrollment success/failure rates via AbdmRequest logs
- Monitor ABDM API response times
- Alert on repeated API failures
- Dashboard for enrollment analytics
This implementation strictly follows the existing ABDM module patterns and conventions, ensuring consistency and maintainability.