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/SubscriptionController.php
<?php

namespace App\Http\Controllers\Refactor\Admin;

use App\Dto\Refactor\Subscription\SubscriptionListFilterDto;
use App\Http\Controllers\Controller;
use App\Http\Requests\Refactor\Admin\Subscription\CreateSubscriptionRequest;
use App\Http\Requests\Refactor\Admin\Subscription\DeleteSubscriptionsRequest;
use App\Http\Requests\Refactor\Admin\Subscription\ListSubscriptionsRequest;
use App\Http\Requests\Refactor\Admin\Subscription\RejectPendingRequest;
use App\Http\Requests\Refactor\Admin\Subscription\SetCreditRequest;
use App\Model\Customer;
use App\Model\Plan;
use App\Model\Subscription;
use App\Services\Admin\SubscriptionService;
use App\Services\Plans\Credits\CreditKey;
use App\Services\Plans\Credits\CreditsService;
use App\Services\Subscription\SubscriptionManagementService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

/**
 * Thin controller — orchestration only.
 *
 * Layering: FormRequest (auth + validate) → DTO (typed transport)
 *         → SubscriptionService (read aggregator)
 *         → SubscriptionManagementService (write side: approve / terminate / sync / credits)
 *         → view / JSON.
 */
class SubscriptionController extends Controller
{
    public function index(Request $request, SubscriptionService $service)
    {
        $admin = $request->user()->admin;

        return view('refactor.pages.admin.subscriptions.index', [
            'plans'     => Plan::where('status', 'active')->get(),
            'lifecycle' => $service->lifecycleCounts($admin),
            'attention' => $service->attention($admin),
            'stats'     => $service->dashboardStats($admin),
            'sidebar'   => $service->sidebarStats($admin),
        ]);
    }

    public function listing(ListSubscriptionsRequest $request, SubscriptionService $service)
    {
        $admin  = $request->user()->admin;
        $filter = SubscriptionListFilterDto::fromRequest($request);

        return view('refactor.pages.admin.subscriptions._list', [
            'subscriptions' => $service->list($filter, $admin),
            'lifecycle'     => $filter->lifecycle,
        ]);
    }

