Production upgrade: Clerk auth, Stripe billing, analytics, PWA support
Backend & Auth: - Add @clerk/nextjs for production authentication - Create src/proxy.ts with clerkMiddleware() for route protection - Implement multi-tenant auth with role-based access control - Add Clerk components (Show, UserButton, SignInButton, SignUpButton) Billing & Payments: - Full Stripe integration (subscriptions, add-ons, customer portal) - Plan tiers: Starter 9/mo, Farm 49/mo, Enterprise 99/mo - Webhook handling for subscription events - createSubscription(), createAddonSubscription(), createCustomerPortalSession() API & Security: - Rate limiting with @upstash/ratelimit (100 req/min API, 20 req/min checkout) - Zod validation schemas for all endpoints (orders, products, campaigns, etc.) - Security headers (CSP, HSTS, X-Frame-Options) - API routes: /api/v1/ with validated, rate-limited endpoints Monitoring: - Sentry error tracking with performance monitoring - PostHog analytics for feature usage, funnels, cohorts - User activity logging and breadcrumb tracking Admin Features: - Analytics dashboard with revenue charts, customer growth, conversion funnel - Onboarding flow with 6-step interactive tour - Referral system with share tracking and reward redemption - Changelog feed with in-app notifications PWA & SEO: - Web app manifest with icons and shortcuts - Service worker for offline support and caching - Full SEO metadata, OpenGraph, Twitter cards - Structured data (JSON-LD) for organization and products Database: - Add referral_codes, changelogs, onboarding_progress tables - Add user_activity_logs, api_keys, notification_preferences - Comprehensive RLS policies for all new tables - Seed data for demo brands and products
This commit is contained in:
@@ -0,0 +1,673 @@
|
||||
// Enhanced Stripe Billing - Full Subscription Management
|
||||
// Supports plans, add-ons, upgrades, cancellations, and customer portal
|
||||
|
||||
import Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// Initialize Stripe
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2025-04-30.basil",
|
||||
});
|
||||
|
||||
// Plan tier configurations
|
||||
export const PLANS = {
|
||||
starter: {
|
||||
id: "starter",
|
||||
name: "Starter",
|
||||
description: "Perfect for small farms getting started",
|
||||
monthlyPrice: 4900, // $49.00 in cents
|
||||
annualPrice: 44100, // $441.00 (15% discount)
|
||||
features: [
|
||||
"Up to 25 products",
|
||||
"10 stops/month",
|
||||
"1 user",
|
||||
"Basic pickup management",
|
||||
"Email support",
|
||||
],
|
||||
limits: {
|
||||
max_users: 1,
|
||||
max_stops_monthly: 10,
|
||||
max_products: 25,
|
||||
},
|
||||
},
|
||||
farm: {
|
||||
id: "farm",
|
||||
name: "Farm",
|
||||
description: "For growing farms with more needs",
|
||||
monthlyPrice: 14900, // $149.00
|
||||
annualPrice: 152280, // $1,522.80 (10% discount)
|
||||
features: [
|
||||
"Unlimited products",
|
||||
"Unlimited stops",
|
||||
"5 users",
|
||||
"Wholesale Portal",
|
||||
"Harvest Reach",
|
||||
"Priority support",
|
||||
],
|
||||
limits: {
|
||||
max_users: 5,
|
||||
max_stops_monthly: -1, // unlimited
|
||||
max_products: -1,
|
||||
},
|
||||
},
|
||||
enterprise: {
|
||||
id: "enterprise",
|
||||
name: "Enterprise",
|
||||
description: "Custom solution for larger operations",
|
||||
monthlyPrice: 39900, // $399.00
|
||||
annualPrice: 0, // Custom pricing
|
||||
features: [
|
||||
"Everything in Farm",
|
||||
"AI Intelligence Pack",
|
||||
"SMS Campaigns",
|
||||
"Square Sync",
|
||||
"Water Log",
|
||||
"Unlimited users",
|
||||
"Unlimited brands",
|
||||
"Custom development",
|
||||
"Dedicated SLA",
|
||||
],
|
||||
limits: {
|
||||
max_users: -1,
|
||||
max_stops_monthly: -1,
|
||||
max_products: -1,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Add-ons configuration
|
||||
export const ADDONS = {
|
||||
wholesale_portal: {
|
||||
id: "wholesale_portal",
|
||||
name: "Wholesale Portal",
|
||||
description: "Full wholesale customer portal with ordering",
|
||||
monthlyPrice: 9900, // $99.00
|
||||
},
|
||||
harvest_reach: {
|
||||
id: "harvest_reach",
|
||||
name: "Harvest Reach",
|
||||
description: "Email and SMS marketing campaigns",
|
||||
monthlyPrice: 7900, // $79.00
|
||||
},
|
||||
ai_tools: {
|
||||
id: "ai_tools",
|
||||
name: "AI Intelligence Pack",
|
||||
description: "AI-powered insights and automation",
|
||||
monthlyPrice: 5900, // $59.00
|
||||
},
|
||||
water_log: {
|
||||
id: "water_log",
|
||||
name: "Water Log",
|
||||
description: "Track irrigation and water usage",
|
||||
monthlyPrice: 3900, // $39.00
|
||||
},
|
||||
square_sync: {
|
||||
id: "square_sync",
|
||||
name: "Square Sync",
|
||||
description: "Sync inventory with Square POS",
|
||||
monthlyPrice: 3900, // $39.00
|
||||
},
|
||||
sms_campaigns: {
|
||||
id: "sms_campaigns",
|
||||
name: "SMS Campaigns",
|
||||
description: "SMS marketing via Twilio",
|
||||
monthlyPrice: 2900, // $29.00
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Stripe Price IDs (configurable via environment)
|
||||
const PRICE_IDS = {
|
||||
starter_monthly: process.env.STRIPE_PRICE_STARTER_MONTHLY || "price_starter_monthly",
|
||||
starter_annual: process.env.STRIPE_PRICE_STARTER_ANNUAL || "price_starter_annual",
|
||||
farm_monthly: process.env.STRIPE_PRICE_FARM_MONTHLY || "price_farm_monthly",
|
||||
farm_annual: process.env.STRIPE_PRICE_FARM_ANNUAL || "price_farm_annual",
|
||||
enterprise_monthly: process.env.STRIPE_PRICE_ENTERPRISE_MONTHLY || "price_enterprise_monthly",
|
||||
wholesale_portal: process.env.STRIPE_PRICE_WHOLESALE_PORTAL || "price_wholesale_portal",
|
||||
harvest_reach: process.env.STRIPE_PRICE_HARVEST_REACH || "price_harvest_reach",
|
||||
ai_tools: process.env.STRIPE_PRICE_AI_TOOLS || "price_ai_tools",
|
||||
water_log: process.env.STRIPE_PRICE_WATER_LOG || "price_water_log",
|
||||
square_sync: process.env.STRIPE_PRICE_SQUARE_SYNC || "price_square_sync",
|
||||
sms_campaigns: process.env.STRIPE_PRICE_SMS_CAMPAIGNS || "price_sms_campaigns",
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// SUBSCRIPTION MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
export interface CreateSubscriptionOptions {
|
||||
brandId: string;
|
||||
brandName: string;
|
||||
email: string;
|
||||
plan: keyof typeof PLANS;
|
||||
interval: "monthly" | "annual";
|
||||
successUrl: string;
|
||||
cancelUrl: string;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function createSubscription(options: CreateSubscriptionOptions) {
|
||||
const { brandId, brandName, email, plan, interval, successUrl, cancelUrl, metadata } = options;
|
||||
|
||||
// Get or create Stripe customer
|
||||
let customerId = await getOrCreateCustomer(brandId, brandName, email);
|
||||
|
||||
// Get price ID for selected plan
|
||||
const priceId = interval === "annual"
|
||||
? PRICE_IDS[`${plan}_annual` as keyof typeof PRICE_IDS]
|
||||
: PRICE_IDS[`${plan}_monthly` as keyof typeof PRICE_IDS];
|
||||
|
||||
// Create checkout session
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: "subscription",
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
brand_id: brandId,
|
||||
brand_name: brandName,
|
||||
plan_tier: plan,
|
||||
interval,
|
||||
},
|
||||
trial_period_days: 14, // 14-day trial for new subscriptions
|
||||
},
|
||||
metadata: {
|
||||
brand_id: brandId,
|
||||
...metadata,
|
||||
},
|
||||
allow_promotion_codes: true,
|
||||
billing_address_collection: "required",
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function createAddonSubscription(options: {
|
||||
customerId: string;
|
||||
addon: keyof typeof ADDONS;
|
||||
brandId: string;
|
||||
successUrl: string;
|
||||
cancelUrl: string;
|
||||
}) {
|
||||
const { customerId, addon, brandId, successUrl, cancelUrl } = options;
|
||||
|
||||
const priceId = PRICE_IDS[addon as keyof typeof PRICE_IDS];
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
mode: "subscription",
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
brand_id: brandId,
|
||||
addon_key: addon,
|
||||
addon_name: ADDONS[addon].name,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
brand_id: brandId,
|
||||
addon_key: addon,
|
||||
type: "addon",
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CUSTOMER PORTAL
|
||||
// ============================================
|
||||
|
||||
export async function createCustomerPortalSession(options: {
|
||||
customerId: string;
|
||||
returnUrl: string;
|
||||
}) {
|
||||
const { customerId, returnUrl } = options;
|
||||
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: returnUrl,
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SUBSCRIPTION UPDATES
|
||||
// ============================================
|
||||
|
||||
export async function cancelSubscription(subscriptionId: string, immediate: boolean = false) {
|
||||
if (immediate) {
|
||||
return await stripe.subscriptions.cancel(subscriptionId);
|
||||
}
|
||||
|
||||
return await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function reactivateSubscription(subscriptionId: string) {
|
||||
return await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at_period_end: false,
|
||||
});
|
||||
}
|
||||
|
||||
export async function changePlan(subscriptionId: string, newPlan: keyof typeof PLANS, interval: "monthly" | "annual") {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
// Get new price ID
|
||||
const priceId = interval === "annual"
|
||||
? PRICE_IDS[`${newPlan}_annual` as keyof typeof PRICE_IDS]
|
||||
: PRICE_IDS[`${newPlan}_monthly` as keyof typeof PRICE_IDS];
|
||||
|
||||
// Prorate the change
|
||||
return await stripe.subscriptions.update(subscriptionId, {
|
||||
items: [
|
||||
{
|
||||
id: subscription.items.data[0].id,
|
||||
price: priceId,
|
||||
},
|
||||
],
|
||||
proration_behavior: "create_prorations",
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CUSTOMER MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
export async function getOrCreateCustomer(brandId: string, name: string, email: string) {
|
||||
// Check if customer exists for this brand
|
||||
const existingCustomers = await stripe.customers.list({
|
||||
email,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (existingCustomers.data.length > 0) {
|
||||
return existingCustomers.data[0].id;
|
||||
}
|
||||
|
||||
// Create new customer
|
||||
const customer = await stripe.customers.create({
|
||||
email,
|
||||
name,
|
||||
metadata: {
|
||||
brand_id: brandId,
|
||||
},
|
||||
});
|
||||
|
||||
return customer.id;
|
||||
}
|
||||
|
||||
export async function getCustomer(customerId: string) {
|
||||
return await stripe.customers.retrieve(customerId);
|
||||
}
|
||||
|
||||
export async function updateCustomer(customerId: string, data: Partial<Stripe.CustomerUpdateParams>) {
|
||||
return await stripe.customers.update(customerId, data);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PAYMENT METHODS
|
||||
// ============================================
|
||||
|
||||
export async function listPaymentMethods(customerId: string) {
|
||||
return await stripe.paymentMethods.list({
|
||||
customer: customerId,
|
||||
type: "card",
|
||||
});
|
||||
}
|
||||
|
||||
export async function attachPaymentMethod(paymentMethodId: string, customerId: string) {
|
||||
return await stripe.paymentMethods.attach(paymentMethodId, {
|
||||
customer: customerId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function setDefaultPaymentMethod(customerId: string, paymentMethodId: string) {
|
||||
return await stripe.customers.update(customerId, {
|
||||
invoice_settings: {
|
||||
default_payment_method: paymentMethodId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// INVOICES
|
||||
// ============================================
|
||||
|
||||
export async function listInvoices(customerId: string, limit: number = 24) {
|
||||
return await stripe.invoices.list({
|
||||
customer: customerId,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getInvoice(invoiceId: string) {
|
||||
return await stripe.invoices.retrieve(invoiceId);
|
||||
}
|
||||
|
||||
export async function getUpcomingInvoice(customerId: string) {
|
||||
return await stripe.invoices.retrieveUpcoming({
|
||||
customer: customerId,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// PAYMENTS & REFUNDS
|
||||
// ============================================
|
||||
|
||||
export async function createPaymentIntent(options: {
|
||||
amount: number;
|
||||
currency?: string;
|
||||
customerId?: string;
|
||||
metadata?: Record<string, string>;
|
||||
}) {
|
||||
return await stripe.paymentIntents.create({
|
||||
amount: options.amount,
|
||||
currency: options.currency || "usd",
|
||||
customer: options.customerId,
|
||||
metadata: options.metadata,
|
||||
automatic_payment_methods: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRefund(paymentIntentId: string, amount?: number, reason?: string) {
|
||||
return await stripe.refunds.create({
|
||||
payment_intent: paymentIntentId,
|
||||
amount,
|
||||
reason: reason as Stripe.RefundCreateParams.Reason || "requested_by_customer",
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// WEBHOOK PROCESSING
|
||||
// ============================================
|
||||
|
||||
export async function processWebhook(payload: Buffer, signature: string) {
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
|
||||
try {
|
||||
const event = stripe.webhooks.constructEvent(payload, signature, webhookSecret);
|
||||
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed":
|
||||
await handleCheckoutCompleted(event.data.object);
|
||||
break;
|
||||
|
||||
case "customer.subscription.created":
|
||||
await handleSubscriptionCreated(event.data.object);
|
||||
break;
|
||||
|
||||
case "customer.subscription.updated":
|
||||
await handleSubscriptionUpdated(event.data.object);
|
||||
break;
|
||||
|
||||
case "customer.subscription.deleted":
|
||||
await handleSubscriptionDeleted(event.data.object);
|
||||
break;
|
||||
|
||||
case "invoice.payment_succeeded":
|
||||
await handleInvoicePaymentSucceeded(event.data.object);
|
||||
break;
|
||||
|
||||
case "invoice.payment_failed":
|
||||
await handleInvoicePaymentFailed(event.data.object);
|
||||
break;
|
||||
|
||||
case "customer.updated":
|
||||
await handleCustomerUpdated(event.data.object);
|
||||
break;
|
||||
}
|
||||
|
||||
return { received: true };
|
||||
} catch (error) {
|
||||
console.error("Webhook processing error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook handlers
|
||||
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
|
||||
const { brand_id, type } = session.metadata || {};
|
||||
|
||||
if (type === "addon") {
|
||||
// Handle addon checkout
|
||||
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
|
||||
await enableAddonFeature(brand_id, subscription.metadata.addon_key);
|
||||
} else {
|
||||
// Handle plan checkout
|
||||
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
|
||||
await updateBrandSubscription(
|
||||
brand_id,
|
||||
subscription.id,
|
||||
subscription.status,
|
||||
subscription.metadata.plan_tier,
|
||||
subscription.current_period_end
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubscriptionCreated(subscription: Stripe.Subscription) {
|
||||
const { brand_id, plan_tier } = subscription.metadata;
|
||||
|
||||
await updateBrandSubscription(
|
||||
brand_id,
|
||||
subscription.id,
|
||||
subscription.status,
|
||||
plan_tier,
|
||||
subscription.current_period_end
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
|
||||
const { brand_id, plan_tier, addon_key } = subscription.metadata;
|
||||
|
||||
if (addon_key) {
|
||||
// Add-on subscription
|
||||
if (subscription.status === "active") {
|
||||
await enableAddonFeature(brand_id, addon_key);
|
||||
} else {
|
||||
await disableAddonFeature(brand_id, addon_key);
|
||||
}
|
||||
} else {
|
||||
// Plan subscription
|
||||
await updateBrandSubscription(
|
||||
brand_id,
|
||||
subscription.id,
|
||||
subscription.status,
|
||||
plan_tier,
|
||||
subscription.current_period_end
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
|
||||
const { brand_id, plan_tier } = subscription.metadata;
|
||||
|
||||
// Downgrade to free tier or cancel
|
||||
await updateBrandSubscription(
|
||||
brand_id,
|
||||
null,
|
||||
"cancelled",
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {
|
||||
// Log successful payment
|
||||
console.log(`Payment succeeded for customer: ${invoice.customer}`);
|
||||
}
|
||||
|
||||
async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
|
||||
// Notify brand about failed payment
|
||||
console.log(`Payment failed for customer: ${invoice.customer}`);
|
||||
|
||||
// You could trigger an email notification here
|
||||
// await sendPaymentFailedNotification(invoice.customer as string);
|
||||
}
|
||||
|
||||
async function handleCustomerUpdated(customer: Stripe.Customer) {
|
||||
// Sync customer data with our database
|
||||
console.log(`Customer updated: ${customer.id}`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DATABASE HELPERS
|
||||
// ============================================
|
||||
|
||||
async function updateBrandSubscription(
|
||||
brandId: string | undefined,
|
||||
subscriptionId: string | null,
|
||||
status: string,
|
||||
planTier: string | undefined,
|
||||
periodEnd: number | null
|
||||
) {
|
||||
if (!brandId) return;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
try {
|
||||
await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/set_brand_subscription`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: serviceKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_subscription_id: subscriptionId,
|
||||
p_subscription_status: status,
|
||||
p_plan_tier: planTier,
|
||||
p_current_period_end: periodEnd ? new Date(periodEnd * 1000).toISOString() : null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to update brand subscription:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function enableAddonFeature(brandId: string | undefined, addonKey: string | undefined) {
|
||||
if (!brandId || !addonKey) return;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
try {
|
||||
await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/set_brand_feature`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: serviceKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_feature_key: addonKey,
|
||||
p_enabled: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to enable addon feature:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function disableAddonFeature(brandId: string | undefined, addonKey: string | undefined) {
|
||||
if (!brandId || !addonKey) return;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
try {
|
||||
await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/set_brand_feature`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: serviceKey,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_feature_key: addonKey,
|
||||
p_enabled: false,
|
||||
}),
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to disable addon feature:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// USAGE BILLING
|
||||
// ============================================
|
||||
|
||||
export async function createUsageRecord(options: {
|
||||
subscriptionItemId: string;
|
||||
quantity: number;
|
||||
timestamp?: number;
|
||||
idempotencyKey?: string;
|
||||
}) {
|
||||
return await stripe.subscriptionItems.createUsageRecord(
|
||||
options.subscriptionItemId,
|
||||
{
|
||||
quantity: options.quantity,
|
||||
timestamp: options.timestamp,
|
||||
},
|
||||
{
|
||||
idempotencyKey: options.idempotencyKey || uuidv4(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getUsageSummary(subscriptionItemId: string, period: { start: number; end: number }) {
|
||||
return await stripe.usageRecordSummaries.list(
|
||||
subscriptionItemId,
|
||||
{
|
||||
limit: 12,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EXPORT STRIPE INSTANCE AND HELPERS
|
||||
// ============================================
|
||||
|
||||
export { stripe };
|
||||
|
||||
export async function getSubscription(subscriptionId: string) {
|
||||
return await stripe.subscriptions.retrieve(subscriptionId);
|
||||
}
|
||||
|
||||
export async function listSubscriptions(customerId: string) {
|
||||
return await stripe.subscriptions.list({
|
||||
customer: customerId,
|
||||
status: "all",
|
||||
limit: 10,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user