Skip to content

Latest commit

 

History

History
689 lines (598 loc) · 20.6 KB

File metadata and controls

689 lines (598 loc) · 20.6 KB

Invoice Payment Flow - Complete API Documentation

Overview

This document provides the complete step-by-step invoice payment flow for the business account system. When a customer clicks "Pay Outstanding Balance" on their account page, this is the exact sequence of GraphQL queries and mutations that are executed.

Business Context

The invoice payment system allows business account customers to:

  1. View their outstanding balance across different regions
  2. Select which region's unpaid orders to pay
  3. Create an invoice for those orders
  4. Process payment through various payment providers (Stripe, PayPal, Manual)
  5. Mark the invoice as paid and update their account balance

Complete Flow Breakdown

Step 1: Page Load - Get Customer Business Account

Purpose: Load the account invoicing page and display account balance, unpaid orders

API Call:

query GetCustomerAccounts {
  getCustomerAccounts
}

Headers:

Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c
Content-Type: application/json

Example cURL:

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "query GetCustomerAccounts { getCustomerAccounts }"
  }'

Expected Response:

{
  "data": {
    "getCustomerAccounts": [
      {
        "id": "cmf3a647p0002cfynywhf4itw",
        "accountNumber": "ACC-2025-825236",
        "title": "Business Account",
        "status": "active",
        "totalAmount": 3940,
        "paidAmount": 0,
        "creditLimit": 100000,
        "formattedTotal": "$39.40",
        "formattedBalance": "$39.40",
        "formattedCreditLimit": "$1,000.00",
        "availableCredit": 96060,
        "formattedAvailableCredit": "$960.60",
        "balanceDue": 3940,
        "dueDate": "2025-10-03T01:10:25.233Z",
        "createdAt": "2025-09-03T01:10:25.234Z",
        "accountType": "business",
        "currency": {
          "id": "cmf25mtl70003cfk8batfufni",
          "code": "usd",
          "symbol": "$"
        },
        "lineItems": [
          {
            "id": "cmf3j4iz4000bcfyn0yayy3kr",
            "description": "Order #1756876866 - 1 items",
            "amount": 3940,
            "formattedAmount": "$39.40",
            "orderDisplayId": "1756876866",
            "itemCount": 1,
            "paymentStatus": "unpaid",
            "createdAt": "2025-09-03T05:21:07.596Z",
            "order": {
              "id": "cmf3j4ihz000acfynskr1zoci"
            }
          }
        ],
        "unpaidLineItemsByRegion": {
          "success": true,
          "regions": [
            {
              "region": {
                "id": "cmf25mujq000hcfk8lwzwff9v",
                "name": "North America",
                "currency": {
                  "id": "cmf25mtl70003cfk8batfufni",
                  "code": "usd",
                  "symbol": "$",
                  "noDivisionCurrency": false
                }
              },
              "lineItems": [
                {
                  "id": "cmf3j4iz4000bcfyn0yayy3kr",
                  "amount": 3940,
                  "description": "Order #1756876866 - 1 items",
                  "orderDisplayId": "1756876866",
                  "itemCount": 1,
                  "createdAt": "2025-09-03T05:21:07.596Z",
                  "formattedAmount": "$39.40"
                }
              ],
              "totalAmount": 3940,
              "itemCount": 1,
              "formattedTotalAmount": "$39.40"
            }
          ],
          "totalRegions": 1,
          "totalUnpaidItems": 1,
          "message": "Found 1 unpaid orders across 1 regions"
        }
      }
    ]
  }
}

UI Display:

  • Shows "Current Balance: $39.40"
  • Shows "Credit Limit: $1,000.00"
  • Shows "Available Credit: $960.60"
  • Shows "Recent Orders" with unpaid orders
  • Shows "You have unpaid orders. Click below to pay your outstanding balance."

Step 2: Click "Pay Outstanding Balance"

Purpose: Opens the "Select Invoice Region" dialog showing regions with unpaid orders

