HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/app/SendingServers/Identities/IdentitySyncService.php
<?php

namespace App\SendingServers\Identities;

use App\Model\Customer;
use App\Model\SendingIdentity;
use App\Model\SendingServer;
use App\SendingServers\Capabilities\SupportsIdentitySync;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use LogicException;
use RuntimeException;

/**
 * Single orchestration site for identity sync. Drivers expose pure
 * `fetchIdentities()`; this service does the diff against the
 * `sending_identities` table:
 *
 *   - vendor only          → INSERT (Acelle defaults: shared=false, enabled=true)
 *   - vendor + Acelle both → UPDATE (vendor fields only; preserve shared/enabled/customer_id)
 *   - Acelle only (stale)  → mark status=ARCHIVED (preserves audit, auto-revives on re-add)
 *
 * Only touches VENDOR_SYNC rows. LOCAL_DKIM (server-less) and MANUAL
 * (admin-curated) identities are never affected by sync.
 */
final class IdentitySyncService
{
    /**
     * @param ?Customer $actor If a specific customer triggered the sync (e.g.
     *                         self-service flow), insert new rows with this
     *                         customer as the registered owner.
     */
    public function __construct(private readonly ?Customer $actor = null)
    {
    }

    public function sync(SendingServer $server): SyncReport
    {
        $driver = $server->driver();
        if (!$driver instanceof SupportsIdentitySync) {
            throw new LogicException(
                "Server {$server->type} does not support identity sync (driver missing SupportsIdentitySync)"
            );
        }

        // Collapse per-key duplicates from the vendor response BEFORE the
        // diff loop — see collapseDuplicates() for the WHY and merge rules.
        $vendorIdentities = $this->collapseDuplicates($driver->fetchIdentities());

        $existing = $server->identities()
            ->ofMode(ManagementMode::VENDOR_SYNC)
            ->get()
            ->keyBy(fn (SendingIdentity $i) => $this->key($i->kind, $i->value));

        $this->guardVendorResponse($vendorIdentities, $existing);

        $vendorKeys = collect($vendorIdentities)
            ->keyBy(fn (Identity $d) => $d->key());

        return DB::transaction(function () use ($server, $vendorIdentities, $existing, $vendorKeys) {
            $report = new SyncReport();

            foreach ($vendorIdentities as $dto) {
                $key = $dto->key();
                if ($existing->has($key)) {
                    $row = $existing[$key];
                    $row->status         = $dto->status;
                    $row->dkim_status    = $dto->dkimStatus;
                    $row->display_name   = $dto->displayName ?? $row->display_name;
                    $row->error_type     = $dto->errorType;
                    $row->vendor_meta    = $dto->vendorMeta;
                    $row->last_synced_at = Carbon::now();
                    $row->save();
                    $report->updated++;
                } else {
                    SendingIdentity::create([
                        'sending_server_id' => $server->id,
                        'customer_id'       => $this->actor?->id,
                        'kind'              => $dto->kind,
                        'value'             => $dto->value,
                        'display_name'      => $dto->displayName,
                        'status'            => $dto->status,
                        'dkim_status'       => $dto->dkimStatus,
                        'error_type'        => $dto->errorType,
                        'management_mode'   => ManagementMode::VENDOR_SYNC,
                        'shared'            => false,
                        'enabled'           => true,
                        'vendor_meta'       => $dto->vendorMeta,
                        'last_synced_at'    => Carbon::now(),
                    ]);
                    $report->inserted++;
                }
            }

            // Stale → ARCHIVED (preserve audit + provenance)
            $staleKeys = $existing->keys()->diff($vendorKeys->keys());
            foreach ($staleKeys as $key) {
                $row = $existing[$key];
                if ($row->last_synced_at !== null
                    && $row->status !== IdentityStatus::ARCHIVED) {
                    $row->status         = IdentityStatus::ARCHIVED;
                    $row->last_synced_at = Carbon::now();
                    $row->save();
                    $report->archived++;
                }
                // Skip if last_synced_at IS NULL (Acelle-optimistic edge) or
                // already ARCHIVED (idempotent).
            }

            return $report;
        });
    }

    /**
     * Sanity guard: a vendor returning empty list while we have rows
     * is more likely a transient API failure than a legitimate "all
     * identities removed" event. Refuse to mark every row archived.
     */
    private function guardVendorResponse(array $vendorIdentities, Collection $existing): void
    {
        if (empty($vendorIdentities) && $existing->isNotEmpty()) {
            throw new RuntimeException(sprintf(
                'Vendor returned empty identity list while Acelle has %d VENDOR_SYNC rows. ' .
                'Refusing to mark all archived — likely transient vendor failure. ' .
                'Re-run manually if intentional.',
                $existing->count()
            ));
        }
    }

    private function key(IdentityKind $kind, string $value): string
    {
        return "{$kind->value}:{$value}";
    }

