403Webshell
Server IP : 35.80.110.71  /  Your IP : 216.73.216.221
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64
User : ubuntu ( 1000)
PHP Version : 8.3.31
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/switchyard/current/app/Jobs/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/switchyard/current/app/Jobs/MirrorClickUpStatusJob.php
<?php

namespace App\Jobs;

use App\Enums\LeadEventKind;
use App\Enums\LeadStatus;
use App\Models\ClientMilestone;
use App\Models\Lead;
use App\Models\LeadEvent;
use App\Models\ProjectTemplate;
use App\Models\Scopes\WorkspaceScope;
use App\Models\Workspace;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

/**
 * Maps an incoming ClickUp webhook event onto local mirrors. A single
 * webhook can touch:
 *
 *   1. A Lead — when the task is the original lead row (Phase 1 path).
 *      Translates via the workspace's `status_mapping` and writes back to
 *      `leads.status` (+ terminal-timestamp columns).
 *
 *   2. A ClientMilestone — when the task lives inside a tracked
 *      ClientProject's ClickUp folder (Phase 2 path). Writes the new
 *      ClickUp status name/color/type onto the milestone mirror so the
 *      public `/status/[token]` page reflects it without a fresh sync.
 *
 * Both mirrors update independently; either, both, or neither may match
 * for a given event.
 */
class MirrorClickUpStatusJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public function __construct(
        public readonly string $workspaceId,
        public readonly array $payload,
    ) {}

    public function handle(): void
    {
        $taskId = $this->payload['task_id'] ?? null;
        if (! $taskId) {
            Log::info('MirrorClickUpStatusJob: payload missing task_id', ['workspace_id' => $this->workspaceId]);

            return;
        }

        $newStatus = $this->extractStatus($this->payload);

        $matched = $this->mirrorLead($taskId, $newStatus);
        $matched = $this->mirrorMilestone($taskId, $newStatus) || $matched;

        if (! $matched) {
            Log::info('MirrorClickUpStatusJob: no matching lead or milestone', [
                'workspace_id' => $this->workspaceId,
                'task_id' => $taskId,
            ]);
        }
    }

    /**
     * @param  array{name: string|null, color: string|null, type: string|null}  $newStatus
     */
    private function mirrorLead(string $taskId, array $newStatus): bool
    {
        $lead = Lead::withoutGlobalScope(WorkspaceScope::class)
            ->where('workspace_id', $this->workspaceId)
            ->where('clickup_task_id', $taskId)
            ->first();

        if (! $lead) {
            return false;
        }

        $workspace = Workspace::find($this->workspaceId);
        $mapping = (array) ($workspace?->clickupConfig?->status_mapping ?? []);
        $statusName = $newStatus['name'];

        $newLocalStatus = $statusName !== null
            ? $this->mapToLocalStatus($statusName, $mapping)
            : null;

        $eventPayload = [
            'event' => $this->payload['event'] ?? null,
            'task_id' => $taskId,
            'clickup_status' => $statusName,
            'mapped_status' => $newLocalStatus?->value,
        ];

        if ($newLocalStatus !== null && $newLocalStatus !== $lead->status) {
            $lead->status = $newLocalStatus;
            $col = $this->terminalColumnFor($newLocalStatus);
            $lead->{$col} = $newLocalStatus->isTerminal() ? now() : $lead->{$col};
            $lead->save();

            $this->emit($lead, LeadEventKind::StatusChanged, $eventPayload);

            if ($newLocalStatus === LeadStatus::Won) {
                $this->maybeAutoScaffold($lead);
            }
        } else {
            $this->emit($lead, LeadEventKind::Mirrored, $eventPayload);
        }

        return true;
    }

    /**
     * Phase 3 hook — if the workspace has a ProjectTemplate matching the
     * lead's service_tag with `auto_scaffold = true`, dispatch a
     * ScaffoldProjectJob. A template with no service_tag set acts as the
     * workspace-wide default if no exact-tag match exists.
     */
    private function maybeAutoScaffold(Lead $lead): void
    {
        $template = ProjectTemplate::withoutGlobalScope(WorkspaceScope::class)
            ->where('workspace_id', $lead->workspace_id)
            ->where('auto_scaffold', true)
            ->where(function ($q) use ($lead) {
                $q->where('service_tag', $lead->service_tag)
                    ->orWhereNull('service_tag');
            })
            ->orderByRaw('CASE WHEN service_tag = ? THEN 0 ELSE 1 END', [$lead->service_tag])
            ->first();

        if (! $template) {
            return;
        }

        ScaffoldProjectJob::dispatch(
            workspaceId: $lead->workspace_id,
            templateId: $template->id,
            leadId: $lead->id,
            trigger: 'lead.status=won',
        );
    }

    /**
     * @param  array{name: string|null, color: string|null, type: string|null}  $newStatus
     */
    private function mirrorMilestone(string $taskId, array $newStatus): bool
    {
        $milestone = ClientMilestone::withoutGlobalScope(WorkspaceScope::class)
            ->where('workspace_id', $this->workspaceId)
            ->where('clickup_task_id', $taskId)
            ->first();

        if (! $milestone) {
            return false;
        }

        $isClosed = $newStatus['type'] === 'closed';

        $milestone->forceFill([
            'status_name' => $newStatus['name'] ?? $milestone->status_name,
            'status_color' => $newStatus['color'] ?? $milestone->status_color,
            'status_type' => $newStatus['type'] ?? $milestone->status_type,
            'is_complete' => $isClosed,
            'completed_at' => $isClosed ? ($milestone->completed_at ?? now()) : null,
            'last_seen_at' => now(),
        ])->save();

        return true;
    }

    /**
     * @return array{name: string|null, color: string|null, type: string|null}
     */
    private function extractStatus(array $payload): array
    {
        foreach ((array) ($payload['history_items'] ?? []) as $item) {
            if (($item['field'] ?? null) === 'status') {
                $after = $item['after'] ?? [];

                return [
                    'name' => is_string($after['status'] ?? null) && $after['status'] !== '' ? $after['status'] : null,
                    'color' => is_string($after['color'] ?? null) ? $after['color'] : null,
                    'type' => is_string($after['type'] ?? null) ? $after['type'] : null,
                ];
            }
        }

        $envelopeStatus = $payload['task']['status'] ?? [];

        return [
            'name' => is_string($envelopeStatus['status'] ?? null) ? $envelopeStatus['status'] : null,
            'color' => is_string($envelopeStatus['color'] ?? null) ? $envelopeStatus['color'] : null,
            'type' => is_string($envelopeStatus['type'] ?? null) ? $envelopeStatus['type'] : null,
        ];
    }

    private function mapToLocalStatus(string $clickupName, array $mapping): ?LeadStatus
    {
        foreach ($mapping as $clickupLabel => $localStatus) {
            if (strcasecmp((string) $clickupLabel, $clickupName) === 0) {
                return LeadStatus::tryFrom((string) $localStatus);
            }
        }

        return null;
    }

    private function terminalColumnFor(LeadStatus $status): string
    {
        return match ($status) {
            LeadStatus::Won => 'won_at',
            LeadStatus::Lost => 'lost_at',
            LeadStatus::Dismissed => 'dismissed_at',
            default => 'updated_at',
        };
    }

    private function emit(Lead $lead, LeadEventKind $kind, array $payload): void
    {
        LeadEvent::create([
            'workspace_id' => $lead->workspace_id,
            'lead_id' => $lead->id,
            'kind' => $kind,
            'payload' => $payload,
            'created_at' => now(),
        ]);
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit