<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ PayPal - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ PayPal - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 23 Aug 2026 21:53:56 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/paypal/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Implement PayPal in a Microservice Architecture Using NestJS, gRPC, and Docker ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll build a production-ready PayPal payment service using NestJS microservices. Along the way, you'll learn how to isolate payment logic into its own service, communicate between  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-implement-paypal-in-a-microservice-architecture-using-nestjs-grpc-and-docker/</link>
                <guid isPermaLink="false">6a59619ee14c719ac88d7a33</guid>
                
                    <category>
                        <![CDATA[ Microservices ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PayPal ]]>
                    </category>
                
                    <category>
                        <![CDATA[ payments ]]>
                    </category>
                
                    <category>
                        <![CDATA[ nestjs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ containers ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Md Tarikul Islam ]]>
                </dc:creator>
                <pubDate>Thu, 16 Jul 2026 22:56:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/665e54b7-b47e-4abe-a417-49b51569868f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll build a production-ready PayPal payment service using NestJS microservices. Along the way, you'll learn how to isolate payment logic into its own service, communicate between services using gRPC, publish payment events with RabbitMQ, and deploy everything with Docker.</p>
<p>By the end, you'll have a scalable payment architecture that can be reused across multiple business domains.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-introduction">Introduction</a></p>
</li>
<li><p><a href="#heading-why-use-a-dedicated-payment-service">Why Use a Dedicated Payment Service?</a></p>
</li>
<li><p><a href="#heading-architecture-overview">Architecture Overview</a></p>
<ul>
<li><a href="#heading-payment-state-machine">Payment State Machine</a></li>
</ul>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-paypal-concepts-you-need-to-know">PayPal Concepts You Need to Know</a></p>
<ul>
<li><p><a href="#heading-sandbox-vs-live">Sandbox vs Live</a></p>
</li>
<li><p><a href="#heading-orders-api-flow-what-we-use">Orders API Flow (What We Use)</a></p>
</li>
<li><p><a href="#heading-environment-variables">Environment Variables</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-step-1-create-the-payment-service">Step 1 — Create the Payment Service</a></p>
</li>
<li><p><a href="#heading-step-2-define-the-grpc-contract">Step 2 — Define the gRPC Contract</a></p>
</li>
<li><p><a href="#heading-step-3-implement-the-paypal-service">Step 3 — Implement the PayPal Service</a></p>
</li>
<li><p><a href="#heading-step-4-build-the-payment-flow-create-approve-capture">Step 4 — Build the Payment Flow (Create → Approve → Capture)</a></p>
<ul>
<li><p><a href="#heading-4a-create-payment">4a. Create Payment</a></p>
</li>
<li><p><a href="#heading-4b-user-approves-on-paypal">4b. User Approves on PayPal</a></p>
</li>
<li><p><a href="#heading-4c-capture-payment">4c. Capture Payment</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-5-connect-domain-services-via-grpc">Step 5 — Connect Domain Services via gRPC</a></p>
<ul>
<li><a href="#heading-domain-service-business-logic-example">Domain Service Business Logic Example</a></li>
</ul>
</li>
<li><p><a href="#heading-step-6-add-the-api-gateway-layer">Step 6 — Add the API Gateway Layer</a></p>
</li>
<li><p><a href="#heading-step-7-publish-payment-events-with-rabbitmq">Step 7 — Publish Payment Events with RabbitMQ</a></p>
<ul>
<li><a href="#heading-two-paths-to-mark-an-order-as-paid">Two Paths to Mark an Order as Paid</a></li>
</ul>
</li>
<li><p><a href="#heading-step-8-database-schema-and-migrations">Step 8 — Database Schema and Migrations</a></p>
<ul>
<li><a href="#heading-production-migration-gotcha">Production Migration Gotcha</a></li>
</ul>
</li>
<li><p><a href="#heading-step-9-local-development-setup-docker">Step 9 — Local Development Setup (Docker)</a></p>
<ul>
<li><p><a href="#heading-environment-variables-env">Environment Variables (.env)</a></p>
</li>
<li><p><a href="#heading-docker-compose-local">Docker Compose (Local)</a></p>
</li>
<li><p><a href="#heading-start-services">Start Services</a></p>
</li>
<li><p><a href="#heading-verify-health">Verify Health</a></p>
</li>
<li><p><a href="#heading-test-payment-flow">Test Payment Flow</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-10-production-deployment">Step 10 — Production Deployment</a></p>
<ul>
<li><p><a href="#heading-paypal-live-credentials">PayPal Live Credentials</a></p>
</li>
<li><p><a href="#heading-production-env-on-server-never-commit">Production .env</a></p>
</li>
<li><p><a href="#heading-docker-compose-production">Docker Compose (Production)</a></p>
</li>
<li><p><a href="#heading-deploy-commands">Deploy Commands</a></p>
</li>
<li><p><a href="#heading-verify-production">Verify Production</a></p>
</li>
<li><p><a href="#heading-frontend-domain-in-production">Frontend Domain in Production</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-11-health-checks-and-monitoring">Step 11 — Health Checks and Monitoring</a></p>
</li>
<li><p><a href="#heading-complete-request-flow-real-example">Complete Request Flow (Real Example)</a></p>
</li>
<li><p><a href="#heading-coupon-support-optional">Coupon Support (Optional)</a></p>
</li>
<li><p><a href="#heading-paypal-webhooks-optional-but-recommended">PayPal Webhooks (Optional but Recommended)</a></p>
</li>
<li><p><a href="#heading-testing-checklist">Testing Checklist</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
<li><p><a href="#heading-further-reading">Further Reading</a></p>
</li>
</ul>
<h2 id="heading-introduction">Introduction</h2>
<p>Payment logic doesn't belong inside every microservice. When you scatter PayPal API calls across <code>user-service</code>, <code>order-service</code>, and <code>billing-service</code>, you end up with:</p>
<ul>
<li><p>Duplicated PayPal credentials and SDK code</p>
</li>
<li><p>Inconsistent error handling and idempotency</p>
</li>
<li><p>Hard-to-audit payment records</p>
</li>
<li><p>Painful environment switching (sandbox to live)</p>
</li>
</ul>
<p>The solution is a dedicated payment microservice that owns all PayPal interactions. Other services call it over gRPC, and payment outcomes are broadcast over RabbitMQ so domain services can update their own data.</p>
<p>This guide walks you through that pattern using a real-world stack:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Technology</th>
</tr>
</thead>
<tbody><tr>
<td>Payment service</td>
<td>NestJS</td>
</tr>
<tr>
<td>Inter-service communication</td>
<td>gRPC</td>
</tr>
<tr>
<td>Event bus</td>
<td>RabbitMQ</td>
</tr>
<tr>
<td>Database</td>
<td>PostgreSQL</td>
</tr>
<tr>
<td>API exposure</td>
<td>API Gateway (HTTP)</td>
</tr>
<tr>
<td>Containerization</td>
<td>Docker Compose</td>
</tr>
<tr>
<td>PayPal API</td>
<td>Orders v2 (Create, Approve, Capture)</td>
</tr>
</tbody></table>
<h2 id="heading-why-use-a-dedicated-payment-service">Why Use a Dedicated Payment Service?</h2>
<p>A dedicated payment service centralizes all payment-related responsibilities in one place. Instead of every microservice communicating directly with PayPal, they simply request payment operations from the payment service.</p>
<p>This service manages PayPal authentication, order creation, payment captures, wallet updates, ledger records, and webhook processing. Meanwhile, domain services remain focused on business logic such as student applications or subscriptions.</p>
<p>Domain services only need to know:</p>
<ol>
<li><p>How much to charge</p>
</li>
<li><p>Who is paying</p>
</li>
<li><p>What business entity the payment is for (<code>referenceId</code>)</p>
</li>
<li><p>Where to redirect the user after payment (<code>returnUrl</code> / <code>cancelUrl</code>)</p>
</li>
</ol>
<p>They do <strong>not</strong> need PayPal credentials.</p>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>Users initiate payments from the Frontend, and requests are routed through the API Gateway to the Students Service. The service uses gRPC to communicate with the Payment Service, which handles all interactions with PayPal.</p>
<p>Once the payment is completed, the Payment Service publishes an event to RabbitMQ, enabling the Students Service to update the payment status asynchronously.</p>
<pre><code class="language-plaintext">┌────────────────────────────────────────────────────────────┐
│                     PRESENTATION LAYER                     │
├────────────────────────────────────────────────────────────┤
│ Frontend (React)                                           │
└───────────────────────┬────────────────────────────────────┘
                        │ HTTP
                        ▼

┌────────────────────────────────────────────────────────────┐
│                       GATEWAY LAYER                        │
├────────────────────────────────────────────────────────────┤
│ student-apigw                                               │
└───────────────────────┬────────────────────────────────────┘
                        │ gRPC
                        ▼

┌────────────────────────────────────────────────────────────┐
│                       DOMAIN LAYER                         │
├────────────────────────────────────────────────────────────┤
│ students-service                                            │
└───────────────────────┬────────────────────────────────────┘
                        │ gRPC
                        ▼

┌────────────────────────────────────────────────────────────┐
│                      PAYMENT LAYER                         │
├────────────────────────────────────────────────────────────┤
│ payment-service                                             │
│                                                            │
│ • Create Payment                                           │
│ • Capture Payment                                          │
│ • Wallet Management                                        │
│ • Ledger                                                   │
│ • Webhooks                                                 │
│ • Event Publishing                                         │
└──────────────┬───────────────────────┬─────────────────────┘
               │                       │
               │ REST                  │ RabbitMQ
               ▼                       ▼

      ┌───────────────┐      ┌────────────────────┐
      │    PayPal     │      │   payment_events   │
      │   Checkout    │      │       Queue        │
      └───────────────┘      └─────────┬──────────┘
                                       │
                                       ▼

                           ┌────────────────────┐
                           │ students-service   │
                           │ Event Consumer     │
                           └────────────────────┘
</code></pre>
<h3 id="heading-payment-state-machine">Payment State Machine</h3>
<p>A payment state machine represents the lifecycle of a payment, tracking its progress from creation to completion (or failure). Each state reflects the current status of the payment, making it easier to monitor, retry, and prevent invalid operations.</p>
<pre><code class="language-plaintext">NOT_STARTED → EXECUTING → SUCCESS
                      └→ FAILED
</code></pre>
<ul>
<li><p><strong>NOT_STARTED</strong> — order record created in DB</p>
</li>
<li><p><strong>EXECUTING</strong> — PayPal order created, waiting for user approval</p>
</li>
<li><p><strong>SUCCESS</strong> — funds captured, ledger updated, event published</p>
</li>
<li><p><strong>FAILED</strong> — capture failed or user cancelled</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, make sure you have:</p>
<ul>
<li><p><a href="https://nodejs.org/">Node.js 18+</a></p>
</li>
<li><p><a href="https://docs.docker.com/get-docker/">Docker and Docker Compose</a></p>
</li>
<li><p><a href="https://nestjs.com/">NestJS</a> basics</p>
</li>
<li><p>A <a href="https://developer.paypal.com/">PayPal Developer</a> account</p>
</li>
<li><p>Basic understanding of gRPC and message queues</p>
</li>
</ul>
<h2 id="heading-paypal-concepts-you-need-to-know">PayPal Concepts You Need to Know</h2>
<p>Before integrating PayPal, it's helpful to understand a few core concepts. PayPal provides separate environments for development and production, along with an order-based payment workflow that your application follows.</p>
<h3 id="heading-sandbox-vs-live">Sandbox vs Live</h3>
<table>
<thead>
<tr>
<th>Environment</th>
<th>API Base URL</th>
<th>Checkout URL</th>
</tr>
</thead>
<tbody><tr>
<td>Sandbox (dev)</td>
<td><code>https://api-m.sandbox.paypal.com</code></td>
<td><code>https://www.sandbox.paypal.com/checkoutnow?token=...</code></td>
</tr>
<tr>
<td>Live (prod)</td>
<td><code>https://api-m.paypal.com</code></td>
<td><code>https://www.paypal.com/checkoutnow?token=...</code></td>
</tr>
</tbody></table>
<p>Always develop in <strong>sandbox</strong>. Switch to live only in production.</p>
<h3 id="heading-orders-api-flow-what-we-use">Orders API Flow (What We Use)</h3>
<p>PayPal's Orders v2 API follows three steps:</p>
<ol>
<li><p><strong>Create Order</strong>: your backend creates an order with amount and return URLs</p>
</li>
<li><p><strong>Approve</strong>: user is redirected to PayPal and approves payment</p>
</li>
<li><p><strong>Capture</strong>: your backend captures the approved funds</p>
</li>
</ol>
<p>This is different from the older Payments REST API. Orders v2 is the recommended approach for new integrations.</p>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>The PayPal service reads its configuration from environment variables. This keeps sensitive credentials out of your source code and makes it easy to switch between sandbox and production environments.</p>
<pre><code class="language-bash">PAYPAL_CLIENT_ID=your_client_id
PAYPAL_CLIENT_SECRET=your_client_secret
PAYPAL_API_BASE=https://api-m.sandbox.paypal.com   # or https://api-m.paypal.com for live
</code></pre>
<p>Never commit real credentials to Git. Use <code>.env</code> files and Docker environment injection.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<pre><code class="language-plaintext">apps/
├── core/
│   └── payment-service/          # Owns all PayPal logic
│       ├── src/
│       │   ├── app/payment/
│       │   │   ├── paypal/paypal.service.ts
│       │   │   ├── payment.service.ts
│       │   │   ├── payment.grpc.controller.ts
│       │   │   ├── payment.http.controller.ts
│       │   │   └── events/payment-events.publisher.ts
│       │   ├── migrations/       # DB schema
│       │   └── routes/health.routes.ts
│       └── Dockerfile
├── services/
│   └── students-service/         # Domain service example
│       └── src/app/payment/
│           ├── payment-client.service.ts      # gRPC client
│           ├── application-payment.service.ts # business logic
│           └── payment-events.consumer.ts     # RabbitMQ listener
└── gateways/
    └── student-apigw/            # HTTP API for frontend
libs/
└── shared/dto/src/lib/payment/
    └── payment.proto             # Shared gRPC contract
</code></pre>
<h2 id="heading-step-1-create-the-payment-service">Step 1 — Create the Payment Service</h2>
<p>The payment service runs two servers in one process</p>
<table>
<thead>
<tr>
<th>Protocol</th>
<th>Port</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td>HTTP</td>
<td>3003</td>
<td>Health checks, webhooks, admin APIs</td>
</tr>
<tr>
<td>gRPC</td>
<td>50061</td>
<td>Internal service-to-service calls</td>
</tr>
</tbody></table>
<p>The payment service exposes both an HTTP server and a gRPC server in the same NestJS application. The HTTP server handles health checks, webhooks, and external requests, while the gRPC server accepts internal requests from other microservices.</p>
<pre><code class="language-typescript">
// apps/core/payment-service/src/main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Health route (outside /api prefix)
  app.use('/health', healthRouter);

  // gRPC microservice
  app.connectMicroservice&lt;MicroserviceOptions&gt;({
    transport: Transport.GRPC,
    options: {
      package: 'payment',
      protoPath: join(process.cwd(), 'libs/shared/dto/src/lib/payment/payment.proto'),
      url: `0.0.0.0:${process.env.GRPC_PORT || '50061'}`,
    },
  });

  app.setGlobalPrefix('api');
  await app.startAllMicroservices();
  await app.listen(process.env.PORT || 3003);
}
</code></pre>
<p>During startup, NestJS initializes both servers, allowing external clients and internal services to communicate through the appropriate protocol.</p>
<p><strong>Key design choice:</strong> HTTP is for external/webhook traffic. gRPC is for fast, typed internal calls between services.</p>
<h2 id="heading-step-2-define-the-grpc-contract">Step 2 — Define the gRPC Contract</h2>
<p>Next, you'll create a shared <code>.proto</code> file so all services speak the same language:</p>
<p>A gRPC contract defines the API shared between microservices. Using a <code>.proto</code> file ensures that every service communicates with the payment service using the same request and response structure, regardless of the programming language.</p>
<pre><code class="language-protobuf">// libs/shared/dto/src/lib/payment/payment.proto

syntax = "proto3";
package payment;

service PaymentService {
  rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {}
  rpc CapturePayment(CapturePaymentRequest) returns (CapturePaymentResponse) {}
  rpc GetPaymentStatus(GetPaymentStatusRequest) returns (GetPaymentStatusResponse) {}
  rpc ListPayments(ListPaymentsRequest) returns (ListPaymentsResponse) {}
}

