init
This commit is contained in:
@@ -0,0 +1,902 @@
|
||||
# Polar Best Practices
|
||||
|
||||
Production-proven patterns from real SaaS implementations covering SDK initialization, checkout flows, webhooks, discounts, fee calculations, and error handling.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Required Environment Variables
|
||||
```bash
|
||||
# Core API
|
||||
POLAR_API_KEY=polar_at_xxx # Access token from Polar Dashboard
|
||||
POLAR_ORGANIZATION_ID=org_xxx # Your organization ID
|
||||
POLAR_WEBHOOK_SECRET=whsec_xxx # Webhook signature verification
|
||||
|
||||
# Product IDs (one per product)
|
||||
POLAR_PRODUCT_ENGINEER_ID=prod_xxx
|
||||
POLAR_PRODUCT_MARKETING_ID=prod_xxx
|
||||
POLAR_PRODUCT_COMBO_ID=prod_xxx
|
||||
|
||||
# Environment (optional, defaults to production)
|
||||
POLAR_ENV=production # 'production' or 'sandbox'
|
||||
```
|
||||
|
||||
### Lazy Initialization Pattern
|
||||
```typescript
|
||||
// lib/polar.ts - Defer validation until first access
|
||||
import { Polar } from '@polar-sh/sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
const polarEnvSchema = z.object({
|
||||
POLAR_API_KEY: z.string().min(1),
|
||||
POLAR_ORGANIZATION_ID: z.string().min(1),
|
||||
POLAR_WEBHOOK_SECRET: z.string().min(1),
|
||||
});
|
||||
|
||||
let _polar: Polar | null = null;
|
||||
let _env: z.infer<typeof polarEnvSchema> | null = null;
|
||||
|
||||
export function getPolarEnv() {
|
||||
if (!_env) {
|
||||
_env = polarEnvSchema.parse({
|
||||
POLAR_API_KEY: process.env.POLAR_API_KEY,
|
||||
POLAR_ORGANIZATION_ID: process.env.POLAR_ORGANIZATION_ID,
|
||||
POLAR_WEBHOOK_SECRET: process.env.POLAR_WEBHOOK_SECRET,
|
||||
});
|
||||
}
|
||||
return _env;
|
||||
}
|
||||
|
||||
export function getPolar() {
|
||||
if (!_polar) {
|
||||
const env = getPolarEnv();
|
||||
const polarEnv = process.env.POLAR_ENV || 'production';
|
||||
_polar = new Polar({
|
||||
accessToken: env.POLAR_API_KEY,
|
||||
server: polarEnv as 'production' | 'sandbox',
|
||||
});
|
||||
}
|
||||
return _polar;
|
||||
}
|
||||
```
|
||||
|
||||
**Key Benefit:** Module imports succeed at build time; validation deferred until runtime when env vars are available.
|
||||
|
||||
## Checkout Flow Implementation
|
||||
|
||||
### Standard Checkout API
|
||||
```typescript
|
||||
// app/api/checkout/polar/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { getPolar, getPolarEnv } from '@/lib/polar';
|
||||
|
||||
const checkoutSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().optional(),
|
||||
productType: z.enum(['engineer_kit', 'marketing_kit', 'combo']),
|
||||
githubUsername: z.string().min(1),
|
||||
referralCode: z.string().regex(/^[A-Z0-9]{8}$/).optional(),
|
||||
couponCode: z.string().optional(),
|
||||
});
|
||||
|
||||
// Pricing in cents
|
||||
const PRODUCT_PRICES = {
|
||||
engineer_kit: 9900, // $99
|
||||
marketing_kit: 9900, // $99
|
||||
combo: 14900, // $149
|
||||
} as const;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const data = checkoutSchema.parse(body);
|
||||
const polar = getPolar();
|
||||
const env = getPolarEnv();
|
||||
|
||||
// 1. Normalize email
|
||||
const normalizedEmail = data.email.toLowerCase().trim();
|
||||
|
||||
// 2. Validate GitHub username against GitHub API
|
||||
const githubValid = await validateGitHubUsername(data.githubUsername);
|
||||
if (!githubValid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid GitHub username' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Get product ID and base price
|
||||
const productId = getProductId(data.productType);
|
||||
const originalAmount = PRODUCT_PRICES[data.productType];
|
||||
|
||||
// 4. Apply discount hierarchy (order matters!)
|
||||
let finalAmount = originalAmount;
|
||||
let polarDiscountId: string | undefined;
|
||||
let discountMetadata: Record<string, any> = {};
|
||||
|
||||
// Step A: Apply coupon FIRST (if provided)
|
||||
if (data.couponCode) {
|
||||
const couponResult = await validateAndApplyCoupon(
|
||||
data.couponCode,
|
||||
productId,
|
||||
originalAmount
|
||||
);
|
||||
if (couponResult.valid) {
|
||||
finalAmount = originalAmount - couponResult.discountAmount;
|
||||
discountMetadata.couponCode = data.couponCode;
|
||||
discountMetadata.couponDiscountAmount = couponResult.discountAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// Step B: Apply referral discount SECOND (on post-coupon price)
|
||||
if (data.referralCode) {
|
||||
const referralResult = await calculateReferralDiscount(
|
||||
data.referralCode,
|
||||
finalAmount, // Applied to post-coupon amount
|
||||
normalizedEmail
|
||||
);
|
||||
|
||||
if (referralResult.valid && referralResult.discountAmount > 0) {
|
||||
// Validate discount calculation
|
||||
if (referralResult.discountAmount <= 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid discount calculation - contact support' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
finalAmount -= referralResult.discountAmount;
|
||||
discountMetadata.referralCode = data.referralCode;
|
||||
discountMetadata.referralDiscountAmount = referralResult.discountAmount;
|
||||
discountMetadata.referrerId = referralResult.referrerId;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Create order record BEFORE Polar checkout
|
||||
const order = await db.insert(orders).values({
|
||||
id: crypto.randomUUID(),
|
||||
email: normalizedEmail,
|
||||
productType: data.productType,
|
||||
amount: finalAmount,
|
||||
originalAmount,
|
||||
currency: 'USD',
|
||||
status: 'pending',
|
||||
paymentProvider: 'polar',
|
||||
referredBy: discountMetadata.referrerId,
|
||||
discountAmount: originalAmount - finalAmount,
|
||||
metadata: JSON.stringify({
|
||||
...discountMetadata,
|
||||
githubUsername: data.githubUsername,
|
||||
}),
|
||||
}).returning();
|
||||
|
||||
// 6. Create dynamic Polar discount (if referral applied)
|
||||
if (discountMetadata.referrerId && discountMetadata.referralDiscountAmount > 0) {
|
||||
try {
|
||||
const discount = await polar.discounts.create({
|
||||
type: 'fixed',
|
||||
name: `referral-${order[0].id.slice(0, 8)}`,
|
||||
amount: discountMetadata.referralDiscountAmount,
|
||||
currency: 'usd',
|
||||
duration: 'once',
|
||||
maxRedemptions: 1,
|
||||
products: [productId],
|
||||
metadata: {
|
||||
orderId: order[0].id,
|
||||
type: 'referral',
|
||||
referrerId: discountMetadata.referrerId,
|
||||
},
|
||||
});
|
||||
polarDiscountId = discount.id;
|
||||
} catch (error) {
|
||||
// FAIL-OPEN: Proceed with full price, flag for manual refund
|
||||
console.error('⚠️ Failed to create Polar discount:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Create Polar checkout session
|
||||
const checkout = await polar.checkouts.create({
|
||||
productPriceId: productId,
|
||||
customerEmail: normalizedEmail,
|
||||
successUrl: `${process.env.NEXT_PUBLIC_URL}/checkout/success?orderId=${order[0].id}`,
|
||||
discountId: polarDiscountId,
|
||||
allowDiscountCodes: !polarDiscountId, // Prevent stacking
|
||||
metadata: {
|
||||
orderId: order[0].id,
|
||||
githubUsername: data.githubUsername,
|
||||
referredBy: discountMetadata.referrerId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
checkoutUrl: checkout.url,
|
||||
orderId: order[0].id,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json({ error: error.errors }, { status: 400 });
|
||||
}
|
||||
console.error('Checkout error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create checkout' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Discount Application Order (Critical)
|
||||
```
|
||||
1. Original price (e.g., $99)
|
||||
2. Apply coupon discount FIRST → post-coupon price (e.g., $79)
|
||||
3. Apply referral discount SECOND → final price (e.g., $63.20)
|
||||
|
||||
Never apply referral to original price if coupon was used!
|
||||
```
|
||||
|
||||
## Webhook Handling
|
||||
|
||||
### Signature Verification
|
||||
```typescript
|
||||
// app/api/webhooks/polar/route.ts
|
||||
import { validateEvent } from '@polar-sh/sdk/webhooks';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const payload = await request.text();
|
||||
const headers = Object.fromEntries(request.headers);
|
||||
const secret = process.env.POLAR_WEBHOOK_SECRET!;
|
||||
|
||||
let webhookEvent;
|
||||
try {
|
||||
webhookEvent = validateEvent(payload, headers, secret);
|
||||
} catch (error) {
|
||||
console.error('Invalid webhook signature:', error);
|
||||
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Extract event ID for idempotency
|
||||
const parsedPayload = JSON.parse(payload);
|
||||
const eventId = parsedPayload.id || `${parsedPayload.type}-${Date.now()}`;
|
||||
|
||||
// Check for duplicate processing
|
||||
const existingEvent = await db.select()
|
||||
.from(webhookEvents)
|
||||
.where(eq(webhookEvents.eventId, eventId))
|
||||
.limit(1);
|
||||
|
||||
if (existingEvent.length > 0) {
|
||||
console.log(`Duplicate webhook ignored: ${eventId}`);
|
||||
return NextResponse.json({ received: true });
|
||||
}
|
||||
|
||||
// Record event BEFORE processing (idempotency)
|
||||
await db.insert(webhookEvents).values({
|
||||
id: crypto.randomUUID(),
|
||||
provider: 'polar',
|
||||
eventType: webhookEvent.type,
|
||||
eventId,
|
||||
payload,
|
||||
processed: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await handleWebhookEvent(webhookEvent);
|
||||
|
||||
// Mark as processed
|
||||
await db.update(webhookEvents)
|
||||
.set({ processed: true, processedAt: new Date() })
|
||||
.where(eq(webhookEvents.eventId, eventId));
|
||||
|
||||
} catch (error) {
|
||||
// Log error but don't fail the webhook
|
||||
await db.update(webhookEvents)
|
||||
.set({
|
||||
processed: true,
|
||||
processedAt: new Date(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
.where(eq(webhookEvents.eventId, eventId));
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Event Handlers
|
||||
```typescript
|
||||
async function handleWebhookEvent(event: WebhookEvent) {
|
||||
switch (event.type) {
|
||||
case 'checkout.created':
|
||||
// Order already exists from API - just log
|
||||
console.log(`Checkout created: ${event.data.id}`);
|
||||
break;
|
||||
|
||||
case 'checkout.updated':
|
||||
await handleCheckoutUpdated(event.data);
|
||||
break;
|
||||
|
||||
case 'order.created':
|
||||
await handleOrderCreated(event.data);
|
||||
break;
|
||||
|
||||
case 'order.refunded':
|
||||
await handleOrderRefunded(event.data);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOrderCreated(order: PolarOrder) {
|
||||
const orderId = order.metadata?.orderId;
|
||||
if (!orderId) {
|
||||
console.error('Order missing orderId in metadata');
|
||||
return;
|
||||
}
|
||||
|
||||
const dbOrder = await db.select()
|
||||
.from(orders)
|
||||
.where(eq(orders.id, orderId))
|
||||
.limit(1);
|
||||
|
||||
if (!dbOrder[0]) {
|
||||
console.error(`Order not found: ${orderId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Update order status
|
||||
await db.update(orders)
|
||||
.set({
|
||||
status: 'completed',
|
||||
paymentId: order.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(orders.id, orderId));
|
||||
|
||||
// 2. Create license (non-blocking)
|
||||
try {
|
||||
await createLicense(dbOrder[0]);
|
||||
} catch (error) {
|
||||
console.error('Failed to create license:', error);
|
||||
}
|
||||
|
||||
// 3. Send confirmation email (non-blocking)
|
||||
try {
|
||||
await sendOrderConfirmation(dbOrder[0], order);
|
||||
} catch (error) {
|
||||
console.error('Failed to send confirmation:', error);
|
||||
}
|
||||
|
||||
// 4. Create referral commission (non-blocking)
|
||||
if (dbOrder[0].referredBy) {
|
||||
try {
|
||||
await createCommission(dbOrder[0]);
|
||||
} catch (error) {
|
||||
console.error('Failed to create commission:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Grant GitHub access (non-blocking)
|
||||
try {
|
||||
const metadata = JSON.parse(dbOrder[0].metadata || '{}');
|
||||
await inviteToGitHub(metadata.githubUsername, dbOrder[0].productType);
|
||||
} catch (error) {
|
||||
console.error('Failed to invite to GitHub:', error);
|
||||
}
|
||||
|
||||
// 6. Send Discord notification (non-blocking)
|
||||
try {
|
||||
await sendSalesNotification(dbOrder[0]);
|
||||
} catch (error) {
|
||||
console.error('Failed to send Discord notification:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Status Mapping
|
||||
```typescript
|
||||
function mapPolarStatusToAppStatus(polarStatus: string): string | null {
|
||||
switch (polarStatus) {
|
||||
case 'succeeded':
|
||||
return 'completed';
|
||||
case 'failed':
|
||||
case 'expired':
|
||||
return 'failed';
|
||||
case 'open':
|
||||
case 'confirmed':
|
||||
return null; // Don't update - still pending
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fee Calculation
|
||||
|
||||
### Platform Fee Structure (Dec 2025)
|
||||
```typescript
|
||||
// lib/polar-fees.ts
|
||||
interface PolarFeeConfig {
|
||||
basePercentage: number; // 4%
|
||||
baseFlatCents: number; // $0.40 per transaction
|
||||
internationalSurcharge: number; // +1.5% for non-US cards
|
||||
subscriptionSurcharge: number; // +0.5% (not for one-time)
|
||||
}
|
||||
|
||||
const POLAR_FEES: PolarFeeConfig = {
|
||||
basePercentage: 0.04,
|
||||
baseFlatCents: 40,
|
||||
internationalSurcharge: 0.015,
|
||||
subscriptionSurcharge: 0.005,
|
||||
};
|
||||
|
||||
export function calculatePolarFees(
|
||||
amountCents: number,
|
||||
isInternational: boolean = true, // Conservative default
|
||||
isSubscription: boolean = false
|
||||
): {
|
||||
baseFee: number;
|
||||
internationalFee: number;
|
||||
subscriptionFee: number;
|
||||
totalFee: number;
|
||||
netRevenue: number;
|
||||
} {
|
||||
// Handle zero/negative
|
||||
if (amountCents <= 0) {
|
||||
return { baseFee: 0, internationalFee: 0, subscriptionFee: 0, totalFee: 0, netRevenue: 0 };
|
||||
}
|
||||
|
||||
const baseFee = Math.round(amountCents * POLAR_FEES.basePercentage + POLAR_FEES.baseFlatCents);
|
||||
const internationalFee = isInternational
|
||||
? Math.round(amountCents * POLAR_FEES.internationalSurcharge)
|
||||
: 0;
|
||||
const subscriptionFee = isSubscription
|
||||
? Math.round(amountCents * POLAR_FEES.subscriptionSurcharge)
|
||||
: 0;
|
||||
|
||||
const totalFee = baseFee + internationalFee + subscriptionFee;
|
||||
const netRevenue = amountCents - totalFee;
|
||||
|
||||
return { baseFee, internationalFee, subscriptionFee, totalFee, netRevenue };
|
||||
}
|
||||
|
||||
// Aggregate fees preserve per-transaction flat fees
|
||||
export function calculateAggregatePolarFees(transactionAmounts: number[]): {
|
||||
totalFees: number;
|
||||
totalNetRevenue: number;
|
||||
} {
|
||||
let totalFees = 0;
|
||||
let totalNetRevenue = 0;
|
||||
|
||||
for (const amount of transactionAmounts) {
|
||||
const { totalFee, netRevenue } = calculatePolarFees(amount);
|
||||
totalFees += totalFee;
|
||||
totalNetRevenue += netRevenue;
|
||||
}
|
||||
|
||||
return { totalFees, totalNetRevenue };
|
||||
}
|
||||
```
|
||||
|
||||
## Discount Management
|
||||
|
||||
### Discount Validation with Timeout
|
||||
```typescript
|
||||
// lib/polar-discounts.ts
|
||||
const VALIDATION_TIMEOUT_MS = 15000;
|
||||
|
||||
export async function validateDiscount(
|
||||
code: string,
|
||||
productId: string
|
||||
): Promise<{ valid: boolean; discount?: PolarDiscount; reason?: string }> {
|
||||
const sanitizedCode = code.trim().toUpperCase();
|
||||
if (!sanitizedCode) {
|
||||
return { valid: false, reason: 'Code cannot be empty' };
|
||||
}
|
||||
|
||||
const polar = getPolar();
|
||||
const env = getPolarEnv();
|
||||
|
||||
try {
|
||||
// Race against timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Validation timeout')), VALIDATION_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
const searchPromise = polar.discounts.list({
|
||||
organizationId: env.POLAR_ORGANIZATION_ID,
|
||||
query: sanitizedCode,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const result = await Promise.race([searchPromise, timeoutPromise]);
|
||||
|
||||
// Find exact match
|
||||
const discount = result.items.find(d =>
|
||||
d.code?.toUpperCase() === sanitizedCode
|
||||
);
|
||||
|
||||
if (!discount) {
|
||||
return { valid: false, reason: 'Code not found' };
|
||||
}
|
||||
|
||||
// Check eligibility
|
||||
const now = new Date();
|
||||
if (discount.startsAt && now < new Date(discount.startsAt)) {
|
||||
return { valid: false, reason: `Code starts on ${discount.startsAt}` };
|
||||
}
|
||||
if (discount.endsAt && now > new Date(discount.endsAt)) {
|
||||
return { valid: false, reason: 'Code has expired' };
|
||||
}
|
||||
if (discount.maxRedemptions && discount.redemptionsCount >= discount.maxRedemptions) {
|
||||
return { valid: false, reason: 'Code redemption limit reached' };
|
||||
}
|
||||
if (!discount.products?.some(p => p.id === productId)) {
|
||||
return { valid: false, reason: 'Code not valid for this product' };
|
||||
}
|
||||
|
||||
return { valid: true, discount };
|
||||
|
||||
} catch (error) {
|
||||
console.error('Discount validation error:', error);
|
||||
return { valid: false, reason: 'Validation failed - please try again' };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VND Conversion for Discounts
|
||||
```typescript
|
||||
const VND_TO_USD_RATE = 25000; // 1 USD = 25,000 VND
|
||||
|
||||
export function convertDiscountToVND(discount: PolarDiscount, amountVND: number): number {
|
||||
if (discount.type === 'percentage') {
|
||||
// Basis points: 1000 = 10%, 10000 = 100%
|
||||
const percentage = discount.basisPoints / 10000;
|
||||
return Math.round(amountVND * percentage);
|
||||
} else {
|
||||
// Fixed amount in USD cents → VND
|
||||
const amountUSD = discount.amount / 100;
|
||||
return Math.round(amountUSD * VND_TO_USD_RATE);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Syncing SePay Redemptions to Polar
|
||||
```typescript
|
||||
// lib/polar-discount-sync.ts
|
||||
// When SePay payment completes, decrement Polar discount redemptions
|
||||
|
||||
export async function syncPolarDiscountRedemption(
|
||||
orderId: string,
|
||||
discountId: string,
|
||||
discountCode: string
|
||||
): Promise<{ success: boolean; action: string }> {
|
||||
const order = await db.select().from(orders).where(eq(orders.id, orderId)).limit(1);
|
||||
if (!order[0]) {
|
||||
return { success: false, action: 'order_not_found' };
|
||||
}
|
||||
|
||||
// Idempotency check
|
||||
const metadata = order[0].metadata ? JSON.parse(order[0].metadata) : {};
|
||||
if (metadata.polarDiscountSynced) {
|
||||
return { success: true, action: 'already_synced' };
|
||||
}
|
||||
|
||||
const polar = getPolar();
|
||||
|
||||
try {
|
||||
const discount = await polar.discounts.get({ id: discountId });
|
||||
|
||||
if (discount.maxRedemptions === null || discount.maxRedemptions === undefined) {
|
||||
return { success: true, action: 'skipped_unlimited' };
|
||||
}
|
||||
|
||||
const currentMax = discount.maxRedemptions;
|
||||
|
||||
if (currentMax <= 1) {
|
||||
await polar.discounts.delete({ id: discountId });
|
||||
await markOrderSynced(orderId, 'deleted');
|
||||
} else {
|
||||
await polar.discounts.update({
|
||||
id: discountId,
|
||||
discountUpdate: { maxRedemptions: currentMax - 1 },
|
||||
});
|
||||
await markOrderSynced(orderId, 'decremented');
|
||||
}
|
||||
|
||||
return { success: true, action: currentMax <= 1 ? 'deleted' : 'decremented' };
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.statusCode === 404) {
|
||||
// Already deleted - treat as success
|
||||
await markOrderSynced(orderId, 'already_deleted');
|
||||
return { success: true, action: 'already_deleted' };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function markOrderSynced(orderId: string, action: string) {
|
||||
const order = await db.select().from(orders).where(eq(orders.id, orderId)).limit(1);
|
||||
const metadata = order[0].metadata ? JSON.parse(order[0].metadata) : {};
|
||||
|
||||
metadata.polarDiscountSynced = true;
|
||||
metadata.polarDiscountSyncAction = action;
|
||||
metadata.polarDiscountSyncedAt = new Date().toISOString();
|
||||
|
||||
await db.update(orders)
|
||||
.set({ metadata: JSON.stringify(metadata) })
|
||||
.where(eq(orders.id, orderId));
|
||||
}
|
||||
```
|
||||
|
||||
## Revenue Tracking with Caching
|
||||
|
||||
```typescript
|
||||
// lib/polar.ts
|
||||
const REVENUE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
let revenueCache: {
|
||||
data: { totalRevenueCents: number; orderCount: number } | null;
|
||||
timestamp: number;
|
||||
} = { data: null, timestamp: 0 };
|
||||
|
||||
export async function getPolarApiRevenue(): Promise<{
|
||||
totalRevenueCents: number;
|
||||
orderCount: number;
|
||||
fromCache: boolean;
|
||||
}> {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cache if valid
|
||||
if (revenueCache.data && now - revenueCache.timestamp < REVENUE_CACHE_TTL_MS) {
|
||||
return { ...revenueCache.data, fromCache: true };
|
||||
}
|
||||
|
||||
const polar = getPolar();
|
||||
const env = getPolarEnv();
|
||||
|
||||
try {
|
||||
let totalRevenueCents = 0;
|
||||
let orderCount = 0;
|
||||
let page = 1;
|
||||
const maxPages = 100; // Safety limit
|
||||
|
||||
while (page <= maxPages) {
|
||||
const response = await polar.orders.list({
|
||||
organizationId: env.POLAR_ORGANIZATION_ID,
|
||||
page,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
for (const order of response.items) {
|
||||
if (order.status === 'succeeded') {
|
||||
totalRevenueCents += order.netAmount; // After discounts, before tax
|
||||
orderCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.pagination.hasMore) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
revenueCache = { data: { totalRevenueCents, orderCount }, timestamp: now };
|
||||
return { totalRevenueCents, orderCount, fromCache: false };
|
||||
|
||||
} catch (error) {
|
||||
// Return stale cache on error
|
||||
if (revenueCache.data) {
|
||||
console.warn('Using stale revenue cache due to API error');
|
||||
return { ...revenueCache.data, fromCache: true };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Fail-Open for Non-Critical Operations
|
||||
```typescript
|
||||
// Discount creation fails → proceed with full price
|
||||
try {
|
||||
const discount = await createReferralDiscount(productId, amount, referralCode);
|
||||
polarDiscountId = discount.id;
|
||||
} catch (error) {
|
||||
console.error('⚠️ Discount creation failed - proceeding with full price:', error);
|
||||
// Flag for manual refund investigation
|
||||
await flagOrderForReview(orderId, 'discount_creation_failed');
|
||||
}
|
||||
```
|
||||
|
||||
### Graceful Degradation in Webhooks
|
||||
```typescript
|
||||
// Non-critical operations don't block order completion
|
||||
const operations = [
|
||||
{ name: 'GitHub invite', fn: () => inviteToGitHub(username, productType) },
|
||||
{ name: 'Welcome email', fn: () => sendWelcomeEmail(order) },
|
||||
{ name: 'Discord notification', fn: () => sendSalesNotification(order) },
|
||||
{ name: 'Tier update', fn: () => updateReferrerTier(referrerId, revenueUsd) },
|
||||
];
|
||||
|
||||
for (const op of operations) {
|
||||
try {
|
||||
await op.fn();
|
||||
} catch (error) {
|
||||
console.error(`❌ ${op.name} failed:`, error);
|
||||
// Continue processing - don't block order
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limit Handling with Exponential Backoff
|
||||
```typescript
|
||||
async function callWithRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number = 3
|
||||
): Promise<T> {
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error: any) {
|
||||
if (error.statusCode === 429) {
|
||||
const retryAfter = parseInt(error.headers?.['retry-after'] || '1', 10);
|
||||
const delay = retryAfter * 1000 * Math.pow(2, attempt);
|
||||
console.log(`Rate limited, retrying in ${delay}ms...`);
|
||||
await sleep(delay);
|
||||
attempt++;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Orders Table
|
||||
```typescript
|
||||
// db/schema/orders.ts
|
||||
export const orders = pgTable('orders', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').references(() => users.id),
|
||||
email: text('email').notNull(),
|
||||
productType: text('product_type').notNull(),
|
||||
amount: integer('amount').notNull(), // Final amount in cents
|
||||
originalAmount: integer('original_amount'), // Before discounts
|
||||
currency: text('currency').default('USD'),
|
||||
status: text('status').default('pending'), // pending, completed, failed, refunded
|
||||
paymentProvider: text('payment_provider').notNull(), // 'polar' or 'sepay'
|
||||
paymentId: text('payment_id'), // External payment ID
|
||||
referredBy: uuid('referred_by').references(() => users.id),
|
||||
discountAmount: integer('discount_amount').default(0),
|
||||
discountRate: numeric('discount_rate', { precision: 5, scale: 2 }),
|
||||
metadata: text('metadata'), // JSON with audit trail
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
### Webhook Events Table (Idempotency)
|
||||
```typescript
|
||||
export const webhookEvents = pgTable('webhook_events', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
provider: text('provider').notNull(), // 'polar' or 'sepay'
|
||||
eventType: text('event_type').notNull(),
|
||||
eventId: text('event_id').notNull().unique(), // Idempotency key
|
||||
payload: text('payload').notNull(),
|
||||
processed: boolean('processed').default(false),
|
||||
processedAt: timestamp('processed_at'),
|
||||
error: text('error'),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
});
|
||||
```
|
||||
|
||||
## Metadata Best Practices
|
||||
|
||||
### Comprehensive Audit Trail
|
||||
```typescript
|
||||
// Store everything needed for debugging and reconciliation
|
||||
metadata: JSON.stringify({
|
||||
// Pricing history
|
||||
originalAmount: 9900,
|
||||
|
||||
// Coupon tracking
|
||||
couponCode: 'LAUNCH20',
|
||||
couponDiscountAmount: 1980,
|
||||
|
||||
// Referral tracking
|
||||
referralCode: 'ABC12345',
|
||||
referralDiscountAmount: 1584,
|
||||
referrerId: 'user-uuid',
|
||||
|
||||
// Customer context
|
||||
githubUsername: 'customer',
|
||||
|
||||
// Polar integration
|
||||
polarDiscountId: 'disc_xxx',
|
||||
polarDiscountSynced: true,
|
||||
polarDiscountSyncAction: 'decremented',
|
||||
polarDiscountSyncedAt: '2025-01-15T10:30:00Z',
|
||||
|
||||
// Team context (if applicable)
|
||||
isTeamPurchase: false,
|
||||
teamId: null,
|
||||
quantity: 1,
|
||||
})
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests for Fee Calculation
|
||||
```typescript
|
||||
// __tests__/lib/polar-fees.test.ts
|
||||
describe('calculatePolarFees', () => {
|
||||
it('handles zero amount', () => {
|
||||
const result = calculatePolarFees(0);
|
||||
expect(result.totalFee).toBe(0);
|
||||
expect(result.netRevenue).toBe(0);
|
||||
});
|
||||
|
||||
it('calculates international one-time correctly', () => {
|
||||
// $100 transaction
|
||||
const result = calculatePolarFees(10000, true, false);
|
||||
expect(result.baseFee).toBe(440); // 4% + $0.40
|
||||
expect(result.internationalFee).toBe(150); // 1.5%
|
||||
expect(result.totalFee).toBe(590);
|
||||
expect(result.netRevenue).toBe(9410); // $94.10
|
||||
});
|
||||
|
||||
it('preserves per-transaction flat fees in aggregate', () => {
|
||||
// Two $100 transactions should each have $0.40 flat fee
|
||||
const aggregate = calculateAggregatePolarFees([10000, 10000]);
|
||||
const single = calculatePolarFees(20000);
|
||||
|
||||
expect(aggregate.totalFees).toBeGreaterThan(single.totalFee);
|
||||
// Difference should be one extra flat fee ($0.40)
|
||||
expect(aggregate.totalFees - single.totalFee).toBe(40);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
- [ ] Environment variables configured in all environments
|
||||
- [ ] Sandbox testing completed for all checkout flows
|
||||
- [ ] Production API key obtained and secured
|
||||
- [ ] Webhook endpoint deployed and reachable
|
||||
- [ ] Webhook signature verification implemented
|
||||
- [ ] Idempotency handling tested with duplicate webhooks
|
||||
- [ ] Fee calculations verified against Polar dashboard
|
||||
- [ ] Discount validation timeout configured
|
||||
- [ ] Error monitoring enabled (Sentry, etc.)
|
||||
- [ ] Structured logging in place
|
||||
- [ ] Database indexes on orders.status, orders.paymentProvider
|
||||
- [ ] Revenue caching configured
|
||||
- [ ] Rate limit handling implemented
|
||||
- [ ] Fail-open patterns for non-critical operations
|
||||
- [ ] Customer email notifications working
|
||||
- [ ] Refund flow tested end-to-end
|
||||
- [ ] GitHub access grant/revoke tested
|
||||
- [ ] Discord sales notifications configured
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Applying discounts in wrong order** - Always coupon first, then referral
|
||||
2. **Trusting success redirect without verification** - Always verify via API or webhook
|
||||
3. **Not handling duplicate webhooks** - Use eventId for idempotency
|
||||
4. **Blocking webhook on non-critical failures** - Wrap in try-catch, log, continue
|
||||
5. **Hardcoding Polar customer IDs** - Use external_id (your user ID) for lookups
|
||||
6. **Not setting timeout on discount validation** - API can be slow
|
||||
7. **Calculating aggregate fees as single transaction** - Each transaction has flat fee
|
||||
8. **Exposing API keys client-side** - Always server-side
|
||||
9. **Not preserving original amount in metadata** - Need for audit/debugging
|
||||
10. **Syncing discount redemptions synchronously** - Can fail; use retry with backoff
|
||||
Reference in New Issue
Block a user