$cap]); } switch ($action) { case 'create': return self::actionCreate($params, $kind); case 'update': return self::actionUpdate($params, $kind); case 'delete': return self::actionDelete($params, $kind); case 'merge': return self::actionMerge($params, $kind); } return MCPHelper::error('invalid_param', __('Unhandled action', 'fluent-crm')); } private static function actionCreate($params, $kind) { $title = trim((string) ($params['title'] ?? '')); if ($title === '') { return MCPHelper::error('invalid_param', __('title is required for create', 'fluent-crm')); } $slug = isset($params['slug']) && $params['slug'] !== '' ? sanitize_title((string) $params['slug']) : sanitize_title($title); $description = sanitize_textarea_field((string) ($params['description'] ?? '')); $existing = self::lookupByTitleOrSlug($title, $slug, $kind); if ($existing) { return MCPHelper::error('contact_exists', sprintf( /* translators: 1: kind (tag/list), 2: matched id */ __('A %1$s with that title or slug already exists (id %2$d). Use update or merge to change it.', 'fluent-crm'), $kind, (int) $existing->id ), ['existing_id' => (int) $existing->id]); } $modelClass = $kind === 'list' ? Lists::class : Tag::class; $row = $modelClass::create([ 'title' => sanitize_text_field($title), 'slug' => $slug, 'description' => $description, ]); do_action(self::createdHook($kind), $row); return [ 'ok' => true, 'action' => 'create', 'kind' => $kind, $kind => self::format($row), 'note' => sprintf( /* translators: 1: kind, 2: title */ __('%1$s "%2$s" created. No subscribers are attached yet.', 'fluent-crm'), ucfirst($kind), $row->title ), ]; } private static function actionUpdate($params, $kind) { $id = (int) ($params[$kind . '_id'] ?? 0); $row = $id ? self::find($id, $kind) : null; if (!$row) { return MCPHelper::error('not_found', sprintf(__('%s not found', 'fluent-crm'), ucfirst($kind)), [$kind . '_id' => $id]); } $changes = []; if (isset($params['title']) && $params['title'] !== '' && $params['title'] !== $row->title) { $changes['title'] = ['from' => $row->title, 'to' => sanitize_text_field((string) $params['title'])]; $row->title = $changes['title']['to']; } if (isset($params['slug']) && $params['slug'] !== '') { $newSlug = sanitize_title((string) $params['slug']); if ($newSlug !== $row->slug) { $changes['slug'] = ['from' => $row->slug, 'to' => $newSlug]; $row->slug = $newSlug; } } if (array_key_exists('description', $params)) { $newDesc = sanitize_textarea_field((string) $params['description']); if ($newDesc !== $row->description) { $changes['description'] = ['from' => $row->description, 'to' => $newDesc]; $row->description = $newDesc; } } if (empty($changes)) { return [ 'ok' => true, 'action' => 'update', 'kind' => $kind, $kind => self::format($row), 'note' => __('No changes — provided fields matched the current values.', 'fluent-crm'), ]; } $row->save(); return [ 'ok' => true, 'action' => 'update', 'kind' => $kind, $kind => self::format($row), 'changes' => $changes, ]; } private static function actionDelete($params, $kind) { $id = (int) ($params[$kind . '_id'] ?? 0); $row = $id ? self::find($id, $kind) : null; if (!$row) { return MCPHelper::error('not_found', sprintf(__('%s not found', 'fluent-crm'), ucfirst($kind)), [$kind . '_id' => $id]); } $force = !empty($params['force']); $attachedCount = self::attachedSubscriberCount($row, $kind); if ($attachedCount > 0 && !$force) { return MCPHelper::error('not_supported', sprintf( /* translators: 1: kind, 2: count */ __('%1$s has %2$d subscribers attached. Pass force=true to delete anyway, or merge into another %1$s first.', 'fluent-crm'), ucfirst($kind), $attachedCount ), [ 'attached_subscribers' => $attachedCount, 'force_required' => true, ]); } $deletedId = (int) $row->id; $deletedTitle = (string) $row->title; $row->delete(); do_action(self::deletedHook($kind), $deletedId); return [ 'ok' => true, 'action' => 'delete', 'kind' => $kind, 'deleted_id' => $deletedId, 'deleted_title' => $deletedTitle, 'detached_subscribers' => $attachedCount, 'note' => $attachedCount > 0 ? __('Deleted with subscribers attached — pivot rows are orphaned and cleaned up by the cleanup hook.', 'fluent-crm') : __('Deleted. No subscribers were attached.', 'fluent-crm'), ]; } private static function actionMerge($params, $kind) { $fromIds = isset($params['from_' . $kind . '_ids']) ? (array) $params['from_' . $kind . '_ids'] : []; $fromIds = array_values(array_unique(array_filter(array_map('intval', $fromIds)))); $toId = (int) ($params['to_' . $kind . '_id'] ?? 0); if (!$toId) { return MCPHelper::error('invalid_param', __('to_*_id is required for merge', 'fluent-crm')); } if (!$fromIds) { return MCPHelper::error('invalid_param', __('from_*_ids must be a non-empty array of ids', 'fluent-crm')); } if (in_array($toId, $fromIds, true)) { return MCPHelper::error('invalid_param', __('to_*_id cannot also be in from_*_ids', 'fluent-crm')); } $to = self::find($toId, $kind); if (!$to) { return MCPHelper::error('not_found', sprintf(__('Target %s not found', 'fluent-crm'), $kind), ['to_id' => $toId]); } $modelClass = $kind === 'list' ? Lists::class : Tag::class; $fromRows = $modelClass::whereIn('id', $fromIds)->get(); $foundFromIds = $fromRows->pluck('id')->map('intval')->toArray(); $missingFromIds = array_values(array_diff($fromIds, $foundFromIds)); // Re-pivot subscribers attached to the `from` set onto the `to` target. $attachedCount = 0; foreach ($fromRows as $fromRow) { $count = self::attachedSubscriberCount($fromRow, $kind); $attachedCount += $count; } $repivoted = 0; foreach ($fromRows as $fromRow) { $subscribers = self::attachedSubscribers($fromRow, $kind); foreach ($subscribers as $sub) { if ($kind === 'list') { $sub->attachLists([$to->id]); $sub->detachLists([$fromRow->id]); } else { $sub->attachTags([$to->id]); $sub->detachTags([$fromRow->id]); } $repivoted++; } } // Delete the source rows. foreach ($fromRows as $fromRow) { $fromId = (int) $fromRow->id; $fromRow->delete(); do_action(self::deletedHook($kind), $fromId); } return [ 'ok' => true, 'action' => 'merge', 'kind' => $kind, 'merged_from' => $foundFromIds, 'merged_into' => self::format($to), 'subscribers_repivoted' => $repivoted, 'subscribers_seen' => $attachedCount, 'missing_from_ids' => $missingFromIds, 'note' => __('Each subscriber attached to a "from" target is now attached to "to" and the "from" rows are deleted. Re-running this merge with the same ids is a safe no-op.', 'fluent-crm'), ]; } // ----------------------------------------------------------------- // helpers // ----------------------------------------------------------------- private static function find($id, $kind) { return $kind === 'list' ? Lists::find($id) : Tag::find($id); } private static function lookupByTitleOrSlug($title, $slug, $kind) { $modelClass = $kind === 'list' ? Lists::class : Tag::class; return $modelClass::where('title', $title)->orWhere('slug', $slug)->first(); } private static function format($row) { return [ 'id' => (int) $row->id, 'title' => $row->title, 'slug' => $row->slug, 'description' => $row->description ?? '', ]; } private static function attachedSubscriberCount($row, $kind) { return (int) ($kind === 'list' ? $row->subscribers()->count() : $row->subscribers()->count()); } private static function attachedSubscribers($row, $kind) { return $kind === 'list' ? $row->subscribers()->get() : $row->subscribers()->get(); } private static function createdHook($kind) { return $kind === 'list' ? 'fluent_crm/list_created' : 'fluent_crm/tag_created'; } private static function deletedHook($kind) { return $kind === 'list' ? 'fluent_crm/list_deleted' : 'fluent_crm/tag_deleted'; } }