Invoicing (Invoice Generation) for SaaS Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

SaaS invoicing and billing

Invoicing is the documentation of a transaction for the client's accounting department. Stripe automatically generates invoices for subscriptions. The developer's task is to properly configure company details, taxes, and custom branding.

Stripe: invoice configuration

// Customer setup with billing details
const customer = await stripe.customers.create({
  email: '[email protected]',
  name: 'Acme Corp',
  address: {
    line1: '123 Main Street',
    city: 'San Francisco',
    country: 'US',
    postal_code: '94105',
  },
  tax_ids: [{
    type: 'us_ein',
    value: '12-3456789',
  }],
  metadata: { tenantId },
});

// Custom branding via Stripe Dashboard:
// Settings → Branding → Logo, colors, footer text
// Customer updates billing details
export async function updateBillingDetails(
  tenantId: string,
  data: BillingDetailsInput
): Promise<void> {
  const subscription = await db.subscription.findUnique({
    where: { tenantId }
  });

  await stripe.customers.update(subscription!.stripeCustomerId, {
    name: data.companyName,
    email: data.billingEmail,
    address: {
      line1: data.address,
      city: data.city,
      country: data.country,
      postal_code: data.postalCode,
    },
  });

  // Tax ID (EIN for US, VAT for EU)
  if (data.taxId) {
    // First remove old tax IDs
    const existingCustomer = await stripe.customers.retrieve(
      subscription!.stripeCustomerId,
      { expand: ['tax_ids'] }
    ) as Stripe.Customer;

    for (const taxId of (existingCustomer.tax_ids as Stripe.ApiList<Stripe.TaxId>).data) {
      await stripe.customers.deleteTaxId(subscription!.stripeCustomerId, taxId.id);
    }

    // Add new one
    await stripe.customers.createTaxId(subscription!.stripeCustomerId, {
      type: data.taxIdType as Stripe.TaxIdCreateParams.Type,
      value: data.taxId,
    });
  }
}

Custom invoices: PDF generation

// npm install @react-pdf/renderer
import { pdf } from '@react-pdf/renderer';
import { InvoicePDF } from '@/components/pdf/InvoicePDF';

export async function generateInvoicePDF(invoiceId: string): Promise<Buffer> {
  const invoice = await db.invoice.findUnique({
    where: { id: invoiceId },
    include: {
      tenant: { include: { branding: true } },
      lineItems: true,
    }
  });

  const pdfStream = await pdf(
    <InvoicePDF invoice={invoice!} />
  ).toBuffer();

  return pdfStream;
}

// Save to S3 and return URL
export async function getInvoicePdfUrl(invoiceId: string): Promise<string> {
  const key = `invoices/${invoiceId}.pdf`;

  // Check if already created
  try {
    await s3.headObject({ Bucket: process.env.AWS_BUCKET!, Key: key }).promise();
    return `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}`;
  } catch {
    // Not found — generate
  }

  const pdfBuffer = await generateInvoicePDF(invoiceId);
  await s3.putObject({
    Bucket: process.env.AWS_BUCKET!,
    Key: key,
    Body: pdfBuffer,
    ContentType: 'application/pdf',
    ContentDisposition: `attachment; filename="invoice-${invoiceId}.pdf"`,
  }).promise();

  return `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}`;
}

PDF component

// components/pdf/InvoicePDF.tsx
import {
  Document, Page, Text, View, Image, StyleSheet
} from '@react-pdf/renderer';

const styles = StyleSheet.create({
  page: { padding: 40, fontSize: 11, fontFamily: 'Helvetica' },
  header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 40 },
  title: { fontSize: 24, fontWeight: 'bold' },
  table: { marginTop: 20 },
  tableRow: { flexDirection: 'row', borderBottom: '1px solid #eee', padding: '8px 0' },
  tableHeader: { backgroundColor: '#f5f5f5', fontWeight: 'bold' },
});