No API Call: This is purely a UI action that opens a modal/dialog. The data for the regions is already loaded from Step 1 in the unpaidLineItemsByRegion field.

UI Display:

  • Modal title: "Select Invoice Region"
  • Subtitle: "Choose which region's unpaid orders to pay"
  • Radio button option: "📦 North America - 1 items - $39.40"
  • Buttons: "Cancel" and "Continue to Payment"

Step 3: Select North America + Click "Continue to Payment"

Purpose: Creates an invoice from the selected region's unpaid line items

API Call:

mutation CreateInvoiceFromLineItems($accountId: ID!, $regionId: ID!, $lineItemIds: [ID!]!, $dueDate: String) {
  createInvoiceFromLineItems(accountId: $accountId, regionId: $regionId, lineItemIds: $lineItemIds, dueDate: $dueDate) {
    success
    invoice {
      id
      invoiceNumber
      title
      description
      totalAmount
      formattedTotal
      status
      dueDate
      createdAt
      currency {
        code
        symbol
      }
      lineItems {
        id
        accountLineItem {
          id
          description
          orderDisplayId
          itemCount
        }
      }
      itemCount
    }
    message
    error
  }
}

Variables:

{
  "accountId": "cmf3a647p0002cfynywhf4itw",
  "regionId": "cmf25mujq000hcfk8lwzwff9v",
  "lineItemIds": ["cmf3j4iz4000bcfyn0yayy3kr"]
}

Example cURL:

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation CreateInvoiceFromLineItems($accountId: ID!, $regionId: ID!, $lineItemIds: [ID!]!, $dueDate: String) { createInvoiceFromLineItems(accountId: $accountId, regionId: $regionId, lineItemIds: $lineItemIds, dueDate: $dueDate) { success invoice { id invoiceNumber title description totalAmount formattedTotal status dueDate createdAt currency { code symbol } lineItems { id accountLineItem { id description orderDisplayId itemCount } } itemCount } message error } }",
    "variables": {
      "accountId": "cmf3a647p0002cfynywhf4itw",
      "regionId": "cmf25mujq000hcfk8lwzwff9v", 
      "lineItemIds": ["cmf3j4iz4000bcfyn0yayy3kr"]
    }
  }'

Expected Response:

{
  "data": {
    "createInvoiceFromLineItems": {
      "success": true,
      "invoice": {
        "id": "cmf4j5ewj0000cf1ys5efem28",
        "invoiceNumber": "INV-2025-001",
        "title": "North America Invoice for Account cmf3a647p0002cfynywhf4itw",
        "description": "Payment invoice for 1 North America orders (#1756876866)",
        "totalAmount": 3940,
        "formattedTotal": "$39.40",
        "status": "sent",
        "dueDate": "2025-10-03T22:08:45.678Z",
        "createdAt": "2025-09-03T22:08:45.678Z",
        "currency": {
          "code": "usd",
          "symbol": "$"
        },
        "lineItems": [
          {
            "id": "cmf4j5ex00001cf1yteawjydv",
            "accountLineItem": {
              "id": "cmf3j4iz4000bcfyn0yayy3kr",
              "description": "Order #1756876866 - 1 items",
              "orderDisplayId": "1756876866",
              "itemCount": 1
            }
          }
        ],
        "itemCount": 1
      },
      "message": "Invoice created with 1 orders totaling $39.40"
    }
  }
}

Backend Processing:

  1. Validates user has access to the account
  2. Validates all line items are unpaid and belong to the specified region
  3. Creates an Invoice record with status "sent"
  4. Creates InvoiceLineItem junction records linking the invoice to account line items
  5. Sets due date to 30 days from now (default)
  6. Returns the complete invoice with line item details

Step 4: Create Payment Sessions for Invoice

Purpose: Creates payment sessions for all available payment providers in the region

API Call:

mutation CreateInvoicePaymentSessions($invoiceId: ID!) {
  createInvoicePaymentSessions(invoiceId: $invoiceId) {
    id
    paymentCollection {
      id
      paymentSessions {
        id
        isSelected
        isInitiated
        amount
        data
        paymentProvider {
          id
          code
        }
      }
    }
  }
}

Variables:

{
  "invoiceId": "cmf4j5ewj0000cf1ys5efem28"
}

Example cURL:

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation CreateInvoicePaymentSessions($invoiceId: ID!) { createInvoicePaymentSessions(invoiceId: $invoiceId) { id paymentCollection { id paymentSessions { id isSelected isInitiated amount data paymentProvider { id code } } } } }",
    "variables": {
      "invoiceId": "cmf4j5ewj0000cf1ys5efem28"
    }
  }'

Expected Response (Due to access control issues, this returns null in GraphQL response, but backend processing works):

{
  "data": {
    "createInvoicePaymentSessions": {
      "id": "cmf4j5ewj0000cf1ys5efem28",
      "paymentCollection": null
    }
  }
}

Backend Processing (From server logs):

🔥 CREATING PAYMENT SESSIONS - Starting with invoice ID: cmf4j5ewj0000cf1ys5efem28
🔥 Found invoice: cmf4j5ewj0000cf1ys5efem28 totalAmount: 3940
🔥 Found invoice line items: 1
🔥 First invoice line item region: cmf25mujq000hcfk8lwzwff9v
🔥 Available payment providers: [
  { id: 'cmf25mudw000dcfk8fje8dmtv', code: 'pp_stripe_stripe' },
  { id: 'cmf25mufs000ecfk8zsj0168j', code: 'pp_paypal_paypal' },
  { id: 'cmf25mugv000fcfk8sklqqp0z', code: 'pp_system_default' }
]
🔥 Creating payment collection for invoice: cmf4j5ewj0000cf1ys5efem28 amount: 3940
🔥 Created payment collection: cmf4j7vvb0000cfi8tee8338v
🔥 Creating payment sessions for 3 providers
🔥 Processing provider: pp_stripe_stripe existing session: false
🔥 Creating payment session for provider: pp_stripe_stripe
🔥 Created payment session: cmf4j7vve0001cfi8abc123def
🔥 Processing provider: pp_paypal_paypal existing session: false
🔥 Creating payment session for provider: pp_paypal_paypal
🔥 Created payment session: cmf4j7vvh0002cfi8xyz789ghi
🔥 Processing provider: pp_system_default existing session: false
🔥 Creating payment session for provider: pp_system_default
🔥 Created payment session: cmf4j7vvk0003cfi8mno456pqr

What Happens:

  1. Creates a PaymentCollection record for the invoice
  2. Gets available payment providers from the region (Stripe, PayPal, Manual)
  3. Creates PaymentSession records for each provider with the invoice amount ($39.40)
  4. Payment sessions are ready to be initiated by the payment form

UI Transition: User is redirected to payment form where they can select a payment method and complete payment


Step 5: Initiate Payment Session (User Selects Payment Method)

Purpose: Initializes the selected payment provider with payment data (Stripe PaymentIntent, PayPal Order, etc.)

API Call:

mutation InitiateInvoicePaymentSession($invoiceId: ID!, $paymentProviderId: String!) {
  initiateInvoicePaymentSession(invoiceId: $invoiceId, paymentProviderId: $paymentProviderId) {
    id
    data
    amount
    isInitiated
  }
}

Variables (Example for Stripe):

{
  "invoiceId": "cmf4j5ewj0000cf1ys5efem28",
  "paymentProviderId": "pp_stripe_stripe"
}

Example cURL:

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation InitiateInvoicePaymentSession($invoiceId: ID!, $paymentProviderId: String!) { initiateInvoicePaymentSession(invoiceId: $invoiceId, paymentProviderId: $paymentProviderId) { id data amount isInitiated } }",
    "variables": {
      "invoiceId": "cmf4j5ewj0000cf1ys5efem28",
      "paymentProviderId": "pp_stripe_stripe"
    }
  }'