message CreatePaymentRequest {
  string checkout_id = 1;
  string payment_order_id = 2;
  string domain = 3;           // e.g. "application", "subscription"
  string reference_id = 4;     // business entity ID
  string payer_id = 5;
  string amount = 6;
  string currency = 7;
  string buyer_email = 8;
  string seller_account = 9;
  string payment_category = 10;
  string return_url = 11;      // PayPal redirect on success
  string cancel_url = 12;      // PayPal redirect on cancel
  string idempotency_key = 13;
  string metadata = 14;
  string description = 15;
}

message CreatePaymentResponse {
  int32 status = 1;
  string message = 2;
  string payment_order_id = 3;
  string paypal_order_id = 4;
  string approve_url = 5;      // Redirect user here
  string payment_order_status = 6;
}
</code></pre>
<p>The <code>domain</code> + <code>reference_id</code> pair lets one payment service handle payments for applications, subscriptions, university fees, and more without coupling to any single business model.</p>
<h2 id="heading-step-3-implement-the-paypal-service">Step 3 — Implement the PayPal Service</h2>
<p>Now, you'll create a dedicated <code>PayPalService</code> that wraps the PayPal REST API.</p>
<p>Instead of calling the PayPal API throughout the application, we encapsulate all PayPal communication inside a dedicated service. This keeps authentication, order creation, and payment capture logic centralized and easier to maintain.</p>
<pre><code class="language-typescript">// apps/core/payment-service/src/app/payment/paypal/paypal.service.ts

@Injectable()
export class PayPalService {
  private accessToken: string | null = null;
  private tokenExpiresAt = 0;

  private get apiBase(): string {
    return this.configService.get('PAYPAL_API_BASE')
      || 'https://api-m.sandbox.paypal.com';
  }

  // Step 1: Get OAuth access token (cached until expiry)
  private async getAccessToken(): Promise&lt;string&gt; {
    const now = Date.now();
    if (this.accessToken &amp;&amp; now &lt; this.tokenExpiresAt) {
      return this.accessToken;
    }

    const response = await axios.post(
      `${this.apiBase}/v1/oauth2/token`,
      'grant_type=client_credentials',
      {
        auth: {
          username: this.configService.get('PAYPAL_CLIENT_ID'),
          password: this.configService.get('PAYPAL_CLIENT_SECRET'),
        },
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      }
    );

    this.accessToken = response.data.access_token;
    this.tokenExpiresAt = now + (response.data.expires_in - 60) * 1000;
    return this.accessToken;
  }

  // Step 2: Create PayPal checkout order
  async createOrder(input: PayPalCreateOrderInput) {
    const token = await this.getAccessToken();

    const response = await axios.post(
      `${this.apiBase}/v2/checkout/orders`,
      {
        intent: 'CAPTURE',
        purchase_units: [{
          custom_id: input.paymentOrderId,
          description: input.description,
          amount: {
            currency_code: input.currency,
            value: input.amount,
          },
        }],
        application_context: {
          return_url: input.returnUrl,
          cancel_url: input.cancelUrl,
          brand_name: 'YourApp',
          user_action: 'PAY_NOW',
        },
      },
      {
        headers: {
          Authorization: `Bearer ${token}`,
          'PayPal-Request-Id': input.idempotencyKey,
        },
      }
    );

    const paypalOrderId = response.data.id;
    const approveUrl = response.data.links
      ?.find((l) =&gt; l.rel === 'approve')?.href;

    return { paypalOrderId, approveUrl };
  }

  // Step 3: Capture approved order
  async captureOrder(paypalOrderId: string) {
    const token = await this.getAccessToken();

    const response = await axios.post(
      `${this.apiBase}/v2/checkout/orders/${paypalOrderId}/capture`,
      {},
      { headers: { Authorization: `Bearer ${token}` } }
    );

    const capture = response.data.purchase_units?.[0]?.payments?.captures?.[0];
    return { status: response.data.status, captureId: capture?.id || '' };
  }
}
</code></pre>
<p>On startup, log configuration (with masked secrets) so you can verify sandbox vs live at a glance:</p>
<pre><code class="language-plaintext">PayPal configuration check:
  PAYPAL_API_BASE: https://api-m.paypal.com
  PAYPAL_CLIENT_ID: AQb2...aq1M (80 chars)
  credentialsPresent: true
  environment: live
