Skip to main content
The BillingSchedule model manages the scheduling of subscription billing jobs, allowing shops to configure when their subscription billing should be processed.

Overview

Billing schedules determine when subscription billing jobs run for each shop. This model handles:
  • Creating and updating billing schedules
  • Configuring billing time and timezone
  • Batch processing of active schedules
  • Enabling/disabling billing automation

Prisma Schema

model BillingSchedule {
  id        Int      @id @default(autoincrement())
  shop      String   @unique
  hour      Int      @default(10)
  timezone  String   @default("America/Toronto")
  active    Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Core Methods

findActiveBillingSchedulesInBatches

Iterates through all active billing schedules in batches, executing a callback for each batch.
callback
PaginatorCallbackFn
required
Async function called with each batch of billing schedules
Returns: Promise<void> TypeScript Signature:
type PaginatorCallbackFn = (records: BillingSchedule[]) => Promise<void>;

async function findActiveBillingSchedulesInBatches(
  callback: PaginatorCallbackFn,
): Promise<void>
Usage Example:
import { findActiveBillingSchedulesInBatches } from '~/models/BillingSchedule/BillingSchedule.server';
import { DateTime } from 'luxon';

await findActiveBillingSchedulesInBatches(async (schedules) => {
  console.log(`Processing batch of ${schedules.length} schedules`);
  
  for (const schedule of schedules) {
    const now = DateTime.now().setZone(schedule.timezone);
    
    if (now.hour === schedule.hour) {
      console.log(`Running billing for shop: ${schedule.shop}`);
      // Process billing for this shop
      await processBillingForShop(schedule.shop);
    }
  }
});
Implementation Notes:
  • Processes schedules in batches of 1000 (configurable)
  • Only fetches schedules where active = true
  • Uses cursor-based pagination to handle large datasets efficiently
  • Orders by id ascending for consistent pagination

createActiveBillingSchedule

Creates or updates a billing schedule for a shop, automatically fetching the shop’s timezone.
shop
string
required
The shop domain (e.g., example.myshopify.com)
Returns: Promise<BillingSchedule> TypeScript Signature:
async function createActiveBillingSchedule(
  shop: string,
): Promise<BillingSchedule>
Usage Example:
import { createActiveBillingSchedule } from '~/models/BillingSchedule/BillingSchedule.server';

const schedule = await createActiveBillingSchedule('example.myshopify.com');

console.log('Billing Schedule Created:');
console.log('- Shop:', schedule.shop);
console.log('- Hour:', schedule.hour);
console.log('- Timezone:', schedule.timezone);
console.log('- Active:', schedule.active);

// Output:
// Billing Schedule Created:
// - Shop: example.myshopify.com
// - Hour: 10
// - Timezone: America/New_York
// - Active: true
Behavior:
  • Queries Shopify to get the shop’s IANA timezone
  • Creates a new schedule if one doesn’t exist
  • Updates existing schedule to active if one exists
  • Sets default hour to 10 (10:00 AM)
  • Uses upsert operation (create or update)

Helper Functions

paginate

Internal pagination helper that processes database records in batches.
args
Prisma.BillingScheduleFindManyArgs
required
Prisma query arguments (where, orderBy, etc.)
callback
PaginatorCallbackFn
required
Function to execute for each batch
cursor
number
Optional cursor for pagination (handled automatically)
Returns: Promise<void> TypeScript Signature:
async function paginate(
  args: Prisma.BillingScheduleFindManyArgs,
  callback: PaginatorCallbackFn,
  cursor?: number,
): Promise<void>
Usage Example:
import { paginate } from '~/models/BillingSchedule/paginate';

// Process all schedules for a specific timezone
await paginate(
  {
    where: { 
      active: true,
      timezone: 'America/New_York',
    },
    orderBy: { id: 'asc' },
    take: 500, // Custom batch size
  },
  async (schedules) => {
    console.log(`Processing ${schedules.length} NY timezone schedules`);
    // Process each schedule...
  }
);
Configuration:
const defaults: Prisma.BillingScheduleFindManyArgs = {
  orderBy: { id: 'asc' },
  take: 1000, // Default batch size
};

queryTimezone

Internal helper that fetches a shop’s timezone from Shopify. TypeScript Signature:
async function queryTimezone(shop: string): Promise<string>
GraphQL Query Used:
query ShopInfo {
  shop {
    ianaTimezone
  }
}

upsert

Internal helper that creates or updates a billing schedule. TypeScript Signature:
async function upsert(
  billingSchedule: Pick<BillingSchedule, 'shop' | 'active' | 'timezone'>,
): Promise<BillingSchedule>
Prisma Operation:
return await prisma.billingSchedule.upsert({
  where: { shop },
  create: { shop, active, timezone },
  update: { active, timezone },
});

TypeScript Interfaces

BillingSchedule

interface BillingSchedule {
  id: number;
  shop: string;
  hour: number;
  timezone: string;
  active: boolean;
  createdAt: Date;
  updatedAt: Date;
}

PaginatorCallbackFn

type PaginatorCallbackFn = (records: BillingSchedule[]) => Promise<void>;

Common Patterns

Running Billing Jobs Based on Schedule

import { findActiveBillingSchedulesInBatches } from '~/models/BillingSchedule/BillingSchedule.server';
import { DateTime } from 'luxon';
import { processBillingJob } from '~/jobs/billing';

export async function runScheduledBilling() {
  await findActiveBillingSchedulesInBatches(async (schedules) => {
    const jobPromises = schedules.map(async (schedule) => {
      const now = DateTime.now().setZone(schedule.timezone);
      
      // Check if current hour matches scheduled hour
      if (now.hour === schedule.hour) {
        console.log(`Starting billing for ${schedule.shop}`);
        await processBillingJob(schedule.shop);
      }
    });
    
    await Promise.all(jobPromises);
  });
}

Updating Billing Time

import prisma from '~/db.server';

export async function updateBillingTime(
  shop: string,
  hour: number,
) {
  if (hour < 0 || hour > 23) {
    throw new Error('Hour must be between 0 and 23');
  }
  
  return await prisma.billingSchedule.update({
    where: { shop },
    data: { hour },
  });
}

const schedule = await updateBillingTime('example.myshopify.com', 14);
console.log(`Billing time updated to ${schedule.hour}:00`);

Deactivating Billing

import prisma from '~/db.server';

export async function deactivateBilling(shop: string) {
  return await prisma.billingSchedule.update({
    where: { shop },
    data: { active: false },
  });
}

await deactivateBilling('example.myshopify.com');
console.log('Billing deactivated for shop');

Getting Schedule for a Specific Shop

import prisma from '~/db.server';

export async function getBillingSchedule(shop: string) {
  return await prisma.billingSchedule.findUnique({
    where: { shop },
  });
}

const schedule = await getBillingSchedule('example.myshopify.com');

if (schedule) {
  console.log(`Billing runs at ${schedule.hour}:00 ${schedule.timezone}`);
  console.log(`Status: ${schedule.active ? 'Active' : 'Inactive'}`);
} else {
  console.log('No billing schedule found for this shop');
}

Database Operations

The BillingSchedule model uses Prisma ORM for database operations:
import prisma from '~/db.server';

// Create
const schedule = await prisma.billingSchedule.create({
  data: {
    shop: 'example.myshopify.com',
    hour: 10,
    timezone: 'America/Toronto',
    active: true,
  },
});

// Read
const schedule = await prisma.billingSchedule.findUnique({
  where: { shop: 'example.myshopify.com' },
});

// Update
const schedule = await prisma.billingSchedule.update({
  where: { shop: 'example.myshopify.com' },
  data: { hour: 14 },
});

// Delete
await prisma.billingSchedule.delete({
  where: { shop: 'example.myshopify.com' },
});

// Find many
const schedules = await prisma.billingSchedule.findMany({
  where: { active: true },
  orderBy: { hour: 'asc' },
});

Build docs developers (and LLMs) love