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
}
Show Schema Field Details
Auto-incrementing primary key
Unique shop domain (e.g., example.myshopify.com). Ensures one schedule per shop.
Hour of day (0-23) when billing should be processed. Defaults to 10 (10:00 AM).
IANA timezone identifier (e.g., America/Toronto, Europe/London). Defaults to America/Toronto.
Whether billing is enabled for this shop. Defaults to true.
Timestamp when the schedule was created
Timestamp when the schedule was last updated
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.
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
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' },
});