Skip to content

Paper.id Payment Gateway Integration

Complete guide to Paper.id payment gateway integration in the Reserva platform, covering all payment flows, data synchronization, webhook handling, and database connections.


๐ŸŽฏ Overview

The Reserva platform integrates with Paper.id as the primary payment gateway for processing all payment transactions in Indonesia. This integration handles three main payment flows:

  1. Subscription Payments - Merchants paying for platform subscription plans (FREE โ†’ PRO โ†’ ENTERPRISE)
  2. Appointment Payments - Customers paying for service bookings
  3. Wallet Top-ups - Customers adding funds to their digital wallet

Key Integration Points:

  • Partner account creation during tenant registration
  • Sales invoice generation for B2B transactions
  • Real-time payment webhooks for status updates
  • Merchant balance tracking and withdrawals
  • Platform fee calculations

๐Ÿ—๏ธ Architecture Overview

graph TB
    subgraph "Reserva Platform"
        API[FastAPI Backend]
        DB[(MongoDB Database)]
        PaperClient[Paper.id Client]
        WebhookHandler[Webhook Handler]

        API --> PaperClient
        API --> DB
        WebhookHandler --> DB
    end

    subgraph "Paper.id Gateway"
        PaperAPI[Paper.id API]
        PaperWebhook[Paper.id Webhooks]
    end

    subgraph "Customer Journey"
        Customer[Customer/Merchant]
        PaymentPage[Paper.id Payment Page]
    end

    PaperClient -->|Create Invoice| PaperAPI
    PaperClient -->|Create Partner| PaperAPI
    PaperClient -->|Create Withdrawal| PaperAPI

    PaperAPI -->|Payment URL| PaymentPage
    Customer -->|Complete Payment| PaymentPage
    PaperWebhook -->|Payment Status| WebhookHandler

    Customer -->|API Request| API

๐Ÿ“ฆ Database Collections & Paper.id Mapping

Core Collections

1. tenants Collection

Stores business information and Paper.id partner linkage.

Schema:

{
  _id: ObjectId,
  name: String,                    // Business name
  slug: String,                    // URL-friendly identifier
  email: String,                   // Business contact email
  phone: String,                   // Business phone (E.164 format)

  // Paper.id Integration
  client_partner_id: String,       // Paper.id partner ID (e.g., "partner_abc123xyz")

  // Subscription
  subscription: {
    plan: String,                  // "free", "pro", "enterprise"
    status: String,                // "active", "past_due", "canceled"
    current_period_start: Date,
    current_period_end: Date
  },

  // Timestamps
  created_at: Date,
  updated_at: Date,
  is_active: Boolean
}

Paper.id Connection:

  • client_partner_id links to Paper.id partner account
  • Created automatically during tenant registration via /api/v2/partners
  • Used for all invoices and payments for this tenant

2. invoices Collection

Stores invoice records synchronized with Paper.id sales invoices.

Schema:

{
  _id: ObjectId,
  tenant_id: ObjectId,              // FK to tenants
  invoice_number: String,           // "INV-202501-001"
  invoice_type: String,             // "subscription", "appointment"

  // Paper.id Integration Fields
  paper_invoice_id: String,         // Paper.id invoice ID (e.g., "PI-20250115-ABC123")
  paper_pdf_url: String,            // URL to invoice PDF hosted on Paper.id
  paper_payment_url: String,        // Short payment link (e.g., "https://payper.id/short123")

  // Billing Details
  invoice_date: Date,
  due_date: Date,
  subtotal: Decimal,
  tax_amount: Decimal,
  discount_amount: Decimal,
  total_amount: Decimal,
  paid_amount: Decimal,

  // Status Tracking
  status: String,                   // "draft", "sent", "paid", "overdue", "cancelled"

  // Line Items
  line_items: [
    {
      description: String,
      quantity: Number,
      unit_price: Decimal,
      amount: Decimal,
      tax_rate: Number,
      tax_amount: Decimal
    }
  ],

  // Metadata for routing
  metadata: {
    renewal: Boolean,               // true = renewal, false/absent = upgrade
    subscription_id: ObjectId,      // For subscription invoices
    appointment_id: ObjectId,       // For appointment invoices
    previous_plan: String,          // For upgrades
    new_plan: String               // For upgrades
  },

  // Timestamps
  created_at: Date,
  updated_at: Date,
  paid_at: Date
}

