KAHIBARO
Discord Login Register

31.8. Payments

Understanding Payments in an E‑Commerce Backend

Handling payments is one of the most sensitive parts of an e‑commerce backend. Money is involved, users expect reliability, and security requirements are strict. In this chapter you will learn how payments fit into your e‑commerce system, how to integrate with payment providers, and how to design the payment flow safely and predictably.

We assume you already have basic order, product, and user management in place from earlier chapters of the project.

Key Concepts in E‑Commerce Payments

Payment vs Order

An order is the business record of what the customer wants to buy. A payment is how the customer pays for that order.

You should treat them as related but separate concepts:

Keeping these states separate prevents confusion and makes error handling easier.

Authorization vs Capture

In card payments there are usually two steps:

Common patterns:

Business casePattern
Digital goods (instant delivery)Authorize and capture together
Physical goods (ship later)Authorize at checkout, capture when shipping

Important rule: Never mark an order as finally paid until the payment is captured or fully settled by your payment provider.

One-Time Payments vs Recurring

For an e‑commerce store you often handle:

In this project we focus mainly on one-time payments, but you should design your payment model so it could be extended to recurring payments in the future.

Payment Provider Integration Basics

You almost never handle raw card numbers in your backend. Instead, you integrate with a Payment Service Provider (PSP) like Stripe, Braintree, Adyen, PayPal, etc.

Why You Do Not Handle Card Data Directly

Handling card data directly means you must comply with strict PCI DSS rules. These are hard and expensive to meet. PSPs provide:

Your backend’s job is mainly to:

Typical Payment Flow Overview

A simple card payment flow usually looks like this:

  1. Client calls your backend: Create payment for order.
  2. Backend creates a payment record in your database and calls the PSP API.
  3. PSP returns a client secret / redirect URL / token.
  4. Backend returns necessary data to the client (front end).
  5. Front end uses PSP SDK or hosted page to complete payment.
  6. PSP:
    • Either returns result immediately (succeeded / failed), or
    • Sends asynchronous webhook to your backend when payment status changes.
  7. Backend updates:
    • Payment status
    • Order status
    • Inventory, emails, etc.

Designing Payment Data Models

Payment Table

You will usually have a payments table linked to orders.

Example schema (simplified):

ColumnTypeExample
idUUID / intpmt_123
order_idUUID / intord_456
user_idUUID / intusr_789
providertext"stripe"
provider_payment_idtext"pi_3Nl..."
amount_centsinteger2599
currencytext"USD"
statustext / enum"PENDING", "CAPTURED", "FAILED"
error_codetext"card_declined"
error_messagetext"Your card was declined."
created_attimestamp
updated_attimestamp

You may also store:

Order Fields Related to Payments

In your orders table you might have:

FieldPurpose
total_amountFinal amount to be paid
currencyCurrency code, e.g. "USD"
payment_status"UNPAID", "PAID", "PARTIALLY_REFUNDED"
payment_idPrimary payment record (if 1:1)

You can design one order to many payments if you want to support multiple attempts:

In that case do not put payment_id on orders, but instead query payments by order_id and choose the last successful one.

Important rule: Never trust any amount or currency from the client when creating a payment. Always use the order total stored in your database.

Creating a Payment

Backend Endpoint Design

You might expose an endpoint like:

Body from client could be very minimal, for example:

json
{
  "payment_method": "card"
}

The server should:

  1. Authenticate the user.
  2. Fetch the order by order_id and ensure:
    • It belongs to the user.
    • It is not already fully paid or cancelled.
  3. Calculate the final amount:
    • Sum of line items.
    • Taxes and shipping.
    • Discounts, vouchers.
  4. Create payment record in your DB with status PENDING.
  5. Call PSP API to create a payment entity with:
    • amount
    • currency
    • A unique internal reference (e.g. order.id).
  6. Store provider_payment_id.
  7. Return client-specific data for the PSP SDK.

Example response (Stripe-like):

json
{
  "payment_id": "pmt_123",
  "client_secret": "pi_3Nl..."
}

The front end will use client_secret with Stripe JS to collect card details and confirm the payment, without card data ever passing through your backend.

Idempotency for Payment Creation

Payments are sensitive to double charges. Network issues or client retries can cause the same request to be sent twice.

To prevent this, many PSPs support idempotency keys. You also need idempotency in your own backend.

Common pattern:

  1. Client sends a unique request_id when creating a payment.
  2. You store request_id and enforce uniqueness in DB.
  3. If you receive the same request_id again:
    • Return the existing payment instead of creating a new one.

At the PSP level you also send an idempotency key with the API request.

Important rule: Use idempotency for payment creation to avoid double charges.

Handling Payment Confirmation and Status

Synchronous vs Asynchronous Results

Depending on the PSP, confirmation may be:

You must support both.

A typical approach:

Updating Order and Payment States

When you know a payment succeeded, your backend should:

  1. Set payment.status = "CAPTURED" (or provider-specific final state).
  2. Set order.payment_status = "PAID".
  3. Reserve or decrease inventory (if not already done at checkout).
  4. Trigger:
    • Order confirmation email.
    • Task to send invoice.
    • Task to start fulfillment flow.