</code></pre>
<p>Notice that the access token is cached until it expires. This avoids requesting a new OAuth token for every payment, improving performance and reducing unnecessary API calls.</p>
<h2 id="heading-step-4-build-the-payment-flow-create-approve-capture">Step 4 — Build the Payment Flow (Create, Approve, Capture)</h2>
<h3 id="heading-create-payment">Create Payment</h3>
<p><code>PaymentService.createPayment()</code> does the following:</p>
<ol>
<li><p>Checks <strong>idempotency key</strong> and returns an existing order if one is already created</p>
</li>
<li><p>Creates a <code>payment_events</code> checkout record</p>
</li>
<li><p>Creates a <code>payment_orders</code> row with status <code>NOT_STARTED</code></p>
</li>
<li><p>Calls <code>PayPalService.createOrder()</code></p>
</li>
<li><p>Updates order status to <code>EXECUTING</code></p>
</li>
<li><p>Returns <code>approveUrl</code> to the caller</p>
</li>
</ol>
<pre><code class="language-typescript">async createPayment(input: CreatePaymentPayload) {
  // Idempotency: prevent duplicate charges
  const existing = await this.paymentOrderModel.findOne({
    where: { idempotencyKey: input.idempotencyKey },
  });
  if (existing) return this.buildCreateResponse(existing);

  const order = await this.paymentOrderModel.create({
    paymentOrderId: input.paymentOrderId,
    amount: input.amount,
    currency: input.currency,
    paymentOrderStatus: PaymentOrderStatus.NOT_STARTED,
    domain: input.domain,
    referenceId: input.referenceId,
    // ...
  });

  const paypalOrder = await this.paypalService.createOrder({
    paymentOrderId: order.paymentOrderId,
    amount: input.amount,
    currency: input.currency,
    returnUrl: input.returnUrl,
    cancelUrl: input.cancelUrl,
    idempotencyKey: input.idempotencyKey,
  });

  await order.update({
    paymentOrderStatus: PaymentOrderStatus.EXECUTING,
    paypalOrderId: paypalOrder.paypalOrderId,
  });

  return {
    approveUrl: paypalOrder.approveUrl,
    paypalOrderId: paypalOrder.paypalOrderId,
    paymentOrderStatus: PaymentOrderStatus.EXECUTING,
  };
}
</code></pre>
<h3 id="heading-user-approves-on-paypal">User Approves on PayPal</h3>
<p>The frontend redirects the user to <code>approveUrl</code>. PayPal handles authentication and approval, then redirects back to your <code>returnUrl</code>.</p>
<h3 id="heading-capture-payment">Capture Payment</h3>
<p>After approval, call <code>capturePayment()</code> with either <code>paymentOrderId</code> or <code>paypalOrderId</code>:</p>
<pre><code class="language-typescript">async capturePayment(paymentOrderId?: string, paypalOrderId?: string) {
  const order = await this.findOrder(paymentOrderId, paypalOrderId);

  if (order.paymentOrderStatus === PaymentOrderStatus.SUCCESS) {
    return this.buildCaptureResponse(order); // already captured
  }

  const capture = await this.paypalService.captureOrder(order.paypalOrderId);

  if (capture.status !== 'COMPLETED') {
    throw new Error(`PayPal capture status: ${capture.status}`);
  }

  await this.finalizeSuccessfulPayment(order, capture.captureId);
  return this.buildCaptureResponse(order);
}
</code></pre>
<p><code>finalizeSuccessfulPayment()</code> runs in a database transaction:</p>
<ol>
<li><p>Updates order status to <code>SUCCESS</code></p>
</li>
<li><p>Updates seller wallet balance</p>
</li>
<li><p>Creates ledger entries (audit trail)</p>
</li>
<li><p>Mark scheckout event as done</p>
</li>
<li><p>Publishes a <code>payment.{domain}.completed</code> event to RabbitMQ</p>
</li>
</ol>
<h2 id="heading-step-5-connect-domain-services-via-grpc">Step 5 — Connect Domain Services via gRPC</h2>
<p>Domain services (like <code>students-service</code>) never talk to PayPal directly. They use a gRPC client:</p>
<p>The Students Service communicates with the Payment Service through a gRPC client. Rather than calling the PayPal API directly, it invokes strongly typed remote procedures exposed by the payment service.</p>
<pre><code class="language-typescript">// apps/services/students-service/src/app/payment/payment.module.ts

ClientsModule.registerAsync([{
  name: 'PAYMENT_SERVICE',
  useFactory: () =&gt; ({
    transport: Transport.GRPC,
    options: {
      package: 'payment',
      protoPath: 'libs/shared/dto/src/lib/payment/payment.proto',
      url: process.env.PAYMENT_SERVICE_URL || 'payment-service:50061',
    },
  }),
}])
</code></pre>
<pre><code class="language-typescript">// payment-client.service.ts

@Injectable()
export class PaymentClientService implements OnModuleInit {
  private paymentService: PaymentGrpcService;

  constructor(@Inject('PAYMENT_SERVICE') private client: ClientGrpc) {}

  onModuleInit() {
    this.paymentService = this.client.getService('PaymentService');
  }

  async createPayment(data: CreatePaymentRequest) {
    return firstValueFrom(this.paymentService.CreatePayment(data));
  }

  async capturePayment(data: { payment_order_id?: string; paypal_order_id?: string }) {
    return firstValueFrom(this.paymentService.CapturePayment(data));
  }
}
</code></pre>
<h3 id="heading-domain-service-business-logic-example">Domain Service Business Logic Example:</h3>
<p>This example shows how a domain service prepares business-specific data before delegating payment processing to the Payment Service.</p>
<pre><code class="language-typescript">// application-payment.service.ts

async initiateTuitionPayment(applicationId: number, options: { domain: string }) {
  const application = await this.applicationModel.findByPk(applicationId);

  // Build PayPal return URLs from frontend domain
  const frontendBase = options.domain; // e.g. https://crm.yourapp.com
  const returnUrl = `${frontendBase}/payment/successful?applicationId=${application.applicationId}`;
  const cancelUrl = `${frontendBase}/payment/failure?applicationId=${application.applicationId}`;

  const result = await this.paymentClient.createPayment({
    checkout_id: `checkout-app-${application.id}`,
    payment_order_id: uuidv4(),
    domain: 'application',
    reference_id: String(application.id),
    payer_id: application.studentId,
    amount: finalAmount.toFixed(2),
    currency: 'USD',
    buyer_email: buyerEmail,
    seller_account: `university-${application.universityId}`,
    payment_category: 'tuition_deposit',
    return_url: returnUrl,
    cancel_url: cancelUrl,
    idempotency_key: `app-${application.id}-tuition-${uuidv4()}`,
  });

  return {
    approveUrl: result.approve_url,
    paypalOrderId: result.paypal_order_id,
    paymentOrderId: result.payment_order_id,
  };
}
</code></pre>
<p>The domain service remains responsible for business rules, while the payment service handles the payment workflow itself.</p>
<p><strong>Important:</strong> The frontend must send its own origin as <code>domain</code> so return URLs point to the correct environment (localhost in dev, production URL in prod).</p>
<h2 id="heading-step-6-add-the-api-gateway-layer">Step 6 — Add the API Gateway Layer</h2>
<p>The API gateway exposes HTTP endpoints to the frontend and forwards to domain services:</p>
<pre><code class="language-plaintext">POST /applications/:id/pay/applicationfee
Body: { "domain": "https://crm.yourapp.com", "couponCode": "SAVE10" }
</code></pre>
<pre><code class="language-typescript">// student-apigw → students-service (gRPC) → payment-service (gRPC) → PayPal
</code></pre>
<p>Gateway responsibilities:</p>
<ul>
<li><p>Authentication (JWT)</p>
</li>
<li><p>Request validation</p>
</li>
<li><p>No PayPal credentials</p>
</li>
</ul>
<p>Capture the endpoint after the PayPal redirect:</p>
<pre><code class="language-plaintext">POST /applications/:id/pay/applicationfee/capture
Body: { "paypalOrderId": "PAYPAL_ORDER_ID_FROM_URL" }
</code></pre>
<h2 id="heading-step-7-publish-payment-events-with-rabbitmq">Step 7 — Publish Payment Events with RabbitMQ</h2>
<p>RabbitMQ enables asynchronous communication between services. Instead of waiting for every service to finish processing after a payment succeeds, the payment service simply publishes an event and lets interested services handle it independently.</p>
<p>After a successful capture, the payment service publishes an event:</p>
<pre><code class="language-typescript">// payment-events.publisher.ts

async publishCompleted(event: PaymentCompletedEvent) {
  const pattern = `payment.${event.domain}.completed`; // e.g. payment.application.completed
  this.eventsClient.emit(pattern, { ...event, eventId: uuidv4() });
}
</code></pre>
<p>Each domain service subscribes to payment events that are relevant to its business domain. For example, the Students Service listens for <code>payment.application.completed</code> events so it can mark student applications as paid.</p>
<pre><code class="language-typescript">// payment-events.consumer.ts (students-service)

@EventPattern('payment.application.completed')
async handlePaymentCompleted(@Payload() data: PaymentCompletedPayload) {
  await this.applicationPaymentService.handlePaymentCompletedEvent(data);
  // Marks application as PAID, records payment history
}
</code></pre>
<p>This decouples payment completion from domain updates. Even if <code>students-service</code> is temporarily down, you can replay events from the queue.</p>
<h3 id="heading-two-paths-to-mark-an-order-as-paid">Two Paths to Mark an Order as Paid</h3>
<table>
<thead>
<tr>
<th>Path</th>
<th>When used</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Synchronous capture</strong></td>
<td>Frontend calls capture API after PayPal redirect</td>
</tr>
<tr>
<td><strong>Async event</strong></td>
<td>RabbitMQ consumer updates domain state after payment service publishes event</td>
</tr>
</tbody></table>
<p>Using both (with idempotency) gives you reliability: the sync path gives immediate UX feedback. The async path is a safety net.</p>
<h2 id="heading-step-8-database-schema-and-migrations">Step 8 — Database Schema and Migrations</h2>
<p>The payment service maintains its own database schema. Each table has a specific responsibility, allowing payment records, financial transactions, and webhook processing to remain isolated from other business services.</p>
<table>
<thead>
<tr>
<th>Table</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>payment_events</code></td>
<td>Checkout session (buyer/seller info)</td>
</tr>
<tr>
<td><code>payment_orders</code></td>
<td>Individual payment attempts with PayPal IDs</td>
</tr>
<tr>
<td><code>ledger_entries</code></td>
<td>Financial audit trail</td>
</tr>
<tr>
<td><code>wallets</code></td>
<td>Seller balance tracking</td>
</tr>
<tr>
<td><code>processed_webhooks</code></td>
<td>Webhook deduplication</td>
</tr>
<tr>
<td><code>coupons</code> / <code>coupon_redemptions</code></td>
<td>Discount codes (optional)</td>
</tr>
<tr>
<td><code>sequelize_meta</code></td>
<td>Migration tracking</td>
</tr>
</tbody></table>
<h3 id="heading-production-migration-gotcha">Production Migration Gotcha</h3>
<p>In production Docker images, migration <code>.ts</code> files are <strong>not</strong> available unless you compile them to JavaScript and copy them into the image:</p>
<pre><code class="language-dockerfile"># Dockerfile — compile migrations for production
RUN pnpm exec tsc --project apps/core/payment-service/tsconfig.migrations.json
COPY --from=builder /app/dist/apps/core/payment-service/migrations ./migrations
</code></pre>
<p>Without this, you'll see <code>Executed 0 migrations</code> in logs and <strong>no tables will be created</strong>.</p>
<p>Create the database user before first deploy:</p>
<pre><code class="language-sql">CREATE USER payment_user WITH PASSWORD 'payment_pass';
CREATE DATABASE payment_db;
GRANT ALL PRIVILEGES ON DATABASE payment_db TO payment_user;
</code></pre>
<h2 id="heading-step-9-local-development-setup-docker">Step 9 — Local Development Setup (Docker)</h2>
<h3 id="heading-environment-variables-env">Environment Variables (<code>.env</code>)</h3>
<pre><code class="language-bash">PAYPAL_CLIENT_ID=your_sandbox_client_id
PAYPAL_CLIENT_SECRET=your_sandbox_client_secret
PAYPAL_API_BASE=https://api-m.sandbox.paypal.com
</code></pre>
<p>In this section, we'll configure the payment service for local development using Docker Compose. This setup provides a complete environment for testing payments without deploying to production.</p>
<h3 id="heading-docker-compose-local">Docker Compose (local)</h3>
<p>The following configuration starts the payment service together with its required dependencies, including PostgreSQL and RabbitMQ.</p>
<pre><code class="language-yaml">payment-service:
  build:
    dockerfile: apps/core/payment-service/Dockerfile.dev
  ports:
    - '3003:3003'    # HTTP
    - '50061:50061'  # gRPC
  environment:
    - PAYPAL_API_BASE=https://api-m.sandbox.paypal.com
    - PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
    - PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
    - DB_HOST=postgres
    - DB_NAME=payment_db
    - DB_USER=payment_user
    - DB_PASSWORD=payment_pass
    - RABBITMQ_URL=amqp://rabbitmq:5672