Paper.id Connection:

  • Created via /api/v1/store-invoice (Sales Invoice API)
  • paper_invoice_id is the primary key for Paper.id invoice
  • paper_payment_url redirects customer to payment page
  • Webhook updates use paper_invoice_id to find local invoice

3. payments Collection

Tracks payment transactions processed through Paper.id.

Schema:

{
  _id: ObjectId,
  tenant_id: ObjectId,              // FK to tenants

  // Payment Type Routing
  payment_type: String,             // "subscription", "appointment", "wallet_topup"
  subscription_id: ObjectId,        // For subscription payments
  appointment_id: ObjectId,         // For appointment payments
  customer_id: ObjectId,            // For customer payments

  // Paper.id Integration
  paper_transaction_id: String,     // Paper.id payment transaction ID
  reference_id: String,             // Our reference ID (sent to Paper.id)

  // Amounts
  amount: Decimal,                  // Base amount
  platform_fee: Decimal,            // Fee charged to merchant (5% default)
  platform_fee_rate: Decimal,       // Fee percentage (0.05 = 5%)
  merchant_amount: Decimal,         // Amount credited to merchant (amount - fee)
  total_amount: Decimal,            // Total paid by customer
  currency: String,                 // "IDR"

  // Payment Details
  payment_method: String,           // "bank_transfer", "e_wallet", "qris"
  payment_provider: String,         // "paper_id"
  status: String,                   // "pending", "processing", "completed", "failed"

  // Gateway Data
  gateway_status: String,           // Raw status from Paper.id (e.g., "PAID", "PENDING")
  gateway_data: Object,             // Full webhook payload from Paper.id

  // Timestamps
  created_at: Date,
  updated_at: Date,
  completed_at: Date
}

Paper.id Connection:

  • reference_id sent to Paper.id during invoice creation
  • paper_transaction_id received from Paper.id webhook
  • gateway_data stores complete webhook payload for debugging
  • Status synchronized via webhook callbacks

4. subscriptions Collection

Manages subscription plans and billing cycles.

Schema:

{
  _id: ObjectId,
  tenant_id: ObjectId,              // FK to tenants

  // Plan Details
  plan_type: String,                // "free", "pro", "enterprise"
  billing_cycle: String,            // "monthly", "quarterly", "yearly"

  // Period Tracking
  current_period_start: Date,
  current_period_end: Date,
  next_billing_date: Date,

  // Status
  status: String,                   // "active", "past_due", "canceled", "expired"
  auto_renew: Boolean,

  // Scheduled Changes (for downgrades)
  scheduled_changes: {
    target_plan: String,
    effective_date: Date,
    reason: String
  },

  // Timestamps
  created_at: Date,
  updated_at: Date
}

Paper.id Connection:

  • Invoices generated for upgrades/renewals
  • Invoice metadata contains subscription_id for linking
  • Webhook handlers update subscription status after payment

5. merchant_balances Collection

Tracks merchant earnings and available balance for withdrawals.

Schema:

{
  _id: ObjectId,
  tenant_id: ObjectId,              // FK to tenants (unique index)

  // Balance Tracking
  available_balance: Decimal,       // Available for withdrawal
  pending_balance: Decimal,         // Pending settlement (not yet available)
  total_earned: Decimal,            // Lifetime earnings
  total_withdrawn: Decimal,         // Lifetime withdrawals

  // Paper.id Integration
  paper_partner_id: String,         // Paper.id partner ID
  last_synced_at: Date,             // Last sync with Paper.id balance API

  // Timestamps
  created_at: Date,
  updated_at: Date
}

Paper.id Connection:

  • Updated when appointment payments complete
  • Synchronized with Paper.id balance via /api/v1/balance
  • Used for withdrawal validations

6. withdrawals Collection

Tracks merchant withdrawal requests.

Schema:

{
  _id: ObjectId,
  tenant_id: ObjectId,              // FK to tenants

  // Withdrawal Details
  amount: Decimal,
  currency: String,                 // "IDR"
  bank_account_id: String,          // Paper.id bank account ID

  // Paper.id Integration
  paper_withdrawal_id: String,      // Paper.id withdrawal transaction ID
  otp_verified: Boolean,

  // Status Tracking
  status: String,                   // "pending", "processing", "completed", "failed", "rejected"

  // Timestamps
  requested_at: Date,
  processed_at: Date,
  completed_at: Date
}

Paper.id Connection:

  • Created via /api/v2/withdrawals with OTP verification
  • Status updated via Paper.id webhook
  • Merchant balance reduced when approved

๐Ÿ”„ Data Flow Diagrams

Flow 1: Tenant Registration โ†’ Partner Creation

sequenceDiagram
    participant User
    participant API as FastAPI
    participant DB as MongoDB
    participant Paper as Paper.id API

    User->>API: POST /api/v1/public/register
    Note over User,API: business_name, email, phone, admin_email, password

    API->>DB: Check for duplicates
    DB-->>API: No duplicates found

    API->>DB: Create tenant record
    Note over DB: tenant_id generated

    API->>Paper: POST /api/v2/partners
    Note over API,Paper: Create partner account<br/>name, email, phone, type=CLIENT

    Paper-->>API: Partner created
    Note over Paper: client_partner_id returned

    API->>DB: Update tenant.client_partner_id
    API->>DB: Create admin user
    API->>DB: Create subscription (FREE plan)

    API-->>User: Registration successful
    Note over User: tenant_id, slug, client_partner_id

Key Points:

  1. Partner creation happens automatically during tenant registration
  2. client_partner_id is stored in tenants.client_partner_id
  3. Partner ID format: myreserva-{tenant_id}
  4. If Paper.id creation fails, tenant is still created (manual partner setup needed)

Flow 2: Subscription Upgrade โ†’ Invoice โ†’ Payment โ†’ Webhook

sequenceDiagram
    participant Tenant
    participant API as FastAPI
    participant DB as MongoDB
    participant Paper as Paper.id API
    participant Customer
    participant Webhook as Webhook Handler

    Tenant->>API: POST /api/v1/subscriptions/upgrade
    Note over Tenant: target_plan=pro, billing_period=monthly

    API->>DB: Get current subscription
    API->>API: Calculate prorated amount
    Note over API: (new_price - old_price) * (days_remaining / days_in_period)

    API->>DB: Create invoice record
    Note over DB: invoice_type=subscription<br/>status=draft<br/>metadata.renewal=false

    API->>Paper: POST /api/v1/store-invoice
    Note over API,Paper: Customer info, items, amounts<br/>callback_url, metadata

    Paper-->>API: Invoice created
    Note over Paper: paper_invoice_id<br/>paper_payment_url<br/>paper_pdf_url

    API->>DB: Update invoice with Paper.id details
    Note over DB: paper_invoice_id saved<br/>status=sent

    API-->>Tenant: Invoice created
    Note over Tenant: payment_url returned

    Tenant->>Customer: Share payment link
    Customer->>Paper: Open paper_payment_url
    Customer->>Paper: Complete payment

    Paper->>Webhook: POST /api/v1/webhooks/paper-invoice
    Note over Paper,Webhook: X-Paper-Signature header<br/>invoice paid payload

    Webhook->>Webhook: Verify HMAC signature
    Webhook->>DB: Find invoice by paper_invoice_id
    Webhook->>DB: Update invoice.status = paid

    Webhook->>DB: Check metadata.renewal flag
    Note over Webhook: renewal=false โ†’ upgrade handler

    Webhook->>DB: Update subscription.plan_type = pro
    Note over DB: current_period_end unchanged

    Webhook-->>Paper: 200 OK
    Webhook->>Tenant: Send upgrade confirmation email

Key Database Updates:

  1. Invoice created: invoices collection with status=draft
  2. Paper.id invoice: paper_invoice_id, paper_payment_url, paper_pdf_url added
  3. Invoice sent: status changed to sent
  4. Webhook received: status changed to paid, paid_at timestamp set
  5. Subscription updated: subscriptions.plan_type changed, current_period_end unchanged

