RFC-001: Payment Integration Implementation
Superseded in part by
/docs/rfc/RFC-009-dual-entitlement-billing.mdfor 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
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/payments/create | Create payment (ONE_TIME or SUBSCRIPTION) for existing customer |
| POST | /api/payments/webhook | Xendit webhook handler |
| GET | /api/payments/status/:id | Check payment status |
| GET | /api/license/verify/:key | Verify license is active |
| GET | /api/license/customer/:id | Get 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:
customerIdmust already exist.- For
SUBSCRIPTION, onlyMONTHLYis 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:MANUALTRANSFER_ERROR
- No frontend/UI-triggered refund flow