    public function approve(Request $request, $id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$request->user()->admin->can('approve', $sub)) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        try {
            $svc->approvePendingSubscription($sub);
        } catch (\Throwable $e) {
            Log::warning('Subscription approve failed', ['uid' => $id, 'error' => $e->getMessage()]);
            return response()->json(['message' => $e->getMessage()], 400);
        }

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.approved')]);
    }

    public function terminate(Request $request, $id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$request->user()->admin->can('terminate', $sub)) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        try {
            $svc->terminate($sub);
        } catch (\Throwable $e) {
            Log::warning('Subscription terminate failed', ['uid' => $id, 'error' => $e->getMessage()]);
            return response()->json(['message' => $e->getMessage()], 400);
        }

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.terminated')]);
    }

    public function disableRecurring(Request $request, $id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$request->user()->admin->can('disableRecurring', $sub)) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        $svc->disableRecurring($sub);

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.recurring_disabled')]);
    }

    public function enableRecurring(Request $request, $id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$request->user()->admin->can('enableRecurring', $sub)) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        $svc->enableRecurring($sub);

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.recurring_enabled')]);
    }

    public function delete(DeleteSubscriptionsRequest $request, SubscriptionManagementService $svc)
    {
        $admin = $request->user()->admin;

        foreach ($request->uids() as $uid) {
            $sub = Subscription::findByUid($uid);
            if ($sub && $admin->can('delete', $sub)) {
                $svc->deleteSubscription($sub);
            }
        }

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.deleted')]);
    }

    public function setCredit(SetCreditRequest $request, string $id, string $creditKey, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$request->user()->admin->can('replenishSendingCredits', $sub)) {
            return response()->json(['error' => 'Unauthorized'], 403);
        }

        $key = CreditKey::tryFrom($creditKey);
        if ($key === null) {
            return response()->json(['error' => 'Unknown credit key'], 422);
        }

        try {
            $svc->setCreditRemaining(
                $sub,
                $key,
                (int) $request->input('remaining'),
                $request->user()->id,
                $request->input('note'),
            );
        } catch (\Throwable $e) {
            Log::warning('Subscription setCredit failed', ['uid' => $id, 'key' => $creditKey, 'error' => $e->getMessage()]);
            return response()->json(['error' => $e->getMessage()], 422);
        }

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.credits_updated')]);
    }

    public function refreshCredits(string $id, CreditsService $credits)
    {
        $sub = $this->findOr404($id);

        // DB-snapshot refresh — see docs/cache "DB-snapshot cache" section. Mirror
        // of `AccountController::refreshCredits` (customer side). Sync because the
        // work is trivial (1 file read + a few row UPDATEs); returns the fresh
        // values inline so the JS can update credit spans in place.
        return response()->json([
            'credits'        => $credits->syncSnapshotFromTracker($sub),
            'last_synced_at' => now()->toIso8601String(),
        ]);
    }

    /**
     * Credits Debug page action: heal presence drift for ONE credit on this
     * subscription immediately (without waiting for the next billing cycle).
     * Single endpoint; the action (insert vs delete) is derived from the
     * current plan_credits / subscription_credit_state state, so double-clicks
     * and concurrent submissions resolve to a clean no-op rather than corrupt.
     */
    public function syncCreditFromPlan(Request $request, string $id, string $creditKey, CreditsService $credits)
    {
        $sub = $this->findOr404($id);
        // Reuse the same admin policy that gates "Set Credits" on the show page —
        // both actions modify per-sub credit state, identical authority required.
        if (!$request->user()->admin->can('replenishSendingCredits', $sub)) {
            return redirect()->route('refactor.admin.subscriptions.credits-debug', $sub->uid)
                ->with('error', trans('messages.not_authorized'));
        }

        $key = CreditKey::tryFrom($creditKey);
        if ($key === null) {
            return redirect()->route('refactor.admin.subscriptions.credits-debug', $sub->uid)
                ->with('error', trans('refactor/admin_subscriptions.credits_debug.sync.unknown_key'));
        }

        try {
            $result = $credits->syncOneCreditFromPlan($sub, $key);
        } catch (\Throwable $e) {
            Log::warning('Subscription credits-debug sync failed', [
                'uid' => $sub->uid, 'key' => $creditKey, 'error' => $e->getMessage(),
            ]);
            return redirect()->route('refactor.admin.subscriptions.credits-debug', $sub->uid)
                ->with('error', $e->getMessage());
        }

        $flashKey = match ($result['action']) {
            'inserted' => 'refactor/admin_subscriptions.credits_debug.sync.flash_inserted',
            'deleted'  => 'refactor/admin_subscriptions.credits_debug.sync.flash_deleted',
            default    => 'refactor/admin_subscriptions.credits_debug.sync.flash_noop',
        };

        // Audit trail — mirror Set-Credits pattern so admin overrides are searchable in one place.
        try {
            \App\Library\Facades\SubscriptionFacade::log(
                $sub,
                \App\ActivityLog\Activities\SubscriptionEvent::TYPE_CREDIT_MODIFIED,
                null,
                [
                    'credit_key' => $key->value,
                    'action'     => $result['action'],
                    'plan_value' => $result['plan_value'],
                    'note'       => 'manual sync from credits-debug page',
                ],
                $request->user()->id,
            );
        } catch (\Throwable $e) {
            Log::warning('credits-debug sync audit log failed (non-fatal)', ['error' => $e->getMessage()]);
        }

        return redirect()
            ->route('refactor.admin.subscriptions.credits-debug', $sub->uid)
            ->with('success', trans($flashKey, ['key' => $key->label()]));
    }

    /**
     * GET — render the create-subscription popup form. Plain Request (NOT a
     * FormRequest) so the validator doesn't fire on form-render; submission
     * goes to the sibling store() action.
     */
    public function create()
    {
        return view('refactor.pages.admin.subscriptions.create', [
            'plans' => Plan::where('status', 'active')->get(),
        ]);
    }

    /**
     * POST — process the create-subscription submission. Mirrors the customer
     * self-subscribe flow in Refactor\SubscriptionController::assignPlan so the
     * admin path enforces the same invariants:
     *
     *   1. Customer + plan must exist.
     *   2. Customer must NOT have an active subscription — assigning a plan on
     *      top of an existing one would fork billing state. The Customer
     *      select2 already filters these out (whereDoesntHave 'subscriptions'
     *      newOrActive), but check defensively here in case a stale dropdown
     *      submits.
     *   3. Use SubscriptionManagementService::replacePendingPlan (NOT raw
     *      assignPlan) — it cleans up any abandoned new/pending sub before
     *      assigning, matching what a customer hitting their /select-plan
     *      route would experience.
     *
     * Side-effect parity with the customer flow:
     *   - Lifecycle::onSubscribe primes credits (cold snapshot + hot tracker).
     *   - An init Order + Invoice are created via createNewSubscriptionOrder,
     *     awaiting payment. Sub status = 'new' until invoice settles.
     *   - The popup just returns success; admin can then navigate to the
     *     subscription show page to walk the customer through payment (or
     *     mark the invoice paid via the invoice approve action for offline).
     */
    public function store(CreateSubscriptionRequest $request, SubscriptionManagementService $svc)
    {
        $customer = Customer::findByUid($request->input('customer_uid'));
        $plan     = Plan::findByUid($request->input('plan_uid'));

        if (!$customer || !$plan) {
            return response()->json([
                'message' => trans('refactor/admin_subscriptions.create.errors.not_found'),
            ], 422);
        }

        if ($customer->getCurrentActiveSubscription()) {
            return response()->json([
                'message' => trans('refactor/admin_subscriptions.create.errors.has_active_sub'),
                'errors'  => ['customer_uid' => [trans('refactor/admin_subscriptions.create.errors.has_active_sub')]],
            ], 422);
        }

        try {
            $svc->replacePendingPlan($customer, $plan);
        } catch (\Throwable $e) {
            Log::warning('Admin assign plan failed', [
                'customer_uid' => $customer->uid,
                'plan_uid'     => $plan->uid,
                'error'        => $e->getMessage(),
            ]);
            return response()->json(['message' => $e->getMessage()], 422);
        }

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.created')]);
    }

    public function show($id)
    {
        $sub      = $this->findOr404($id);
        $customer = $sub->customer()->first();
        $plan     = $sub->plan()->first();

        // Credits (editable — per-sub snapshot, NOT plan live; Plan Versioning §10).
        $creditsService = app(CreditsService::class);
        $subCreditMap   = $creditsService->creditsFromSnapshot($sub);
        $credits = [];
        foreach (CreditKey::cases() as $ck) {
            if (!array_key_exists($ck->value, $subCreditMap)) {
                continue;
            }

            $amountPerCycle = $subCreditMap[$ck->value];
            $remaining      = $creditsService->remainingSnapshot($sub, $ck);
            $used = ($amountPerCycle === null || $remaining === null)
                ? null
                : max(0, (int) $amountPerCycle - (int) $remaining);

            $credits[] = [
                'key'              => $ck->value,
                'label'            => $ck->label(),
                'amount_per_cycle' => $amountPerCycle,
                'remaining'        => $remaining,
                'used'             => $used,
            ];
        }

        $creditsLastSynced = \App\Model\SubscriptionCreditState::where('subscription_id', $sub->id)
            ->max('last_synced_at');
        if ($creditsLastSynced) {
            $creditsLastSynced = \Carbon\Carbon::parse($creditsLastSynced);
        }

        // Quotas (read-only — derived from plan)
        $quotasService = app(\App\Services\Plans\Quotas\QuotasService::class);
        $quotas = [];
        foreach ($quotasService->quotasOfPlan($plan) as $quotaKey => $limitValue) {
            $key = \App\Services\Plans\Quotas\QuotaKey::tryFrom($quotaKey);
            if (!$key) {
                continue;
            }

            $used = null;
            try {
                $used = $quotasService->used($customer, $key);
            } catch (\Throwable $e) {
                Log::debug('Subscription quota used() unavailable', ['key' => $key->value, 'reason' => $e->getMessage()]);
            }

            $quotas[] = [
                'key'         => $key->value,
                'label'       => str_replace('_', ' ', $key->value),
                'limit_value' => $limitValue,
                'used'        => $used,
            ];
        }

        // Entitlements (read-only — grouped)
        $entitlementMap = app(\App\Services\Plans\Entitlements\EntitlementsService::class)
            ->entitlementsOfPlan($plan);
        $entitlements = [];
        foreach (\App\Services\Plans\Entitlements\EntitlementKey::cases() as $ek) {
            $entitlements[] = [
                'key'     => $ek->value,
                'label'   => $ek->label(),
                'group'   => $ek->group(),
                'enabled' => (bool) ($entitlementMap[$ek->value] ?? false),
            ];
        }

        // Rate Limits (read-only)
        $rateLimits = array_map(
            fn ($r) => [
                'key'         => $r->rate_key,
                'limit_value' => (int) $r->limit_value,
                'time_amount' => (int) $r->time_amount,
                'time_unit'   => $r->time_unit,
            ],
            array_values(app(\App\Services\Plans\RateLimits\RateLimitsService::class)->configsOfPlan($plan))
        );

        // Activity timeline (unified activity_logs filtered by subject)
        $activityLogs = \App\Model\ActivityLog::where('subject_type', 'subscription')
            ->where('subject_id', $sub->id)
            ->orderByDesc('created_at')
            ->limit(50)
            ->get();

        return view('refactor.pages.admin.subscriptions.show', [
            'subscription'      => $sub,
            'customer'          => $customer,
            'plan'              => $plan,
            'credits'           => $credits,
            'creditsLastSynced' => $creditsLastSynced,
            'quotas'            => $quotas,
            'entitlements'      => $entitlements,
            'rateLimits'        => $rateLimits,
            'activityLogs'      => $activityLogs,
        ]);
    }

    /**
     * Credits debug — side-by-side view of plan_credits row vs
     * subscription_credit_state snapshot vs live tracker file. Surfaces
     * drift between plan-config and per-sub state caused by mid-cycle plan
     * edits (Plan Versioning §10 — snapshot frozen at onSubscribe/onRenew).
     */
    public function creditsDebug($id)
    {
        $sub  = $this->findOr404($id);
        $plan = $sub->plan()->first();

        $creditsService = app(CreditsService::class);
        $planCredits    = $plan ? $creditsService->creditsOfPlan($plan) : [];      // credit_key => ?int  (null=unlimited; key absent=no row)
        $subGranted     = $creditsService->creditsFromSnapshot($sub);              // credit_key => ?int  (same convention)

        $rows = [];
        foreach (CreditKey::cases() as $key) {
            $planHas = array_key_exists($key->value, $planCredits);
            $subHas  = array_key_exists($key->value, $subGranted);

            $state = \App\Model\SubscriptionCreditState::where('subscription_id', $sub->id)
                ->where('credit_key', $key->value)
                ->first();

            $tracker = $this->peekTrackerLive($sub, $key);

            $rows[] = [
                'key'             => $key->value,
                'label'           => $key->label(),
                'plan_has_row'    => $planHas,
                'plan_value'      => $planHas ? $planCredits[$key->value] : null,
                'sub_has_row'     => $subHas,
                'sub_granted'     => $subHas ? $subGranted[$key->value] : null,
                'sub_remaining'   => $state?->remaining,
                'tracker_exists'  => $tracker['exists'],
                'tracker_value'   => $tracker['value'],
            ];
        }

        return view('refactor.pages.admin.subscriptions.credits_debug', [
            'subscription' => $sub,
            'plan'         => $plan,
            'rows'         => $rows,
            'distributedMode' => (bool) config('custom.distributed_mode'),
        ]);
    }

    /**
     * Read the live tracker without side effects (no createFileIfNotExists).
     * Returns ['exists' => bool|null, 'value' => ?int].
     *   exists=null means distributed mode (we don't peek in-memory state).
     *   value=null means unlimited (when exists=true) or unknown.
     */
    private function peekTrackerLive(Subscription $sub, CreditKey $key): array
    {
        if (config('custom.distributed_mode')) {
            return ['exists' => null, 'value' => null];
        }
        $file = storage_path('app/quota/' . $key->trackerBasename($sub->uid));
        if (!file_exists($file)) {
            return ['exists' => false, 'value' => null];
        }
        $raw = trim((string) @file_get_contents($file));
        if ($raw === '') {
            return ['exists' => true, 'value' => 0];
        }
        $int = (int) $raw;
        return ['exists' => true, 'value' => $int === -1 ? null : $int];
    }

    /** GET — render the reject-pending popup form. */
    public function rejectPendingForm($id)
    {
        $sub = $this->findOr404($id);
        return view('refactor.pages.admin.subscriptions.reject_pending', ['subscription' => $sub]);
    }

    /** POST — process the rejection submission. RejectPendingRequest validates the body. */
    public function rejectPending(RejectPendingRequest $request, $id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);

        $order = $sub->getUnpaidOrder();
        if ($order) {
            try {
                $svc->rejectPendingSubscription($sub, $request->input('reason'));
            } catch (\Throwable $e) {
                Log::warning('Subscription rejectPending failed', ['uid' => $id, 'error' => $e->getMessage()]);
                return response()->json(['error' => $e->getMessage()], 422);
            }
        }

        return response()->json(['message' => trans('refactor/admin_subscriptions.flash.rejected')]);
    }

    public function invoices($id)
    {
        $sub = $this->findOr404($id);

        $orderItems = $sub->orderItems()
            ->with([
                'order.currency',
                'order.invoices.billingCountry',
                'order.invoices.customer',
                'order.invoices.paymentIntents.paymentGateway',
            ])
            ->orderBy('created_at', 'desc')
            ->get();

        $logs = \App\Model\ActivityLog::where('subject_type', 'subscription')
            ->where('subject_id', $sub->id)
            ->orderBy('created_at', 'desc')
            ->limit(50)
            ->get();

        return view('refactor.pages.admin.subscriptions.invoices', [
            'subscription' => $sub,
            'orderItems'   => $orderItems,
            'logs'         => $logs,
        ]);
    }

    public function syncRemote($id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$sub->hasRemoteSubscription()) {
            return response()->json(['error' => 'No remote subscription'], 400);
        }

        $result = $svc->syncRemoteSubscription($sub);

        $warningsStr = '';
        if (!empty($result->warnings)) {
            $warningsStr = ' | Warnings: ' . implode('; ', $result->warnings);
        }

        return response()->json([
            'status'      => $result->hasWarnings() ? 'warning' : 'success',
            'sync_status' => $result->status,
            'changes'     => $result->changes,
            'warnings'    => $result->warnings,
            'error'       => $result->error,
            'message'     => match ($result->status) {
                'in_sync'          => 'Subscription is in sync with remote.' . $warningsStr,
                'drifted'          => 'Subscription was updated from remote: ' . count($result->changes) . ' change(s).' . $warningsStr,
                'mismatch'         => 'Subscription has plan mismatches: ' . implode('; ', $result->warnings),
                'remote_not_found' => 'Remote subscription not found: ' . ($result->error ?? ''),
                default            => 'Sync error: ' . ($result->error ?? 'unknown'),
            },
        ]);
    }

    public function remoteInfo($id, SubscriptionManagementService $svc)
    {
        $sub = $this->findOr404($id);
        if (!$sub->hasRemoteSubscription()) {
            abort(404);
        }

        $meta    = $sub->getRemoteMetadataArray();
        $gateway = $sub->remoteGateway()->first();

        $mapping = ($sub->plan_id && $gateway)
            ? \App\Model\PlanRemoteMapping::where('plan_id', $sub->plan_id)
                ->where('payment_gateway_id', $gateway->id)
                ->first()
            : null;
        $mismatches = $mapping ? $mapping->getMismatches() : [];

        $remoteData  = null;
        $remoteError = null;
        try {
            $remoteData = $svc->fetchRemoteSubscription($sub);
        } catch (\Throwable $e) {
            $remoteError = $e->getMessage();
        }

        $remotePaymentMethod      = null;
        $remotePaymentMethodError = null;
        try {
            $remotePaymentMethod = $svc->fetchRemotePaymentMethod($sub);
        } catch (\Throwable $e) {
            $remotePaymentMethodError = $e->getMessage();
        }

        $logs = \App\Model\ActivityLog::where('subject_type', 'subscription')
            ->where('subject_id', $sub->id)
            ->orderByDesc('created_at')
            ->limit(50)
            ->get();

        // ─── Invoice join ─── local Invoices (linked via Order→OrderItem.subscription_id)
        // FULL OUTER JOIN with remote billing events (vendor's invoices/transactions list).
        // Keys:
        //   - matched   → local + remote both present
        //   - local-only→ row exists locally but vendor doesn't show it (rare; orphan/race)
        //   - remote-only→ vendor billed but sync hasn't materialized yet (or skipped MANUAL/REFUND)
        // The view marks the row whose remote_id == subscription.last_seen_remote_invoice_id with " *".
        $localInvoices = \App\Model\Invoice::whereHas(
            'order.orderItems',
            fn ($q) => $q->where('subscription_id', $sub->id)
        )->orderBy('id')->get();

        $remoteInvoices = [];
        $remoteInvoicesError = null;
        if ($gateway) {
            try {
                $service = \App\Library\Facades\Billing::resolveService($gateway);
                if ($service instanceof \App\Cashier\Contracts\RemoteSubscriptionGatewayInterface
                    && $sub->remote_subscription_id) {
                    $cursor = null;
                    do {
                        $page = $service->getRemoteInvoices($sub->remote_subscription_id, $cursor, 100);
                        foreach ($page['data'] as $dto) {
                            $remoteInvoices[] = $dto;
                            $cursor = $dto->id;
                        }
                    } while (!empty($page['has_more']));
                }
            } catch (\Throwable $e) {
                $remoteInvoicesError = $e->getMessage();
            }
        }

        // Build join keyed by remote id. Local rows with remote_id stamped → match by id.
        // Local rows with NULL remote_id → bucket "LOCAL:<local_id>" (won't collide with vendor ids).
        $invoiceJoin = [];
        foreach ($localInvoices as $inv) {
            $key = $inv->remote_invoice_id ?: ('LOCAL:' . $inv->id);
            $invoiceJoin[$key] = ['local' => $inv, 'remote' => null];
        }
        foreach ($remoteInvoices as $dto) {
            if (isset($invoiceJoin[$dto->id])) {
                $invoiceJoin[$dto->id]['remote'] = $dto;
            } else {
                $invoiceJoin[$dto->id] = ['local' => null, 'remote' => $dto];
            }
        }

        // ─── Local PaymentMethod lookup map ─── keyed by vendor's pm id stored in
        // autobilling_data. Used by the view to render "Visa **3155 (PM #42)" with
        // a link to the local row when the card is still on file, or "(deleted
        // locally)" when the customer removed it but the vendor remembered the
        // historical charge.
        //
        // Each driver knows its own autobilling_data shape — we go through the
        // interface (extractRemotePaymentMethodId) instead of hardcoding key
        // names here, so adding a new gateway plugin doesn't require touching
        // this controller.
        $pmMap = [];
        // $gatewayHasLocalPmConcept stays false if no local PMs exist for this
        // gateway+customer at all. View uses it to distinguish "deleted locally"
        // (gateway DOES track PMs but THIS row's pm is gone) vs "vendor-managed"
        // (Paddle hosted-checkout etc. — never had local PMs).
        $gatewayHasLocalPmConcept = false;
        if ($gateway && $sub->customer) {
            try {
                $svcForPm = \App\Library\Facades\Billing::resolveService($gateway);
            } catch (\Throwable $e) {
                $svcForPm = null;  // gateway type unregistered (plugin disabled)
            }
            if ($svcForPm instanceof \App\Cashier\Contracts\RemoteSubscriptionGatewayInterface) {
                $localPms = \App\Model\PaymentMethod::where('payment_gateway_id', $gateway->id)
                    ->where('customer_id', $sub->customer_id)
                    ->get();
                $gatewayHasLocalPmConcept = $localPms->isNotEmpty();
                foreach ($localPms as $pm) {
                    $remoteId = $svcForPm->extractRemotePaymentMethodId($pm->getAutoBillingData());
                    if ($remoteId) {
                        $pmMap[$remoteId] = $pm;
                    }
                }
            }
        }

        return view('refactor.pages.admin.subscriptions.remote_info', [
            'subscription'             => $sub,
            'meta'                     => $meta,
            'gateway'                  => $gateway,
            'mapping'                  => $mapping,
            'mismatches'               => $mismatches,
            'remoteData'               => $remoteData,
            'remoteError'              => $remoteError,
            'remotePaymentMethod'      => $remotePaymentMethod,
            'remotePaymentMethodError' => $remotePaymentMethodError,
            'logs'                     => $logs,
            'invoiceJoin'              => $invoiceJoin,
            'remoteInvoicesError'      => $remoteInvoicesError,
            'pmMap'                    => $pmMap,
            'gatewayHasLocalPmConcept' => $gatewayHasLocalPmConcept,
        ]);
    }

    private function findOr404(string $uid): Subscription
    {
        $sub = Subscription::findByUid($uid);
        if (!$sub) {
            abort(response()->json(['error' => 'Not found'], 404));
        }

        return $sub;
    }
}