Flow 3: Subscription Renewal โ†’ Extended Period

sequenceDiagram
    participant Tenant
    participant API
    participant DB as MongoDB
    participant Paper as Paper.id
    participant Webhook

    Tenant->>API: POST /api/v1/subscriptions/renew
    Note over Tenant: subscription_id

    API->>DB: Get subscription details
    API->>API: Calculate renewal amount (full period)

    API->>DB: Create invoice
    Note over DB: metadata.renewal = TRUE<br/>metadata.billing_cycle = monthly

    API->>Paper: POST /api/v1/store-invoice
    Note over API,Paper: Full period amount<br/>renewal flag in metadata

    Paper-->>API: Invoice created
    API->>DB: Update invoice with Paper.id URLs
    API-->>Tenant: Payment URL returned

    Tenant->>Paper: Complete payment
    Paper->>Webhook: POST /api/v1/webhooks/paper-invoice

    Webhook->>DB: Find invoice by paper_invoice_id
    Webhook->>DB: Check metadata.renewal = true
    Note over Webhook: Routes to RENEWAL handler

    Webhook->>DB: Extend subscription period
    Note over DB: current_period_end += 30 days<br/>current_period_start = old end date<br/>plan_type UNCHANGED

    Webhook-->>Paper: 200 OK

Key Differences from Upgrade:

Aspect Upgrade Renewal
Invoice metadata renewal: false or absent renewal: true
Amount Prorated (remaining days) Full period
Subscription change plan_type changes plan_type unchanged
Period change current_period_end unchanged current_period_end extended by billing cycle
Webhook handler handle_invoice_payment_for_subscription() handle_invoice_payment_for_renewal()

Flow 4: Appointment Payment โ†’ Merchant Balance

sequenceDiagram
    participant Customer
    participant API
    participant DB as MongoDB
    participant Paper as Paper.id
    participant Webhook

    Customer->>API: POST /api/v1/customer/booking/appointments
    Note over Customer: service_id, outlet_id, staff_id, date, time

    API->>DB: Create appointment
    Note over DB: status=pending<br/>payment_status=unpaid

    API->>DB: Create invoice
    Note over DB: invoice_type=appointment<br/>metadata.appointment_id

    API->>Paper: POST /api/v1/store-invoice
    Paper-->>API: Invoice created

    API->>DB: Update invoice with Paper.id details
    API-->>Customer: Payment URL

    Customer->>Paper: Complete payment
    Paper->>Webhook: POST /api/v1/webhooks/paper-invoice

    Webhook->>DB: Find invoice by paper_invoice_id
    Webhook->>DB: Update invoice.status = paid

    Webhook->>DB: Get appointment_id from metadata
    Webhook->>DB: Update appointment.status = confirmed
    Webhook->>DB: Update appointment.payment_status = paid

    Webhook->>Webhook: Calculate platform fee (5%)
    Note over Webhook: platform_fee = amount * 0.05<br/>merchant_amount = amount - platform_fee

    Webhook->>DB: Update merchant_balances
    Note over DB: available_balance += merchant_amount<br/>total_earned += merchant_amount

    Webhook->>DB: Create payment record
    Note over DB: payment_type=appointment<br/>platform_fee saved<br/>merchant_amount saved

    Webhook-->>Paper: 200 OK
    Webhook->>Customer: Send booking confirmation email

Database Updates:

  1. Appointment created: status=pending, payment_status=unpaid
  2. Invoice created: Linked via metadata.appointment_id
  3. Payment completed: Appointment becomes confirmed, paid
  4. Merchant balance: available_balance increased by merchant_amount (amount - 5% platform fee)
  5. Payment record: Complete audit trail with fee breakdown

๐Ÿ” Webhook Architecture

Single Endpoint, Multiple Handlers

Paper.id allows only one webhook URL per account. The platform uses intelligent routing to handle different invoice types:

Webhook URL: POST /api/v1/webhooks/paper-invoice

