Skip to content

Instantly share code, notes, and snippets.

@jgdovin
Created April 1, 2025 15:20
Show Gist options
  • Save jgdovin/be19a5bc1f95ce1632a21bbe826c81e8 to your computer and use it in GitHub Desktop.
Save jgdovin/be19a5bc1f95ce1632a21bbe826c81e8 to your computer and use it in GitHub Desktop.
duplicate search
// TZ='UTC' npx tsx ./src/scratch.ts
import Stripe from 'stripe';
import { subDays, differenceInDays } from 'date-fns';
import groupBy from 'lodash/groupBy';
import * as dotenv from 'dotenv';
dotenv.config();
dotenv.config({ path: '.env.local' });
if (!process.env.STRIPE_SECRET_KEY) {
console.error('STRIPE_SECRET_KEY env var needs to be defined');
process.exit(1);
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2025-02-24.acacia' });
interface DuplicateCharge {
customerId: string;
customerEmail: string | null;
amount: number;
charges: Array<{
id: string;
created: number;
description: string | null;
}>;
daysBetween: number;
}
async function findPotentialDuplicateCharges(daysToLookBack: number = 30, maxDaysBetween: number = 2) {
const endDate = new Date();
const startDate = subDays(endDate, daysToLookBack);
console.log(`Searching for potential duplicates between ${startDate.toISOString()} and ${endDate.toISOString()}`);
// Get all charges in the date range
const charges = await stripe.charges.list({
created: {
gte: Math.floor(startDate.getTime() / 1000),
lte: Math.floor(endDate.getTime() / 1000)
},
limit: 100,
expand: ['data.customer']
});
console.log(charges)
// Group charges by customer
const chargesByCustomer = groupBy(charges.data, 'customer.id');
const potentialDuplicates: DuplicateCharge[] = [];
// For each customer, look for charges with the same amount within maxDaysBetween days
for (const [customerId, customerCharges] of Object.entries(chargesByCustomer)) {
// Skip if customer has only one charge
if (customerCharges.length <= 1) continue;
// Group charges by amount
const chargesByAmount = groupBy(customerCharges, 'amount');
for (const [, chargesWithSameAmount] of Object.entries(chargesByAmount)) {
// Skip if there's only one charge with this amount
if (chargesWithSameAmount.length <= 1) continue;
// Sort charges by creation date
const sortedCharges = chargesWithSameAmount.sort((a, b) => a.created - b.created);
// Check each pair of charges
for (let i = 0; i < sortedCharges.length - 1; i++) {
const charge1 = sortedCharges[i];
const charge2 = sortedCharges[i + 1];
const date1 = new Date(charge1.created * 1000);
const date2 = new Date(charge2.created * 1000);
const daysBetween = differenceInDays(date2, date1);
if (daysBetween <= maxDaysBetween) {
const customer = charge1.customer as Stripe.Customer;
potentialDuplicates.push({
customerId: customer.id,
customerEmail: customer.email,
amount: charge1.amount / 100, // Convert from cents to dollars
charges: [
{
id: charge1.id,
created: charge1.created,
description: charge1.description
},
{
id: charge2.id,
created: charge2.created,
description: charge2.description
}
],
daysBetween
});
}
}
}
}
return potentialDuplicates;
}
// Run the script
async function main() {
try {
const duplicates = await findPotentialDuplicateCharges();
if (duplicates.length === 0) {
console.log('No potential duplicates found.');
return;
}
console.log(`Found ${duplicates.length} potential duplicate charges:\n`);
duplicates.forEach((dup, index) => {
console.log(`${index + 1}. Customer: ${dup.customerEmail || dup.customerId}`);
console.log(` Amount: $${dup.amount}`);
console.log(` Days between charges: ${dup.daysBetween}`);
console.log(' Charges:');
dup.charges.forEach(charge => {
console.log(` - ID: ${charge.id}`);
console.log(` Date: ${new Date(charge.created * 1000).toISOString()}`);
if (charge.description) {
console.log(` Description: ${charge.description}`);
}
});
console.log('');
});
} catch (error) {
console.error('Error:', error);
}
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment