EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
Public Invoice Payment System

Overview

Customers can now pay invoices without logging in using secure tokenized payment links sent via email.

How It Works

1. Payment Token Generation

  • Every invoice automatically gets a unique 64-character payment token
  • Tokens are cryptographically secure (256-bit entropy via crypto.randomBytes)
  • Tokens are stored in invoices.payment_token column

2. Payment Flow

1. Send invoice email to customer with payment link
2. Customer clicks link → /pay/{token}
3. Public payment page loads (no login required)
4. Customer enters payment details
5. Stripe processes payment on connected account
6. Invoice marked as paid, payment recorded

3. Security Features

  • ✅ No authentication required - token-based access
  • ✅ Tokens are unique per invoice
  • ✅ Cannot guess tokens (2^256 combinations)
  • ✅ Automatic validation (invoice exists, can be paid)
  • ✅ Prevents payment of void/already-paid invoices

Payment Link Format

https://rmm-psa-dashboard-5jyun.ondigitalocean.app/pay/{payment_token}

Example:

https://rmm-psa-dashboard-5jyun.ondigitalocean.app/pay/b7b0249982974e6e24e492e9c7f628c0f16163ae24c73c4c22c4bc9e34c72615

API Endpoints

1. GET /api/public/pay/:token

Fetches invoice details for payment page

Response:

{
"invoice_id": 502,
"currency": "AUD",
"subtotal": 75.00,
"tax_rate": 10.00,
"tax_amount": 7.50,
"total": 82.50,
"payment_status": "unpaid",
"status": "sent",
"customer_name": "The Vacuum Shop",
"items": [...],
"canPay": true
}

2. POST /api/public/pay/:token/create-payment

Creates Stripe payment intent

Response:

{
"clientSecret": "pi_xxx_secret_yyy",
"publishableKey": "pk_test_xxx",
"stripeAccount": "acct_xxx",
"amount": 8250,
"currency": "aud"
}

3. POST /api/public/pay/:token/confirm-payment

Confirms successful payment

Request:

{
"paymentIntentId": "pi_xxx"
}

Response:

{
"success": true,
"message": "Payment confirmed",
"invoice_id": 502
}

Database Schema

-- Added to invoices table
ALTER TABLE invoices ADD COLUMN payment_token VARCHAR(64) UNIQUE;
CREATE INDEX idx_invoices_payment_token ON invoices(payment_token) WHERE payment_token IS NOT NULL;

Usage Examples

Get Payment Token for Invoice

const invoice = await pool.query(
'SELECT invoice_id, payment_token FROM invoices WHERE invoice_id = $1',
[invoiceId]
);
const paymentUrl = `https://rmm-psa-dashboard-5jyun.ondigitalocean.app/pay/${invoice.rows[0].payment_token}`;

Email Template (HTML)

<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.invoice-email { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #2563eb; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9fafb; }
.pay-button {
display: inline-block;
background: #10b981;
color: white;
padding: 14px 32px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
margin: 20px 0;
}
.invoice-details { margin: 20px 0; }
.total { font-size: 24px; font-weight: bold; color: #1a202c; }
</style>
</head>
<body>
<div class="invoice-email">
<div class="header">
<h1>Invoice #${invoice.invoice_id}</h1>
</div>
<div class="content">
<p>Dear ${customer.name},</p>
<p>Thank you for your business. Please find your invoice details below:</p>
<div class="invoice-details">
<p><strong>Invoice Number:</strong> #${invoice.invoice_id}</p>
<p><strong>Issue Date:</strong> ${invoice.issued_date}</p>
<p><strong>Due Date:</strong> ${invoice.due_date}</p>
<hr>
<p><strong>Subtotal (ex GST):</strong> ${invoice.currency} $${invoice.subtotal}</p>
<p><strong>GST (${invoice.tax_rate}%):</strong> ${invoice.currency} $${invoice.tax_amount}</p>
<p class="total"><strong>Total:</strong> ${invoice.currency} $${invoice.total}</p>
</div>
<center>
<a href="${paymentUrl}" class="pay-button">Pay Invoice Online</a>
</center>
<p style="font-size: 12px; color: #64748b; margin-top: 30px;">
This payment link is secure and expires when the invoice is paid.
</p>
</div>
</div>
</body>
</html>

Sample Payment Links (Test)

Invoice #502 - The Vacuum Shop - AUD $82.50

https://rmm-psa-dashboard-5jyun.ondigitalocean.app/pay/b7b0249982974e6e24e492e9c7f628c0f16163ae24c73c4c22c4bc9e34c72615

Invoice #501 - The Vacuum Shop - AUD $165.00

https://rmm-psa-dashboard-5jyun.ondigitalocean.app/pay/0afaa6a843a66491f15cc2eb7e5f96c5cc6cb316ff0c8e03a9e2bc6b3a661038

Invoice #500 - The Vacuum Shop - AUD $550.00

https://rmm-psa-dashboard-5jyun.ondigitalocean.app/pay/74aa23f60234 0c2a3a0cefa9b82480786fa60283ce4b32fb0cb34f968c62fc96

Testing

Use Stripe test cards:

  • Success: 4242 4242 4242 4242 (any future date, any CVC)
  • Decline: 4000 0000 0000 0002
  • 3D Secure: 4000 0025 0000 3155

Token Regeneration

const crypto = require('crypto');
// Generate new token for invoice
const newToken = crypto.randomBytes(32).toString('hex');
await pool.query(
'UPDATE invoices SET payment_token = $1 WHERE invoice_id = $2',
[newToken, invoiceId]
);

Migration Status

  • ✅ All 502 invoices have payment tokens
  • ✅ New invoices auto-generate tokens on creation
  • ✅ Public payment endpoints deployed
  • ✅ Payment page UI deployed
  • ✅ Stripe integration configured for AUD
  • ✅ GST calculations included

Next Steps (Optional)

  1. Email Integration: Build email sender for invoices
  2. PDF Attachments: Attach invoice PDF to emails
  3. Payment Receipts: Auto-email receipt after payment
  4. Token Expiry: Add token expiration (e.g., 30 days)
  5. Payment Reminders: Schedule reminder emails for overdue invoices
  6. Payment Plans: Allow partial payments with payment schedules

Deployment Date: March 17, 2026
Backend: https://rmm-psa-backend-t9f7k.ondigitalocean.app
Frontend: https://rmm-psa-dashboard-5jyun.ondigitalocean.app
Status: ✅ LIVE