Routing Logic:

async def handle_paper_invoice_webhook(payload: dict):
    invoice_data = payload.get('data', {})
    invoice_type = invoice_data.get('invoice_type')
    metadata = invoice_data.get('metadata', {})

    # Route based on invoice_type and metadata
    if invoice_type == "SUBSCRIPTION":
        if metadata.get('renewal') == True:
            # Renewal: extend period, keep plan
            await handle_invoice_payment_for_renewal(invoice_data)
        else:
            # Upgrade: change plan, keep period
            await handle_invoice_payment_for_subscription(invoice_data)

    elif invoice_type == "APPOINTMENT":
        # Appointment booking payment
        await handle_invoice_payment_for_appointment(invoice_data)

Webhook Payload Structure

Paper.id sends:

{
  "event": "invoice.paid",
  "data": {
    "invoice_id": "PI-20250115-ABC123",
    "invoice": {
      "id": "507f1f77bcf86cd799439011",
      "number": "INV-202501-001",
      "status": "paid",
      "amount": "249950",
      "paid_amount": "249950"
    },
    "invoice_type": "SUBSCRIPTION",
    "metadata": {
      "tenant_id": "12345abcdef67890abcdef12",
      "subscription_id": "67890abcdef1234567890123",
      "renewal": false,
      "previous_plan": "free",
      "new_plan": "pro"
    },
    "paid_at": "2025-01-15T14:30:00Z"
  }
}

Security: HMAC Signature Verification

Header: X-Paper-Signature: sha256_abc123def456...

Verification Process:

def verify_signature(payload: str, signature: str) -> bool:
    # Remove prefix if present
    if signature.startswith('sha256_'):
        signature = signature[7:]

    # Calculate expected signature
    expected = hmac.new(
        client_secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected, signature)

Security Measures:

  1. โœ… HMAC-SHA256 signature verification
  2. โœ… Idempotency checks (track processed invoice IDs)
  3. โœ… Amount verification (payment matches invoice)
  4. โœ… Tenant isolation (invoice belongs to correct tenant)
  5. โœ… IP whitelisting (production: only accept from Paper.id IPs)

๐Ÿ’ณ Paper.id API Endpoints Used

Partner Management

Create Partner

POST /api/v2/partners
Headers:
  client_id: {PAPER_ID_CLIENT_ID}
  client_secret: {PAPER_ID_CLIENT_SECRET}

Body:
{
  "name": "Bella Vista Spa",
  "number": "myreserva-507f1f77bcf86cd799439011",
  "phone": "628123456789",
  "type": "CLIENT",
  "email": "contact@bellavista.com",
  "business_type": "pt",
  "address": {
    "address_line_1": "Jl. Example No. 123",
    "district": "Jakarta Selatan",
    "province": "DKI Jakarta",
    "zip_code": "12345",
    "country": "IDN"
  }
}

Response:
{
  "data": {
    "id": "partner_abc123xyz",
    "number": "myreserva-507f1f77bcf86cd799439011",
    "name": "Bella Vista Spa",
    ...
  }
}

Database Update:

db.tenants.updateOne(
  { _id: ObjectId("507f1f77bcf86cd799439011") },
  { $set: { client_partner_id: "partner_abc123xyz" } }
)


Sales Invoice API

Create Invoice

POST /api/v1/store-invoice
Headers:
  client_id: {PAPER_ID_CLIENT_ID}
  client_secret: {PAPER_ID_CLIENT_SECRET}

Body:
{
  "invoice_date": "15-01-2025",
  "due_date": "22-01-2025",
  "customer": {
    "id": "myreserva-507f1f77bcf86cd799439011",
    "name": "Bella Vista Spa",
    "email": "contact@bellavista.com",
    "phone": "628123456789"
  },
  "items": [
    {
      "item_name": "PRO Plan - Monthly Subscription",
      "unit": "month",
      "unit_count": 1,
      "unit_price": 249950,
      "amount": 249950
    }
  ],
  "callback_url": "https://api.myreserva.com/api/v1/webhooks/paper-invoice",
  "send": {
    "email": true,
    "whatsapp": false,
    "sms": false
  },
  "metadata": {
    "tenant_id": "507f1f77bcf86cd799439011",
    "subscription_id": "67890abcdef1234567890123",
    "renewal": false,
    "previous_plan": "free",
    "new_plan": "pro"
  }
}

