user()?->isHades()) { abort(403, 'Hades tier required for subscription management.'); } } public function updatingSearch(): void { $this->resetPage(); $this->selected = []; $this->selectAll = false; } public function updatedSelectAll(bool $value): void { if ($value) { $this->selected = $this->subscriptions->pluck('id')->map(fn ($id) => (string) $id)->all(); } else { $this->selected = []; } } public function exportSelected(): void { if (empty($this->selected)) { session()->flash('error', __('commerce::commerce.bulk.no_selection')); return; } $subscriptions = Subscription::with(['workspace', 'workspacePackage.package'])->whereIn('id', $this->selected)->get(); $csv = "Workspace,Package,Gateway,Status,Billing Cycle,Period End,Created\n"; foreach ($subscriptions as $sub) { $csv .= sprintf( "%s,%s,%s,%s,%s,%s,%s\n", str_replace(',', ' ', $sub->workspace?->name ?? 'Unknown'), str_replace(',', ' ', $sub->workspacePackage?->package?->name ?? 'Unknown'), $sub->gateway ?? 'unknown', $sub->status, $sub->billing_cycle ?? 'monthly', $sub->current_period_end?->format('Y-m-d') ?? '', $sub->created_at->format('Y-m-d H:i:s') ); } $this->dispatch('download-csv', filename: 'subscriptions-export-'.now()->format('Y-m-d').'.csv', content: $csv); session()->flash('message', __('commerce::commerce.bulk.export_success', ['count' => count($this->selected)])); $this->selected = []; $this->selectAll = false; } public function bulkUpdateStatus(string $status): void { if (empty($this->selected)) { session()->flash('error', __('commerce::commerce.bulk.no_selection')); return; } $validStatuses = ['active', 'trialing', 'past_due', 'paused', 'cancelled', 'incomplete', 'expired']; if (! in_array($status, $validStatuses)) { session()->flash('error', 'Invalid status selected.'); return; } $count = Subscription::whereIn('id', $this->selected)->update([ 'status' => $status, ]); // Handle status-specific updates if ($status === 'cancelled') { Subscription::whereIn('id', $this->selected)->update([ 'cancelled_at' => now(), 'ended_at' => now(), ]); } elseif ($status === 'paused') { Subscription::whereIn('id', $this->selected)->update(['paused_at' => now()]); } session()->flash('message', __('commerce::commerce.bulk.status_updated', ['count' => $count, 'status' => ucfirst($status)])); $this->selected = []; $this->selectAll = false; } public function bulkExtendPeriod(): void { if (empty($this->selected)) { session()->flash('error', __('commerce::commerce.bulk.no_selection')); return; } $count = Subscription::whereIn('id', $this->selected) ->whereNotNull('current_period_end') ->update([ 'current_period_end' => \Illuminate\Support\Facades\DB::raw('DATE_ADD(current_period_end, INTERVAL 30 DAY)'), ]); session()->flash('message', __('commerce::commerce.bulk.period_extended', ['count' => $count, 'days' => 30])); $this->selected = []; $this->selectAll = false; } public function viewSubscription(int $id): void { $this->selectedSubscription = Subscription::with([ 'workspace', 'workspacePackage.package', ])->findOrFail($id); $this->showDetailModal = true; } public function openStatusChange(int $id): void { $this->selectedSubscription = Subscription::findOrFail($id); $this->newStatus = $this->selectedSubscription->status; $this->statusNote = ''; $this->showStatusModal = true; } public function updateStatus(): void { if (! $this->selectedSubscription) { return; } $validStatuses = ['active', 'trialing', 'past_due', 'paused', 'cancelled', 'incomplete', 'expired']; if (! in_array($this->newStatus, $validStatuses)) { session()->flash('error', 'Invalid status selected.'); return; } $oldStatus = $this->selectedSubscription->status; $this->selectedSubscription->update([ 'status' => $this->newStatus, 'metadata' => array_merge($this->selectedSubscription->metadata ?? [], [ 'status_history' => array_merge( $this->selectedSubscription->metadata['status_history'] ?? [], [[ 'from' => $oldStatus, 'to' => $this->newStatus, 'note' => $this->statusNote ?: null, 'by' => auth()->id(), 'at' => now()->toIso8601String(), ]] ), ]), ]); // Handle status-specific updates if ($this->newStatus === 'cancelled') { $this->selectedSubscription->update([ 'cancelled_at' => now(), 'ended_at' => now(), ]); } elseif ($this->newStatus === 'paused') { $this->selectedSubscription->update(['paused_at' => now()]); } elseif ($this->newStatus === 'active' && $oldStatus === 'paused') { $this->selectedSubscription->update(['paused_at' => null]); } session()->flash('message', "Subscription status updated from {$oldStatus} to {$this->newStatus}."); $this->closeStatusModal(); } public function openExtendPeriod(int $id): void { $this->selectedSubscription = Subscription::findOrFail($id); $this->extendDays = 30; $this->showExtendModal = true; } public function extendPeriod(): void { if (! $this->selectedSubscription) { return; } $newEndDate = $this->selectedSubscription->current_period_end->addDays($this->extendDays); $this->selectedSubscription->update([ 'current_period_end' => $newEndDate, 'metadata' => array_merge($this->selectedSubscription->metadata ?? [], [ 'period_extensions' => array_merge( $this->selectedSubscription->metadata['period_extensions'] ?? [], [[ 'days' => $this->extendDays, 'by' => auth()->id(), 'at' => now()->toIso8601String(), ]] ), ]), ]); session()->flash('message', "Subscription extended by {$this->extendDays} days."); $this->closeExtendModal(); } public function cancelSubscription(int $id): void { $subscription = Subscription::findOrFail($id); app(SubscriptionService::class)->cancel($subscription, 'Cancelled by admin'); session()->flash('message', 'Subscription cancelled.'); } public function resumeSubscription(int $id): void { $subscription = Subscription::findOrFail($id); app(SubscriptionService::class)->resume($subscription); session()->flash('message', 'Subscription resumed.'); } public function closeDetailModal(): void { $this->showDetailModal = false; $this->selectedSubscription = null; } public function closeStatusModal(): void { $this->showStatusModal = false; $this->selectedSubscription = null; $this->newStatus = ''; $this->statusNote = ''; } public function closeExtendModal(): void { $this->showExtendModal = false; $this->selectedSubscription = null; $this->extendDays = 30; } #[Computed] public function subscriptions() { return Subscription::query() ->with(['workspace', 'workspacePackage.package']) ->when($this->search, function ($query) { $query->whereHas('workspace', fn ($q) => $q->where('name', 'like', "%{$this->search}%")); }) ->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter)) ->when($this->gatewayFilter, fn ($q) => $q->where('gateway', $this->gatewayFilter)) ->when($this->workspaceFilter, fn ($q) => $q->where('workspace_id', $this->workspaceFilter)) ->latest() ->paginate(25); } #[Computed] public function workspaces() { return Workspace::orderBy('name')->get(['id', 'name']); } #[Computed] public function statuses(): array { return [ 'active' => 'Active', 'trialing' => 'Trialing', 'past_due' => 'Past Due', 'paused' => 'Paused', 'cancelled' => 'Cancelled', 'incomplete' => 'Incomplete', 'expired' => 'Expired', ]; } #[Computed] public function gateways(): array { return [ 'stripe' => 'Stripe', 'btcpay' => 'BTCPay', ]; } #[Computed] public function tableColumns(): array { return [ 'Workspace', 'Package', 'Gateway', ['label' => 'Status', 'align' => 'center'], 'Billing', 'Period Ends', ['label' => 'Actions', 'align' => 'center'], ]; } #[Computed] public function tableRowIds(): array { return $this->subscriptions->pluck('id')->all(); } #[Computed] public function tableRows(): array { return $this->subscriptions->map(function ($s) { $statusColors = [ 'active' => 'green', 'trialing' => 'blue', 'past_due' => 'amber', 'paused' => 'gray', 'cancelled' => 'red', 'incomplete' => 'amber', 'expired' => 'gray', ]; $actions = [ ['icon' => 'eye', 'click' => "viewSubscription({$s->id})", 'title' => 'View details'], ['icon' => 'pencil', 'click' => "openStatusChange({$s->id})", 'title' => 'Change status'], ['icon' => 'clock', 'click' => "openExtendPeriod({$s->id})", 'title' => 'Extend period'], ]; if ($s->cancelled_at && ! $s->ended_at) { $actions[] = ['icon' => 'play', 'click' => "resumeSubscription({$s->id})", 'title' => 'Resume', 'class' => 'text-green-600']; } elseif ($s->isActive()) { $actions[] = ['icon' => 'x-mark', 'click' => "cancelSubscription({$s->id})", 'confirm' => 'Are you sure you want to cancel this subscription?', 'title' => 'Cancel', 'class' => 'text-red-600']; } return [ ['bold' => $s->workspace?->name ?? 'Unknown'], [ 'lines' => [ ['bold' => $s->workspacePackage?->package?->name ?? 'Unknown'], ['mono' => $s->workspacePackage?->package?->code], ], ], ['badge' => ucfirst($s->gateway ?? 'unknown'), 'color' => 'gray'], [ 'lines' => array_filter([ ['badge' => ucfirst($s->status), 'color' => $statusColors[$s->status] ?? 'gray'], $s->cancel_at_period_end ? ['muted' => 'Cancels at period end'] : null, ]), ], ucfirst($s->billing_cycle ?? 'monthly'), $s->current_period_end ? [ 'lines' => [ ['bold' => $s->current_period_end->format('d M Y')], ['muted' => $s->current_period_end->isPast() ? 'Ended '.$s->current_period_end->diffForHumans() : $s->current_period_end->diffForHumans()], ], ] : '-', ['actions' => $actions], ]; })->all(); } public function render() { return view('commerce::admin.subscription-manager') ->layout('hub::admin.layouts.app', ['title' => 'Subscriptions']); } }