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/be.naniguide.com/app/Http/Controllers/Refactor/Admin/PlanRemoteMappingController.php
<?php

namespace App\Http\Controllers\Refactor\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Model\Plan;
use App\Model\PlanRemoteMapping;
use App\Model\PaymentGateway;

class PlanRemoteMappingController extends Controller
{
    /**
     * Remote plan mapping page — grid of local plans x remote gateways.
     */
    public function index(Request $request)
    {
        if (\Gate::denies('read', Plan::newDefaultPlan())) {
            return $this->notAuthorized();
        }

        $subscriptionService = app(\App\Services\Subscription\SubscriptionManagementService::class);
        $remoteGateways = $subscriptionService->getRemoteGateways();
        $localPlans = Plan::active()->orderBy('name')->get();

        // Preload existing mappings keyed by plan_id => gateway_id => mapping
        $gatewayIds = array_map(fn($g) => $g->id, $remoteGateways);
        $allMappings = PlanRemoteMapping::with(['plan', 'paymentGateway'])
            ->whereIn('payment_gateway_id', $gatewayIds)
            ->get();

        $mappingIndex = [];
        foreach ($allMappings as $m) {
            $mappingIndex[$m->plan_id][$m->payment_gateway_id] = $m;
        }

        $stats = [
            'total_mappings' => $allMappings->count(),
            'gateways' => count($remoteGateways),
            'plans' => $localPlans->count(),
        ];

        return view('refactor.pages.admin.remote-plan-mappings.index', [
            'remoteGateways' => $remoteGateways,
            'localPlans' => $localPlans,
            'mappingIndex' => $mappingIndex,
            'stats' => $stats,
        ]);
    }

    /**
     * AJAX: list remote plans for a gateway.
     */
    public function remotePlans(Request $request)
    {
        if (\Gate::denies('read', Plan::newDefaultPlan())) {
            return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
        }

        $gateway = PaymentGateway::findOrFail($request->gateway_id);
        $subscriptionService = app(\App\Services\Subscription\SubscriptionManagementService::class);

        try {
            $remotePlans = $subscriptionService->fetchRemotePlans($gateway);
        } catch (\Throwable $e) {
            return response()->json([
                'status' => 'error',
                'message' => $e->getMessage(),
            ], 400);
        }

        $localPlans = Plan::active()->get();
        $existingMappings = PlanRemoteMapping::where('payment_gateway_id', $gateway->id)
            ->get()
            ->keyBy('remote_plan_id');

        $items = [];
        foreach ($remotePlans as $rp) {
            $mapping = $existingMappings->get($rp->id);
            $mismatches = $mapping ? $mapping->getMismatches() : [];

            $items[] = [
                'remote_plan_id' => $rp->id,
                'remote_plan_name' => $rp->name,
                'remote_price' => $rp->price,
                'remote_currency' => $rp->currency,
                'remote_interval_count' => $rp->intervalCount,
                'remote_interval_unit' => $rp->intervalUnit,
                'remote_interval' => $rp->intervalCount . ' ' . $rp->intervalUnit,
                'remote_trial_days' => $rp->trialDays,
                'remote_status' => $rp->status,
                'mapped_plan_id' => $mapping?->plan_id,
                'mapped_plan_name' => $mapping?->plan?->name,
                'mapping_uid' => $mapping?->uid,
                'has_mismatches' => !empty($mismatches),
                'mismatches' => $mismatches,
            ];
        }

        return response()->json([
            'status' => 'success',
            'items' => $items,
            'local_plans' => $localPlans->map(fn($p) => [
                'id' => $p->id,
                'name' => $p->name,
                'price' => $p->price,
                'currency' => $p->currency?->code,
                'frequency_amount' => $p->frequency_amount,
                'frequency_unit' => $p->frequency_unit,
                'trial_amount' => $p->trial_amount,
                'trial_unit' => $p->trial_unit,
            ]),
            'total' => count($remotePlans),
            'gateway' => [
                'id' => $gateway->id,
                'uid' => $gateway->uid,
                'name' => $gateway->name,
                'type' => $gateway->type,
            ],
        ]);
    }

    /**
     * Store or update a plan mapping.
     */
    public function store(Request $request)
    {
        if (\Gate::denies('read', Plan::newDefaultPlan())) {
            return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
        }

        $request->validate([
            'gateway_id' => 'required|exists:payment_gateways,id',
            'remote_plan_id' => 'required|string',
            'plan_id' => 'required|exists:plans,id',
        ]);

        // Reject mapping free plans to remote gateways — free plans bypass checkout
        // so a remote subscription would never be created at the provider
        $plan = Plan::findOrFail($request->plan_id);
        if ($plan->isFree()) {
            return response()->json([
                'status' => 'error',
                'message' => trans('refactor/admin_remote_plan_mappings.flash.free_plan_cannot_map'),
            ], 422);
        }

        $gateway = PaymentGateway::findOrFail($request->gateway_id);

        try {
            $result = app(\App\Services\Subscription\SubscriptionManagementService::class)
                ->savePlanRemoteMapping($gateway, $plan, $request->remote_plan_id);
        } catch (\Throwable $e) {
            return response()->json([
                'status' => 'error',
                'message' => trans('refactor/admin_remote_plan_mappings.flash.cannot_fetch_remote', ['error' => $e->getMessage()]),
            ], 400);
        }

        $mapping = $result['mapping'];
        $mismatches = $result['mismatches'];

        $rowData = $this->buildRowResponse($mapping, $mapping->plan, $gateway);

        if (!empty($mismatches)) {
            $mismatchDetails = array_map(fn($m) => $m['message'], $mismatches);

            return response()->json(array_merge($rowData, [
                'status' => 'warning',
                'message' => trans('refactor/admin_remote_plan_mappings.flash.mapping_saved_mismatches', ['details' => implode('; ', $mismatchDetails)]),
            ]));
        }

        return response()->json(array_merge($rowData, [
            'status' => 'success',
            'message' => trans('refactor/admin_remote_plan_mappings.flash.mapping_saved'),
        ]));
    }

    /**
     * Remove a plan mapping.
     */
    public function destroy(Request $request)
    {
        if (\Gate::denies('read', Plan::newDefaultPlan())) {
            return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
        }

        $mapping = PlanRemoteMapping::where('uid', $request->uid)->firstOrFail();
        $result = app(\App\Services\Subscription\SubscriptionManagementService::class)
            ->removePlanRemoteMapping($mapping);

        $rowData = $this->buildRowResponse(null, $result['plan'], $result['gateway']);

        return response()->json(array_merge($rowData, [
            'status' => 'success',
            'message' => trans('refactor/admin_remote_plan_mappings.flash.mapping_removed'),
        ]));
    }

    /**
     * Sync one plan mapping: fetch latest from remote, update local plan to match.
     */
    public function syncOne(Request $request)
    {
        if (\Gate::denies('read', Plan::newDefaultPlan())) {
            return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
        }

        $mapping = PlanRemoteMapping::where('uid', $request->uid)->firstOrFail();
        $mapping->load(['plan', 'plan.currency', 'paymentGateway']);

        $logs = [];

        try {
            $result = app(\App\Services\Subscription\SubscriptionManagementService::class)
                ->syncPlanRemoteMappingToLocalPlan($mapping);
            $mapping = $result['mapping'];
            $remotePlan = $result['remote_plan'];
            $localUpdates = $result['local_updates'];
            $hasError = $result['has_error'];
            $mismatches = $result['mismatches'];
            $siblingMappings = $result['sibling_mappings'];

            $logs[] = ['type' => 'info', 'message' => trans('refactor/admin_remote_plan_mappings.sync.fetched_remote', ['name' => $remotePlan->name, 'price' => $remotePlan->price, 'currency' => $remotePlan->currency, 'interval' => $remotePlan->intervalUnit])];
        } catch (\Throwable $e) {
            $logs[] = ['type' => 'error', 'message' => trans('refactor/admin_remote_plan_mappings.sync.failed_fetch', ['id' => $mapping->remote_plan_id, 'error' => $e->getMessage()])];
            return response()->json([
                'status' => 'error',
                'message' => trans('refactor/admin_remote_plan_mappings.sync.sync_failed_fetch'),
                'logs' => $logs,
            ]);
        }

        $logs[] = ['type' => 'info', 'message' => trans('refactor/admin_remote_plan_mappings.sync.updated_mapping')];

        if ($hasError && $result['missing_currency']) {
            $logs[] = ['type' => 'error', 'message' => trans('refactor/admin_remote_plan_mappings.sync.currency_not_found', ['currency' => $result['missing_currency']])];
        }

        if (!empty($localUpdates)) {
            if (!$hasError) {
                foreach ($localUpdates as $upd) {
                    $logs[] = ['type' => 'success', 'message' => trans('refactor/admin_remote_plan_mappings.sync.plan_updated', ['detail' => $upd])];
                }
            } else {
                foreach ($localUpdates as $upd) {
                    $logs[] = ['type' => 'warning', 'message' => trans('refactor/admin_remote_plan_mappings.sync.skipped_currency_error', ['detail' => $upd])];
                }
            }
        } else {
            $logs[] = ['type' => 'success', 'message' => trans('refactor/admin_remote_plan_mappings.sync.already_in_sync')];
        }

        foreach ($mismatches as $m) {
            $logs[] = ['type' => 'warning', 'message' => trans('refactor/admin_remote_plan_mappings.sync.remaining_mismatch', ['message' => $m['message']])];
        }

        $rowData = $this->buildRowResponse($mapping, $mapping->plan, $mapping->paymentGateway);

        $siblingCells = [];
        if (!empty($localUpdates) && !$hasError) {
            foreach ($siblingMappings as $sibling) {
                $siblingCells[] = $this->buildRowResponse($sibling, $sibling->plan, $sibling->paymentGateway);
            }
        }

        return response()->json(array_merge($rowData, [
            'status' => $hasError ? 'error' : (empty($mismatches) ? 'success' : 'warning'),
            'message' => $hasError
                ? trans('refactor/admin_remote_plan_mappings.sync.sync_errors')
                : (empty($localUpdates) ? trans('refactor/admin_remote_plan_mappings.sync.plan_in_sync') : trans('refactor/admin_remote_plan_mappings.sync.plan_updated_from_remote')),
            'logs' => $logs,
            'sibling_cells' => $siblingCells,
        ]));
    }

    /**
     * Sync all plan mappings for a gateway.
     */
    public function syncAll(Request $request)
    {
        if (\Gate::denies('read', Plan::newDefaultPlan())) {
            return response()->json(['status' => 'error', 'message' => 'Not authorized'], 403);
        }

        $gateway = PaymentGateway::findOrFail($request->gateway_id);
        $result = app(\App\Services\Subscription\SubscriptionManagementService::class)
            ->syncAllPlanRemoteMappingsToLocalPlans($gateway);
        $mappings = $result['mappings'];

        if ($mappings->isEmpty()) {
            return response()->json([
                'status' => 'info',
                'message' => trans('refactor/admin_remote_plan_mappings.sync.no_mappings'),
                'logs' => [['type' => 'info', 'message' => trans('refactor/admin_remote_plan_mappings.sync.nothing_to_sync')]],
            ]);
        }

        $allLogs = [];
        foreach ($result['results'] as $syncResult) {
            $mapping = $syncResult['mapping'];
            $allLogs[] = ['type' => 'info', 'message' => "--- Syncing: {$mapping->plan->name} ↔ {$mapping->remote_plan_name} ---"];

            if (!empty($syncResult['error'])) {
                $allLogs[] = ['type' => 'error', 'message' => trans('refactor/admin_remote_plan_mappings.sync.failed_fetch', ['id' => $mapping->remote_plan_id, 'error' => $syncResult['error']])];
                continue;
            }

            $remotePlan = $syncResult['remote_plan'];
            $localUpdates = $syncResult['local_updates'];
            $planError = $syncResult['has_error'];

            $allLogs[] = ['type' => 'info', 'message' => trans('refactor/admin_remote_plan_mappings.sync.fetched_remote', ['name' => $remotePlan->name, 'price' => $remotePlan->price, 'currency' => $remotePlan->currency, 'interval' => $remotePlan->intervalUnit])];

            if ($planError && $syncResult['missing_currency']) {
                $allLogs[] = ['type' => 'error', 'message' => trans('refactor/admin_remote_plan_mappings.sync.currency_not_found', ['currency' => $syncResult['missing_currency']])];
            }

            if (!empty($localUpdates) && !$planError) {
                foreach ($localUpdates as $upd) {
                    $allLogs[] = ['type' => 'success', 'message' => $upd];
                }
            } elseif ($planError) {
                foreach ($localUpdates as $upd) {
                    $allLogs[] = ['type' => 'warning', 'message' => trans('refactor/admin_remote_plan_mappings.sync.skipped', ['detail' => $upd])];
                }
            } else {
                $allLogs[] = ['type' => 'success', 'message' => trans('refactor/admin_remote_plan_mappings.sync.already_in_sync')];
            }
        }

        $inSync = $result['in_sync'];
        $totalUpdated = $result['updated'];
        $totalErrors = $result['errors'];
        $msg = trans('refactor/admin_remote_plan_mappings.sync.synced_gateway', [
            'gateway' => $gateway->name,
            'in_sync' => $inSync,
            'updated' => $totalUpdated,
            'errors' => $totalErrors,
        ]);

        // Build row HTML for each mapping so UI can refresh inline
        $cells = [];
        foreach ($mappings as $mapping) {
            $mapping->load(['plan', 'plan.currency', 'paymentGateway']);
            $rowData = $this->buildRowResponse($mapping, $mapping->plan, $mapping->paymentGateway);
            $cells[] = $rowData;
        }

        return response()->json([
            'status' => $totalErrors > 0 ? 'error' : 'success',
            'message' => $msg,
            'logs' => $allLogs,
            'cells' => $cells,
            'total' => $mappings->count(),
            'updated' => $totalUpdated,
            'errors' => $totalErrors,
            'in_sync' => $inSync,
        ]);
    }

