Skip to main content

RFC-001: Payment Integration Implementation

Superseded in part by /docs/rfc/RFC-009-dual-entitlement-billing.md for the current dual-model billing architecture. RFC-001 remains useful for original payment integration context.

Overview

This document outlines the technical implementation for integrating Xendit payment gateway for:

  • One-time license purchases (Rp 8,000,000)
  • Monthly subscription plan purchases (monthly only for now)

Feature Requirements

Feature: Combine with a Payment Gateway that supports QRIS payments (Example: Xendit).

Details:

  • Payment is for the tenant/operator using the app, not the end-user taking photos.
  • Support one-time lifetime license purchases and future recurring subscription billing.
  • Use a centralized Frameingo-managed Xendit account.
  • End-users at the booth do not pay per print or per download.

Implementation Model

One-Time Purchase (Selected)

  • Single Frameingo Xendit account
  • Customer purchases lifetime license once
  • No recurring fees or platform charges
  • License key activated upon payment

Integration Architecture

Post-Purchase Flow

Database Schema

API Endpoints

MethodEndpointDescription
POST/api/payments/createCreate payment (ONE_TIME or SUBSCRIPTION) for existing customer
POST/api/payments/webhookXendit webhook handler
GET/api/payments/status/:idCheck payment status
GET/api/license/verify/:keyVerify license is active
GET/api/license/customer/:idGet customer license info

Payment Status States

Xendit Integration

// Create payment
const payment = await xendit.qrCode.create({
externalID: `license-${customerId}-${Date.now()}`,
amount: 8000000,
type: "DYNAMIC",
callbackUrl: `${BASE_URL}/api/payments/webhook`,
});
// Webhook handler
export const onRequest: PagesFunction = async ({ request, env }) => {
const payload = await request.json();

if (payload.status === "COMPLETED") {
// Activate license
await activateLicense(payload.externalID, payload.customer_id);
}
};

License Key Generation

function generateLicenseKey(): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let key = "";
for (let i = 0; i < 16; i++) {
if (i > 0 && i % 4 === 0) key += "-";
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
return key; // e.g., ABCD-EFGH-IJKL-MNOP
}

Implementation Phases

Phase 1: Payment Processing

  • Xendit QRIS integration
  • Payment webhook handler
  • License key generation
  • Existing customer validation before payment creation
  • Monthly subscription creation support (SUBSCRIPTION + MONTHLY)

Payment Creation Contract (Current)

POST /api/v1/payments/create

{
"customerId": "cust-123",
"currency": "IDR",
"paymentType": "ONE_TIME"
}
{
"customerId": "cust-123",
"currency": "IDR",
"paymentType": "SUBSCRIPTION",
"subscriptionInterval": "MONTHLY"
}

Notes:

  • customerId must already exist.
  • For SUBSCRIPTION, only MONTHLY is accepted.

Phase 2: License Verification

  • API to verify license is active
  • Frontend license check before features
  • Graceful fallback for inactive licenses

Phase 3: License Management

  • Customer dashboard to view license
  • License transfer (future)
  • Refund process via backend API only (POST /api/v1/payments/:id/refund) with reasons:
    • MANUAL
    • TRANSFER_ERROR
  • No frontend/UI-triggered refund flow