Skip to content

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 orderId is provided in the request, one is auto-generated by PaymentOrderIdGeneratorUtil.
  • A payment record is created with status INITIATED before any Razorpay call succeeds, so every attempt is traceable.
  • Verification is only possible for orders that already exist in the database — unrecognised razorpay_order_id values are rejected.
  • Commerce Service is notified on every verification result, whether success or failure.

Validation Rules

  • razorpay_order_id, razorpay_payment_id, and razorpay_signature are 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:

  1. 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.

  2. 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.

  3. Draft-to-Order conversion is atomic. On success, Commerce creates the Orders document, deletes the OrderDraft, deletes the Cart, and creates an OrderHistory record in a single flow.

  4. Payment failure does not delete the draft. On status != SUCCESS for ORDER_PAID, Commerce only marks the draft PAYMENT_FAILED — the draft persists so the customer can retry.

  5. Monthly payment validates penalty. The expected amount for RENTAL_MONTHLY is monthlyPayable + calculateOverduePenalty(order). A partial or incorrect amount is rejected.

  6. Refund blocks order cancellation. If paymentGatewayProxy.refundPayment() returns success: false, the order status is NOT updated — the order stays in its current state.

  7. 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:

  1. Amount in INR is multiplied by 100 to convert to paise.
  2. A Razorpay order is created with amount, currency, receipt (internal order ID), and notes containing module_name, module_order_id, customer_email, and tenant_id.
  3. The Razorpay-assigned order_id (e.g. order_xxx) is stored as gatewayOrderId in the payment record.
  4. 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-secret via razorpay-java SDK Utils.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

  • RazorpayException during order creation throws RuntimeException with message "Failed to create Razorpay order".
  • RazorpayException during verification throws RuntimeException with 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: IllegalArgumentException returned as HTTP 400.
  • Razorpay API error: RuntimeException propagates and is caught by MainExceptionHandler, 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-Signature header using the webhook-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.captured or payment.failed event: 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

  • ABORTED and EXPIRED are defined in the enum but are not yet set by automated transitions in the current codebase.
  • Refunds create a new PaymentOrder document with paymentType = REFUND and orderId = "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 RazorpayService and RazorpayWebhookService classes.

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: LongStatusResponsesuccess, 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:

  1. 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.

  2. Refunds are only permitted on successful payments. If the original payment does not have status SUCCESS, the refund is rejected with a clear message.

  3. A payment record is created before Razorpay is called. The INITIATED record exists in MongoDB from the moment the initiation request is accepted, enabling full audit traceability.

  4. 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.

  5. Webhook events do not require JWT. The webhook endpoint bypasses JWT validation intentionally and uses signature-based authentication only.

  6. Duplicate webhooks are safe. Refund events with refund.processed are idempotency-checked against the stored status before any updates are applied.

  7. Refund creates a new document. A refund does not modify only the original payment; it creates a new document with paymentType = REFUND and orderId = "REF-" + originalOrderId.

  8. Both the refund document and original payment are updated on refund completion. When refund.processed arrives, both records have their status set to REFUNDED.

  9. 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.

  10. Unrecognised razorpay_order_id values are rejected. If no internal payment record matches the razorpay_order_id during verification, the request fails with an error.

  11. Payments are multi-tenant. Every payment is scoped to a tenantId. MongoDB connections are resolved dynamically per tenant using the x-tenant header.

  12. Only INR is supported. The gateway configuration declares INR as 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, and OPTIONS requests 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 via CustomFeignInterceptor.

Multi-Tenant Database Isolation

  • The x-tenant header 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 a finally block.

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.yml uses ${RAZORPAY_KEY_ID:} syntax — empty by default, required in production.

Replay Attack Protection

  • Webhook idempotency is enforced via the payment_webhook_events collection with a unique compound index on (gateway, gatewayEventId).
  • The refund.processed handler explicitly checks the current refund status before re-applying updates.

Spring Security Configuration

  • SecurityAutoConfiguration is excluded from auto-configuration. Security is implemented entirely via the custom JwtAuthenticationFilter.

HTTPS

  • The Razorpay webhook registration URL in payment-gateway.yml uses 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)

INITIATEDPENDINGPROCESSINGSUCCESS / FAILURE / ABORTED / EXPIREDREFUND_INITIATEDREFUNDED


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 @Slf4j annotation is used throughout.
  • Log level for the service package is set to DEBUG in application.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 an ErrorContext, 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_id checked 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_events collection (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