export function InvoicePDF({ invoice }: { invoice: Invoice }) {
  return (
    <Document>
      <Page size="A4" style={styles.page}>
        <View style={styles.header}>
          <View>
            {invoice.tenant.branding?.logoUrl && (
              <Image src={invoice.tenant.branding.logoUrl} style={{ height: 40 }} />
            )}
            <Text style={styles.title}>INVOICE</Text>
            <Text>No. {invoice.number}</Text>
            <Text>Date: {invoice.createdAt.toLocaleDateString('en-US')}</Text>
          </View>
          <View style={{ alignItems: 'flex-end' }}>
            <Text style={{ fontSize: 18, color: '#6366f1' }}>
              {formatCurrency(invoice.total, invoice.currency)}
            </Text>
            <Text style={{ color: invoice.status === 'paid' ? '#22c55e' : '#f59e0b' }}>
              {invoice.status === 'paid' ? 'Paid' : 'Awaiting payment'}
            </Text>
          </View>
        </View>

        {/* Company details */}
        <View style={{ flexDirection: 'row', gap: 40, marginBottom: 30 }}>
          <View>
            <Text style={{ fontWeight: 'bold', marginBottom: 4 }}>From:</Text>
            <Text>{process.env.COMPANY_NAME}</Text>
            <Text>Tax ID: {process.env.COMPANY_TAX_ID}</Text>
          </View>
          <View>
            <Text style={{ fontWeight: 'bold', marginBottom: 4 }}>To:</Text>
            <Text>{invoice.customerName}</Text>
            {invoice.taxId && <Text>Tax ID: {invoice.taxId}</Text>}
          </View>
        </View>

        {/* Invoice line items */}
        <View style={styles.table}>
          <View style={[styles.tableRow, styles.tableHeader]}>
            <Text style={{ flex: 3 }}>Description</Text>
            <Text style={{ flex: 1, textAlign: 'right' }}>Qty</Text>
            <Text style={{ flex: 1, textAlign: 'right' }}>Price</Text>
            <Text style={{ flex: 1, textAlign: 'right' }}>Amount</Text>
          </View>
          {invoice.lineItems.map((item) => (
            <View key={item.id} style={styles.tableRow}>
              <Text style={{ flex: 3 }}>{item.description}</Text>
              <Text style={{ flex: 1, textAlign: 'right' }}>{item.quantity}</Text>
              <Text style={{ flex: 1, textAlign: 'right' }}>
                {formatCurrency(item.unitAmount, invoice.currency)}
              </Text>
              <Text style={{ flex: 1, textAlign: 'right' }}>
                {formatCurrency(item.amount, invoice.currency)}
              </Text>
            </View>
          ))}
        </View>

        {/* Total */}
        <View style={{ alignItems: 'flex-end', marginTop: 20 }}>
          <Text style={{ fontSize: 14, fontWeight: 'bold' }}>
            Total: {formatCurrency(invoice.total, invoice.currency)}
          </Text>
        </View>
      </Page>
    </Document>
  );
}

Webhook: Stripe invoice synchronization

case 'invoice.finalized': {
  const stripeInvoice = event.data.object as Stripe.Invoice;

  await db.invoice.upsert({
    where: { stripeInvoiceId: stripeInvoice.id },
    create: {
      stripeInvoiceId: stripeInvoice.id,
      tenantId: stripeInvoice.metadata.tenantId,
      number: stripeInvoice.number!,
      total: stripeInvoice.amount_due,
      currency: stripeInvoice.currency,
      status: 'open',
      pdfUrl: stripeInvoice.invoice_pdf,
      periodStart: new Date(stripeInvoice.period_start * 1000),
      periodEnd: new Date(stripeInvoice.period_end * 1000),
    },
    update: { status: 'open' }
  });
  break;
}

Configuring invoicing with Stripe, PDF generation via React PDF, and S3 storage — 3–4 working days.