3 * WHMCS to RMM Backend Migration Script
5 * Migrates all data from WHMCS to RMM PostgreSQL:
6 * - Customers → customers table
7 * - Products → products table
8 * - Invoices → invoices table
9 * - Client Services → contracts + contract_line_items tables
12const https = require('https');
13const { Pool } = require('pg');
15// ===========================
17// ===========================
19// WHMCS API Configuration
21 hostname: 'clientarea.precisewebhosting.com.au',
22 identifier: '6o30xS9dkk1iQE6KjaPh1HwTFhTzgW5e',
23 secret: 'oHw4epOsuhTJuJcdTyFZpvHrgT9bCZp3'
26// RMM PostgreSQL Configuration
28 host: 'rmm-psa-db-do-user-28531160-0.i.db.ondigitalocean.com',
30 database: 'defaultdb',
32 password: 'AVNS_J8RJAmsEwsHFG52_-F2',
34 rejectUnauthorized: false
38// Default tenant (Independent Business Group)
39const DEFAULT_TENANT_NAME = 'Independent Business Group';
41// ===========================
43// ===========================
50function whmcsAPI(action, params = {}) {
51 return new Promise((resolve, reject) => {
52 const postData = new URLSearchParams({
54 identifier: WHMCS_CONFIG.identifier,
55 secret: WHMCS_CONFIG.secret,
61 hostname: WHMCS_CONFIG.hostname,
62 path: '/includes/api.php',
65 'Content-Type': 'application/x-www-form-urlencoded',
66 'Content-Length': postData.length
68 rejectUnauthorized: false
71 const req = https.request(options, (res) => {
73 res.on('data', (chunk) => data += chunk);
76 const json = JSON.parse(data);
77 if (json.result === 'error') {
78 reject(new Error(json.message || 'WHMCS API Error'));
83 reject(new Error(`Failed to parse WHMCS response: ${e.message}`));
88 req.on('error', (e) => reject(e));
94// ===========================
96// ===========================
103async function getTenantId(pool, tenantName) {
104 const result = await pool.query(
105 'SELECT tenant_id FROM tenants WHERE name = $1 OR subdomain = $2 LIMIT 1',
106 [tenantName, tenantName.toLowerCase().replace(/\s+/g, '-')]
109 if (result.rows.length > 0) {
110 return result.rows[0].tenant_id;
113 // Create tenant if doesn't exist
114 console.log(`Creating tenant: ${tenantName}...`);
115 const insertResult = await pool.query(`
116 INSERT INTO tenants (name, subdomain, status, is_msp)
117 VALUES ($1, $2, 'active', false)
119 `, [tenantName, tenantName.toLowerCase().replace(/\s+/g, '-')]);
121 return insertResult.rows[0].tenant_id;
124// ===========================
125// MIGRATION FUNCTIONS
126// ===========================
133async function migrateCustomers(pool, tenantId) {
134 console.log('\n=== MIGRATING CUSTOMERS ===');
142 const response = await whmcsAPI('GetClients', {
143 limitstart: page * limit,
147 const clients = response.clients?.client || [];
148 if (clients.length === 0) break;
150 console.log(`Processing page ${page + 1} (${clients.length} clients)...`);
152 for (const client of clients) {
154 // Check if customer already exists
155 const existing = await pool.query(
156 'SELECT customer_id FROM customers WHERE email = $1 AND tenant_id = $2',
157 [client.email, tenantId]
160 if (existing.rows.length > 0) {
165 // Map WHMCS fields to RMM schema
166 const customerName = client.companyname ||
167 `${client.firstname} ${client.lastname}`.trim();
168 const contactName = `${client.firstname} ${client.lastname}`.trim();
169 const address = `${client.address1}\n${client.address2 || ''}`.trim();
170 const status = client.status === 'Active' ? 'active' : 'inactive';
173 const result = await pool.query(`
174 INSERT INTO customers (
175 tenant_id, name, contact_name, email, phone,
176 address, city, state, postal_code, country, status
177 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
178 RETURNING customer_id
180 tenantId, customerName, contactName, client.email, client.phonenumber,
181 address, client.city, client.state, client.postcode, client.country, status
184 // Store mapping for later use
185 client.rmm_customer_id = result.rows[0].customer_id;
188 console.log(`✓ Imported: ${customerName} (${client.email})`);
191 console.error(`✗ Failed to import ${client.email}:`, error.message);
196 // Break if we got less than limit (last page)
197 if (clients.length < limit) break;
200 console.log(`\n✓ Customers migration complete: ${imported} imported, ${skipped} skipped`);
208async function migrateProducts(pool, tenantId) {
209 console.log('\n=== MIGRATING PRODUCTS ===');
211 const response = await whmcsAPI('GetProducts');
212 const products = response.products?.product || [];
217 for (const product of products) {
219 // Check if product already exists
220 const existing = await pool.query(
221 'SELECT product_id FROM products WHERE name = $1 AND tenant_id = $2',
222 [product.name, tenantId]
225 if (existing.rows.length > 0) {
226 product.rmm_product_id = existing.rows[0].product_id;
231 // Determine product type
232 const isService = product.type === 'hostingaccount' ||
233 product.type === 'reselleraccount' ||
234 product.type === 'server';
237 const result = await pool.query(`
238 INSERT INTO products (
239 tenant_id, name, description, unit_price, created_at
240 ) VALUES ($1, $2, $3, $4, NOW())
245 product.description || '',
246 parseFloat(product.pricing?.USD?.monthly || 0)
249 product.rmm_product_id = result.rows[0].product_id;
252 console.log(`✓ Imported: ${product.name}`);
255 console.error(`✗ Failed to import ${product.name}:`, error.message);
259 console.log(`\n✓ Products migration complete: ${imported} imported, ${skipped} skipped`);
260 return products; // Return for contract mapping
268async function migrateInvoices(pool, tenantId) {
269 console.log('\n=== MIGRATING INVOICES ===');
277 const response = await whmcsAPI('GetInvoices', {
278 limitstart: page * limit,
282 const invoices = response.invoices?.invoice || [];
283 if (invoices.length === 0) break;
285 console.log(`Processing page ${page + 1} (${invoices.length} invoices)...`);
287 for (const invoice of invoices) {
289 // Get customer by email
290 const customerResult = await pool.query(
291 'SELECT customer_id FROM customers WHERE tenant_id = $1 LIMIT 1 OFFSET $2',
292 [tenantId, parseInt(invoice.userid) - 1]
295 if (customerResult.rows.length === 0) {
296 console.log(`⏭️ Skipping invoice ${invoice.id} - customer not found`);
301 const customerId = customerResult.rows[0].customer_id;
303 // Check if invoice already exists
304 const existing = await pool.query(
305 'SELECT invoice_id FROM invoices WHERE customer_id = $1 AND date = $2 AND total = $3',
306 [customerId, invoice.date, parseFloat(invoice.total)]
309 if (existing.rows.length > 0) {
314 // Map invoice status
315 let status = 'draft';
316 if (invoice.status === 'Paid') status = 'paid';
317 else if (invoice.status === 'Unpaid') status = 'sent';
318 else if (invoice.status === 'Cancelled') status = 'void';
322 INSERT INTO invoices (
323 tenant_id, customer_id, invoice_number, date, due_date,
324 subtotal, tax, total, status, notes
325 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
329 invoice.invoicenum || `INV-${invoice.id}`,
332 parseFloat(invoice.subtotal || 0),
333 parseFloat(invoice.tax || 0),
334 parseFloat(invoice.total || 0),
336 `Migrated from WHMCS Invoice #${invoice.id}`
342 console.error(`✗ Failed to import invoice ${invoice.id}:`, error.message);
347 if (invoices.length < limit) break;
350 console.log(`\n✓ Invoices migration complete: ${imported} imported, ${skipped} skipped`);
359async function migrateServicesAndContracts(pool, tenantId, products) {
360 console.log('\n=== MIGRATING CLIENT SERVICES & CREATING CONTRACTS ===');
367 // Build product lookup map
368 const productMap = {};
369 products.forEach(p => {
370 if (p.id && p.rmm_product_id) {
371 productMap[p.id] = p.rmm_product_id;
376 const response = await whmcsAPI('GetClientsProducts', {
377 limitstart: page * limit,
381 const services = response.products?.product || [];
382 if (services.length === 0) break;
384 console.log(`Processing page ${page + 1} (${services.length} services)...`);
386 for (const service of services) {
388 // Get customer by userid
389 const customerResult = await pool.query(
390 'SELECT customer_id, name FROM customers WHERE tenant_id = $1 LIMIT 1 OFFSET $2',
391 [tenantId, parseInt(service.clientid) - 1]
394 if (customerResult.rows.length === 0) {
395 console.log(`⏭️ Skipping service ${service.id} - customer not found`);
400 const customer = customerResult.rows[0];
402 // Skip if service is terminated
403 if (service.status === 'Terminated' || service.status === 'Cancelled') {
408 // Check if contract already exists
409 const existing = await pool.query(
410 'SELECT contract_id FROM contracts WHERE customer_id = $1 AND title ILIKE $2',
411 [customer.customer_id, `%${service.name}%`]
414 if (existing.rows.length > 0) {
419 // Map billing cycle to interval
420 let billingInterval = 'monthly';
421 if (service.billingcycle === 'Annually') billingInterval = 'yearly';
422 else if (service.billingcycle === 'Quarterly') billingInterval = 'quarterly';
423 else if (service.billingcycle === 'Semi-Annually') billingInterval = 'semi_annually';
425 // Calculate next billing date
426 const nextBillingDate = service.nextduedate ||
427 new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
429 // Determine contract status
430 let contractStatus = 'active';
431 if (service.status === 'Pending') contractStatus = 'draft';
432 else if (service.status === 'Suspended') contractStatus = 'suspended';
435 const contractResult = await pool.query(`
436 INSERT INTO contracts (
437 tenant_id, customer_id, title, description, status,
438 start_date, billing_interval, next_billing_date,
439 auto_invoice_enabled, labor_rate, notes
440 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
441 RETURNING contract_id
444 customer.customer_id,
445 service.name || service.product || 'Service Contract',
446 service.product || '',
448 service.regdate || new Date().toISOString().split('T')[0],
451 true, // auto_invoice_enabled
452 0, // labor_rate (can be updated later)
453 `Migrated from WHMCS Service #${service.id}\nDomain: ${service.domain || 'N/A'}`
456 const contractId = contractResult.rows[0].contract_id;
458 // Add contract line item for the service
459 const productId = productMap[service.pid] || null;
460 const amount = parseFloat(service.amount || 0);
463 INSERT INTO contract_line_items (
464 contract_id, product_id, description, quantity, unit_price, recurring
465 ) VALUES ($1, $2, $3, $4, $5, $6)
469 service.product || service.name || 'Service',
476 console.log(`✓ Created contract: ${service.name} for ${customer.name} ($${amount}/${billingInterval})`);
479 console.error(`✗ Failed to create contract for service ${service.id}:`, error.message);
484 if (services.length < limit) break;
487 console.log(`\n✓ Contracts migration complete: ${imported} created, ${skipped} skipped`);
490// ===========================
492// ===========================
497async function main() {
498 console.log('╔══════════════════════════════════════════════════════════╗');
499 console.log('║ WHMCS → RMM Backend Migration Script ║');
500 console.log('╚══════════════════════════════════════════════════════════╝\n');
502 const pool = new Pool(PG_CONFIG);
505 // Test database connection
506 console.log('Testing database connection...');
507 await pool.query('SELECT NOW()');
508 console.log('✓ Database connected\n');
510 // Get or create tenant
511 console.log(`Getting tenant ID for: ${DEFAULT_TENANT_NAME}...`);
512 const tenantId = await getTenantId(pool, DEFAULT_TENANT_NAME);
513 console.log(`✓ Tenant ID: ${tenantId}\n`);
516 console.log('Testing WHMCS API connection...');
517 await whmcsAPI('GetClients', { limitnum: 1 });
518 console.log('✓ WHMCS API connected\n');
520 // Run migrations in sequence
521 await migrateCustomers(pool, tenantId);
522 const products = await migrateProducts(pool, tenantId);
523 await migrateInvoices(pool, tenantId);
524 await migrateServicesAndContracts(pool, tenantId, products);
526 console.log('\n╔══════════════════════════════════════════════════════════╗');
527 console.log('║ MIGRATION COMPLETE ✓ ║');
528 console.log('╚══════════════════════════════════════════════════════════╝\n');
531 const stats = await pool.query(`
533 (SELECT COUNT(*) FROM customers WHERE tenant_id = $1) as customers,
534 (SELECT COUNT(*) FROM products WHERE tenant_id = $1) as products,
535 (SELECT COUNT(*) FROM invoices WHERE tenant_id = $1) as invoices,
536 (SELECT COUNT(*) FROM contracts WHERE tenant_id = $1) as contracts
539 console.log('Final Statistics:');
540 console.log(` Customers: ${stats.rows[0].customers}`);
541 console.log(` Products: ${stats.rows[0].products}`);
542 console.log(` Invoices: ${stats.rows[0].invoices}`);
543 console.log(` Contracts: ${stats.rows[0].contracts}`);
547 console.error('\n❌ MIGRATION FAILED:', error.message);
548 console.error(error.stack);
556if (require.main === module) {
560module.exports = { main };