Response:
{
  "data": {
    "invoice_id": "PI-20250115-ABC123",
    "invoice_url": "https://stg-v2.paper.id/invoice/abc123",
    "pdf_url": "https://stg-v2.paper.id/pdf/abc123.pdf",
    "short_url": "https://payper.id/short123",
    "status": "unpaid"
  }
}

Database Update:

db.invoices.updateOne(
  { _id: ObjectId("...") },
  {
    $set: {
      paper_invoice_id: "PI-20250115-ABC123",
      paper_payment_url: "https://payper.id/short123",
      paper_pdf_url: "https://stg-v2.paper.id/pdf/abc123.pdf",
      status: "sent"
    }
  }
)


Withdrawal API

Request OTP

GET /api/v2/verification-request?otp_action=withdrawal&delivery_method=email&email=admin@example.com
Headers:
  client_id: {PAPER_ID_CLIENT_ID}
  client_secret: {PAPER_ID_CLIENT_SECRET}

Response:
{
  "data": {
    "otp_sent": true,
    "delivery_method": "email",
    "expires_in": 300
  }
}

Create Withdrawal

POST /api/v2/withdrawals
Headers:
  client_id: {PAPER_ID_CLIENT_ID}
  client_secret: {PAPER_ID_CLIENT_SECRET}

Body:
{
  "amount": 1000000,
  "bank_account_id": "ba_abc123",
  "otp_code": "123456",
  "description": "Merchant withdrawal"
}

Response:
{
  "data": {
    "withdrawal_id": "WD-20250115-XYZ789",
    "amount": 1000000,
    "status": "processing",
    "created_at": "2025-01-15T14:30:00Z"
  }
}

Database Update:

// Create withdrawal record
db.withdrawals.insertOne({
  tenant_id: ObjectId("..."),
  amount: 1000000,
  paper_withdrawal_id: "WD-20250115-XYZ789",
  status: "processing",
  requested_at: new Date()
})

// Update merchant balance
db.merchant_balances.updateOne(
  { tenant_id: ObjectId("...") },
  {
    $inc: {
      available_balance: -1000000,
      total_withdrawn: 1000000
    }
  }
)


๐Ÿ”„ Complete Integration Checklist

โœ… Tenant Onboarding

  • [ ] User submits registration form
  • [ ] System creates tenant record in tenants collection
  • [ ] System calls Paper.id /api/v2/partners to create partner
  • [ ] System stores client_partner_id in tenant record
  • [ ] System creates FREE subscription in subscriptions collection
  • [ ] System creates admin user in users collection
  • [ ] System returns registration confirmation with tenant_id, slug, client_partner_id

โœ… Subscription Upgrade

  • [ ] Tenant requests upgrade via /api/v1/subscriptions/upgrade
  • [ ] System calculates prorated amount
  • [ ] System creates invoice in invoices collection with metadata.renewal=false
  • [ ] System calls Paper.id /api/v1/store-invoice
  • [ ] System updates invoice with paper_invoice_id, paper_payment_url, paper_pdf_url
  • [ ] System returns payment URL to tenant
  • [ ] Customer completes payment on Paper.id
  • [ ] Paper.id sends webhook to /api/v1/webhooks/paper-invoice
  • [ ] System verifies HMAC signature
  • [ ] System routes to upgrade handler (no renewal flag)
  • [ ] System updates subscription.plan_type to new plan
  • [ ] System keeps current_period_end unchanged
  • [ ] System marks invoice as paid
  • [ ] System sends upgrade confirmation email

โœ… Subscription Renewal

  • [ ] Tenant requests renewal via /api/v1/subscriptions/renew
  • [ ] System creates invoice with metadata.renewal=true
  • [ ] System calls Paper.id /api/v1/store-invoice with full period amount
  • [ ] System returns payment URL
  • [ ] Customer completes payment
  • [ ] Paper.id sends webhook
  • [ ] System routes to renewal handler (renewal flag present)
  • [ ] System extends current_period_end by billing cycle duration
  • [ ] System keeps plan_type unchanged
  • [ ] System sends renewal confirmation email

โœ… Appointment Payment

  • [ ] Customer creates appointment via /api/v1/customer/booking/appointments
  • [ ] System creates appointment with status=pending, payment_status=unpaid
  • [ ] System creates invoice with invoice_type=appointment, metadata.appointment_id
  • [ ] System calls Paper.id /api/v1/store-invoice
  • [ ] System returns payment URL
  • [ ] Customer completes payment
  • [ ] Paper.id sends webhook
  • [ ] System updates invoice to paid
  • [ ] System updates appointment to confirmed, paid
  • [ ] System calculates platform fee (5%)
  • [ ] System updates merchant_balances.available_balance with merchant amount
  • [ ] System creates payment record with fee breakdown
  • [ ] System sends booking confirmation email

โœ… Merchant Withdrawal

  • [ ] Merchant requests OTP via /api/v1/withdrawals/request-otp
  • [ ] System calls Paper.id /api/v2/verification-request
  • [ ] Merchant receives OTP via email
  • [ ] Merchant submits withdrawal with OTP via /api/v1/withdrawals
  • [ ] System validates balance and OTP
  • [ ] System calls Paper.id /api/v2/withdrawals
  • [ ] System creates withdrawal record in withdrawals collection
  • [ ] System reduces merchant_balances.available_balance
  • [ ] Paper.id processes withdrawal
  • [ ] Paper.id sends webhook with status update
  • [ ] System updates withdrawal status to completed

๐Ÿงช Testing Guide

Prerequisites

  1. ngrok - Expose local server for webhook testing

    ngrok http 8000
    

  2. Paper.id Dashboard - Configure webhook URL

  3. Go to Settings โ†’ Webhooks

  4. Set URL: https://your-ngrok-url.ngrok.io/api/v1/webhooks/paper-invoice
  5. Save

  6. Test Credentials - Use Paper.id staging environment

    PAPER_ID_BASE_URL=https://open-api.stag-v2.paper.id
    PAPER_ID_CLIENT_ID=your_staging_client_id
    PAPER_ID_CLIENT_SECRET=your_staging_client_secret
    

Test Scenario 1: Tenant Registration

# 1. Register new tenant
curl -X POST http://localhost:8000/api/v1/public/register \
  -H "Content-Type: application/json" \
  -d '{
    "business_name": "Test Spa",
    "business_email": "test@spa.com",
    "business_phone": "+628123456789",
    "admin_email": "admin@test.com",
    "admin_password": "SecurePass123!",
    "terms_accepted": true,
    "privacy_accepted": true
  }'

# 2. Verify in MongoDB
mongo beauty_saas_db
db.tenants.findOne({ name: "Test Spa" })
# Check: client_partner_id is populated

# 3. Verify in Paper.id Dashboard
# Go to Partners โ†’ Search for "myreserva-{tenant_id}"

Test Scenario 2: Subscription Upgrade

# 1. Get current subscription
curl -X GET http://localhost:8000/api/v1/subscriptions/current \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

# 2. Request upgrade
curl -X POST http://localhost:8000/api/v1/subscriptions/upgrade \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "target_plan": "pro",
    "billing_period": "monthly",
    "prorate_charges": true
  }'

# Response contains paper_payment_url

# 3. Check invoice in MongoDB
db.invoices.findOne({ paper_invoice_id: "PI-..." })
# Verify: metadata.renewal is false or absent

# 4. Complete payment (open paper_payment_url in browser)

# 5. Monitor webhook
tail -f logs/app.log | grep "Paper.id webhook"

# 6. Verify subscription updated
db.subscriptions.findOne({ tenant_id: ObjectId("...") })
# Check: plan_type changed, current_period_end unchanged

Test Scenario 3: Appointment Payment