students-service:
  environment:
    - PAYMENT_SERVICE_URL=payment-service:50061
  depends_on:
    payment-service:
      condition: service_healthy
</code></pre>
<h3 id="heading-start-services">Start Services</h3>
<p>Once the configuration is complete, start the containers and verify that every service is running correctly before testing the payment flow.</p>
<pre><code class="language-bash">docker compose up -d payment-service students-service student-apigw
</code></pre>
<h3 id="heading-verify-health">Verify Health</h3>
<pre><code class="language-bash">curl http://localhost:3003/health
# {"status":"healthy","service":"payment-service",...}
</code></pre>
<h3 id="heading-test-payment-flow">Test Payment Flow</h3>
<ol>
<li><p>Call <code>POST /applications/:id/pay/applicationfee</code> with <code>{ "domain": "http://localhost:3000" }</code></p>
</li>
<li><p>Open the returned <code>approveUrl</code> in a browser</p>
</li>
<li><p>Log in with a <a href="https://developer.paypal.com/dashboard/accounts">PayPal sandbox buyer account</a></p>
</li>
<li><p>After approval, call <code>POST /applications/:id/pay/applicationfee/capture</code> with the <code>paypalOrderId</code></p>
</li>
<li><p>Confirm application status is <code>PAID</code></p>
</li>
</ol>
<h2 id="heading-step-10-production-deployment">Step 10 — Production Deployment</h2>
<p>After verifying everything locally, the next step is deploying the payment service to production. The main differences are using PayPal Live credentials, production environment variables, and production-ready Docker images.</p>
<h3 id="heading-paypal-live-credentials">PayPal Live Credentials</h3>
<ol>
<li><p>Go to <a href="https://developer.paypal.com/dashboard/applications/live">PayPal Developer Dashboard → Live apps</a></p>
</li>
<li><p>Create a Live REST API app</p>
</li>
<li><p>Copy Client ID and Secret</p>
</li>
</ol>
<h3 id="heading-production-env-on-server-never-commit">Production <code>.env</code> (on Server — Never Commit)</h3>
<pre><code class="language-bash">PAYPAL_CLIENT_ID=your_live_client_id
PAYPAL_CLIENT_SECRET=your_live_secret
PAYPAL_API_BASE=https://api-m.paypal.com
</code></pre>
<h3 id="heading-docker-compose-production">Docker Compose (Production)</h3>
<pre><code class="language-yaml">payment-service:
  build:
    dockerfile: apps/core/payment-service/Dockerfile
  environment:
    - NODE_ENV=production
    - PAYPAL_API_BASE=${PAYPAL_API_BASE:-https://api-m.paypal.com}
    - PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
    - PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
    - DB_HOST=${DB_HOST}
    - DB_NAME=payment_db
    - DB_USER=payment_user
    - DB_PASSWORD=payment_pass
    - RABBITMQ_URL=amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq:5672
  labels:
    - 'traefik.http.routers.payment.rule=Host(`payment-service.yourapp.com`)'

students-service:
  environment:
    - PAYMENT_SERVICE_URL=payment-service:50061
  depends_on:
    payment-service:
      condition: service_healthy
</code></pre>
<h3 id="heading-deploy-commands">Deploy Commands</h3>
<pre><code class="language-bash">docker compose -f docker-compose.prod.yml build --no-cache payment-service
docker compose -f docker-compose.prod.yml up -d payment-service students-service
</code></pre>
<h3 id="heading-verify-production">Verify Production</h3>
<pre><code class="language-bash">curl https://payment-service.yourapp.com/health

docker logs -f apply-goal-payment-service
# Look for:
#   environment: live
#   Found 8 pending migrations
#   Executed 8 migrations
</code></pre>
<h3 id="heading-frontend-domain-in-production">Frontend Domain in Production</h3>
<p>The frontend must send the production CRM URL when initiating payment:</p>
<pre><code class="language-json">{ "domain": "https://crm.yourapp.com" }
</code></pre>
<p>Not <code>localhost</code>. This controls where PayPal redirects after payment.</p>
<h2 id="heading-step-11-health-checks-and-monitoring">Step 11 — Health Checks and Monitoring</h2>
<p>Health checks allow orchestration tools such as Docker and Traefik to verify that the payment service is running correctly. Monitoring these endpoints helps detect failures early and improves application reliability.</p>
<pre><code class="language-typescript">// GET /health
{ "status": "healthy", "service": "payment-service", "timestamp": "...", "version": "1.0.0" }
</code></pre>
<p>Used by:</p>
<ul>
<li><p>Docker <code>HEALTHCHECK</code></p>
</li>
<li><p>Traefik load balancer</p>
</li>
<li><p>Uptime monitoring</p>
</li>
</ul>
<p>PayPal credential check runs on startup via <code>PayPalService.logConfiguration()</code>.</p>
<h2 id="heading-complete-request-flow-real-example">Complete Request Flow (Real Example)</h2>
<p><strong>Scenario:</strong> Student pays tuition fee for university application.</p>
<pre><code class="language-plaintext">1. Frontend
   POST /applications/42/pay/applicationfee
   Body: { "domain": "https://crm.yourapp.com" }
        │
        ▼
2. student-apigw (HTTP → gRPC)
   InitiateApplicationTuitionPayment(applicationId: 42)
        │
        ▼
3. students-service
   - Validates application not already paid
   - Resolves tuition amount
   - Optionally validates coupon via payment-service gRPC
   - Builds returnUrl / cancelUrl from domain
   - Calls payment-service CreatePayment (gRPC)
        │
        ▼
4. payment-service
   - Creates payment_orders record (EXECUTING)
   - Calls PayPal POST /v2/checkout/orders
   - Returns approveUrl
        │
        ▼
5. Frontend redirects user to approveUrl (PayPal checkout)
        │
        ▼
6. User approves → PayPal redirects to returnUrl
        │
        ▼
7. Frontend
   POST /applications/42/pay/applicationfee/capture
   Body: { "paypalOrderId": "PAYPAL_ORDER_ID" }
        │
        ▼
8. payment-service
   - POST /v2/checkout/orders/{id}/capture
   - Updates order → SUCCESS
   - Updates wallet + ledger
   - Publishes payment.application.completed → RabbitMQ
        │
        ▼
9. students-service (event consumer)
   - Marks application paymentStatus = PAID
   - Records payment in application_payments table
</code></pre>
<h2 id="heading-coupon-support-optional">Coupon Support (Optional)</h2>
<p>Before creating a PayPal order, validate a coupon via gRPC:</p>
<pre><code class="language-typescript">const validation = await this.paymentClient.validateCoupon({
  code: 'SAVE20',
  universityId: application.universityId,
  originalAmount: 500,
  paymentType: 'application_fee',
});

const finalAmount = validation.data.finalAmount;

// If coupon covers 100% — skip PayPal entirely
if (finalAmount &lt;= 0) {
  await this.markApplicationPaid(applicationId, { amount: 0, source: 'coupon' });
  return { paymentOrderStatus: 'COMPLETED' };
}
</code></pre>
<p>Coupon logic lives in <code>payment-service</code> so discount rules are centralized.</p>
<h2 id="heading-paypal-webhooks-optional-but-recommended">PayPal Webhooks (Optional but Recommended)</h2>
<p>Register a webhook URL in the PayPal dashboard:</p>
<pre><code class="language-plaintext">https://payment-service.yourapp.com/api/v1/payments/webhooks/paypal
</code></pre>
<p>The payment service handles:</p>
<table>
<thead>
<tr>
<th>Event</th>
<th>Action</th>
</tr>
</thead>
<tbody><tr>
<td><code>CHECKOUT.ORDER.APPROVED</code></td>
<td>Auto-capture the order</td>
</tr>
<tr>
<td><code>PAYMENT.CAPTURE.COMPLETED</code></td>
<td>Finalize payment if not already done</td>
</tr>
</tbody></table>
<p>Webhook events are deduplicated via <code>processed_webhooks</code> table to prevent double-processing.</p>
<h2 id="heading-testing-checklist">Testing Checklist</h2>
<ul>
<li><p>[ ] <code>GET /health</code> returns 200</p>
</li>
<li><p>[ ] PayPal logs show <code>credentialsPresent: true</code></p>
</li>
<li><p>[ ] Database tables exist after startup (<code>payment_orders</code>, <code>payment_events</code>, etc.)</p>
</li>
<li><p>[ ] Create payment returns valid <code>approveUrl</code></p>
</li>
<li><p>[ ] Sandbox buyer can complete checkout</p>
</li>
<li><p>[ ] Capture returns <code>payment_order_status: SUCCESS</code></p>
</li>
<li><p>[ ] Application marked as <code>PAID</code> in domain service</p>
</li>
<li><p>[ ] RabbitMQ event <code>payment.application.completed</code> is consumed</p>
</li>
<li><p>[ ] Duplicate capture is handled gracefully (idempotent)</p>
</li>
<li><p>[ ] Coupon 100% discount skips PayPal</p>
</li>
<li><p>[ ] Production uses <code>https://api-m.paypal.com</code> (live)</p>
</li>
</ul>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Integrating PayPal in a microservice architecture comes down to a few principles:</p>
<ol>
<li><p>One payment service owns all PayPal API calls</p>
</li>
<li><p>gRPC connects domain services to the payment service internally</p>
</li>
<li><p>RabbitMQ broadcasts payment outcomes so domain services stay decoupled</p>
</li>
<li><p>Idempotency keys prevent duplicate charges</p>
</li>
<li><p>Environment variables switch between sandbox and live — no code changes</p>
</li>
<li><p>Migrations must be compiled for production Docker images</p>
</li>
<li><p>Frontend sends <code>domain</code> so return URLs work in every environment</p>
</li>
</ol>
<p>This pattern scales: add a new payment type (subscription, agency fee, university service fee) by sending a different <code>domain</code> and <code>payment_category</code> — no changes to PayPal integration code.</p>
<h2 id="heading-further-reading">Further Reading</h2>
<ul>
<li><p><a href="https://developer.paypal.com/docs/api/orders/v2/">PayPal Orders API v2 Documentation</a></p>
</li>
<li><p><a href="https://developer.paypal.com/tools/sandbox/">PayPal Sandbox Testing Guide</a></p>
</li>
<li><p><a href="https://docs.nestjs.com/microservices/grpc">NestJS Microservices (gRPC)</a></p>
</li>
<li><p><a href="https://docs.nestjs.com/microservices/rabbitmq">NestJS RabbitMQ Transport</a></p>
</li>
<li><p><a href="https://github.com/sequelize/umzug">Umzug Database Migrations</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
