isExportOnly() && $this->shouldSkip()) { $this->logger->info('Phase 6 already complete, skipping'); return; } $this->logger->section('Phase 6: ' . $this->getName()); if ($this->isExportOnly()) { $this->runExport(); return; } if ($this->isFromExport()) { $this->runFromExport(); return; } $this->runFromApi(); } private function runExport(): void { $this->logger->info('Exporting promotions from WHMCS API...'); $this->exportManager->resetExport('promotions'); try { $response = $this->api->call('GetPromotions'); if (($response['result'] ?? '') !== 'success') { $this->logger->warning('GetPromotions API returned non-success, skipping'); return; } $promotions = $response['promotions']['promotion'] ?? []; } catch (Throwable $e) { $this->logger->warning('GetPromotions API call failed, skipping', [ 'error' => $e->getMessage(), ]); return; } $totalPromotions = (int) ($response['totalresults'] ?? count($promotions)); $this->logger->info("Found {$totalPromotions} promotions in WHMCS"); if (empty($promotions)) { return; } $this->exportManager->writeBatch('promotions', $promotions); $this->logger->info("Exported {$totalPromotions} promotion records to JSONL"); } private function runFromExport(): void { if (! $this->exportManager->hasExport('promotions')) { $this->logger->warning('No promotions export found. Skipping coupon migration.'); $this->markComplete(0, 0); return; } $this->markStarted(); $promotions = $this->exportManager->readAll('promotions'); $totalPromotions = count($promotions); $this->logger->info("Found {$totalPromotions} promotions in export"); if ($totalPromotions === 0) { $this->markComplete(0, 0); return; } $count = 0; $errors = 0; $tracker = new ProgressTracker('Importing coupons', $totalPromotions); foreach ($promotions as $promotion) { $whmcsPromoId = (int) ($promotion['id'] ?? 0); if ($whmcsPromoId === 0) { $this->logger->warning('Skipping promotion with no ID', ['promotion' => $promotion]); $errors++; $tracker->advance(); continue; } try { $result = $this->migratePromotion($whmcsPromoId, $promotion); if ($result) { $count++; } } catch (Throwable $e) { $errors++; $this->logger->error("Failed to migrate promotion {$whmcsPromoId}", [ 'error' => $e->getMessage(), ]); } $tracker->advance(); } $tracker->finish(); $this->markComplete($count, $errors); $this->logger->info("Phase 6 complete: {$count} coupons migrated, {$errors} errors (elapsed: {$tracker->getElapsed()})"); } private function runFromApi(): void { $this->markStarted(); $promotions = []; try { $response = $this->api->call('GetPromotions'); if (($response['result'] ?? '') !== 'success') { $this->logger->warning('GetPromotions API returned non-success, skipping coupon migration', [ 'response' => $response, ]); $this->markComplete(0, 0); return; } $promotions = $response['promotions']['promotion'] ?? []; } catch (Throwable $e) { $this->logger->warning('GetPromotions API call failed, skipping coupon migration', [ 'error' => $e->getMessage(), ]); $this->markComplete(0, 0); return; } $totalPromotions = (int) ($response['totalresults'] ?? count($promotions)); $this->logger->info("Found {$totalPromotions} promotions in WHMCS"); if (empty($promotions)) { $this->markComplete(0, 0); return; } $count = 0; $errors = 0; $tracker = new ProgressTracker('Migrating coupons', $totalPromotions); foreach ($promotions as $promotion) { $whmcsPromoId = (int) ($promotion['id'] ?? 0); if ($whmcsPromoId === 0) { $this->logger->warning('Skipping promotion with no ID', ['promotion' => $promotion]); $errors++; $tracker->advance(); continue; } try { $result = $this->migratePromotion($whmcsPromoId, $promotion); if ($result) { $count++; } } catch (Throwable $e) { $errors++; $this->logger->error("Failed to migrate promotion {$whmcsPromoId}", [ 'error' => $e->getMessage(), ]); } $tracker->advance(); } $tracker->finish(); $this->markComplete($count, $errors); $this->logger->info("Phase 6 complete: {$count} coupons migrated, {$errors} errors (elapsed: {$tracker->getElapsed()})"); } /** * Migrate a single WHMCS promotion to an EZSCALE coupon. */ private function migratePromotion(int $whmcsPromoId, array $promoData): bool { if ($this->state->getMapping('promotions', $whmcsPromoId) !== null) { $this->logger->debug("Promotion {$whmcsPromoId} already mapped, skipping"); return false; } $code = strtoupper(trim((string) ($promoData['code'] ?? ''))); if ($code === '') { $this->logger->warning("Promotion {$whmcsPromoId} has no code, skipping"); $this->logSkipped('promotion', "No code for promotion {$whmcsPromoId}"); return false; } if ($this->db->tableHasRow('coupons', ['code' => $code])) { $this->logger->warning("Coupon with code '{$code}' already exists in DB, skipping"); $this->logSkipped('promotion', "Duplicate code {$code} for promotion {$whmcsPromoId}"); return false; } $whmcsType = trim((string) ($promoData['type'] ?? '')); $type = ($whmcsType === 'Percentage') ? 'percentage' : 'fixed'; $value = (string) ($promoData['value'] ?? '0.00'); $currency = ($type === 'fixed') ? 'USD' : null; $maxUses = (int) ($promoData['maxuses'] ?? 0); $maxUsesValue = ($maxUses > 0) ? $maxUses : null; $timesUsed = (int) ($promoData['uses'] ?? 0); $expirationDate = $this->parseExpirationDate((string) ($promoData['expirationdate'] ?? '')); $active = $this->isPromotionActive($expirationDate); $now = date('Y-m-d H:i:s'); if ($this->isDryRun()) { $this->logger->info("[DRY RUN] Would create coupon for WHMCS promotion {$whmcsPromoId}: code={$code}, type={$type}, value={$value}, active=" . ($active ? 'yes' : 'no')); return true; } $newCouponId = $this->db->insert('coupons', [ 'code' => $code, 'type' => $type, 'value' => $value, 'currency' => $currency, 'applies_to' => null, 'max_uses' => $maxUsesValue, 'times_used' => $timesUsed, 'active' => $active ? 1 : 0, 'expires_at' => $expirationDate, 'created_at' => $now, 'updated_at' => $now, ]); $this->state->setMapping('promotions', $whmcsPromoId, $newCouponId); $this->logger->debug("Migrated promotion {$whmcsPromoId} → coupon {$newCouponId} (code={$code}, type={$type}, value={$value})"); return true; } private function parseExpirationDate(string $date): ?string { $date = trim($date); if ($date === '' || str_starts_with($date, '0000-00-00')) { return null; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return $date . ' 23:59:59'; } if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $date)) { return $date; } $timestamp = strtotime($date); if ($timestamp === false) { return null; } return date('Y-m-d H:i:s', $timestamp); } private function isPromotionActive(?string $expirationDate): bool { if ($expirationDate === null) { return true; } $expirationTimestamp = strtotime($expirationDate); if ($expirationTimestamp === false) { return true; } return $expirationTimestamp > time(); } }