RentOne Payment Gateway Service
Version: 0.0.1-SNAPSHOT
Runtime: Java 21 · Spring Boot 3.5.8 · Spring Cloud 2025.0.0
Database: MongoDB (multi-tenant)
Payment Provider: Razorpay
Port: 6009
Eureka Service Name:payment-gateway
1. Service Overview
Purpose
The RentOne Payment Gateway Service is a dedicated microservice responsible for all payment-related operations within the RentOne platform. It acts as the single point of integration between the platform and external payment providers, currently Razorpay.
Core responsibilities:
- Accept payment initiation requests from upstream services or the frontend
- Create payment orders on Razorpay and return checkout parameters to the client
- Verify payment authenticity using HMAC-SHA256 signature validation
- Process asynchronous payment events via Razorpay webhooks
- Manage the full payment lifecycle, including refunds
- Notify the Commerce Service of all payment status changes
- Store complete payment audit trails per tenant in MongoDB
Position in RentOne Architecture
The service sits between the frontend/API Gateway, the Commerce Service, and Razorpay. It does not own order business logic — it receives a module order reference, processes the payment, and reports back.
flowchart TD
Client["Client / Frontend"]
Gateway["API Gateway"]
PGS["Payment Gateway Service\nPort 6009"]
Commerce["Commerce Service\nPort 6008"]
Notifications["Notifications Service"]
Razorpay["Razorpay\nExternal API"]
MongoDB["MongoDB\nPer-Tenant Database"]
Eureka["Eureka Discovery\nPort 6001"]
Client --> Gateway
Gateway --> PGS
Commerce -->|"POST /payment/initiate\nPOST /refund-payment"| PGS
PGS -->|"POST /orders/payment/status"| Commerce
PGS --> Razorpay
PGS --> Notifications
PGS --> MongoDB
PGS --> Eureka
Razorpay -->|Webhook Events| PGS
2. Payment Processing Workflow
Business Flow
flowchart TD
A["Client Requests Payment"] --> B["Validate Request\n& Generate Order ID"]
B --> C["Create Order on Razorpay"]
C --> D["Save Payment Record\nStatus: INITIATED"]
D --> E["Return Checkout Parameters\nto Client"]
E --> F["Customer Completes\nPayment in Browser"]
F --> G{"Payment Outcome"}
G -->|Success| H["Client Submits Verify Request\nwith Razorpay Params"]
G -->|Failure| I["Webhook: payment.failed\nReceived"]
H --> J["Validate HMAC Signature"]
J -->|Invalid| K["Reject — Return Error"]
J -->|Valid| L["Fetch Payment from Razorpay API"]
L --> M["Update Payment Status\nand Save to MongoDB"]
M --> N["Notify Commerce Service\nof Payment Result"]
I --> O["Update Status: FAILURE\nNotify Commerce Service"]
N --> P["Return Verification Response\nto Client"]
Data Flow — Payment Initiation
flowchart TD
Client["Client"] -->|POST /payment/initiate\nPaymentRequest + Tenant Header| PGS["Payment Gateway Service"]
PGS -->|Create Order\nAmount in paise, currency, notes| Razorpay["Razorpay API"]
Razorpay -->|razorpay_order_id| PGS
PGS -->|Save PaymentOrder\nStatus: INITIATED| MongoDB["MongoDB"]
PGS -->|PaymentInitiationResponse\norderId, gatewayOrderId, key, additionalData| Client
Data Flow — Payment Verification
flowchart TD
Client["Client"] -->|POST /razorpay/verify\nrazorpay_order_id, payment_id, signature| PGS["Payment Gateway Service"]
PGS -->|HMAC-SHA256 Verify\norderId + paymentId| Signature["Signature Check"]
Signature -->|Valid| RazorpayAPI["Razorpay API\n/v1/payments/:id"]
RazorpayAPI -->|Payment status, method, amount| PGS
PGS -->|Update Status + Save| MongoDB["MongoDB"]
PGS -->|POST /orders/payment/status| Commerce["Commerce Service"]
PGS -->|PaymentVerificationResponse| Client
REST API Summary
| Endpoint | Method | Purpose | Auth |
|---|---|---|---|
/rentone/payment-gateway/payment/initiate |
POST | Initiate a new payment order | JWT + Tenant |
/rentone/payment-gateway/razorpay/verify |
POST | Verify payment after checkout | JWT + Tenant |
/rentone/payment-gateway/refund-payment |
POST | Initiate a refund | JWT |
/rentone/payment-gateway/razorpay/webhook |
POST | Receive Razorpay webhook events | Signature only |
/rentone/payment-gateway/get-all/payment-history |
GET | Retrieve all payments with filters | JWT |
/rentone/payment-gateway/get-user-payment-history |
GET | Retrieve payment history for a user | JWT |
/rentone/payment-gateway/get/payment-count |
GET | Get total payment count for a user | JWT |
Business Rules
- Amount is submitted in INR as a decimal string and converted to paise (×100) before sending to Razorpay.
- If no
orderIdis provided in the request, one is auto-generated byPaymentOrderIdGeneratorUtil. - A payment record is created with status
INITIATEDbefore any Razorpay call succeeds, so every attempt is traceable. - Verification is only possible for orders that already exist in the database — unrecognised
razorpay_order_idvalues are rejected. - Commerce Service is notified on every verification result, whether success or failure.
Validation Rules
razorpay_order_id,razorpay_payment_id, andrazorpay_signatureare all required for verification.- HMAC-SHA256 signature must match; otherwise the request is rejected immediately.
- Tenant header (
x-tenant) is mandatory on all JWT-protected endpoints. - Bearer token must be present and valid.
Error Scenarios
| Scenario | HTTP Status | Response |
|---|---|---|
| Missing or invalid JWT | 401 | Empty body |
| Missing tenant header | 401 | Empty body |
| Invalid HMAC signature | 400 | Error message |
| Payment record not found | 400 | IllegalArgumentException message |
| Razorpay API failure | 500 | Generic error message |
| Refund on non-successful payment | 200 | success: false with reason |
2a. Commerce Service ↔ Payment Gateway Integration
This is a bidirectional integration. Commerce Service calls Payment Gateway to start and refund payments. Payment Gateway calls Commerce Service back to report payment results.
Relationship Overview
flowchart LR
CS["Commerce Service\nPort 6008"]
PGS["Payment Gateway\nPort 6009"]
CS -->|"① POST /payment/initiate\n PaymentRequest"| PGS
PGS -->|" PaymentInitiationResponse\n gatewayOrderId + checkout config"| CS
CS -->|"② POST /refund-payment\n RefundRequest"| PGS
PGS -->|"③ POST /orders/payment/status\n PaymentVerificationResponse"| CS
Three interactions happen across the two services:
| # | Direction | Trigger | Endpoint |
|---|---|---|---|
| ① | Commerce → Payment Gateway | Customer checks out or pays monthly rent | POST /rentone/payment-gateway/payment/initiate |
| ② | Commerce → Payment Gateway | Order cancellation requested | POST /rentone/payment-gateway/refund-payment |
| ③ | Payment Gateway → Commerce | Payment verified (frontend or webhook) | POST /orders/payment/status |
Interaction ① — Payment Initiation
Commerce Service calls this when a customer checks out a new order or pays a monthly rental instalment.
Who calls it: CommerceService → PaymentGatewayProxy.initiatePayment()
Feign client (Commerce side):
@FeignClient(name = "payment-gateway")
POST /rentone/payment-gateway/payment/initiate
Two module types Commerce sends:
| moduleName | When used | moduleOrderId contains |
|---|---|---|
ORDER_PAID |
New order checkout | OrderDraft.id |
RENTAL_MONTHLY |
Monthly rent payment | Orders.id |
What Commerce sends (PaymentRequest):
| Field | Source in Commerce |
|---|---|
moduleName |
"ORDER_PAID" or "RENTAL_MONTHLY" |
moduleOrderId |
draft.getId() or order.getId() |
amount |
draft.getPricingSummary().getPayableNow() or monthlyPayable + penalty |
gstAmount |
draft.getPricingSummary().getGstAmount() |
currency |
"INR" (hardcoded) |
userId |
draft.getUserId() |
customerName |
draft.getAddress().getName() |
customerPhone |
draft.getAddress().getPhone() |
moduleCallbackUrl |
"http://localhost:6008/orders/payment/status" |
What Payment Gateway returns (PaymentInitiationResponse):
Commerce uses gatewayOrderId to store as OrderDraft.paymentIntentId, sets draft status to PENDING_PAYMENT, then returns checkout config to the frontend. The frontend uses additionalData.key and gatewayOrderId to launch the Razorpay checkout widget.
flowchart TD
A["Commerce: Customer Checks Out"] --> B["Create OrderDraft\nCalculate pricing"]
B --> C["Build PaymentRequest\nmoduleName, moduleOrderId, amount"]
C --> D["POST /payment/initiate\n→ Payment Gateway"]
D --> E["Payment Gateway:\nCreate Razorpay Order\nSave payment record INITIATED"]
E --> F["Return gatewayOrderId\n+ checkout config"]
F --> G["Commerce: Save paymentIntentId on Draft\nSet status = PENDING_PAYMENT"]
G --> H["Return CheckoutResponse\nto Frontend"]
H --> I["Frontend:\nLaunch Razorpay Checkout Widget"]
Interaction ② — Refund
Commerce Service calls this when an order is cancelled. It only calls this path for orders in PAID, CONFIRMED, or SCHEDULED status that have a successful PaymentInfo record.
Who calls it: CommerceService → PaymentGatewayProxy.refundPayment()
Feign client (Commerce side):
@FeignClient(name = "payment-gateway")
POST /rentone/payment-gateway/refund-payment
What Commerce sends (RefundRequest):
| Field | Source in Commerce |
|---|---|
userId |
order.getUserId() |
transactionId |
First successful PaymentInfo.transactionId (Razorpay pay_xxx ID) |
reason |
request.getCancelReason() |
amount |
Not set — defaults to full refund |
What happens on success: Commerce sets order.status = CANCELLED, sends a cancellation notification. If the refund call returns success: false, the order is NOT cancelled.
flowchart TD
A["POST /cancel-order"] --> B["Load Order\nValidate status is\nPAID / CONFIRMED / SCHEDULED"]
B --> C["Find first successful\nPaymentInfo record"]
C --> D["Build RefundRequest\nuserId, transactionId, reason"]
D --> E["POST /refund-payment\n→ Payment Gateway"]
E --> F{"success?"}
F -->|"Yes"| G["Set order status = CANCELLED\nSend cancellation notification"]
F -->|"No"| H["Return failure\nOrder unchanged"]
Interaction ③ — Payment Status Callback
Payment Gateway calls this after a payment is verified (either via /razorpay/verify by the frontend, or via the payment.captured / payment.failed webhook from Razorpay).
Who calls it: PaymentGateway → CommerceServiceProxy.notifyPaymentStatus()
Feign client (Payment Gateway side):
@FeignClient(name = "commerce-service")
POST /orders/payment/status
What Payment Gateway sends (PaymentVerificationResponse):
| Field | Value |
|---|---|
orderDraftId |
The moduleOrderId that was passed during initiation |
moduleName |
"ORDER_PAID" or "RENTAL_MONTHLY" |
status |
SUCCESS or FAILURE |
gatewayPaymentId |
Razorpay pay_xxx ID |
gatewayRefNo |
Bank transaction reference |
amount |
Amount in INR |
gstAmount |
GST component |
How Commerce routes on receipt:
flowchart TD
A["POST /orders/payment/status\nreceived from Payment Gateway"] --> B{"moduleName?"}
B -->|"ORDER_PAID"| C["handleInitialPayment"]
B -->|"RENTAL_MONTHLY"| D["handleMonthlyPayment"]
B -->|"other"| E["Throw RuntimeException\nUnsupported payment module"]
C --> F["Look up OrderDraft\nby orderDraftId"]
F --> G{"Order already\nexists for cartId?"}
G -->|"Yes — idempotent"| H["Return: Order already processed"]
G -->|"No"| I{"status ==\nSUCCESS?"}
I -->|"No"| J["Set Draft status = PAYMENT_FAILED\nReturn failure"]
I -->|"Yes"| K["Validate amount matches\ndraft.pricingSummary.payableNow"]
K --> L["Create Orders document\nstatus=PAID\nBillingCycle=MONTHLY\nnextPaymentDate=+1 month"]
L --> M["Append PaymentInfo\n(type=MONTHLY_RENT)"]
M --> N["Delete OrderDraft + Cart\nCreate OrderHistory\nSend order-placed notification"]
N --> O["Return: Order placed successfully"]
D --> P["Look up Orders\nby orderDraftId"]
P --> Q["Validate amount ==\nmonthlyPayable + overduePenalty"]
Q --> R{"status ==\nSUCCESS?"}
R -->|"No"| S["Record lastPaymentAttempt\nReturn failure"]
R -->|"Yes"| T["Append PaymentInfo\n(type=MONTHLY_RENT)"]
T --> U["nextPaymentDate += 1 month\nReset overdue flags\nSubscriptionStatus = ACTIVE"]
U --> V["Return: Monthly rent paid"]
Key Cross-Service Business Rules
These rules are enforced in Commerce Service when it receives the callback from Payment Gateway:
-
Amount validation is independent. Commerce re-derives the expected amount from its own pricing records. A mismatch throws an exception — the amount in the callback is not trusted blindly.
-
Initial payment is idempotent. Before creating an order, Commerce checks
orderRepository.existByCartId(). If an order for that cart already exists, it returns success without creating a duplicate. -
Draft-to-Order conversion is atomic. On success, Commerce creates the
Ordersdocument, deletes theOrderDraft, deletes theCart, and creates anOrderHistoryrecord in a single flow. -
Payment failure does not delete the draft. On
status != SUCCESSforORDER_PAID, Commerce only marks the draftPAYMENT_FAILED— the draft persists so the customer can retry. -
Monthly payment validates penalty. The expected amount for
RENTAL_MONTHLYismonthlyPayable + calculateOverduePenalty(order). A partial or incorrect amount is rejected. -
Refund blocks order cancellation. If
paymentGatewayProxy.refundPayment()returnssuccess: false, the order status is NOT updated — the order stays in its current state. -
Commerce Service owns order state. Payment Gateway only stores payment audit records. All order lifecycle transitions (PENDING_PAYMENT → PAID → CANCELLED) happen exclusively in Commerce Service.
3. Razorpay Integration
Integration Flow
flowchart TD
A["Receive Payment Request"] --> B["Load Gateway Config\nfrom payment-gateway.yml"]
B --> C["Build Razorpay Order JSON\namount paise, currency, receipt, notes"]
C --> D["Call Razorpay Orders API\nPOST /v1/orders"]
D --> E["Store razorpay_order_id\nin PaymentOrder"]
E --> F["Return Checkout Config\nto Frontend"]
F --> G["User Pays via\nRazorpay Checkout SDK"]
G --> H["Frontend Receives\nPayment Credentials"]
H --> I["POST /razorpay/verify\nwith three params"]
I --> J["HMAC-SHA256 Signature\nVerification"]
J --> K["Fetch Payment Details\nGET /v1/payments/:id"]
K --> L["Update Payment Record\nand Notify Commerce"]
Order Creation
When a payment is initiated:
- Amount in INR is multiplied by 100 to convert to paise.
- A Razorpay order is created with
amount,currency,receipt(internal order ID), andnotescontainingmodule_name,module_order_id,customer_email, andtenant_id. - The Razorpay-assigned
order_id(e.g.order_xxx) is stored asgatewayOrderIdin the payment record. - The frontend receives the Razorpay
key-id, order details, and prefill data to launch the checkout widget.
Signature Verification
Payment verification uses HMAC-SHA256:
payload = razorpay_order_id + "|" + razorpay_payment_id
expected = HMAC-SHA256(key-secret, payload)
The expected value must exactly match razorpay_signature submitted by the client. Any mismatch results in immediate rejection.
External APIs Used
| API | Purpose |
|---|---|
POST https://api.razorpay.com/v1/orders |
Create a payment order |
GET https://api.razorpay.com/v1/payments/:id |
Fetch payment details post-verification |
POST https://api.razorpay.com/v1/payments/:id/refund |
Initiate a refund |
Security Mechanisms
- All credentials (
key-id,key-secret,webhook-secret) are injected via environment variables — never hardcoded. - Signature verification is performed client-side AND re-validated server-side before any status update.
- Webhook signature is verified using the
webhook-secretviarazorpay-javaSDKUtils.verifyWebhookSignature().
Retry Logic
- Spring Retry is enabled (
@EnableRetry). - Feign client built-in retry is disabled; exponential backoff is configured for Commerce Service calls:
- Max attempts: 3
- Initial delay: 1000ms
- Multiplier: 2×
- Max delay: 10000ms
Error Handling
RazorpayExceptionduring order creation throwsRuntimeExceptionwith message "Failed to create Razorpay order".RazorpayExceptionduring verification throwsRuntimeExceptionwith message "Failed to verify Razorpay payment".- Refund failures return
StatusResponse(false, "Refund initiation failed")without throwing. - Razorpay client initialisation failures are logged as errors but do not prevent the service from starting.
4. Payment Verification Flow
Business Flow
flowchart TD
A["Client Submits Verification\nrazorpay_order_id, payment_id, signature"] --> B["Extract Three Parameters"]
B --> C["Compute Expected Signature\nHMAC-SHA256"]
C --> D{"Signature Match?"}
D -->|No| E["Reject Request\nReturn Error"]
D -->|Yes| F["Fetch Payment Details\nfrom Razorpay API"]
F --> G["Look Up Internal Payment\nby razorpay_order_id"]
G --> H{"Record Found?"}
H -->|No| I["Throw Not Found Error"]
H -->|Yes| J["Map Razorpay Status\nto Internal Status"]
J --> K["Update Payment Record\nStatus, Method, RefNo, Timestamps"]
K --> L["Build Verification Response"]
L --> M["Notify Commerce Service"]
M --> N["Return Response to Client"]
Data Flow
flowchart LR
Client["Client"] -->|3 Razorpay params| Verify["Verify Endpoint"]
Verify --> SigCheck["HMAC Signature\nValidation"]
SigCheck -->|Pass| RazorpayFetch["Fetch from\nRazorpay API"]
RazorpayFetch -->|status, method, amount,\nacquirer_data| StatusMap["Map Status\n& Method"]
StatusMap --> DB["Update MongoDB\nPaymentOrder"]
DB --> Commerce["Notify\nCommerce Service"]
Commerce --> Response["Return\nVerificationResponse"]
Validation Rules
| Check | Rule |
|---|---|
razorpay_order_id |
Must be present and non-empty |
razorpay_payment_id |
Must be present and non-empty |
razorpay_signature |
Must match HMAC-SHA256 of order_id\|payment_id |
| Payment record | Must exist in MongoDB; otherwise rejected |
Status Mapping
| Razorpay Status | Internal Status |
|---|---|
captured, authorized |
SUCCESS |
failed |
FAILURE |
created, pending |
PENDING |
refunded |
REFUNDED |
| Any other | PROCESSING |
Payment Method Mapping
| Razorpay Method | Internal Method |
|---|---|
card |
CREDIT_CARD |
upi |
UPI |
netbanking |
NET_BANKING |
wallet |
WALLET |
google_pay, gpay |
GOOGLE_PAY |
phonepe |
PHONEPE |
paytm |
PAYTM |
| Unknown | UNKNOWN |
Failure Scenarios
- Signature mismatch: Request rejected immediately; no database or Razorpay call is made.
- Payment not found:
IllegalArgumentExceptionreturned as HTTP 400. - Razorpay API error:
RuntimeExceptionpropagates and is caught byMainExceptionHandler, reported to Notifications Service.
5. Payment Callback / Webhook
Business Flow
flowchart TD
A["Razorpay Sends\nWebhook Event"] --> B["Extract X-Razorpay-Signature\nHeader"]
B --> C{"Signature\nPresent?"}
C -->|No| D["Return 400\nMissing Signature"]
C -->|Yes| E["Verify Signature\nUsing Webhook Secret"]
E -->|Invalid| F["Return 400\nInvalid Signature"]
E -->|Valid| G["Parse Event Type"]
G --> H{"Event Type"}
H -->|payment.captured| I["Update Status: SUCCESS\nNotify Commerce Service"]
H -->|payment.failed| J["Update Status: FAILURE\nNotify Commerce Service"]
H -->|refund.created| K["Log Audit Confirmation"]
H -->|refund.processed| L["Update Refund Doc: REFUNDED\nUpdate Original Payment: REFUNDED"]
H -->|Other| M["Log and Ignore"]
I --> N["Return 200 OK"]
J --> N
K --> N
L --> N
Data Flow
flowchart LR
Razorpay["Razorpay"] -->|POST webhook payload\nX-Razorpay-Signature header| PGS["Payment Gateway Service"]
PGS --> SigVerify["Verify Webhook\nSignature"]
SigVerify -->|Valid| EventParser["Parse JSON\nEvent Payload"]
EventParser --> DB["Look Up Payment\nby gatewayOrderId"]
DB --> StatusUpdate["Update Payment\nStatus in MongoDB"]
StatusUpdate --> Commerce["Notify\nCommerce Service"]
Webhook Endpoint
POST /rentone/payment-gateway/razorpay/webhook
- No JWT authentication required — this endpoint is exempt from the
JwtAuthenticationFilter. - Authentication is performed exclusively via the
X-Razorpay-Signatureheader using thewebhook-secret.
Supported Webhook Events
| Event | Action |
|---|---|
payment.captured |
Marks payment SUCCESS, records gatewayPaymentId and payment method, notifies Commerce Service |
payment.failed |
Marks payment FAILURE, notifies Commerce Service |
refund.created |
Confirms refund document existence (audit log only) |
refund.processed |
Marks refund document REFUNDED, marks original payment REFUNDED |
| All others | Logged and ignored |
Security Validation
The webhook secret is stored under the payment.gateways.razorpay.credentials.webhook-secret configuration key, injected via the RAZORPAY_WEBHOOK_SECRET environment variable. The razorpay-java SDK's Utils.verifyWebhookSignature() computes and compares the HMAC-SHA256 of the raw payload body.
Idempotency
The payment_webhook_events collection stores a unique compound index on (gateway, gatewayEventId). Before writing a duplicate event, PaymentWebhookEventService.existsByGatewayEventId() can be used to check existence. The refund.processed handler also performs an explicit status check: if the refund document already has status REFUNDED, it skips processing and logs an idempotency notice.
Error Handling
- Empty or blank payload: logged as warning, processing skipped.
- Webhook secret not configured: signature verification returns
false, request is rejected with 400. - Payment not found for a
payment.capturedorpayment.failedevent: logged as warning, processing skipped without error response. - Exceptions in refund handlers are caught and logged; they do not propagate to return a non-200 response to Razorpay.
6. Payment State Management
State Diagram
stateDiagram-v2
[*] --> INITIATED : Payment initiation request received
INITIATED --> PENDING : Awaiting customer action
PENDING --> PROCESSING : Payment being processed by gateway
PROCESSING --> SUCCESS : Payment captured / authorized
PROCESSING --> FAILURE : Payment declined or error
INITIATED --> FAILURE : Webhook payment.failed received
INITIATED --> SUCCESS : Webhook payment.captured received
SUCCESS --> REFUND_INITIATED : Refund request submitted
REFUND_INITIATED --> REFUNDED : Webhook refund.processed received
FAILURE --> [*] : Terminal state
ABORTED --> [*] : Terminal state
EXPIRED --> [*] : Terminal state
REFUNDED --> [*] : Terminal state
State Transition Reference
| From State | To State | Trigger |
|---|---|---|
| (new) | INITIATED |
Payment initiation API call |
INITIATED |
PENDING |
Customer opens checkout but has not paid |
PENDING |
PROCESSING |
Payment submitted to bank/gateway |
PROCESSING |
SUCCESS |
Gateway reports captured or authorized |
PROCESSING |
FAILURE |
Gateway reports failed |
INITIATED |
SUCCESS |
Webhook payment.captured arrives |
INITIATED |
FAILURE |
Webhook payment.failed arrives |
SUCCESS |
REFUND_INITIATED |
Refund API call, refund doc created |
REFUND_INITIATED |
REFUNDED |
Webhook refund.processed received |
| Any | ABORTED |
Payment abandoned (manual/admin action) |
| Any | EXPIRED |
Payment order expired before completion |
Notes
ABORTEDandEXPIREDare defined in the enum but are not yet set by automated transitions in the current codebase.- Refunds create a new
PaymentOrderdocument withpaymentType = REFUNDandorderId = "REF-" + originalOrderId. The original payment document is also updated to reflect the refund status. - Status transitions are not enforced as a strict state machine at the code level; logic is distributed across the
RazorpayServiceandRazorpayWebhookServiceclasses.
7. REST API Reference
POST /rentone/payment-gateway/payment/initiate
Initiates a new payment order with Razorpay and returns all parameters needed to launch the checkout widget.
Authentication: Bearer JWT + x-tenant header
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
orderId |
String | No | Internal order ID; auto-generated if absent |
amount |
String | Yes | Amount in INR (e.g. "499.00") |
gstAmount |
String | No | GST component of the amount |
currency |
String | Yes | Currency code (e.g. "INR") |
userId |
String | Yes | Customer ID |
moduleName |
String | Yes | Originating module (e.g. "RENTAL") |
moduleOrderId |
String | Yes | Order ID from the calling module |
customerName |
String | No | Customer display name for prefill |
customerEmail |
String | No | Customer email for prefill |
customerPhone |
String | No | Customer phone for prefill |
moduleCallbackUrl |
String | No | Callback URL for the originating module |
customParams |
Map | No | Arbitrary key-value metadata |
Response Body: PaymentInitiationResponse
| Field | Type | Description |
|---|---|---|
orderId |
String | Internal payment order ID |
gateway |
String | RAZORPAY |
gatewayOrderId |
String | Razorpay order ID (e.g. order_xxx) |
paymentToken |
String | Same as gatewayOrderId |
additionalData |
Object | Razorpay checkout config: key, amount, currency, prefill, theme |
initiatedAt |
Date | Timestamp of initiation |
POST /rentone/payment-gateway/razorpay/verify
Verifies a completed Razorpay payment. Called by the frontend after the user completes checkout.
Authentication: Bearer JWT + x-tenant header
Request Body: Map of strings
| Key | Required | Description |
|---|---|---|
razorpay_order_id |
Yes | Razorpay order ID |
razorpay_payment_id |
Yes | Razorpay payment ID |
razorpay_signature |
Yes | HMAC-SHA256 signature from Razorpay |
Response Body: PaymentVerificationResponse
| Field | Type | Description |
|---|---|---|
orderDraftId |
String | Module order ID for the originating module |
moduleName |
String | Originating module name |
status |
PaymentStatus | Result: SUCCESS, FAILURE, etc. |
gatewayPaymentId |
String | Razorpay payment ID |
gatewayRefNo |
String | Bank transaction reference number |
paymentMethod |
String | Payment method used |
amount |
String | Amount in INR |
currency |
String | Currency code |
statusMessage |
String | Human-readable status description |
metadata |
Map | Raw Razorpay order/payment IDs |
POST /rentone/payment-gateway/refund-payment
Initiates a refund against a captured payment.
Authentication: Bearer JWT
Request Body: RefundRequest
| Field | Type | Required | Description |
|---|---|---|---|
userId |
String | Yes | Requesting user ID |
transactionId |
String | Yes | Razorpay payment ID (gatewayPaymentId) |
amount |
BigDecimal | No | Partial refund amount; full amount if absent |
reason |
String | No | Reason for refund; defaults to ORDER_CANCELLED |
Response Body: StatusResponse
| Field | Type | Description |
|---|---|---|
success |
Boolean | Whether refund was initiated successfully |
message |
String | Status description |
data |
String | Razorpay refund ID on success |
Business Rules:
- Refund is only allowed when the original payment has status SUCCESS.
- Partial refunds are supported when an amount is provided.
POST /rentone/payment-gateway/razorpay/webhook
Receives asynchronous webhook events from Razorpay.
Authentication: X-Razorpay-Signature header (no JWT)
Request: Raw JSON string payload as body
Response: 200 OK with body "OK" on success; 400 on signature failure.
GET /rentone/payment-gateway/get-all/payment-history
Returns paginated, filterable list of all payment records. Intended for admin/support use.
Authentication: Bearer JWT
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
searchPattern |
String | No | Regex search across orderId, email, gateway IDs |
userId |
String | No | Filter by customer ID |
status |
PaymentStatus | No | Filter by payment status |
method |
PaymentMethod | No | Filter by payment method |
startDate |
String | No | Date range start dd-MM-yyyy HH:mm (IST) |
endDate |
String | No | Date range end dd-MM-yyyy HH:mm (IST) |
sort |
SortDirection | No | ASC or DESC on createdAt; default ASC |
page |
Integer | No | Page number (1-based) |
size |
Integer | No | Page size |
Response Body: PaymentListResponse
| Field | Type | Description |
|---|---|---|
success |
Boolean | Whether records were found |
message |
String | Status message |
totalCount |
Long | Total matching records |
data |
List | Full PaymentOrder documents |
GET /rentone/payment-gateway/get-user-payment-history
Returns a simplified payment history for a specific customer.
Authentication: Bearer JWT
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
userId |
Yes | Customer ID |
page |
No | Page number (1-based) |
size |
No | Page size |
Response Body: UsersPaymentResponse — contains a simplified UserPaymentsResponseDTO list with: orderId, moduleName, moduleOrderId, amount, status, paymentMethod, paymentType, gatewayOrderId, customerId, customerEmail, createdAt.
GET /rentone/payment-gateway/get/payment-count
Returns total number of payment records for a user.
Authentication: Bearer JWT
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
userId |
Yes | Customer ID |
Response Body: LongStatusResponse — success, message, data (Long count).
8. MongoDB Collections
Collection: payments
The primary collection. Stores every payment and refund transaction.
| Field | Type | Purpose |
|---|---|---|
_id (orderId) |
String | Internal unique payment identifier |
tenantId |
String | Tenant this payment belongs to |
paymentGateway |
Enum | RAZORPAY, PAYTM, CC_AVENUE |
paymentType |
Enum | PAYMENT or REFUND |
moduleName |
String | Originating business module |
moduleOrderId |
String | Order ID from the originating module |
amount |
String | Payment amount in INR |
gstAmount |
String | GST component |
currency |
String | Currency code (e.g. INR) |
status |
Enum | Current payment status |
gatewayOrderId |
String | Razorpay order ID (order_xxx) |
gatewayPaymentId |
String | Razorpay payment ID (pay_xxx) |
gatewayRefNo |
String | Bank transaction reference |
gatewayRefundId |
String | Razorpay refund ID (rfnd_xxx); sparse index |
originalOrderId |
String | For refunds: links back to original payment |
refundAmount |
String | Refund amount |
refundReason |
String | Reason for refund |
customerId |
String | Customer / user ID |
customerEmail |
String | Customer email |
paymentMethod |
Enum | Method used (UPI, Card, etc.) |
paymentInstrument |
String | Specific instrument (UPI ID, card name) |
statusMessage |
String | Human-readable status description |
moduleCallbackUrl |
String | Callback URL for originating module |
gatewayRequest |
String | Raw gateway request (audit) |
gatewayResponse |
String | Raw gateway response (audit) |
customParams |
String | Arbitrary metadata |
routingStrategy |
String | PRIMARY, FALLBACK, COST_OPTIMIZED |
retryCount |
Integer | Number of retry attempts |
fallbackGateway |
String | Gateway used as fallback |
createdAt |
Date | Record creation time |
updatedAt |
Date | Last update time |
completedAt |
Date | Time payment reached terminal state |
expiresAt |
Date | Expiry time of the payment order |
Relationships:
- Refund documents link to originals via originalOrderId.
- gatewayRefundId has a sparse unique index (null values excluded).
- Queried by gatewayOrderId, gatewayPaymentId, gatewayRefundId via custom repository.
Collection: payment_webhook_events
Stores a de-duplication record for every processed webhook event.
| Field | Type | Purpose |
|---|---|---|
_id |
Long | Auto-incremented sequence ID |
gateway |
String | Gateway name (e.g. RAZORPAY) |
gatewayEventId |
String | Unique event identifier from gateway |
receivedAt |
Date | Timestamp event was received |
Indexes:
- Unique compound index on (gateway, gatewayEventId) — prevents duplicate event processing.
Collection: db_sequences
Internal collection managed by SequenceGeneratorService. Stores named auto-increment counters.
| Field | Type | Purpose |
|---|---|---|
_id |
String | Sequence name (e.g. webhook_event_sequence) |
seq |
Long | Current sequence value |
9. Business Rules
The following rules are directly derived from the source code and are enforced at runtime:
-
Signature verification is mandatory. Payment verification is rejected immediately if the HMAC-SHA256 signature does not match. No database lookup is attempted on invalid signatures.
-
Refunds are only permitted on successful payments. If the original payment does not have status
SUCCESS, the refund is rejected with a clear message. -
A payment record is created before Razorpay is called. The
INITIATEDrecord exists in MongoDB from the moment the initiation request is accepted, enabling full audit traceability. -
Commerce Service is always notified. Every verification result — success or failure — triggers a notification to the Commerce Service via Feign client, including on webhook events.
-
Webhook events do not require JWT. The webhook endpoint bypasses JWT validation intentionally and uses signature-based authentication only.
-
Duplicate webhooks are safe. Refund events with
refund.processedare idempotency-checked against the stored status before any updates are applied. -
Refund creates a new document. A refund does not modify only the original payment; it creates a new document with
paymentType = REFUNDandorderId = "REF-" + originalOrderId. -
Both the refund document and original payment are updated on refund completion. When
refund.processedarrives, both records have their status set toREFUNDED. -
Amount conversion is always applied. All amounts sent to Razorpay are multiplied by 100 (INR → paise). All amounts received from Razorpay are divided by 100 before storage.
-
Unrecognised
razorpay_order_idvalues are rejected. If no internal payment record matches therazorpay_order_idduring verification, the request fails with an error. -
Payments are multi-tenant. Every payment is scoped to a
tenantId. MongoDB connections are resolved dynamically per tenant using thex-tenantheader. -
Only INR is supported. The gateway configuration declares
INRas the only supported currency for Razorpay.
10. Commerce Service Integration
The Commerce Service is the primary downstream consumer of payment outcomes. This section documents the complete integration contract between the two services as implemented in the Payment Gateway Service source code.
Relationship Overview
The Payment Gateway Service is a producer of payment events. The Commerce Service is a consumer that reacts to those events to advance order state. The Payment Gateway Service never reads from the Commerce Service — the relationship is strictly one-directional, push-based notification.
flowchart LR
PGS["Payment Gateway Service"]
CS["Commerce Service\n(Eureka: commerce-service)"]
PGS -->|"POST /orders/payment/status\n+ x-tenant header\nPaymentVerificationResponse"| CS
When the Notification Is Sent
The Commerce Service is called in three distinct situations, all using the same Feign endpoint:
| Trigger | Sending Component | Payment Status in Payload |
|---|---|---|
Frontend calls /razorpay/verify (success or failure) |
PaymentGatewayController |
Whatever Razorpay returned |
Razorpay sends payment.captured webhook |
RazorpayWebhookService |
SUCCESS |
Razorpay sends payment.failed webhook |
RazorpayWebhookService |
FAILURE |
This means Commerce Service can receive the payment result via two paths for the same payment — the synchronous verify call from the frontend and the asynchronous webhook from Razorpay. The Commerce Service is responsible for handling that idempotently.
Integration Contract
Endpoint called on Commerce Service:
POST /orders/payment/status
Header: x-tenant: {tenantId}
Payload sent — PaymentVerificationResponse:
| Field | Type | Populated By | Description |
|---|---|---|---|
orderDraftId |
String | Both paths | The moduleOrderId stored on the payment — links back to the Commerce order |
moduleName |
String | Verify + webhook.failed | Module name (e.g. RENTAL) — not sent on payment.captured webhook path |
status |
PaymentStatus | Both paths | SUCCESS or FAILURE |
gatewayPaymentId |
String | Verify path + webhook.captured | Razorpay pay_xxx ID |
gatewayRefNo |
String | Verify path only | Bank transaction reference from acquirer_data.bank_transaction_id |
paymentMethod |
String | Verify path only | Payment method string (e.g. upi, card) |
amount |
String | Both paths | Amount in INR |
gstAmount |
String | Not currently set | Reserved field |
currency |
String | Both paths | Currency code (e.g. INR) |
statusMessage |
String | Verify path only | Human-readable status |
metadata |
Map | Verify path only | Contains razorpay_order_id and razorpay_payment_id |
Important difference between paths:
The webhook path (payment.captured) builds a minimal payload — it sets orderDraftId, status, gatewayPaymentId, amount, and currency only. The synchronous verify path builds a richer payload that also includes moduleName, gatewayRefNo, paymentMethod, statusMessage, and metadata.
Integration Flow — Synchronous Path
flowchart TD
Client["Frontend"] -->|"POST /razorpay/verify\nrazorpay_order_id, payment_id, signature"| PGS["Payment Gateway Service"]
PGS --> SigVerify["Validate HMAC Signature"]
SigVerify --> RazorpayAPI["Fetch Payment\nfrom Razorpay API"]
RazorpayAPI --> UpdateDB["Update Payment Status\nin MongoDB"]
UpdateDB --> NotifyCS["POST /orders/payment/status\nto Commerce Service"]
NotifyCS --> ReturnClient["Return Response\nto Frontend"]
Integration Flow — Webhook Path
flowchart TD
Razorpay["Razorpay"] -->|"POST /razorpay/webhook"| PGS["Payment Gateway Service"]
PGS --> SigCheck["Verify Webhook\nSignature"]
SigCheck --> EventType{"Event Type"}
EventType -->|"payment.captured"| Captured["Update Status: SUCCESS\nStore gatewayPaymentId"]
EventType -->|"payment.failed"| Failed["Update Status: FAILURE"]
Captured --> NotifyCS1["POST /orders/payment/status\nstatus=SUCCESS"]
Failed --> NotifyCS2["POST /orders/payment/status\nstatus=FAILURE"]
The orderDraftId — Key Linking Field
The field that ties the payment record to the Commerce order is orderDraftId in the notification payload. This is populated from moduleOrderId stored on the PaymentOrder document, which was originally passed in from the Commerce Service when it called /payment/initiate. This forms the traceability chain:
Commerce Order ID → PaymentOrder.moduleOrderId → PaymentVerificationResponse.orderDraftId → Commerce Service handles order update
Feign Client Configuration
| Setting | Value |
|---|---|
| Service discovery | Eureka name commerce-service |
| Connect timeout | 5000 ms |
| Read timeout | 10000 ms |
| Follows redirects | Yes |
| Built-in Feign retry | Disabled |
| Spring Retry | 3 attempts, 1s → 2s → 4s exponential backoff (max 10s) |
| Auth propagation | JWT token from AuthTokenStorage injected via CustomFeignInterceptor |
| Log level | BASIC |
Auth Token Propagation
The JWT Bearer token from the original inbound request is stored in AuthTokenStorage (a thread-local) by JwtAuthenticationFilter. The CustomFeignInterceptor reads it and injects it as the Authorization header on all outbound Feign calls, including to the Commerce Service. For webhook-triggered calls, the tenantId is read from the stored PaymentOrder.tenantId field rather than from an inbound request header.
Failure Behaviour
If the Commerce Service is unreachable or returns an error:
- Spring Retry retries the call up to 3 times.
- After exhausting retries, the exception propagates to
MainExceptionHandler. - The error is reported to the Notifications Service with full context.
- On the synchronous path, the exception surfaces as HTTP 500 to the frontend — the payment record in MongoDB is already updated regardless.
- On the webhook path, the exception is caught within the handler; Razorpay receives HTTP 200 and does not retry the webhook. Manual reconciliation may be needed if the Commerce Service was not notified.
11. External Dependencies
Dependency Diagram
flowchart LR
PGS["Payment Gateway Service"]
Razorpay["Razorpay\nExternal Payment API"]
Commerce["Commerce Service\nEureka: commerce-service"]
Notifications["Notifications Service\nEureka: notifications"]
Eureka["Eureka Discovery Server\nPort 6001"]
SharedUtils["rentone-shared-utils\nInternal Library"]
MongoDB["MongoDB\nPer-Tenant Database"]
PGS --> Razorpay
PGS --> Commerce
PGS --> Notifications
PGS --> Eureka
PGS --> SharedUtils
PGS --> MongoDB
Razorpay
| Item | Detail |
|---|---|
| Purpose | Create payment orders, fetch payment details, initiate refunds |
| Integration | razorpay-java SDK v1.4.8 |
| APIs Used | POST /v1/orders, GET /v1/payments/:id, POST /v1/payments/:id/refund |
| Authentication | API Key ID + Key Secret |
| Configuration | RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET, RAZORPAY_WEBHOOK_SECRET env vars |
Commerce Service
| Item | Detail |
|---|---|
| Purpose | Notify of payment status so orders can be confirmed or cancelled; also initiates payments and refunds |
| Integration | Spring Cloud OpenFeign client (CommerceServiceProxy) |
| API Used | POST /orders/payment/status — receives PaymentVerificationResponse |
| Reverse integration | Commerce calls POST /payment/initiate and POST /refund-payment on Payment Gateway |
| Why Required | Commerce Service owns order state; payment outcome creates or cancels orders |
| Timeout | Connect: 5s, Read: 10s |
| Retry | 3 attempts, exponential backoff up to 10s |
| Module names | ORDER_PAID (new order checkout), RENTAL_MONTHLY (recurring rent payment) |
Notifications Service
| Item | Detail |
|---|---|
| Purpose | Centralised error reporting for production monitoring |
| Integration | Spring Cloud OpenFeign client (NotificationsProxy) |
| API Used | POST /notifications/error-reporting/report |
| Why Required | All unhandled exceptions are reported with file, line, method context |
rentone-shared-utils
| Item | Detail |
|---|---|
| Purpose | Shared cross-cutting utilities |
| Contents | JwtUtils, GlobalConstants, standard response types (StatusResponse, LongStatusResponse, etc.), MongoDB multi-tenant infrastructure (MongoConnectionStorage, AuthTokenStorage), EnvironmentResolver, SeverityResolver, ErrorContext |
| Why Required | Prevents duplication of auth, tenant handling, and response contracts across services |
11. Security
Security Architecture
flowchart TD
Client["Client Request"]
WebhookClient["Razorpay Webhook"]
Filter["JwtAuthenticationFilter"]
TokenCheck["JWT Token\nValidation"]
TenantCheck["Tenant Header\nValidation"]
MongoCtx["MongoDB Connection\nContext (per tenant)"]
Controller["Controller Layer"]
WebhookSig["Webhook Signature\nVerification (HMAC-SHA256)"]
PaySig["Payment Signature\nVerification (HMAC-SHA256)"]
Feign["Feign Interceptor\nPropagates Auth Token"]
Client --> Filter
Filter --> TokenCheck
TokenCheck -->|Invalid| Reject401["HTTP 401"]
TokenCheck -->|Valid| TenantCheck
TenantCheck -->|Missing| Reject401
TenantCheck -->|Valid| MongoCtx
MongoCtx --> Controller
Controller --> PaySig
WebhookClient -->|Bypasses JWT filter| WebhookSig
WebhookSig -->|Invalid| Reject400["HTTP 400"]
WebhookSig -->|Valid| Controller
Controller --> Feign
JWT Authentication
- All endpoints except
/razorpay/webhook,/actuator, andOPTIONSrequests require a valid Bearer JWT. - Token is validated using
JwtUtils.validateToken()from the shared utilities library. - On success, the authenticated username is set in the Spring
SecurityContext. - Token is stored in
AuthTokenStorage(thread-local) and propagated to Feign calls viaCustomFeignInterceptor.
Multi-Tenant Database Isolation
- The
x-tenantheader is mandatory on all authenticated requests. - The filter resolves the MongoDB connection string for the tenant — first from an in-memory cache (
CLIENT_DB_CONNECTIONS), then by calling the tenants registry service. MongoConnectionStorage(thread-local) holds the connection string for the duration of the request, then clears it in afinallyblock.
Signature Verification
| Context | Algorithm | Secret Source |
|---|---|---|
| Payment verification | HMAC-SHA256 | RAZORPAY_KEY_SECRET env var |
| Webhook validation | HMAC-SHA256 | RAZORPAY_WEBHOOK_SECRET env var |
Credential Management
- No credentials are hardcoded. All keys are injected via environment variables.
payment-gateway.ymluses${RAZORPAY_KEY_ID:}syntax — empty by default, required in production.
Replay Attack Protection
- Webhook idempotency is enforced via the
payment_webhook_eventscollection with a unique compound index on(gateway, gatewayEventId). - The
refund.processedhandler explicitly checks the current refund status before re-applying updates.
Spring Security Configuration
SecurityAutoConfigurationis excluded from auto-configuration. Security is implemented entirely via the customJwtAuthenticationFilter.
HTTPS
- The Razorpay webhook registration URL in
payment-gateway.ymluses HTTPS (https://cloud.rentone.co.in/...). Production deployments must ensure the service is accessible over TLS.
12. Error Scenarios
Payment Failed
| Detail | |
|---|---|
| Cause | Customer cancelled, bank declined, timeout, or 3DS failure |
| Handling | Webhook payment.failed updates status to FAILURE; Commerce Service is notified |
| User Response | Commerce Service handles order-side actions; payment record shows FAILURE with statusMessage |
Signature Mismatch
| Detail | |
|---|---|
| Cause | Tampered or incorrect razorpay_signature in verify request, or wrong webhook-secret for webhooks |
| Handling | Verify endpoint: RuntimeException("Invalid payment signature") → HTTP 500. Webhook: HTTP 400 "Invalid signature" |
| User Response | Client receives error; no state change occurs |
Invalid Order (Not Found)
| Detail | |
|---|---|
| Cause | razorpay_order_id does not match any record in MongoDB |
| Handling | IllegalArgumentException → HTTP 400 via MainExceptionHandler |
| User Response | Client receives success: false with a not-found message |
Duplicate Payment / Webhook
| Detail | |
|---|---|
| Cause | Razorpay retries a webhook that was already processed |
| Handling | refund.processed: idempotency check on status — skipped if already REFUNDED. Webhook events stored with unique constraint |
| User Response | No visible effect; duplicate silently ignored |
Timeout (Commerce Service)
| Detail | |
|---|---|
| Cause | Commerce Service does not respond within 10 seconds |
| Handling | Spring Retry retries up to 3 times with exponential backoff; exception reported to Notifications Service |
| User Response | Payment verification response may still be returned to client even if Commerce Service notification failed |
Webhook Failure
| Detail | |
|---|---|
| Cause | Unhandled exception inside a webhook event handler |
| Handling | Each handler wraps processing in try/catch and logs the error; HTTP 200 is returned to Razorpay to prevent retries on unrecoverable errors |
| User Response | Razorpay receives 200; the payment may require manual reconciliation |
Network Failure (Razorpay API)
| Detail | |
|---|---|
| Cause | Network interruption or Razorpay service unavailability |
| Handling | RazorpayException is caught; thrown as RuntimeException; caught by MainExceptionHandler; reported to Notifications Service |
| User Response | HTTP 500 with generic error message; retry should be initiated by the client |
Refund on Non-Successful Payment
| Detail | |
|---|---|
| Cause | Caller attempts refund on a payment that is not in SUCCESS state |
| Handling | Early return with StatusResponse(false, "Refund allowed only for successful payments") |
| User Response | HTTP 200 with success: false and reason |
Missing Webhook Secret
| Detail | |
|---|---|
| Cause | RAZORPAY_WEBHOOK_SECRET environment variable not set |
| Handling | Signature verification returns false; all webhook calls are rejected with HTTP 400 |
| User Response | Razorpay retries the webhook; alerts should fire on repeated 400 responses |
13. Entity Reference
PaymentOrder
The primary data entity. Every payment initiation and refund creates one document.
| Field | Type | Description |
|---|---|---|
orderId |
String (PK) | Internal order ID; auto-generated or caller-provided |
tenantId |
String | Tenant identifier |
paymentGateway |
PaymentGateway | Gateway used (RAZORPAY, CC_AVENUE, PAYTM) |
paymentType |
PaymentType | PAYMENT or REFUND |
moduleName |
String | Module that triggered the payment |
moduleOrderId |
String | Order ID from originating module |
amount |
String | Amount in INR |
gstAmount |
String | GST component |
currency |
String | INR |
status |
PaymentStatus | Current status |
gatewayOrderId |
String | Razorpay order_xxx ID |
gatewayPaymentId |
String | Razorpay pay_xxx ID |
gatewayRefNo |
String | Bank transaction reference |
gatewayRefundId |
String | Razorpay rfnd_xxx ID (refunds only; sparse index) |
originalOrderId |
String | For refunds: original orderId |
refundAmount |
String | Refund amount (refunds only) |
refundReason |
String | Reason for refund |
customerId |
String | Customer user ID |
customerEmail |
String | Customer email |
paymentMethod |
PaymentMethod | Method used |
paymentInstrument |
String | Specific instrument |
statusMessage |
String | Human-readable status |
moduleCallbackUrl |
String | Callback URL |
gatewayRequest |
String | Raw request to gateway (audit) |
gatewayResponse |
String | Raw response from gateway (audit) |
customParams |
String | Arbitrary metadata |
routingStrategy |
String | Gateway routing strategy |
retryCount |
Integer | Number of retries |
fallbackGateway |
String | Fallback gateway name |
createdAt |
Date | Creation timestamp |
updatedAt |
Date | Last modification timestamp |
completedAt |
Date | Completion timestamp |
expiresAt |
Date | Order expiry timestamp |
PaymentRequest (Inbound)
| Field | Type | Description |
|---|---|---|
orderId |
String | Optional; auto-generated if missing |
amount |
String | Amount in INR |
gstAmount |
String | GST component |
currency |
String | Currency code |
userId |
String | Customer ID |
moduleName |
String | Calling module name |
moduleOrderId |
String | Calling module's order ID |
customerName |
String | Customer name for checkout prefill |
customerEmail |
String | Email for checkout prefill |
customerPhone |
String | Phone for checkout prefill |
moduleCallbackUrl |
String | Callback URL |
customParams |
Map | Arbitrary key-value pairs |
PaymentInitiationResponse (Outbound)
| Field | Type | Description |
|---|---|---|
orderId |
String | Internal payment order ID |
gateway |
PaymentGateway | RAZORPAY |
gatewayOrderId |
String | Razorpay order ID |
paymentToken |
String | Same as gatewayOrderId |
additionalData |
Map | Full Razorpay checkout config |
initiatedAt |
Date | Initiation timestamp |
PaymentVerificationResponse (Outbound + Commerce Notification)
| Field | Type | Description |
|---|---|---|
orderDraftId |
String | Module order ID |
moduleName |
String | Originating module |
status |
PaymentStatus | Payment result |
gatewayPaymentId |
String | Razorpay payment ID |
gatewayRefNo |
String | Bank transaction reference |
paymentMethod |
String | Method used |
amount |
String | Amount in INR |
gstAmount |
String | GST component |
currency |
String | Currency |
statusMessage |
String | Status description |
metadata |
Map | razorpay_order_id, razorpay_payment_id |
PaymentWebhookEvent
| Field | Type | Description |
|---|---|---|
id |
Long | Auto-incremented sequence ID |
gateway |
String | Gateway name (e.g. RAZORPAY) |
gatewayEventId |
String | Unique event ID from gateway |
receivedAt |
Date | Timestamp of receipt |
PaymentStatus (Enum)
INITIATED → PENDING → PROCESSING → SUCCESS / FAILURE / ABORTED / EXPIRED → REFUND_INITIATED → REFUNDED
PaymentGateway (Enum)
CC_AVENUE, PAYTM, RAZORPAY
PaymentType (Enum)
PAYMENT, REFUND
PaymentMethod (Enum)
CREDIT_CARD, DEBIT_CARD, GOOGLE_PAY, PHONEPE, PAYTM, NET_BANKING, UPI, WALLET, UNKNOWN
14. Cross-Cutting Concerns
Logging
- SLF4J with Lombok
@Slf4jannotation is used throughout. - Log level for the service package is set to
DEBUGinapplication.properties. - Key business events logged at INFO: order creation, verification, refund initiation, webhook events received.
- Errors logged at ERROR with full stack traces.
- Sensitive HTTP headers are not logged; log injection is prevented by sanitising headers with
.replaceAll("[\r\n]", "").
Exception Handling
MainExceptionHandler provides two global handlers:
Exception— catches all unhandled exceptions. Extracts source file and line number, builds anErrorContext, reports to Notifications Service, returns HTTP 500 with a generic message.IllegalArgumentException— returns HTTP 400 with the exception message directly.
All RazorpayException instances are wrapped in RuntimeException or handled inline before reaching the global handler.
Validation
- Validated at the service layer, not via Bean Validation annotations.
razorpay_order_idchecked for null/blank before proceeding.- Payment status checked before allowing refund.
- Signature verified before any database write.
- Webhook payload checked for null/blank before parsing.
Retry Strategy
- Spring Retry (
@EnableRetry) is active. - Feign built-in retry is disabled to avoid double-retry.
- Commerce Service notification: 3 attempts, 1s → 2s → 4s delays (max 10s).
- No retry is applied to Razorpay API calls — failures are propagated immediately.
Idempotency
- Webhook events are de-duplicated via
payment_webhook_eventscollection (unique compound index). - Refund processing checks current document status before re-applying state.
- Payment initiation does not enforce idempotency by
moduleOrderId— calling the endpoint twice with the same data creates two payment records.
Security (Summary)
See Section 11 — Security for full details. Key points:
- JWT-based authentication on all endpoints except webhook.
- Signature-based authentication for webhook.
- Per-tenant MongoDB connection isolation.
- No credentials in source code.
Configuration
| Property | Source | Description |
|---|---|---|
RAZORPAY_KEY_ID |
Environment variable | Razorpay public key |
RAZORPAY_KEY_SECRET |
Environment variable | Razorpay secret key for HMAC |
RAZORPAY_WEBHOOK_SECRET |
Environment variable | Webhook signature secret |
RAZORPAY_ENVIRONMENT |
Environment variable | TEST or PROD; defaults to TEST |
server.port |
application.properties |
6009 |
eureka.client.serviceUrl.defaultZone |
application.properties |
Eureka URL |
commerce.service.retry.* |
application.properties |
Retry configuration |
payment.* |
payment-gateway.yml |
Gateway credentials and URLs |