feat(portal): lead module, WHMCS sync report/contacts/mapping, service lifecycle email templates
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
3c7710f1de
commit
a443ec3bf6
@@ -5,14 +5,22 @@ namespace App\Integrations\Whmcs\Support;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Resolves a WHMCS `tblclients.id` to the matching local `customers.id`,
|
||||
* bridged through the uuid both sides already share (see ClientWriter).
|
||||
* Resolves a WHMCS `tblclients.id` to the matching local `customers.id`: by the uuid both
|
||||
* sides share (see ClientWriter), falling back to email for customers that already existed
|
||||
* on the portal with a different uuid.
|
||||
*/
|
||||
class CustomerIdResolver
|
||||
{
|
||||
/** @var array<int, int>|null whmcs client id => local customer id */
|
||||
private static ?array $map = null;
|
||||
|
||||
/** @var array<int, true>|null whmcs client ids that exist in tblclients */
|
||||
private static ?array $known = null;
|
||||
|
||||
public function __construct(private SyncReport $report)
|
||||
{
|
||||
}
|
||||
|
||||
public function resolve(int $whmcsClientId): ?int
|
||||
{
|
||||
self::$map ??= $this->build();
|
||||
@@ -20,15 +28,97 @@ class CustomerIdResolver
|
||||
return self::$map[$whmcsClientId] ?? null;
|
||||
}
|
||||
|
||||
public function resolveFor(string $entity, string|int $ref, int $whmcsClientId): ?int
|
||||
{
|
||||
$customerId = $this->resolve($whmcsClientId);
|
||||
|
||||
if ($customerId === null) {
|
||||
$exists = isset(self::$known[$whmcsClientId]);
|
||||
$this->report->note(
|
||||
$entity,
|
||||
(string) $ref,
|
||||
$whmcsClientId,
|
||||
$exists ? 'khach_chua_co_tren_portal' : 'khach_khong_ton_tai_trong_whmcs',
|
||||
$exists
|
||||
? "Khách WHMCS #{$whmcsClientId} chưa có tài khoản portal khớp uuid/email — bỏ qua {$entity}."
|
||||
: "Khách WHMCS #{$whmcsClientId} không còn trong tblclients (đã xoá) — bỏ qua {$entity}.",
|
||||
);
|
||||
}
|
||||
|
||||
return $customerId;
|
||||
}
|
||||
|
||||
public function resolveDomainOwner(string $domain, int $whmcsClientId): ?int
|
||||
{
|
||||
$customerId = $this->resolve($whmcsClientId);
|
||||
if ($customerId !== null) {
|
||||
return $customerId;
|
||||
}
|
||||
|
||||
$name = mb_strtolower(trim($domain));
|
||||
$whmcs = DB::connection('whmcs');
|
||||
|
||||
$holderClientId = $whmcs->table('tbldomains as d')
|
||||
->join('tblclients as c', 'c.id', '=', 'd.userid')
|
||||
->whereRaw('LOWER(TRIM(d.domain)) = ?', [$name])
|
||||
->orderByRaw("d.status = 'Active' DESC")
|
||||
->orderByDesc('d.id')
|
||||
->value('d.userid');
|
||||
if ($holderClientId && ($customerId = $this->resolve((int) $holderClientId)) !== null) {
|
||||
return $this->traced($name, $whmcsClientId, $customerId, 'domain_cung_ten_khach_khac', "Tên miền cùng tên đang do khách WHMCS #{$holderClientId} giữ.");
|
||||
}
|
||||
|
||||
$linkedCustomerId = DB::table('dns_domain_user as l')
|
||||
->join('dns_manager_domain as d', 'd.id', '=', 'l.domain_id')
|
||||
->whereRaw('LOWER(d.domain_name) = ?', [$name])
|
||||
->orderBy('l.id')
|
||||
->value('l.user_id');
|
||||
if ($linkedCustomerId) {
|
||||
return $this->traced($name, $whmcsClientId, (int) $linkedCustomerId, 'domain_da_gan_khach_trong_dns', 'Tên miền đã gắn khách trong DNS Manager của portal.');
|
||||
}
|
||||
|
||||
$contactEmails = $whmcs->table('tblcontacts')->where('userid', $whmcsClientId)->pluck('email')->map(fn ($email) => strtolower((string) $email))->filter()->all();
|
||||
if ($contactEmails !== [] && ($customerId = DB::table('customers')->whereIn(DB::raw('LOWER(email)'), $contactEmails)->value('id'))) {
|
||||
return $this->traced($name, $whmcsClientId, (int) $customerId, 'domain_khop_email_lien_he', 'Khớp theo email liên hệ của khách đã xoá.');
|
||||
}
|
||||
|
||||
$this->report->note(
|
||||
'domain',
|
||||
$name,
|
||||
$whmcsClientId,
|
||||
'khach_khong_ton_tai_trong_whmcs',
|
||||
"Khách WHMCS #{$whmcsClientId} đã xoá, không suy ra được chủ. Email liên hệ còn lại: " . ($contactEmails ? implode(', ', $contactEmails) : 'không có'),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function traced(string $domain, int $whmcsClientId, int $customerId, string $issue, string $detail): int
|
||||
{
|
||||
$this->report->note('domain', $domain, $whmcsClientId, $issue, "Khách WHMCS #{$whmcsClientId} đã xoá; chủ suy ra → khách portal #{$customerId}. {$detail}");
|
||||
|
||||
return $customerId;
|
||||
}
|
||||
|
||||
public static function flush(): void
|
||||
{
|
||||
self::$map = null;
|
||||
self::$known = null;
|
||||
}
|
||||
|
||||
private function build(): array
|
||||
{
|
||||
$uuidsByWhmcsId = DB::connection('whmcs')->table('tblclients')->pluck('uuid', 'id');
|
||||
$customerIdsByUuid = DB::table('customers')->pluck('id', 'uuid');
|
||||
$clients = DB::connection('whmcs')->table('tblclients')->get(['id', 'uuid', 'email']);
|
||||
$byUuid = DB::table('customers')->pluck('id', 'uuid');
|
||||
$byEmail = DB::table('customers')->whereNotNull('email')->pluck('id', DB::raw('LOWER(email)'));
|
||||
|
||||
$map = [];
|
||||
foreach ($uuidsByWhmcsId as $whmcsId => $uuid) {
|
||||
if (isset($customerIdsByUuid[$uuid])) {
|
||||
$map[$whmcsId] = $customerIdsByUuid[$uuid];
|
||||
self::$known = [];
|
||||
foreach ($clients as $client) {
|
||||
self::$known[$client->id] = true;
|
||||
$customerId = $byUuid[$client->uuid] ?? $byEmail[strtolower((string) $client->email)] ?? null;
|
||||
if ($customerId !== null) {
|
||||
$map[$client->id] = $customerId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Integrations\Whmcs\Support;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Container\Attributes\Singleton;
|
||||
|
||||
#[Singleton]
|
||||
class SyncReport
|
||||
{
|
||||
/** @var array<int, array{entity: string, ref: string, whmcs_id: string, issue: string, detail: string}> */
|
||||
private array $notes = [];
|
||||
|
||||
private bool $batch = false;
|
||||
|
||||
public function note(string $entity, string $ref, int|string $whmcsId, string $issue, string $detail): void
|
||||
{
|
||||
$this->notes[] = ['entity' => $entity, 'ref' => $ref, 'whmcs_id' => (string) $whmcsId, 'issue' => $issue, 'detail' => $detail];
|
||||
}
|
||||
|
||||
public function beginBatch(): void
|
||||
{
|
||||
$this->batch = true;
|
||||
}
|
||||
|
||||
public function finishBatch(Command $command): void
|
||||
{
|
||||
$this->batch = false;
|
||||
$this->finish($command);
|
||||
}
|
||||
|
||||
public function finish(Command $command): void
|
||||
{
|
||||
if ($this->batch || $this->notes === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = [];
|
||||
foreach ($this->notes as $note) {
|
||||
$key = $note['entity'] . ' / ' . $note['issue'];
|
||||
$summary[$key] = ($summary[$key] ?? 0) + 1;
|
||||
}
|
||||
ksort($summary);
|
||||
|
||||
$command->newLine();
|
||||
$command->warn('Ghi chú đồng bộ (' . count($this->notes) . ' mục):');
|
||||
$command->table(['Loại / vấn đề', 'Số lượng'], array_map(fn ($key, $count) => [$key, $count], array_keys($summary), $summary));
|
||||
$command->info('Chi tiết: ' . $this->writeCsv());
|
||||
|
||||
$this->notes = [];
|
||||
}
|
||||
|
||||
private function writeCsv(): string
|
||||
{
|
||||
$path = storage_path('logs/whmcs-sync-report-' . now()->format('Ymd-His-u') . '.csv');
|
||||
$handle = fopen($path, 'w');
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
fputcsv($handle, ['entity', 'ref', 'whmcs_id', 'issue', 'detail']);
|
||||
foreach ($this->notes as $note) {
|
||||
fputcsv($handle, $note);
|
||||
}
|
||||
fclose($handle);
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Integrations\Whmcs\Support;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SyncedLegacyIds
|
||||
{
|
||||
/**
|
||||
* @return array<int|string, int> legacy ids already present locally, as a lookup set
|
||||
*/
|
||||
public static function of(string $table, bool $requireCustomer = false): array
|
||||
{
|
||||
$query = DB::table($table)->whereNotNull('legacy_id');
|
||||
if ($requireCustomer) {
|
||||
$query->whereNotNull('customer_id');
|
||||
}
|
||||
|
||||
return $query->pluck('legacy_id')->flip()->all();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user