# 1. Create appointment
curl -X POST http://localhost:8000/api/v1/customer/booking/appointments \
  -H "Authorization: Bearer CUSTOMER_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "outlet_id": "...",
    "service_id": "...",
    "staff_id": "...",
    "appointment_date": "2025-01-20",
    "appointment_time": "14:00"
  }'

# 2. Check appointment status
db.appointments.findOne({ _id: ObjectId("...") })
# Verify: status=pending, payment_status=unpaid

# 3. Complete payment via returned URL

# 4. Verify after webhook
db.appointments.findOne({ _id: ObjectId("...") })
# Check: status=confirmed, payment_status=paid

db.merchant_balances.findOne({ tenant_id: ObjectId("...") })
# Check: available_balance increased

๐Ÿ“ Environment Variables

# Paper.id Configuration
PAPER_ID_BASE_URL=https://open-api.stag-v2.paper.id  # Staging
# PAPER_ID_BASE_URL=https://open-api.paper.id  # Production

PAPER_ID_CLIENT_ID=your_client_id_here
PAPER_ID_CLIENT_SECRET=your_client_secret_here

# Backend URL (for webhook callbacks)
BACKEND_URL=https://api.myreserva.com

# Optional: Next.js webhook proxy
NEXTJS_WEBHOOK_URL=https://app.myreserva.com/api/webhooks

๐Ÿšจ Troubleshooting

Issue: Partner Creation Fails

Symptoms: Tenant created but client_partner_id is empty

Checks:

db.tenants.findOne({ _id: ObjectId("...") })
// Check: client_partner_id field

Solutions:

  1. Verify Paper.id credentials in .env
  2. Check phone number format (E.164 without +)
  3. Manually create partner via Paper.id dashboard
  4. Update tenant record:
    db.tenants.updateOne(
      { _id: ObjectId("...") },
      { $set: { client_partner_id: "partner_xxx" } }
    )
    

Issue: Webhook Not Received

Symptoms: Payment completed but status not updated

Checks:

  1. ngrok running: ngrok http 8000
  2. Webhook URL configured in Paper.id dashboard
  3. Check ngrok web interface: http://localhost:4040
  4. Check server logs: tail -f logs/app.log

Solutions:

  1. Resend webhook from Paper.id dashboard
  2. Manually update invoice status:
    db.invoices.updateOne(
      { paper_invoice_id: "PI-..." },
      { $set: { status: "paid", paid_at: new Date() } }
    )
    

Issue: Wrong Handler Invoked

Symptoms: Renewal processed as upgrade (or vice versa)

Checks:

db.invoices.findOne({ paper_invoice_id: "PI-..." })
// Check: metadata.renewal flag

Solutions:

  1. Verify invoice metadata during creation
  2. If incorrect, revert subscription changes manually
  3. Correct metadata and resend webhook

๐Ÿ“Š Monitoring & Metrics

Key Metrics to Track

  1. Partner Creation Success Rate

    db.tenants.aggregate([
      {
        $group: {
          _id: null,
          total: { $sum: 1 },
          with_partner: { $sum: { $cond: [{ $ne: ["$client_partner_id", null] }, 1, 0] } }
        }
      }
    ])
    

  2. Webhook Processing Success Rate

    db.invoices.aggregate([
      {
        $match: { paper_invoice_id: { $exists: true } }
      },
      {
        $group: {
          _id: "$status",
          count: { $sum: 1 }
        }
      }
    ])
    

  3. Merchant Balance Accuracy

    db.merchant_balances.find({}).forEach(function(balance) {
      var earned = db.payments.aggregate([
        { $match: { tenant_id: balance.tenant_id, status: "completed" } },
        { $group: { _id: null, total: { $sum: "$merchant_amount" } } }
      ]).toArray()[0].total;
    
      var withdrawn = db.withdrawals.aggregate([
        { $match: { tenant_id: balance.tenant_id, status: "completed" } },
        { $group: { _id: null, total: { $sum: "$amount" } } }
      ]).toArray()[0].total;
    
      print("Tenant: " + balance.tenant_id);
      print("Expected: " + (earned - withdrawn));
      print("Actual: " + balance.available_balance);
    });
    


๐Ÿ”— Additional Resources