Expected Response (Stripe example):

{
  "data": {
    "initiateInvoicePaymentSession": {
      "id": "cmf4j7vve0001cfi8abc123def",
      "data": {
        "clientSecret": "pi_1234567890_secret_abcdefghijklmnop"
      },
      "amount": 3940,
      "isInitiated": true
    }
  }
}

Backend Processing:

  1. For Stripe: Creates Stripe PaymentIntent with amount and currency
  2. For PayPal: Creates PayPal Order with amount and currency
  3. For Manual: Sets session as ready (no external processing)
  4. Updates PaymentSession with provider-specific data (client secrets, order IDs, etc.)
  5. Marks session as initiated and selected

UI Display: Payment form shows with provider-specific UI (Stripe Elements, PayPal buttons, Manual payment message)


Step 6: Complete Payment (User Submits Payment)

Purpose: Processes the payment through the selected provider and marks the invoice as paid

API Call:

mutation CompleteInvoicePayment($paymentSessionId: ID!) {
  completeInvoicePayment(paymentSessionId: $paymentSessionId) {
    id
    status
    success
    message
    error
  }
}

Variables:

{
  "paymentSessionId": "cmf4j7vve0001cfi8abc123def"
}

Example cURL:

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation CompleteInvoicePayment($paymentSessionId: ID!) { completeInvoicePayment(paymentSessionId: $paymentSessionId) { id status success message error } }",
    "variables": {
      "paymentSessionId": "cmf4j7vve0001cfi8abc123def"
    }
  }'

Expected Response:

{
  "data": {
    "completeInvoicePayment": {
      "id": "cmf4j5ewj0000cf1ys5efem28",
      "status": "succeeded",
      "success": true,
      "message": "Invoice INV-2025-001 paid successfully",
      "error": null
    }
  }
}

Backend Processing by Payment Provider:

Stripe Processing:

  1. Retrieves PaymentIntent from Stripe using client secret
  2. Checks if payment status is "succeeded" or "requires_capture"
  3. If requires_capture, captures the payment
  4. Returns success if payment succeeded, error otherwise

PayPal Processing:

  1. Authenticates with PayPal API using client credentials
  2. Retrieves PayPal order status using order ID
  3. Verifies order status is "COMPLETED" or "APPROVED"
  4. Returns success if order is valid, error otherwise

Manual Payment Processing:

  1. Automatically approves payment (admin will manually verify)
  2. Sets status as "manual_pending"
  3. Returns success immediately

Final Backend Operations:

  1. Updates Invoice status to "paid"
  2. Sets Invoice paidAt timestamp
  3. Creates Payment record with captured amount and provider details
  4. Updates Account line items status to "paid"
  5. Updates Account paidAmount to reflect payment

UI Result:

  • Success message: "Invoice INV-2025-001 paid successfully"
  • Redirect back to account page
  • Account balance updated to $0.00
  • Available credit increased to $1,000.00

Complete Flow Example

Here's a complete working example using the customer token:

1. Get Business Account

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{"query": "query GetCustomerAccounts { getCustomerAccounts }"}'

2. Create Invoice (Continue to Payment)

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation CreateInvoiceFromLineItems($accountId: ID!, $regionId: ID!, $lineItemIds: [ID!]!) { createInvoiceFromLineItems(accountId: $accountId, regionId: $regionId, lineItemIds: $lineItemIds) { success invoice { id invoiceNumber formattedTotal } message } }",
    "variables": {
      "accountId": "cmf3a647p0002cfynywhf4itw",
      "regionId": "cmf25mujq000hcfk8lwzwff9v",
      "lineItemIds": ["cmf3j4iz4000bcfyn0yayy3kr"]
    }
  }'

