diff --git a/services/web/scripts/stripe/create_coupons.mjs b/services/web/scripts/stripe/create_coupons.mjs new file mode 100644 index 0000000000..ca855dc757 --- /dev/null +++ b/services/web/scripts/stripe/create_coupons.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +import minimist from 'minimist' +import { scriptRunner } from '../lib/ScriptRunner.mjs' +import { getRegionClient } from '../../modules/subscriptions/app/src/StripeClient.mjs' +// eslint-disable-next-line import/no-unresolved +import * as csv from 'csv/sync' +import { readFile } from 'node:fs/promises' + +/** + * This script creates Stripe coupons and promotion codes from a CSV file. + * + * Usage: + * node scripts/stripe/create_coupons.mjs --region=us INPUT.CSV + * + * Options: + * --region=us|uk Required. Stripe region to process (us or uk) + * + * CSV Format: + * id,name,percent_off,duration,code,max_redemptions + */ + +async function main(trackProgress) { + const args = minimist(process.argv.slice(2), { + string: ['region'], + }) + + const inputCSV = args._[0] + const region = args.region + + await trackProgress( + `Starting script for Stripe ${region.toUpperCase()} region` + ) + + const file = await readFile(inputCSV, { encoding: 'utf8' }) + const couponsPlannedToCreate = csv.parse(file, { columns: true }) + await trackProgress( + `Successfully parsed "${inputCSV}" CSV file with ${couponsPlannedToCreate.length} coupons to create` + ) + + const client = getRegionClient(region) + + const existingCoupons = await client.stripe.coupons.list({ limit: 100 }) + const existingIdsAndNames = existingCoupons.data.map(ec => ({ + id: ec.id, + name: ec.name, + })) + await trackProgress( + `Successfully parsed ${existingIdsAndNames.length} existing coupons for verification` + ) + + const couponsToCreate = couponsPlannedToCreate.filter( + c => !existingIdsAndNames.some(e => e.id === c.id || e.name === c.name) + ) + if (couponsToCreate.length === 0) { + await trackProgress(`There are no coupons to create`) + } else if (couponsToCreate.length < couponsPlannedToCreate.length) { + const filteredOut = couponsPlannedToCreate + .filter(c => + existingIdsAndNames.some(e => e.id === c.id || e.name === c.name) + ) + .map(c => c.name) + await trackProgress( + `Successfully filtered out: ${filteredOut.join(', ')} existing coupons from the ones to create` + ) + } + + const errors = [] + for (const toCreate of couponsToCreate) { + try { + const createdCoupon = await client.stripe.coupons.create({ + id: toCreate.id, + name: toCreate.name, + percent_off: parseFloat(toCreate.percent_off), + duration: toCreate.duration, + }) + + const promotionPayload = { + coupon: createdCoupon.id, + code: toCreate.code, + } + const maxRedemptions = parseInt(toCreate.max_redemptions, 10) + if (maxRedemptions > 0) { + promotionPayload.max_redemptions = maxRedemptions + } + + await client.stripe.promotionCodes.create(promotionPayload) + } catch (error) { + await trackProgress( + `Failed to create coupon "${toCreate.name}" (${toCreate.id})`, + error.message + ) + errors.push(toCreate.name) + } + } + + if (errors.length > 0) { + await trackProgress( + `Could not create the following coupons: ${errors.join(', ')}` + ) + } else { + await trackProgress( + `Successfully created ${couponsToCreate.length} coupon(s) and promotion code(s).` + ) + } +} + +// Execute the script using the runner +try { + await scriptRunner(main) + process.exit(0) +} catch (error) { + console.error('Script failed:', error.message) + process.exit(1) +}