mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-19 20:14:59 +00:00
Merge remote-tracking branch 'origin/next' into fix/database-import-modal-not-closing-v2
This commit is contained in:
commit
7bee8a5668
107 files changed
+7384
-1226
No files matched your search
@@ -63,10 +63,16 @@ class DockerCompose extends Component
|
||||
]);
|
||||
|
||||
$variables = parseEnvFormatToArray($this->envFile);
|
||||
foreach ($variables as $key => $variable) {
|
||||
foreach ($variables as $key => $data) {
|
||||
// Extract value and comment from parsed data
|
||||
// Handle both array format ['value' => ..., 'comment' => ...] and plain string values
|
||||
$value = is_array($data) ? ($data['value'] ?? '') : $data;
|
||||
$comment = is_array($data) ? ($data['comment'] ?? null) : null;
|
||||
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $variable,
|
||||
'value' => $value,
|
||||
'comment' => $comment,
|
||||
'is_preview' => false,
|
||||
'resourceable_id' => $service->id,
|
||||
'resourceable_type' => $service->getMorphClass(),
|
||||
|
||||
@@ -31,6 +31,8 @@ class Add extends Component
|
||||
|
||||
public bool $is_buildtime = true;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public array $problematicVariables = [];
|
||||
|
||||
protected $listeners = ['clearAddEnv' => 'clear'];
|
||||
@@ -42,6 +44,7 @@ class Add extends Component
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -51,6 +54,7 @@ class Add extends Component
|
||||
'is_literal' => 'literal',
|
||||
'is_runtime' => 'runtime',
|
||||
'is_buildtime' => 'buildtime',
|
||||
'comment' => 'comment',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -136,6 +140,7 @@ class Add extends Component
|
||||
'is_runtime' => $this->is_runtime,
|
||||
'is_buildtime' => $this->is_buildtime,
|
||||
'is_preview' => $this->is_preview,
|
||||
'comment' => $this->comment,
|
||||
]);
|
||||
$this->clear();
|
||||
}
|
||||
@@ -148,5 +153,6 @@ class Add extends Component
|
||||
$this->is_literal = false;
|
||||
$this->is_runtime = true;
|
||||
$this->is_buildtime = true;
|
||||
$this->comment = null;
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,62 @@ class All extends Component
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(false);
|
||||
}
|
||||
|
||||
public function getHardcodedEnvironmentVariablesPreviewProperty()
|
||||
{
|
||||
return $this->getHardcodedVariables(true);
|
||||
}
|
||||
|
||||
protected function getHardcodedVariables(bool $isPreview)
|
||||
{
|
||||
// Only for services and docker-compose applications
|
||||
if ($this->resource->type() !== 'service' &&
|
||||
($this->resourceClass !== 'App\Models\Application' ||
|
||||
($this->resourceClass === 'App\Models\Application' && $this->resource->build_pack !== 'dockercompose'))) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
$dockerComposeRaw = $this->resource->docker_compose_raw ?? $this->resource->docker_compose;
|
||||
|
||||
if (blank($dockerComposeRaw)) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
// Extract all hard-coded variables
|
||||
$hardcodedVars = extractHardcodedEnvironmentVariables($dockerComposeRaw);
|
||||
|
||||
// Filter out magic variables (SERVICE_FQDN_*, SERVICE_URL_*, SERVICE_NAME_*)
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) {
|
||||
$key = $var['key'];
|
||||
|
||||
return ! str($key)->startsWith(['SERVICE_FQDN_', 'SERVICE_URL_', 'SERVICE_NAME_']);
|
||||
});
|
||||
|
||||
// Filter out variables that exist in database (user has overridden/managed them)
|
||||
// For preview, check against preview variables; for production, check against production variables
|
||||
if ($isPreview) {
|
||||
$managedKeys = $this->resource->environment_variables_preview()->pluck('key')->toArray();
|
||||
} else {
|
||||
$managedKeys = $this->resource->environment_variables()->where('is_preview', false)->pluck('key')->toArray();
|
||||
}
|
||||
|
||||
$hardcodedVars = $hardcodedVars->filter(function ($var) use ($managedKeys) {
|
||||
return ! in_array($var['key'], $managedKeys);
|
||||
});
|
||||
|
||||
// Apply sorting based on is_env_sorting_enabled
|
||||
if ($this->is_env_sorting_enabled) {
|
||||
$hardcodedVars = $hardcodedVars->sortBy('key')->values();
|
||||
}
|
||||
// Otherwise keep order from docker-compose file
|
||||
|
||||
return $hardcodedVars;
|
||||
}
|
||||
|
||||
public function getDevView()
|
||||
{
|
||||
$this->variables = $this->formatEnvironmentVariables($this->environmentVariables);
|
||||
@@ -240,6 +296,7 @@ class All extends Component
|
||||
$environment->is_runtime = $data['is_runtime'] ?? true;
|
||||
$environment->is_buildtime = $data['is_buildtime'] ?? true;
|
||||
$environment->is_preview = $data['is_preview'] ?? false;
|
||||
$environment->comment = $data['comment'] ?? null;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
|
||||
@@ -280,18 +337,37 @@ class All extends Component
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($variables as $key => $value) {
|
||||
foreach ($variables as $key => $data) {
|
||||
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL') || str($key)->startsWith('SERVICE_NAME')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract value and comment from parsed data
|
||||
// Handle both array format ['value' => ..., 'comment' => ...] and plain string values
|
||||
$value = is_array($data) ? ($data['value'] ?? '') : $data;
|
||||
$comment = is_array($data) ? ($data['comment'] ?? null) : null;
|
||||
|
||||
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
|
||||
$found = $this->resource->$method()->where('key', $key)->first();
|
||||
|
||||
if ($found) {
|
||||
if (! $found->is_shown_once && ! $found->is_multiline) {
|
||||
// Only count as a change if the value actually changed
|
||||
$changed = false;
|
||||
|
||||
// Update value if it changed
|
||||
if ($found->value !== $value) {
|
||||
$found->value = $value;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
// Only update comment from inline comment if one is provided (overwrites existing)
|
||||
// If $comment is null, don't touch existing comment field to preserve it
|
||||
if ($comment !== null && $found->comment !== $comment) {
|
||||
$found->comment = $comment;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if ($changed) {
|
||||
$found->save();
|
||||
$count++;
|
||||
}
|
||||
@@ -300,6 +376,7 @@ class All extends Component
|
||||
$environment = new EnvironmentVariable;
|
||||
$environment->key = $key;
|
||||
$environment->value = $value;
|
||||
$environment->comment = $comment; // Set comment from inline comment
|
||||
$environment->is_multiline = false;
|
||||
$environment->is_preview = $isPreview;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
|
||||
@@ -24,6 +24,8 @@ class Show extends Component
|
||||
|
||||
public bool $isLocked = false;
|
||||
|
||||
public bool $isMagicVariable = false;
|
||||
|
||||
public bool $isSharedVariable = false;
|
||||
|
||||
public string $type;
|
||||
@@ -34,6 +36,8 @@ class Show extends Component
|
||||
|
||||
public ?string $real_value = null;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public bool $is_shared = false;
|
||||
|
||||
public bool $is_multiline = false;
|
||||
@@ -63,6 +67,7 @@ class Show extends Component
|
||||
protected $rules = [
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
@@ -104,6 +109,7 @@ class Show extends Component
|
||||
$this->validate([
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
@@ -118,6 +124,7 @@ class Show extends Component
|
||||
}
|
||||
$this->env->key = $this->key;
|
||||
$this->env->value = $this->value;
|
||||
$this->env->comment = $this->comment;
|
||||
$this->env->is_multiline = $this->is_multiline;
|
||||
$this->env->is_literal = $this->is_literal;
|
||||
$this->env->is_shown_once = $this->is_shown_once;
|
||||
@@ -125,6 +132,7 @@ class Show extends Component
|
||||
} else {
|
||||
$this->key = $this->env->key;
|
||||
$this->value = $this->env->value;
|
||||
$this->comment = $this->env->comment;
|
||||
$this->is_multiline = $this->env->is_multiline;
|
||||
$this->is_literal = $this->env->is_literal;
|
||||
$this->is_shown_once = $this->env->is_shown_once;
|
||||
@@ -140,9 +148,13 @@ class Show extends Component
|
||||
public function checkEnvs()
|
||||
{
|
||||
$this->isDisabled = false;
|
||||
$this->isMagicVariable = false;
|
||||
|
||||
if (str($this->env->key)->startsWith('SERVICE_FQDN') || str($this->env->key)->startsWith('SERVICE_URL') || str($this->env->key)->startsWith('SERVICE_NAME')) {
|
||||
$this->isDisabled = true;
|
||||
$this->isMagicVariable = true;
|
||||
}
|
||||
|
||||
if ($this->env->is_shown_once) {
|
||||
$this->isLocked = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ShowHardcoded extends Component
|
||||
{
|
||||
public array $env;
|
||||
|
||||
public string $key;
|
||||
|
||||
public ?string $value = null;
|
||||
|
||||
public ?string $comment = null;
|
||||
|
||||
public ?string $serviceName = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->key = $this->env['key'];
|
||||
$this->value = $this->env['value'] ?? null;
|
||||
$this->comment = $this->env['comment'] ?? null;
|
||||
$this->serviceName = $this->env['service_name'] ?? null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.environment-variable.show-hardcoded');
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Livewire\Server\New;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Rules\ValidServerIp;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
@@ -55,8 +56,8 @@ class ByIp extends Component
|
||||
'new_private_key_value' => 'nullable|string',
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'ip' => 'required|string',
|
||||
'user' => 'required|string',
|
||||
'ip' => ['required', 'string', new ValidServerIp],
|
||||
'user' => ['required', 'string', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||
'port' => 'required|integer|between:1,65535',
|
||||
'is_build_server' => 'required|boolean',
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Actions\Server\StopSentinel;
|
||||
use App\Events\ServerReachabilityChanged;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Server;
|
||||
use App\Rules\ValidServerIp;
|
||||
use App\Services\HetznerService;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@@ -106,9 +107,9 @@ class Show extends Component
|
||||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'ip' => 'required',
|
||||
'user' => 'required',
|
||||
'port' => 'required',
|
||||
'ip' => ['required', new ValidServerIp],
|
||||
'user' => ['required', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||
'port' => 'required|integer|between:1,65535',
|
||||
'validationLogs' => 'nullable',
|
||||
'wildcardDomain' => 'nullable|url',
|
||||
'isReachable' => 'required',
|
||||
|
||||
@@ -95,7 +95,9 @@ class Advanced extends Component
|
||||
// Check if it's valid CIDR notation
|
||||
if (str_contains($entry, '/')) {
|
||||
[$ip, $mask] = explode('/', $entry);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= 32) {
|
||||
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||
$maxMask = $isIpv6 ? 128 : 32;
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= $maxMask) {
|
||||
return $entry;
|
||||
}
|
||||
$invalidEntries[] = $entry;
|
||||
@@ -111,7 +113,7 @@ class Advanced extends Component
|
||||
$invalidEntries[] = $entry;
|
||||
|
||||
return null;
|
||||
})->filter()->unique();
|
||||
})->filter()->values()->all();
|
||||
|
||||
if (! empty($invalidEntries)) {
|
||||
$this->dispatch('error', 'Invalid IP addresses or subnets: '.implode(', ', $invalidEntries));
|
||||
@@ -119,13 +121,15 @@ class Advanced extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
if ($validEntries->isEmpty()) {
|
||||
if (empty($validEntries)) {
|
||||
$this->dispatch('error', 'No valid IP addresses or subnets provided');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->allowed_ips = $validEntries->implode(',');
|
||||
$validEntries = deduplicateAllowlist($validEntries);
|
||||
|
||||
$this->allowed_ips = implode(',', $validEntries);
|
||||
}
|
||||
|
||||
$this->instantSave();
|
||||
|
||||
@@ -40,6 +40,7 @@ class Show extends Component
|
||||
'value' => $data['value'],
|
||||
'is_multiline' => $data['is_multiline'],
|
||||
'is_literal' => $data['is_literal'],
|
||||
'comment' => $data['comment'] ?? null,
|
||||
'type' => 'environment',
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
@@ -33,6 +33,7 @@ class Show extends Component
|
||||
'value' => $data['value'],
|
||||
'is_multiline' => $data['is_multiline'],
|
||||
'is_literal' => $data['is_literal'],
|
||||
'comment' => $data['comment'] ?? null,
|
||||
'type' => 'project',
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
@@ -33,6 +33,7 @@ class Index extends Component
|
||||
'value' => $data['value'],
|
||||
'is_multiline' => $data['is_multiline'],
|
||||
'is_literal' => $data['is_literal'],
|
||||
'comment' => $data['comment'] ?? null,
|
||||
'type' => 'team',
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
@@ -2,21 +2,204 @@
|
||||
|
||||
namespace App\Livewire\Subscription;
|
||||
|
||||
use App\Actions\Stripe\CancelSubscriptionAtPeriodEnd;
|
||||
use App\Actions\Stripe\RefundSubscription;
|
||||
use App\Actions\Stripe\ResumeSubscription;
|
||||
use App\Actions\Stripe\UpdateSubscriptionQuantity;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
class Actions extends Component
|
||||
{
|
||||
public $server_limits = 0;
|
||||
|
||||
public function mount()
|
||||
public int $quantity = UpdateSubscriptionQuantity::MIN_SERVER_LIMIT;
|
||||
|
||||
public int $minServerLimit = UpdateSubscriptionQuantity::MIN_SERVER_LIMIT;
|
||||
|
||||
public int $maxServerLimit = UpdateSubscriptionQuantity::MAX_SERVER_LIMIT;
|
||||
|
||||
public ?array $pricePreview = null;
|
||||
|
||||
public bool $isRefundEligible = false;
|
||||
|
||||
public int $refundDaysRemaining = 0;
|
||||
|
||||
public bool $refundCheckLoading = true;
|
||||
|
||||
public bool $refundAlreadyUsed = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->server_limits = Team::serverLimit();
|
||||
$this->quantity = (int) $this->server_limits;
|
||||
}
|
||||
|
||||
public function stripeCustomerPortal()
|
||||
public function loadPricePreview(int $quantity): void
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
$result = (new UpdateSubscriptionQuantity)->fetchPricePreview(currentTeam(), $quantity);
|
||||
$this->pricePreview = $result['success'] ? $result['preview'] : null;
|
||||
}
|
||||
|
||||
// Password validation is intentionally skipped for quantity updates.
|
||||
// Unlike refunds/cancellations, changing the server limit is a
|
||||
// non-destructive, reversible billing adjustment (prorated by Stripe).
|
||||
public function updateQuantity(string $password = ''): bool
|
||||
{
|
||||
if ($this->quantity < UpdateSubscriptionQuantity::MIN_SERVER_LIMIT) {
|
||||
$this->dispatch('error', 'Minimum server limit is '.UpdateSubscriptionQuantity::MIN_SERVER_LIMIT.'.');
|
||||
$this->quantity = UpdateSubscriptionQuantity::MIN_SERVER_LIMIT;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->quantity === (int) $this->server_limits) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = (new UpdateSubscriptionQuantity)->execute(currentTeam(), $this->quantity);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->server_limits = $this->quantity;
|
||||
$this->pricePreview = null;
|
||||
$this->dispatch('success', 'Server limit updated to '.$this->quantity.'.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->dispatch('error', $result['error'] ?? 'Failed to update server limit.');
|
||||
$this->quantity = (int) $this->server_limits;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function loadRefundEligibility(): void
|
||||
{
|
||||
$this->checkRefundEligibility();
|
||||
$this->refundCheckLoading = false;
|
||||
}
|
||||
|
||||
public function stripeCustomerPortal(): void
|
||||
{
|
||||
$session = getStripeCustomerPortalSession(currentTeam());
|
||||
redirect($session->url);
|
||||
}
|
||||
|
||||
public function refundSubscription(string $password): bool|string
|
||||
{
|
||||
if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
|
||||
return 'Invalid password.';
|
||||
}
|
||||
|
||||
$result = (new RefundSubscription)->execute(currentTeam());
|
||||
|
||||
if ($result['success']) {
|
||||
$this->dispatch('success', 'Subscription refunded successfully.');
|
||||
$this->redirect(route('subscription.index'), navigate: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->dispatch('error', 'Something went wrong with the refund. Please <a href="'.config('constants.urls.contact').'" target="_blank" class="underline">contact us</a>.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cancelImmediately(string $password): bool|string
|
||||
{
|
||||
if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
|
||||
return 'Invalid password.';
|
||||
}
|
||||
|
||||
$team = currentTeam();
|
||||
$subscription = $team->subscription;
|
||||
|
||||
if (! $subscription?->stripe_subscription_id) {
|
||||
$this->dispatch('error', 'Something went wrong with the cancellation. Please <a href="'.config('constants.urls.contact').'" target="_blank" class="underline">contact us</a>.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$stripe = new StripeClient(config('subscription.stripe_api_key'));
|
||||
$stripe->subscriptions->cancel($subscription->stripe_subscription_id);
|
||||
|
||||
$subscription->update([
|
||||
'stripe_cancel_at_period_end' => false,
|
||||
'stripe_invoice_paid' => false,
|
||||
'stripe_trial_already_ended' => false,
|
||||
'stripe_past_due' => false,
|
||||
'stripe_feedback' => 'Cancelled immediately by user',
|
||||
'stripe_comment' => 'Subscription cancelled immediately by user at '.now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
$team->subscriptionEnded();
|
||||
|
||||
\Log::info("Subscription {$subscription->stripe_subscription_id} cancelled immediately for team {$team->name}");
|
||||
|
||||
$this->dispatch('success', 'Subscription cancelled successfully.');
|
||||
$this->redirect(route('subscription.index'), navigate: true);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("Immediate cancellation error for team {$team->id}: ".$e->getMessage());
|
||||
|
||||
$this->dispatch('error', 'Something went wrong with the cancellation. Please <a href="'.config('constants.urls.contact').'" target="_blank" class="underline">contact us</a>.');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public function cancelAtPeriodEnd(string $password): bool|string
|
||||
{
|
||||
if (! shouldSkipPasswordConfirmation() && ! Hash::check($password, auth()->user()->password)) {
|
||||
return 'Invalid password.';
|
||||
}
|
||||
|
||||
$result = (new CancelSubscriptionAtPeriodEnd)->execute(currentTeam());
|
||||
|
||||
if ($result['success']) {
|
||||
$this->dispatch('success', 'Subscription will be cancelled at the end of the billing period.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->dispatch('error', 'Something went wrong with the cancellation. Please <a href="'.config('constants.urls.contact').'" target="_blank" class="underline">contact us</a>.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function resumeSubscription(): bool
|
||||
{
|
||||
$result = (new ResumeSubscription)->execute(currentTeam());
|
||||
|
||||
if ($result['success']) {
|
||||
$this->dispatch('success', 'Subscription resumed successfully.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->dispatch('error', 'Something went wrong resuming the subscription. Please <a href="'.config('constants.urls.contact').'" target="_blank" class="underline">contact us</a>.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function checkRefundEligibility(): void
|
||||
{
|
||||
if (! isCloud() || ! currentTeam()->subscription?->stripe_subscription_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->refundAlreadyUsed = currentTeam()->subscription?->stripe_refunded_at !== null;
|
||||
$result = (new RefundSubscription)->checkEligibility(currentTeam());
|
||||
$this->isRefundEligible = $result['eligible'];
|
||||
$this->refundDaysRemaining = $result['days_remaining'];
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('Refund eligibility check failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user