EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
import_whmcs_domains.js
Go to the documentation of this file.
1#!/usr/bin/env node
2
3/**
4 * Import domains from WHMCS export to PostgreSQL
5 * Links domains to imported customers in the Precise PCs tenant
6 * Imports ALL domains, even if not registered through us
7 */
8
9require('dotenv').config();
10const fs = require('fs');
11const path = require('path');
12const pool = require('../services/db');
13
14const EXPORT_DIR = path.join(__dirname, '../exports');
15
16// Get tenant ID from Precise PCs tenant file
17/**
18 *
19 */
20function getTenantId() {
21 const tenantFilePath = path.join(EXPORT_DIR, 'precise_pcs_tenant.json');
22
23 if (fs.existsSync(tenantFilePath)) {
24 try {
25 const tenantData = JSON.parse(fs.readFileSync(tenantFilePath, 'utf8'));
26 console.log(`šŸ“‚ Using tenant: ${tenantData.name} (${tenantData.tenant_id})`);
27 return tenantData.tenant_id;
28 } catch (error) {
29 console.warn('āš ļø Could not read tenant file, using default');
30 }
31 }
32
33 return process.env.TENANT_ID || '00000000-0000-0000-0000-000000000001';
34}
35
36/**
37 * Map WHMCS status to our domain status
38 * @param whmcsStatus
39 */
40function mapDomainStatus(whmcsStatus) {
41 const statusMap = {
42 'Active': 'active',
43 'Pending': 'pending',
44 'Pending Transfer': 'pending_transfer',
45 'Expired': 'expired',
46 'Cancelled': 'cancelled',
47 'Fraud': 'cancelled',
48 'Transferred Away': 'transferred_out',
49 'Redemption': 'redemption'
50 };
51
52 return statusMap[whmcsStatus] || 'unknown';
53}
54
55/**
56 * Parse date from WHMCS format
57 * @param dateString
58 */
59function parseDate(dateString) {
60 if (!dateString || dateString === 'Invalid Date') {
61 return null;
62 }
63 const date = new Date(dateString);
64 return isNaN(date.getTime()) ? null : date.toISOString();
65}
66
67/**
68 * Create customer lookup map from WHMCS user ID to our customer ID
69 * @param tenantId
70 */
71async function createCustomerLookup(tenantId) {
72 const importLogPath = path.join(EXPORT_DIR, 'customer_import_log.json');
73
74 if (fs.existsSync(importLogPath)) {
75 const log = JSON.parse(fs.readFileSync(importLogPath, 'utf8'));
76 const lookup = {};
77
78 // Handle both array format and object with 'imported' property
79 const entries = Array.isArray(log) ? log : (log.imported || []);
80
81 for (const entry of entries) {
82 if (entry.whmcs_id && entry.postgres_id) {
83 lookup[entry.whmcs_id] = entry.postgres_id;
84 } else if (entry.whmcs_id && entry.customer_id) {
85 lookup[entry.whmcs_id] = entry.customer_id;
86 }
87 }
88
89 console.log(`šŸ“‹ Customer lookup: ${Object.keys(lookup).length} mappings loaded`);
90 return lookup;
91 }
92
93 // Fallback: query database for customers with WHMCS IDs in notes
94 const result = await pool.query(
95 `SELECT customer_id, notes FROM customers WHERE tenant_id = $1 AND notes LIKE '%WHMCS ID:%'`,
96 [tenantId]
97 );
98
99 const lookup = {};
100 for (const row of result.rows) {
101 const match = row.notes.match(/WHMCS ID:\s*(\d+)/);
102 if (match) {
103 lookup[parseInt(match[1])] = row.customer_id;
104 }
105 }
106
107 console.log(`šŸ“‹ Customer lookup from database: ${Object.keys(lookup).length} mappings found`);
108 return lookup;
109}
110
111/**
112 * Import domains from WHMCS data
113 */
114async function importDomains() {
115 const tenantId = getTenantId();
116
117 try {
118 console.log('========================================');
119 console.log('WHMCS Domain Import');
120 console.log('========================================\n');
121
122 // Check if export file exists
123 const jsonPath = path.join(EXPORT_DIR, 'whmcs_export.json');
124 if (!fs.existsSync(jsonPath)) {
125 throw new Error(`Export file not found: ${jsonPath}\nRun: node migration-scripts/fetch_whmcs_data.js`);
126 }
127
128 // Load export data
129 const exportData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
130 const domains = exportData.domains || [];
131
132 console.log(`Found ${domains.length} domains to import`);
133 console.log(`Target tenant: ${tenantId}\n`);
134
135 // Create customer lookup
136 const customerLookup = await createCustomerLookup(tenantId);
137
138 console.log('Starting domain import...\n');
139
140 let imported = 0;
141 let skipped = 0;
142 let errors = 0;
143 const importLog = {
144 tenant_id: tenantId,
145 imported_at: new Date().toISOString(),
146 imported: [],
147 skipped: [],
148 errors: []
149 };
150
151 for (const domain of domains) {
152 try {
153 // Skip if no domain name
154 if (!domain.domain || domain.domain.trim() === '') {
155 skipped++;
156 importLog.skipped.push({
157 whmcs_id: domain.id,
158 reason: 'No domain name'
159 });
160 continue;
161 }
162
163 // Get customer ID
164 const customerId = customerLookup[domain.userid];
165 if (!customerId) {
166 console.log(`āš ļø Skipping ${domain.domain}: Customer not found (WHMCS user ${domain.userid})`);
167 skipped++;
168 importLog.skipped.push({
169 whmcs_id: domain.id,
170 domain: domain.domain,
171 whmcs_user_id: domain.userid,
172 reason: 'Customer not found in system'
173 });
174 continue;
175 }
176
177 // Check if domain already exists
178 const existing = await pool.query(
179 'SELECT domain_id FROM domains WHERE tenant_id = $1 AND domain_name = $2',
180 [tenantId, domain.domain.toLowerCase()]
181 );
182
183 if (existing.rows.length > 0) {
184 console.log(`ā­ļø Skipping ${domain.domain}: Already exists`);
185 skipped++;
186 importLog.skipped.push({
187 whmcs_id: domain.id,
188 domain: domain.domain,
189 reason: 'Already exists'
190 });
191 continue;
192 }
193
194 // Prepare domain data
195 const domainData = {
196 tenant_id: tenantId,
197 customer_id: customerId,
198 domain_name: domain.domain.toLowerCase(),
199 registrar: domain.registrar || 'unknown',
200 registrar_domain_id: domain.id.toString(),
201 status: mapDomainStatus(domain.status),
202 registration_date: parseDate(domain.registrationdate),
203 expiration_date: parseDate(domain.expirydate),
204 auto_renew: domain.donotrenew ? false : true,
205 whois_privacy: domain.idprotection ? true : false,
206 renewal_price: parseFloat(domain.recurringamount) || 0,
207 currency: 'AUD',
208 notes: `Migrated from WHMCS\nWHMCS Domain ID: ${domain.id}\nRegistration Type: ${domain.type || 'Unknown'}\nPayment Method: ${domain.paymentmethod || 'N/A'}`
209 };
210
211 // Insert domain
212 const insertQuery = `
213 INSERT INTO domains (
214 tenant_id, customer_id, domain_name, registrar, registrar_domain_id,
215 status, registration_date, expiration_date, auto_renew, whois_privacy,
216 renewal_price, currency, notes, created_at, updated_at
217 ) VALUES (
218 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW()
219 )
220 RETURNING domain_id, domain_name, status
221 `;
222
223 const result = await pool.query(insertQuery, [
224 domainData.tenant_id,
225 domainData.customer_id,
226 domainData.domain_name,
227 domainData.registrar,
228 domainData.registrar_domain_id,
229 domainData.status,
230 domainData.registration_date,
231 domainData.expiration_date,
232 domainData.auto_renew,
233 domainData.whois_privacy,
234 domainData.renewal_price,
235 domainData.currency,
236 domainData.notes
237 ]);
238
239 const insertedDomain = result.rows[0];
240
241 // Format expiry date for display
242 const expiryDisplay = domain.expirydate && domain.expirydate !== 'Invalid Date'
243 ? new Date(domain.expirydate).toLocaleDateString()
244 : 'N/A';
245
246 console.log(`āœ… Imported: ${insertedDomain.domain_name} (${insertedDomain.status}) - Expires: ${expiryDisplay}`);
247
248 imported++;
249 importLog.imported.push({
250 domain_id: insertedDomain.domain_id,
251 domain_name: insertedDomain.domain_name,
252 whmcs_id: domain.id,
253 whmcs_user_id: domain.userid,
254 customer_id: customerId,
255 status: insertedDomain.status,
256 registrar: domainData.registrar,
257 expiration_date: domainData.expiration_date
258 });
259
260 } catch (error) {
261 console.error(`āŒ Error importing ${domain.domain}:`, error.message);
262 errors++;
263 importLog.errors.push({
264 whmcs_id: domain.id,
265 domain: domain.domain,
266 error: error.message
267 });
268 }
269 }
270
271 // Save import log
272 const logPath = path.join(EXPORT_DIR, 'domain_import_log.json');
273 fs.writeFileSync(logPath, JSON.stringify(importLog, null, 2));
274
275 console.log('\n========================================');
276 console.log('Domain Import Summary');
277 console.log('========================================');
278 console.log(`āœ… Imported: ${imported}`);
279 console.log(`ā­ļø Skipped: ${skipped}`);
280 console.log(`āŒ Errors: ${errors}`);
281 console.log('\nImport log saved:', logPath);
282 console.log('========================================\n');
283
284 // Show import status by domain status
285 if (imported > 0) {
286 const statusCounts = {};
287 for (const entry of importLog.imported) {
288 statusCounts[entry.status] = (statusCounts[entry.status] || 0) + 1;
289 }
290
291 console.log('Domains by status:');
292 for (const [status, count] of Object.entries(statusCounts)) {
293 console.log(` ${status}: ${count}`);
294 }
295 console.log('');
296 }
297
298 console.log('Next steps:');
299 console.log('1. Review domains in dashboard');
300 console.log('2. Verify domain-customer relationships');
301 console.log('3. Set up domain renewal notifications');
302 console.log('4. Import hosting products/contracts');
303
304 } catch (error) {
305 console.error('āŒ Domain import failed:', error.message);
306 throw error;
307 } finally {
308 await pool.end();
309 }
310}
311
312// Run if executed directly
313if (require.main === module) {
314 importDomains()
315 .then(() => {
316 console.log('\nāœ… Domain import complete');
317 process.exit(0);
318 })
319 .catch((error) => {
320 console.error('\nāŒ Domain import failed:', error);
321 process.exit(1);
322 });
323}
324
325module.exports = { importDomains };