Skip to content

Commit 352cd1a

Browse files
authored
Merge pull request #137 from apsinghdev/feat/onboarding-script
add a script to onboard
2 parents 9813b97 + 5bd603a commit 352cd1a

File tree

1 file changed

+310
-0
lines changed

1 file changed

+310
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import { parse } from "csv-parse/sync";
2+
import { readFileSync } from "fs";
3+
import prismaModule from "../prisma.js";
4+
import {
5+
PAYMENT_STATUS,
6+
SUBSCRIPTION_STATUS,
7+
} from "../constants/subscription.js";
8+
import { randomBytes } from "crypto";
9+
10+
const { prisma } = prismaModule;
11+
12+
// Generate simple ID
13+
function generateId(): string {
14+
return randomBytes(16).toString("hex");
15+
}
16+
17+
interface PaymentRow {
18+
id: string;
19+
amount: string;
20+
currency: string;
21+
status: string;
22+
order_id: string;
23+
email: string;
24+
notes: string;
25+
start_date: string; // Payment date from CSV
26+
subscription_start_date?: string; // Optional custom subscription start date
27+
}
28+
29+
// Parse date from "DD/MM/YYYY HH:mm:ss" format
30+
function parseRazorpayDate(dateStr: string): Date {
31+
const parts = dateStr.split(" ");
32+
const datePart = parts[0];
33+
const timePart = parts[1] || "00:00:00";
34+
35+
if (!datePart) {
36+
throw new Error(`Invalid date format: ${dateStr}`);
37+
}
38+
39+
const dateComponents = datePart.split("/");
40+
const day = dateComponents[0];
41+
const month = dateComponents[1];
42+
const year = dateComponents[2];
43+
44+
if (!day || !month || !year) {
45+
throw new Error(`Invalid date format: ${dateStr}`);
46+
}
47+
48+
return new Date(`${year}-${month}-${day}T${timePart}`);
49+
}
50+
51+
// Extract email from notes JSON or use email column
52+
function extractEmail(row: PaymentRow): string | null {
53+
if (row.email && !row.email.includes("void@razorpay.com")) {
54+
return row.email.trim().toLowerCase();
55+
}
56+
57+
try {
58+
const notes = JSON.parse(row.notes || "{}");
59+
if (notes.email) {
60+
return notes.email.trim().toLowerCase();
61+
}
62+
} catch (e) {
63+
// Invalid JSON
64+
}
65+
66+
return null;
67+
}
68+
69+
// Extract name from notes
70+
function extractName(row: PaymentRow): string {
71+
try {
72+
const notes = JSON.parse(row.notes || "{}");
73+
if (notes.name) {
74+
return notes.name.trim();
75+
}
76+
} catch (e) {
77+
// Invalid JSON
78+
}
79+
80+
const email = extractEmail(row);
81+
return email?.split("@")[0] ?? "User";
82+
}
83+
84+
// Convert INR to paise
85+
function convertToPaise(amountStr: string): number {
86+
return Math.round(parseFloat(amountStr) * 100);
87+
}
88+
89+
// Map status
90+
function mapPaymentStatus(
91+
status: string
92+
): (typeof PAYMENT_STATUS)[keyof typeof PAYMENT_STATUS] {
93+
const statusMap: Record<
94+
string,
95+
(typeof PAYMENT_STATUS)[keyof typeof PAYMENT_STATUS]
96+
> = {
97+
captured: PAYMENT_STATUS.CAPTURED,
98+
authorized: PAYMENT_STATUS.AUTHORIZED,
99+
refunded: PAYMENT_STATUS.REFUNDED,
100+
failed: PAYMENT_STATUS.FAILED,
101+
created: PAYMENT_STATUS.CREATED,
102+
};
103+
return statusMap[status.toLowerCase()] || PAYMENT_STATUS.CREATED;
104+
}
105+
106+
async function importPremiumUsers(csvFilePath: string, planId: string) {
107+
console.log("📖 Reading CSV file...");
108+
const fileContent = readFileSync(csvFilePath, "utf-8");
109+
110+
console.log("📊 Parsing CSV...");
111+
const records = parse(fileContent, {
112+
columns: true,
113+
skip_empty_lines: true,
114+
relax_column_count: true,
115+
}) as PaymentRow[];
116+
117+
console.log(`✅ Found ${records.length} rows\n`);
118+
119+
// Connect to database
120+
await prismaModule.connectDB();
121+
122+
// Verify plan exists
123+
const plan = await prisma.plan.findUnique({
124+
where: { id: planId },
125+
});
126+
127+
if (!plan) {
128+
console.error(`❌ Plan with ID "${planId}" not found!`);
129+
console.error("\nCreate the plan first:");
130+
console.error(`
131+
INSERT INTO "Plan" (id, name, "interval", price, currency, "createdAt", "updatedAt")
132+
VALUES (
133+
'premium_yearly',
134+
'Opensox Premium',
135+
'yearly',
136+
450000,
137+
'INR',
138+
NOW(),
139+
NOW()
140+
);
141+
`);
142+
process.exit(1);
143+
}
144+
145+
console.log(`✅ Using plan: ${plan.name} (${planId})\n`);
146+
147+
let successCount = 0;
148+
let skippedCount = 0;
149+
const errors: string[] = [];
150+
151+
console.log("🔄 Processing payments...\n");
152+
153+
for (const row of records) {
154+
try {
155+
const email = extractEmail(row);
156+
if (!email) {
157+
skippedCount++;
158+
console.log(`⏭️ Skipping ${row.id}: No valid email`);
159+
continue;
160+
}
161+
162+
// Find or create user
163+
let user = await prisma.user.findUnique({
164+
where: { email },
165+
});
166+
167+
if (!user) {
168+
const name = extractName(row);
169+
user = await prisma.user.create({
170+
data: {
171+
email,
172+
firstName: name,
173+
authMethod: "csv_import",
174+
},
175+
});
176+
console.log(`👤 Created user: ${email}`);
177+
}
178+
179+
// Check if payment already exists
180+
const existingPayment = await prisma.payment.findUnique({
181+
where: { razorpayPaymentId: row.id },
182+
});
183+
184+
if (existingPayment) {
185+
skippedCount++;
186+
console.log(`⏭️ Skipping ${row.id}: Payment already exists`);
187+
continue;
188+
}
189+
190+
// Determine subscription start date
191+
// Use custom subscription_start_date if provided, otherwise use payment date (start_date)
192+
let startDate: Date;
193+
if (row.subscription_start_date && row.subscription_start_date.trim()) {
194+
// Try parsing as ISO date first
195+
startDate = new Date(row.subscription_start_date);
196+
if (isNaN(startDate.getTime())) {
197+
// If invalid, try Razorpay date format
198+
startDate = parseRazorpayDate(row.subscription_start_date);
199+
}
200+
} else {
201+
// Use payment date as subscription start date
202+
startDate = parseRazorpayDate(row.start_date);
203+
}
204+
205+
// Calculate end date (1 year from start)
206+
const endDate = new Date(startDate);
207+
endDate.setFullYear(endDate.getFullYear() + 1);
208+
209+
// Create payment (use start_date as payment creation date)
210+
const payment = await prisma.payment.create({
211+
data: {
212+
userId: user.id,
213+
razorpayPaymentId: row.id,
214+
razorpayOrderId: row.order_id,
215+
amount: convertToPaise(row.amount),
216+
currency: row.currency || "INR",
217+
status: mapPaymentStatus(row.status),
218+
createdAt: parseRazorpayDate(row.start_date),
219+
},
220+
});
221+
222+
// Check if subscription already exists for this user
223+
let subscription = await prisma.subscription.findFirst({
224+
where: {
225+
userId: user.id,
226+
planId: planId,
227+
status: SUBSCRIPTION_STATUS.ACTIVE,
228+
},
229+
});
230+
231+
if (!subscription) {
232+
// Create subscription
233+
subscription = await prisma.subscription.create({
234+
data: {
235+
userId: user.id,
236+
planId: planId,
237+
status: SUBSCRIPTION_STATUS.ACTIVE,
238+
startDate: startDate,
239+
endDate: endDate,
240+
autoRenew: true,
241+
},
242+
});
243+
} else {
244+
// Update existing subscription if new payment extends it
245+
if (endDate > subscription.endDate!) {
246+
await prisma.subscription.update({
247+
where: { id: subscription.id },
248+
data: {
249+
endDate: endDate,
250+
status: SUBSCRIPTION_STATUS.ACTIVE,
251+
},
252+
});
253+
console.log(
254+
`📅 Extended subscription for ${email} until ${endDate.toISOString().split("T")[0]}`
255+
);
256+
}
257+
}
258+
259+
// Link payment to subscription
260+
await prisma.payment.update({
261+
where: { id: payment.id },
262+
data: { subscriptionId: subscription.id },
263+
});
264+
265+
successCount++;
266+
console.log(
267+
`✅ ${email} - ₹${row.amount} (${row.id}) | Start: ${startDate.toISOString().split("T")[0]}`
268+
);
269+
} catch (error: any) {
270+
errors.push(`Error processing ${row.id}: ${error.message}`);
271+
console.error(`❌ Error processing ${row.id}:`, error.message);
272+
}
273+
}
274+
275+
console.log("\n" + "=".repeat(60));
276+
console.log("📊 Import Summary:");
277+
console.log(`✅ Successfully imported: ${successCount}`);
278+
console.log(`⏭️ Skipped: ${skippedCount}`);
279+
console.log(`❌ Errors: ${errors.length}`);
280+
if (errors.length > 0) {
281+
console.log("\nErrors:");
282+
errors.forEach((err) => console.log(` - ${err}`));
283+
}
284+
console.log("=".repeat(60));
285+
286+
await prisma.$disconnect();
287+
}
288+
289+
// Run script
290+
const csvFilePath = process.argv[2];
291+
const planId = process.argv[3] || "premium_yearly";
292+
293+
if (!csvFilePath) {
294+
console.error("Usage: tsx import-premium-users.ts <csv-file-path> [plan-id]");
295+
console.error("\nExample:");
296+
console.error(
297+
' tsx import-premium-users.ts "/Users/ajeetpratapsingh/Downloads/users.csv" premium_yearly'
298+
);
299+
process.exit(1);
300+
}
301+
302+
importPremiumUsers(csvFilePath, planId)
303+
.then(() => {
304+
console.log("\n✅ Import completed!");
305+
process.exit(0);
306+
})
307+
.catch((error) => {
308+
console.error("\n❌ Import failed:", error);
309+
process.exit(1);
310+
});

0 commit comments

Comments
 (0)