Skip to main content

Overview

AI Studio supports two payment methods:
  1. Stripe - Credit card payments for international customers ($99 USD)
  2. Invoice - B2B invoicing for Norwegian businesses (1000 NOK)
All billing logic is in lib/actions/payments.ts.

Payment Methods

Stripe Integration

Stripe handles credit card payments with automatic processing.

Configuration

Set up in lib/stripe.ts:
export const STRIPE_CONFIG = {
  PRICE_PROJECT_USD: process.env.STRIPE_PRICE_PROJECT_USD || "price_1Sne...",
  PROJECT_PRICE_USD_CENTS: 9900,  // $99 USD
  PROJECT_PRICE_NOK_ORE: 100_000, // 1000 NOK
};

Environment Variables

Required in .env.local:
# Stripe secret key for API access
STRIPE_SECRET_KEY=sk_live_...

# Stripe product price ID (created via Stripe dashboard)
STRIPE_PRICE_PROJECT_USD=price_1Sne...

# Webhook secret for event verification
STRIPE_WEBHOOK_SECRET=whsec_...

Invoice Billing (Norwegian B2B)

Norwegian businesses can pay via invoice through Fiken accounting integration.

Eligibility Requirements

From lib/actions/payments.ts:89:
export async function canUseInvoiceBilling(
  workspaceId: string
): Promise<{ eligible: boolean; reason?: string }> {
  const workspaceData = await getWorkspaceById(workspaceId);
  
  // Must have Norwegian organization number
  if (!workspaceData.organizationNumber) {
    return { eligible: false, reason: "No organization number" };
  }
  
  // Must be approved by admin
  if (!workspaceData.invoiceEligible) {
    return { eligible: false, reason: "Not approved for invoicing" };
  }
  
  return { eligible: true };
}
1

Customer Adds Org Number

Customer enters 9-digit Norwegian organization number in workspace settings.
2

Admin Approves

System admin reviews and enables invoice eligibility at /admin/workspaces/[id].
3

Invoice Access Granted

Customer can now process projects immediately, billed monthly.

Payment Flow

Stripe Checkout Flow

1

Create Checkout Session

User clicks “Pay with Card” button, triggering createStripeCheckoutSession(projectId):
const checkoutSession = await stripe.checkout.sessions.create({
  customer: stripeCustomerId,
  mode: "payment",
  payment_intent_data: {
    setup_future_usage: "off_session", // Save card for future use
  },
  line_items: [
    { price: STRIPE_CONFIG.PRICE_PROJECT_USD, quantity: 1 }
  ],
  success_url: `${baseUrl}/dashboard/${projectId}?payment=success`,
  cancel_url: `${baseUrl}/dashboard/${projectId}?payment=cancelled`,
});
2

Payment Record Created

System creates a project_payment record with status pending:
await db.insert(projectPayment).values({
  id: nanoid(),
  projectId,
  workspaceId,
  paymentMethod: "stripe",
  stripeCheckoutSessionId: checkoutSession.id,
  amountCents: 9900,
  currency: "usd",
  status: "pending",
});
3

User Completes Payment

User is redirected to Stripe Checkout hosted page and enters payment details.
4

Webhook Updates Status

Stripe webhook at /api/webhooks/stripe/route.ts receives checkout.session.completed event and calls handleStripePaymentSuccess():
await db.update(projectPayment)
  .set({
    status: "completed",
    stripePaymentIntentId: paymentIntentId,
    paidAt: new Date(),
  })
  .where(eq(projectPayment.id, payment.id));
5

AI Processing Starts

Project status changes to processing and AI generation begins.

Invoice Payment Flow

1

Create Invoice Line Item

User clicks “Process Project” (invoice customers), system calls createInvoicePayment(projectId):
const lineItemResult = await createProjectInvoiceLineItemAction(
  workspaceId,
  projectId,
  projectName
);
2

Immediate Processing

Payment marked as completed immediately (invoice customers pay later):
await db.insert(projectPayment).values({
  paymentMethod: "invoice",
  invoiceLineItemId: lineItemResult.data.id,
  amountCents: 100_000, // 1000 NOK in øre
  currency: "nok",
  status: "completed",
  paidAt: new Date(),
});
3

Line Item Pending

Invoice line item created with status pending until included in monthly invoice.
4

Monthly Invoice Generation

Admin generates invoice at /admin/billing, line item status changes to invoiced.

Database Schema

Stripe Customer

Links workspace to Stripe customer ID:
export const stripeCustomer = pgTable("stripe_customer", {
  id: text("id").primaryKey(),
  workspaceId: text("workspace_id").notNull().unique(),
  stripeCustomerId: text("stripe_customer_id").notNull(), // cus_xxx
  createdAt: timestamp("created_at").notNull().defaultNow(),
});

Project Payment

Tracks payment for each project:
export const projectPayment = pgTable("project_payment", {
  id: text("id").primaryKey(),
  projectId: text("project_id").notNull().unique(),
  workspaceId: text("workspace_id").notNull(),
  
  // Payment method: 'stripe' | 'invoice' | 'free'
  paymentMethod: text("payment_method").notNull(),
  
  // Stripe fields
  stripeCheckoutSessionId: text("stripe_checkout_session_id"),
  stripePaymentIntentId: text("stripe_payment_intent_id"),
  
  // Invoice fields
  invoiceLineItemId: text("invoice_line_item_id"),
  
  // Amounts
  amountCents: integer("amount_cents").notNull(),
  currency: text("currency").notNull(), // 'usd' | 'nok'
  
  // Status: 'pending' | 'completed' | 'failed' | 'refunded'
  status: text("status").notNull().default("pending"),
  paidAt: timestamp("paid_at"),
});

Invoice Line Item

Billable items for Norwegian invoices:
export const invoiceLineItem = pgTable("invoice_line_item", {
  id: text("id").primaryKey(),
  workspaceId: text("workspace_id").notNull(),
  projectId: text("project_id"),
  videoProjectId: text("video_project_id"),
  
  description: text("description").notNull(),
  amountOre: integer("amount_ore").notNull(), // Amount in øre
  quantity: integer("quantity").notNull().default(1),
  
  // Status: 'pending' | 'invoiced' | 'cancelled'
  status: text("status").notNull().default("pending"),
  invoiceId: text("invoice_id"), // Set when included in invoice
});

Invoice

Groups line items for monthly billing:
export const invoice = pgTable("invoice", {
  id: text("id").primaryKey(),
  workspaceId: text("workspace_id").notNull(),
  
  // Fiken integration (Norwegian accounting)
  fikenInvoiceId: integer("fiken_invoice_id"),
  fikenInvoiceNumber: text("fiken_invoice_number"),
  
  totalAmountOre: integer("total_amount_ore").notNull(),
  currency: text("currency").notNull().default("NOK"),
  
  // Status: 'draft' | 'sent' | 'paid' | 'cancelled' | 'overdue'
  status: text("status").notNull().default("draft"),
  
  issueDate: timestamp("issue_date"),
  dueDate: timestamp("due_date"),
  paidAt: timestamp("paid_at"),
});

Saved Payment Methods

Saving Cards

Stripe checkout automatically saves payment methods when setup_future_usage: "off_session" is set:
payment_intent_data: {
  setup_future_usage: "off_session", // Save card for future payments
}

Charging Saved Cards

Use saved payment method for subsequent projects:
export async function chargeWithSavedPaymentMethod(
  projectId: string,
  paymentMethodId: string
): Promise<ActionResult<{ status: string; paymentIntentId: string }>> {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: STRIPE_CONFIG.PROJECT_PRICE_USD_CENTS,
    currency: "usd",
    customer: stripeCustomerId,
    payment_method: paymentMethodId,
    off_session: true,  // Charge without customer present
    confirm: true,      // Charge immediately
  });
}

Listing Payment Methods

Retrieve customer’s saved cards:
const paymentMethods = await stripe.paymentMethods.list({
  customer: stripeCustomerId,
  type: "card",
});

Admin Billing Panel

Accessing Billing Admin

Navigate to /admin/billing (requires system admin). Implemented in app/admin/billing/page.tsx:11:
export default async function AdminBillingPage() {
  await requireSystemAdmin();
  
  const [stats, uninvoicedItems, invoices] = await Promise.all([
    getBillingStats(),
    getUninvoicedLineItems(),
    getInvoiceHistory(),
  ]);
}

Billing Stats

Real-time metrics displayed:
  • Uninvoiced Count: Projects completed but not yet invoiced
  • Uninvoiced Amount: Total pending revenue in NOK
  • Pending Payment: Stripe payments awaiting confirmation
  • Invoiced This Month: Invoices generated in current period
  • Total Revenue: All-time invoiced amount

Managing Invoices

Two tabs in the billing panel:
  1. Uninvoiced - Line items awaiting invoice generation
  2. History - Previously generated invoices with Fiken integration
Invoice generation creates records in Fiken accounting system. Test thoroughly in development before generating production invoices.

Fiken Integration (Optional)

Norwegian accounting system integration.

Environment Variables

# Fiken API credentials
FIKEN_API_KEY=your_fiken_api_key_here
FIKEN_COMPANY_SLUG=your-company-slug

Cached Fiken Contact

Workspace pricing stores Fiken contact ID for faster invoice creation:
export const workspacePricing = pgTable("workspace_pricing", {
  fikenContactId: integer("fiken_contact_id"),
});

Enabling Invoice Eligibility

Admin action to approve workspace for invoice billing:
export async function setWorkspaceInvoiceEligibility(
  workspaceId: string,
  eligible: boolean
): Promise<ActionResult<{ success: boolean }>> {
  await db.update(workspace)
    .set({
      invoiceEligible: eligible,
      invoiceEligibleAt: eligible ? new Date() : null,
    })
    .where(eq(workspace.id, workspaceId));
}
Triggered from workspace detail page: /admin/workspaces/[id]

Billing Portal

Stripe customer portal for managing payment methods:
export async function createBillingPortalSession(): Promise<
  ActionResult<{ url: string }>
> {
  const portalSession = await stripe.billingPortal.sessions.create({
    customer: stripeCustomerId,
    return_url: `${getBaseUrl()}/dashboard/settings`,
  });
  
  return { success: true, data: { url: portalSession.url } };
}
Customers can:
  • Update payment methods
  • View payment history
  • Download receipts

Build docs developers (and LLMs) love