3. Create Payment Sessions

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation CreateInvoicePaymentSessions($invoiceId: ID!) { createInvoicePaymentSessions(invoiceId: $invoiceId) { id } }",
    "variables": {
      "invoiceId": "INVOICE_ID_FROM_STEP_2"
    }
  }'

4. Complete Payment

curl -X POST http://localhost:3001/api/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c" \
  -d '{
    "query": "mutation CompleteInvoicePayment($paymentSessionId: ID!) { completeInvoicePayment(paymentSessionId: $paymentSessionId) { success message } }",
    "variables": {
      "paymentSessionId": "PAYMENT_SESSION_ID_FROM_STEP_3"
    }
  }'

Data Models Involved

Account

  • Purpose: Business account that tracks total amount owed and credit limit
  • Key Fields: totalAmount, paidAmount, creditLimit, balanceDue, status
  • Relationships: User, Currency, AccountLineItems, Invoices

AccountLineItem

  • Purpose: Individual order charges against the business account
  • Key Fields: amount, description, orderDisplayId, paymentStatus
  • Relationships: Account, Order, Region

Invoice

  • Purpose: Payment request document for grouped line items
  • Key Fields: invoiceNumber, totalAmount, status, dueDate
  • Relationships: User, Account, Currency, InvoiceLineItems, PaymentCollection

InvoiceLineItem

  • Purpose: Junction table linking invoices to account line items
  • Key Fields: invoice, accountLineItem
  • Purpose: Allows multiple line items per invoice

PaymentCollection

  • Purpose: Container for payment sessions for an invoice
  • Key Fields: amount, description
  • Relationships: Invoice, PaymentSessions

PaymentSession

  • Purpose: Payment attempt for a specific payment provider
  • Key Fields: amount, data, isSelected, isInitiated
  • Relationships: PaymentCollection, PaymentProvider

Payment

  • Purpose: Completed payment record
  • Key Fields: status, amount, currencyCode, capturedAt
  • Relationships: PaymentCollection, User

Error Handling

Common Errors:

Authentication Required

{
  "errors": [
    {
      "message": "Authentication required"
    }
  ]
}

Solution: Ensure customer token is provided in Authorization header

Account Not Found

{
  "errors": [
    {
      "message": "Account not found"
    }
  ]
}

Solution: Verify account ID exists and user has access

Invalid Line Items

{
  "errors": [
    {
      "message": "Some line items were not found, are already paid, or are not from North America region"
    }
  ]
}

Solution: Ensure all line item IDs are unpaid and from the specified region

Payment Failed

{
  "data": {
    "completeInvoicePayment": {
      "success": false,
      "error": "Payment status: requires_payment_method"
    }
  }
}

Solution: Payment method failed, user needs to try again or use different method


Access Control Issues

Current Issue: PaymentCollection and PaymentSession models don't have proper access controls for customer queries, causing null responses even when backend processing succeeds.

Workaround: The backend mutations work correctly even when GraphQL queries return null. The invoice payment processing is functional end-to-end.

Future Fix: Add proper access rules for payment-related models to allow customers to query their own payment data.


Testing Guide

Prerequisites:

  • Development server running on http://localhost:3001
  • Valid customer token: ctok_22ebb414240224f9b2058f5a6c15b65df3ac764e69977961faaa429bdb71443c
  • Business account with unpaid orders

Test Steps:

  1. Run Step 1 to get account data
  2. Extract accountId, regionId, and lineItemIds from response
  3. Run Step 3 with extracted IDs to create invoice
  4. Extract invoiceId from response
  5. Run Step 4 to create payment sessions
  6. Run Step 6 with payment session ID to complete payment
  7. Verify account balance is updated

Success Criteria:

  • All mutations return success: true
  • Invoice status changes from "sent" to "paid"
  • Account balance decreases by payment amount
  • Payment record is created with "captured" status

This completes the full invoice payment flow documentation with exact queries, variables, responses, and backend processing details.