From 3000a9108d036b0879362daca6f001b8f6e4d804 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:43:08 +0100 Subject: [PATCH] [web] fix source on analytics mapping in finalization script (#31143) * fix source on analytics mapping in finalization script * add script to redo mappings for finalized subscriptions GitOrigin-RevId: a62af6c6c7ae11f9bea62ed643c8dfa7595ba99b --- ...finalize-stripe-subscription-migration.mjs | 2 +- .../stripe/register-analytics-mapping.mjs | 244 ++++++++++++++++++ 2 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 services/web/scripts/stripe/register-analytics-mapping.mjs diff --git a/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs b/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs index e2a5b1527a..e715d88670 100755 --- a/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs +++ b/services/web/scripts/stripe/finalize-stripe-subscription-migration.mjs @@ -468,7 +468,7 @@ async function performCutover( AccountMappingHelper.generateSubscriptionToStripeMapping( mongoSubscription._id, stripeSubscription.id, - stripeSubscription.service + stripeClient.serviceName ) ) } catch (err) { diff --git a/services/web/scripts/stripe/register-analytics-mapping.mjs b/services/web/scripts/stripe/register-analytics-mapping.mjs new file mode 100644 index 0000000000..81a15c4343 --- /dev/null +++ b/services/web/scripts/stripe/register-analytics-mapping.mjs @@ -0,0 +1,244 @@ +#!/usr/bin/env node + +/** + * This script registers analytics account mapping for subscriptions migrated to Stripe. + * + * // TODO: delete this when the migration is complete + * + * Usage: + * node scripts/stripe/register-analytics-mapping.mjs [OPTS] [INPUT-FILE] + * + * Options: + * --output PATH Output file path (default: /tmp/register_output_.csv) + * --commit Apply changes (without this, runs in dry-run mode) + * --help Show help message + * + * CSV Input Format: + * recurly_account_code,target_stripe_account,stripe_customer_id + * 507f1f77bcf86cd799439011,stripe-uk,cus_1234567890abcdef + * + * CSV Output Format: + * recurly_account_code,target_stripe_account,stripe_customer_id,status,note + */ + +import fs from 'node:fs' +import path from 'node:path' +import * as csv from 'csv' +import minimist from 'minimist' +import { z } from '../../app/src/infrastructure/Validation.mjs' +import { scriptRunner } from '../lib/ScriptRunner.mjs' +import { Subscription } from '../../app/src/models/Subscription.mjs' +import AnalyticsManager from '../../app/src/Features/Analytics/AnalyticsManager.mjs' +import AccountMappingHelper from '../../app/src/Features/Analytics/AccountMappingHelper.mjs' +import { ReportError } from './helpers.mjs' + +function usage() { + console.error(`Usage: node scripts/stripe/register-analytics-mapping.mjs [OPTS] [INPUT-FILE] + +Options: + --output PATH Output file path (default: /tmp/register_output_.csv) + --commit Apply changes (without this, runs in dry-run mode) + --help Show this help message +`) +} + +async function main(trackProgress) { + const opts = parseArgs() + const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + const outputFile = opts.output ?? `/tmp/register_output_${timestamp}.csv` + + await trackProgress('Starting analytics mapping registration') + await trackProgress(`Run mode: ${opts.commit ? 'COMMIT' : 'DRY RUN'}`) + + const inputStream = opts.inputFile + ? fs.createReadStream(opts.inputFile) + : process.stdin + const csvReader = getCsvReader(inputStream) + const csvWriter = getCsvWriter(outputFile) + + await trackProgress(`Output: ${outputFile}`) + + let processedCount = 0 + let successCount = 0 + let errorCount = 0 + + for await (const input of csvReader) { + processedCount++ + + try { + const result = await processRow(input, opts.commit) + + csvWriter.write({ + recurly_account_code: input.recurly_account_code, + target_stripe_account: input.target_stripe_account, + stripe_customer_id: input.stripe_customer_id, + status: result.status, + note: result.note, + }) + + if (result.status === 'registered' || result.status === 'dry-run') { + successCount++ + } else { + errorCount++ + } + + if (processedCount % 25 === 0) { + await trackProgress( + `Progress: ${processedCount} processed, ${successCount} successful, ${errorCount} errors` + ) + } + } catch (err) { + errorCount++ + if (err instanceof ReportError) { + csvWriter.write({ + recurly_account_code: input.recurly_account_code, + target_stripe_account: input.target_stripe_account, + stripe_customer_id: input.stripe_customer_id, + status: err.status, + note: err.message, + }) + } else { + csvWriter.write({ + recurly_account_code: input.recurly_account_code, + target_stripe_account: input.target_stripe_account, + stripe_customer_id: input.stripe_customer_id, + status: 'error', + note: err.message, + }) + } + } + } + + await trackProgress(`✅ Total processed: ${processedCount}`) + if (opts.commit) { + await trackProgress(`✅ Successfully registered: ${successCount}`) + } else { + await trackProgress(`✅ Successfully validated: ${successCount}`) + await trackProgress('ℹ️ DRY RUN: No changes were applied') + } + await trackProgress(`❌ Errors: ${errorCount}`) + await trackProgress('🎉 Script completed!') + + csvWriter.end() +} + +function getCsvReader(inputStream) { + const parser = csv.parse({ columns: true }) + inputStream.pipe(parser) + return parser +} + +function getCsvWriter(outputFile) { + fs.mkdirSync(path.dirname(outputFile), { recursive: true }) + const outputStream = fs.createWriteStream(outputFile) + + const writer = csv.stringify({ + columns: [ + 'recurly_account_code', + 'target_stripe_account', + 'stripe_customer_id', + 'status', + 'note', + ], + header: true, + }) + + writer.on('error', err => { + console.error(err) + process.exit(1) + }) + + writer.pipe(outputStream) + return writer +} + +async function processRow(input, commit) { + const { + recurly_account_code: accountCode, + target_stripe_account: targetStripeAccount, + } = input + + // 1. Fetch Mongo subscription + const mongoSubscription = await Subscription.findOne({ + admin_id: accountCode, + }).exec() + if (!mongoSubscription) { + throw new ReportError( + 'no-mongo-subscription', + 'No subscription found in Mongo' + ) + } + + // 2. Check if migrated to Stripe + if (!mongoSubscription.paymentProvider?.service?.includes('stripe')) { + throw new ReportError('not-stripe', 'Subscription not using Stripe') + } + + const subscriptionId = mongoSubscription.paymentProvider.subscriptionId + if (!subscriptionId) { + throw new ReportError( + 'no-subscription-id', + 'No Stripe subscription ID in Mongo' + ) + } + + // 3. Register analytics mapping + if (commit) { + AnalyticsManager.registerAccountMapping( + AccountMappingHelper.generateSubscriptionToStripeMapping( + mongoSubscription._id, + subscriptionId, + targetStripeAccount + ) + ) + return { + status: 'registered', + note: 'Analytics mapping registered', + } + } else { + return { + status: 'dry-run', + note: 'DRY RUN: Would register analytics mapping', + } + } +} + +function parseArgs() { + const args = minimist(process.argv.slice(2), { + string: ['output'], + boolean: ['commit', 'help'], + default: { commit: false }, + }) + + if (args.help) { + usage() + process.exit(0) + } + + const inputFile = args._[0] + const paramsSchema = z.object({ + output: z.string().optional(), + commit: z.boolean(), + inputFile: z.string().optional(), + }) + + try { + return paramsSchema.parse({ + output: args.output, + commit: args.commit, + inputFile, + }) + } catch (err) { + console.error('Invalid arguments:', err.message) + usage() + process.exit(1) + } +} + +try { + await scriptRunner(main) + process.exit(0) +} catch (error) { + console.error(error) + process.exit(1) +}