On failure:

  1. Set payment.status = "FAILED".
  2. Store error_code and error_message.
  3. Keep order in PENDING_PAYMENT or allow new payment attempts.
  4. Inform front end to show error message.

Example Payment Status Update Code (Pseudo Python)

Assuming a webhook or callback handler:

python
def handle_payment_succeeded(provider_payment_id: str):
    payment = payment_repo.get_by_provider_id(provider_payment_id)
    if not payment or payment.status == "CAPTURED":
        # Already handled or unknown
        return
    payment.status = "CAPTURED"
    payment.captured_at = now()
    payment_repo.save(payment)
    order = order_repo.get(payment.order_id)
    if order.payment_status != "PAID":
        order.payment_status = "PAID"
        order_repo.save(order)
        inventory_service.reserve_for_order(order.id)
        email_service.send_order_confirmation(order.id)

You always check current status before applying side effects. This makes the handler idempotent, so if you process the same event twice nothing bad happens.

Webhooks: Listening to Payment Events

What Is a Webhook

A webhook is an HTTP callback from the PSP to your backend, telling you that something happened:

Your backend must expose a publicly reachable endpoint, for example:

The PSP sends JSON payloads to this endpoint.

Verifying Webhooks

Never trust any request to your webhook without verification. PSPs provide ways to:

You should:

  1. Read the raw body of the request.
  2. Use the PSP SDK to verify signature with your secret.
  3. If verification fails, return HTTP 400 or 401.

Example (conceptual):

python
def stripe_webhook(request):
    signature = request.headers.get("Stripe-Signature")
    payload = request.body
    try:
        event = stripe.Webhook.construct_event(
            payload=payload,
            sig_header=signature,
            secret=WEBHOOK_SECRET,
        )
    except InvalidSignatureError:
        return Response(status_code=400)
    handle_stripe_event(event)
    return Response(status_code=200)

Important rule: Always verify webhook signatures. Do not accept unauthenticated data that can change payment or order status.

Handling Common Webhook Events

Some common events:

Remember to make your handlers idempotent by checking existing status before applying updates.

Webhooks and Frontend UX

Since webhooks may arrive slightly later than the user’s payment attempt, front end can:

  1. Show a "Processing payment..." state.
  2. Poll an endpoint like GET /orders/{order_id} every few seconds.
  3. When the order becomes PAID, show confirmation page.

Example polling logic:

Refunds and Cancellations

Payments are not only about charging money. You also must handle refunds.

Full vs Partial Refund

In your database:

Refund Flow

  1. Admin or system requests refund:
    • POST /admin/payments/{payment_id}/refund
  2. Backend:
    • Validates that the allowed refundable amount is nonzero:
      • refundable = payment.amount_cents - payment.refunded_amount_cents
    • Calls PSP refund API.
    • Creates refund record in DB with PENDING status.
  3. PSP processes the refund:
    • Often instantly, but confirmation usually via webhook.
  4. Webhook handler:
    • Confirms refund succeeded.
    • Updates refund.status = "COMPLETED".
    • Updates payment.refunded_amount_cents.
    • Sets order.payment_status to:
      • REFUNDED if fully refunded.
      • PARTIALLY_REFUNDED if partial.

Important rule: Never change your internal payment or order refund status to "completed" until you receive confirmation from the payment provider (API response or webhook).

Calculating Refundable Amount

You must ensure you never refund more than you charged.

If $A$ is total amount captured, and $R$ is the sum of all completed refunds, the remaining refundable amount $L$ is:

$$
L = A - R
$$

You must enforce:

$$
0 \leq \text{requested\_refund\_amount} \leq L
$$

Security and Compliance Considerations

Never Store Sensitive Card Data

You should never store:

You may store the last 4 digits and card brand that the PSP gives you, for example:

These are not secret and are useful for support and receipts.

Use HTTPS Everywhere

All communication that touches payment-related endpoints must use HTTPS:

If an attacker can intercept traffic, they might tamper with amounts or order IDs.

Validate Amounts and Ownership on Every Request

For any payment-related endpoints, always:

Example checks when creating a payment for order_id:

Handling Race Conditions

Example race condition:

Prevention strategies:

Example End-to-End Flow for the Project

Let us walk through a concrete, simplified flow that fits your e‑commerce project.

1. User Checks Out

2. Client Requests Payment

3. Frontend Completes Payment

4. Webhook Confirms the Payment

5. User Sees Final Status

6. Refund Example

If customer cancels after payment:

  1. Admin requests refund via admin panel.
  2. Backend checks refundable amount using:
    • $$L = A - R$$
  3. Backend calls PSP refund API.
  4. PSP sends refund_succeeded webhook.
  5. Backend updates payment and order statuses, and sends refund notification email.

Practical Tips When Implementing Payments

python
  class PaymentProvider:
      def create_payment(self, order, payment): ...
      def handle_webhook(self, event): ...
      def refund(self, payment, amount): ...

By following these design principles and patterns, you will have a robust and secure payment module for your e‑commerce backend that can be extended later to support multiple providers, subscriptions, and more advanced scenarios.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!