Billing Refund Requests
This document outlines the billing refund request system implemented in the KulCare platform, which allows users to request refunds for captured payments.
Overview
The billing refund request system provides a structured workflow for handling payment refunds. It includes request creation, status management, Slack notifications, and proper authorization controls.
Database Schema
BillingRefundRequest Model
CREATE TABLE billing_refund_requests (
id BIGINT PRIMARY KEY,
billing_payment_id BIGINT NOT NULL,
status VARCHAR DEFAULT 'PENDING' NOT NULL,
created_by_user_id BIGINT NOT NULL,
reason TEXT,
processed_at TIMESTAMP,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
-- Indexes
CREATE INDEX idx_billing_refund_requests_status ON billing_refund_requests(status);
CREATE INDEX idx_billing_refund_requests_payment_status ON billing_refund_requests(billing_payment_id, status);Status States
- PENDING: Initial state when refund request is created
- APPROVED: Request has been approved by admin
- REJECTED: Request has been rejected
- PROCESSED: Refund has been completed
API Endpoints
Create Refund Request
POST /billing_payments/:id/refund_request
Creates a new refund request for a billing payment.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | Integer | Yes | The billing payment ID |
reason | String | Yes | Reason for refund (max 500 characters) |
Request Example
curl -X POST "https://api.kulcare.com/billing_payments/123/refund_request" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Patient cancelled appointment due to emergency"
}'Response Example
{
"success": true,
"message": "Refund request created successfully",
"type": "create",
"data": {
"id": 456,
"status": "PENDING",
"reason": "Patient cancelled appointment due to emergency",
"processed_at": null,
"created_at": "2025-09-10T12:30:00Z",
"updated_at": "2025-09-10T12:30:00Z",
"billing_payment": {
"id": 123,
"razorpay_payment_id": "pay_abc123xyz",
"amount": 500.0,
"created_at": "2025-09-10T10:15:00Z"
}
}
}Error Responses
// Payment not found
{
"success": false,
"errors": [
{
"source": "billing_payment",
"title": "Billing Payment not found"
}
]
}
// Payment not captured
{
"success": false,
"errors": [
{
"source": "billing_payment",
"title": "Billing Payment is not captures"
}
]
}
// Payment already refunded
{
"success": false,
"errors": [
{
"source": "billing_payment",
"title": "Billing Payment is already refunded"
}
]
}
// Refund request already exists
{
"success": false,
"errors": [
{
"source": "refund_request",
"title": "Refund request already exists for this payment"
}
]
}
// Validation errors
{
"success": false,
"errors": {
"reason": ["can't be blank", "is too long (maximum is 500 characters)"]
}
}Business Logic
SaveRefundRequest Interactor
The BillingPayments::SaveRefundRequest interactor handles the refund request creation workflow:
- Parameter Validation: Ensures billing_payment_id is provided
- Payment Validation:
- Verifies payment exists
- Confirms payment is captured
- Checks payment hasn't been refunded already
- Duplicate Check: Ensures no pending/approved refund request exists for the payment
- Request Creation: Creates the refund request with PENDING status
- Notification: Queues Slack notification for admin review
Model Methods
The BillingRefundRequest model provides status management methods:
# Status transition methods
refund_request.approve! # Sets status to APPROVED, processed_at to current time
refund_request.reject! # Sets status to REJECTED, processed_at to current time
refund_request.mark_processed! # Sets status to PROCESSED, processed_at to current timeAuthorization
Policy Requirements
The BillingPaymentsControllerPolicy requires:
- User must be a clinic owner
- Verified through
user.user_accounts.clinic_accounts&.owner&.present?
Authorization Check
def refund_request?
user.user_accounts.clinic_accounts&.owner&.present?
endNotifications
Slack Integration
When a refund request is created, a Slack notification is sent to the appropriate channel:
- Production: Uses
Rails.application.credentials.slack_payment_support - Non-Production: Uses
Rails.application.credentials.slack_kulcare_bots
Notification Details
The Slack message includes:
- Request amount (formatted as ₹XXX.XX)
- Payment ID (Razorpay ID or internal ID)
- Requester details (name and phone)
- Refund reason
Retry Configuration
The SlackRefundRequestNotification job includes:
- Exponential backoff retry strategy
- Maximum 3 retry attempts
- Automatic failure handling with error raising
class SlackRefundRequestNotification < ActiveJob::Base
queue_as :slack_notifications
retry_on StandardError, wait: :exponentially_longer, attempts: 3
endIntegration Points
Payment Relationship
# BillingPayment model
has_one :billing_refund_request
# BillingRefundRequest model
belongs_to :billing_payment
belongs_to :created_by_user, class_name: 'User'Payment Serialization
The BillingPaymentSerializer now includes refund request details:
{
"payment_status": "captured",
"billing_refund_request": {
"id": 456,
"status": "PENDING",
"reason": "Patient cancelled appointment",
"processed_at": null,
"created_at": "2025-09-10T12:30:00Z"
}
}Performance Optimizations
Query Optimization
The TelemedicinePaymentActions::Index interactor has been optimized to prevent N+1 queries:
- Pagination First: Apply filters and paginate before loading associations
- Selective Loading: Include associations only for paginated results
- Eager Loading: Preload related data to minimize database queries
# Optimized query pattern
base_query = BillingPayment
.joins(:telemedicine_order)
.where(conditions)
paginated_payments = base_query.paginate(page: page, per_page: per_page)
payments_with_associations = BillingPayment
.where(id: paginated_payments.pluck(:id))
.includes(:billing_refund_request, ...)Error Handling
Validation Errors
The system validates:
- Reason presence: Required field for better UX
- Reason length: Maximum 500 characters
- Status values: Must be one of PENDING, APPROVED, REJECTED, PROCESSED
Business Logic Errors
- Prevents duplicate refund requests for same payment
- Ensures only captured payments can be refunded
- Blocks refund requests for already refunded payments
Notification Failures
- Slack notification failures are logged but don't fail the entire operation
- Retry mechanism handles transient network issues
- Error details are preserved for debugging
Security Considerations
- Authorization: Only clinic owners can create refund requests
- Validation: Input sanitization and length limits prevent abuse
- Audit Trail: All refund requests are logged with user and timestamp
- Status Management: Controlled state transitions prevent invalid states
Testing
Unit Tests
RSpec.describe BillingPayments::SaveRefundRequest do
it "creates refund request for valid payment" do
payment = create(:billing_payment, status: 'captured')
result = described_class.call(
id: payment.id,
reason: "Patient cancelled"
)
expect(result).to be_success
expect(result.refund_request.status).to eq('PENDING')
end
it "fails for already refunded payment" do
payment = create(:billing_payment, status: 'captured')
create(:billing_refund, billing_payment: payment)
result = described_class.call(id: payment.id, reason: "Test")
expect(result).to be_failure
expect(result.errors['billing_payment']).to include('is already refunded')
end
endIntegration Tests
RSpec.describe "POST /billing_payments/:id/refund_request" do
it "creates refund request successfully" do
payment = create(:billing_payment, status: 'captured')
post "/billing_payments/#{payment.id}/refund_request",
params: { reason: "Emergency cancellation" },
headers: auth_headers
expect(response).to have_http_status(:success)
expect(json_response['data']['status']).to eq('PENDING')
end
endMonitoring and Alerts
Key Metrics
- Number of refund requests created per day
- Average time from request to approval/rejection
- Refund request success/failure rates
- Slack notification delivery rates
Recommended Alerts
- High volume of refund requests in short time period
- Slack notification failures exceeding threshold
- Refund requests stuck in PENDING status for extended periods
Future Enhancements
- Admin Dashboard: Web interface for managing refund requests
- Automated Processing: Integration with payment gateway for automatic refunds
- Approval Workflow: Multi-step approval process for large amounts
- Reporting: Analytics and reporting on refund patterns
- Email Notifications: Complement Slack with email notifications