    /**
     * Some vendors return MULTIPLE auth records for the same (kind, value)
     * in a single fetchIdentities() response. SendGrid is the canonical
     * case: the same apex domain can be authenticated more than once on
     * one account (e.g., older `automatic_security=true` record with an
     * `em####` branded subdomain + newer `automatic_security=false`
     * record with a hex-hash subdomain). Each yields its own
     * `whitelabel/domains` row with the same `domain` field.
     *
     * Acelle models one identity per `(sending_server_id, kind, value)`
     * (DB unique key `uniq_server_kind_value`). Without this collapse:
     *
     *   - $existing (line 49) is built ONCE before the loop and never
     *     reflects in-loop inserts.
     *   - Iter 1 hits dup A → not in $existing → INSERT (commits inside tx)
     *   - Iter 2 hits dup B (same key) → still not in $existing → INSERT
     *     → 1062 collision on uniq_server_kind_value → whole DB::transaction
     *     rolls back → no row persists → sync never converges → Edit page
     *     catches QueryException → auto-disables the sending server.
     *
     * Past incident 2026-05-15 (zartech): SendGrid server #7 returned
     * `cyberator.net × 2` + `zartech.net × 2`, breaking the Edit page banner
     * and disabling the server. See `.support/clients/abu-sadeq-zartech.md`.
     *
     * Subdomain (`em2195`, `94aaaf03ed9f`, ...) is SendGrid plumbing for
     * the CNAME prefix — it does NOT affect what addresses Acelle can send
     * from. The apex domain IS the identity from Acelle's POV.
     *
     * Winner-of-group rule (best-of-set):
     *   1. Highest IdentityStatus rank: VERIFIED > PENDING > FAILED > DISABLED > ARCHIVED
     *   2. vendor_meta.default == true   (vendor's own "primary" marker)
     *   3. vendor_meta.valid   == true   (working CNAMEs / actually usable)
     *   4. Highest vendor_meta.id        (newest record breaks ties deterministically)
     *
     * dkimStatus is taken as best-of-set independently — apex DKIM is
     * "set up" if ANY auth record's DKIM resolves, not necessarily the
     * record we picked as winner.
     *
     * vendor_meta of the winner is kept verbatim, plus an `aux_records`
     * array describing the dups we discarded — so admin debugging can
     * answer "why does Acelle show 1 row when SendGrid lists 2?" by
     * inspecting vendor_meta without re-calling the vendor API.
     *
     * @param Identity[] $vendorIdentities
     * @return Identity[]
     */
    private function collapseDuplicates(array $vendorIdentities): array
    {
        $statusRank = static fn (IdentityStatus $s): int => match ($s) {
            IdentityStatus::VERIFIED => 5,
            IdentityStatus::PENDING  => 4,
            IdentityStatus::FAILED   => 3,
            IdentityStatus::DISABLED => 2,
            IdentityStatus::ARCHIVED => 1,
        };

        return collect($vendorIdentities)
            ->groupBy(fn (Identity $i) => $i->key())
            ->map(function ($group) use ($statusRank) {
                if ($group->count() === 1) {
                    return $group->first();
                }

                // Multi-criteria sort via array tuple — PHP's <=> compares
                // arrays element-wise, so sortByDesc on [rank, default, valid, id]
                // gives lexicographic priority in that exact order.
                $sorted = $group->sortByDesc(fn (Identity $i) => [
                    $statusRank($i->status),
                    (int) ($i->vendorMeta['default'] ?? 0),
                    (int) ($i->vendorMeta['valid']   ?? 0),
                    (int) ($i->vendorMeta['id']      ?? 0),
                ])->values();
                $winner = $sorted->first();

                $bestDkim = $group
                    ->map(fn (Identity $i) => $i->dkimStatus)
                    ->filter()
                    ->sortByDesc(fn (IdentityStatus $s) => $statusRank($s))
                    ->first();

                $auxRecords = $sorted->slice(1)->map(fn (Identity $i) => [
                    'id'        => $i->vendorMeta['id']        ?? null,
                    'subdomain' => $i->vendorMeta['subdomain'] ?? null,
                    'status'    => $i->status->value,
                    'valid'     => $i->vendorMeta['valid']     ?? null,
                ])->values()->all();

                $mergedMeta = $winner->vendorMeta;
                if (!empty($auxRecords)) {
                    $mergedMeta['aux_records'] = $auxRecords;
                }

                return new Identity(
                    kind:        $winner->kind,
                    value:       $winner->value,
                    status:      $winner->status,
                    dkimStatus:  $bestDkim ?? $winner->dkimStatus,
                    displayName: $winner->displayName
                        ?? $group->map(fn (Identity $i) => $i->displayName)->filter()->first(),
                    errorType:   $winner->errorType,
                    vendorMeta:  $mergedMeta,
                );
            })
            ->values()
            ->all();
    }
}