    /**
     * Render the mapping cell partial as HTML string.
     */
    protected function renderCellHtml(?PlanRemoteMapping $mapping, $plan, $gateway): string
    {
        return view('refactor.pages.admin.remote-plan-mappings._cell', [
            'mapping' => $mapping,
            'plan' => $plan,
            'gateway' => $gateway,
        ])->render();
    }

    /**
     * Render the status badge HTML for a mapping row.
     */
    protected function renderStatusHtml(?PlanRemoteMapping $mapping): string
    {
        if (!$mapping) {
            return '<span class="mc-badge mc-badge-default">Unmapped</span>';
        }

        $mismatches = $mapping->getMismatches();
        if (empty($mismatches)) {
            return '<span class="mc-badge mc-badge-green mc-badge-dot">' . e(trans('refactor/admin_remote_plan_mappings.cell.in_sync')) . '</span>';
        }

        return '<span class="mc-badge mc-badge-orange mc-badge-dot">' . count($mismatches) . ' mismatch</span>';
    }

    /**
     * Render the actions column HTML for a mapping row.
     */
    protected function renderActionsHtml(?PlanRemoteMapping $mapping): string
    {
        if (!$mapping) {
            return '';
        }

        $syncIcon = view('refactor.components.icons.mc-icon', ['icon' => 'refresh', 'size' => 14])->render();
        $xIcon = view('refactor.components.icons.mc-icon', ['icon' => 'x', 'size' => 14])->render();
        $syncLabel = e(trans('refactor/admin_remote_plan_mappings.buttons.sync'));

        return '<div style="display:flex;gap:var(--space-1);justify-content:flex-end;">'
            . '<button class="mc-btn mc-btn-ghost mc-btn-xs rpm-sync-one-btn" data-uid="' . e($mapping->uid) . '">' . $syncIcon . ' ' . $syncLabel . '</button>'
            . '<button class="mc-btn mc-btn-ghost mc-btn-xs rpm-unmap-btn" data-uid="' . e($mapping->uid) . '" style="color:var(--color-error);">' . $xIcon . '</button>'
            . '</div>';
    }

    /**
     * Build full row response data (cell + status + actions).
     */
    protected function buildRowResponse(?PlanRemoteMapping $mapping, $plan, $gateway): array
    {
        return [
            'cell_html' => $this->renderCellHtml($mapping, $plan, $gateway),
            'status_html' => $this->renderStatusHtml($mapping),
            'actions_html' => $this->renderActionsHtml($mapping),
            'plan_html' => $this->renderPlanLabel($plan),
            'plan_id' => $plan->id,
            'gateway_id' => $gateway->id,
        ];
    }

    /**
     * Render the local plan label (name + price) as HTML string.
     */
    protected function renderPlanLabel($plan): string
    {
        $price = \App\Library\Tool::format_price($plan->price, $plan->currency->format);
        $freq = $plan->displayFrequencyTime();
        return '<div style="font-weight:var(--weight-semibold);font-size:var(--text-base);">' . e($plan->name) . '</div>'
            . '<div style="font-size:var(--text-sm);color:var(--color-text-muted);margin-top:2px;">' . $price . ' / ' . $freq . '</div>';
    }
}