Refund
- @if ($refundCheckLoading || ($isRefundEligible && !currentTeam()->subscription->stripe_cancel_at_period_end))
+ @if ($refundCheckLoading || $isRefundEligible)
@if ($refundCheckLoading)
Request Full Refund
@@ -269,7 +291,7 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
@if ($refundCheckLoading)
Checking refund eligibility...
- @elseif ($isRefundEligible && !currentTeam()->subscription->stripe_cancel_at_period_end)
+ @elseif ($isRefundEligible)
Eligible for a full refund — {{ $refundDaysRemaining }} days remaining.
@elseif ($refundAlreadyUsed)
Refund already processed. Each team is eligible for one refund only.
diff --git a/tests/Feature/Subscription/RefundSubscriptionTest.php b/tests/Feature/Subscription/RefundSubscriptionTest.php
index 6f67fca2b..c1d5adda1 100644
--- a/tests/Feature/Subscription/RefundSubscriptionTest.php
+++ b/tests/Feature/Subscription/RefundSubscriptionTest.php
@@ -5,6 +5,7 @@
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use Stripe\Exception\InvalidRequestException;
use Stripe\Service\InvoiceService;
use Stripe\Service\RefundService;
use Stripe\Service\SubscriptionService;
@@ -85,6 +86,28 @@
expect($result['current_period_end'])->toBe($periodEnd);
});
+ test('returns eligible when subscription is set to cancel at period end', function () {
+ $this->subscription->update(['stripe_cancel_at_period_end' => true]);
+
+ $periodEnd = now()->addDays(20)->timestamp;
+ $stripeSubscription = (object) [
+ 'status' => 'active',
+ 'start_date' => now()->subDays(10)->timestamp,
+ 'current_period_end' => $periodEnd,
+ ];
+
+ $this->mockSubscriptions
+ ->shouldReceive('retrieve')
+ ->with('sub_test_123')
+ ->andReturn($stripeSubscription);
+
+ $action = new RefundSubscription($this->mockStripe);
+ $result = $action->checkEligibility($this->team);
+
+ expect($result['eligible'])->toBeTrue();
+ expect($result['days_remaining'])->toBe(20);
+ });
+
test('returns ineligible when subscription is not active', function () {
$periodEnd = now()->addDays(25)->timestamp;
$stripeSubscription = (object) [
@@ -141,7 +164,7 @@
$this->mockSubscriptions
->shouldReceive('retrieve')
->with('sub_test_123')
- ->andThrow(new \Stripe\Exception\InvalidRequestException('No such subscription'));
+ ->andThrow(new InvalidRequestException('No such subscription'));
$action = new RefundSubscription($this->mockStripe);
$result = $action->checkEligibility($this->team);
@@ -269,6 +292,7 @@
$stripeSubscription = (object) [
'status' => 'active',
'start_date' => now()->subDays(10)->timestamp,
+ 'current_period_end' => now()->addDays(20)->timestamp,
];
$this->mockSubscriptions
@@ -298,7 +322,7 @@
$this->mockSubscriptions
->shouldReceive('cancel')
->with('sub_test_123')
- ->andThrow(new \Exception('Stripe cancel API error'));
+ ->andThrow(new Exception('Stripe cancel API error'));
$action = new RefundSubscription($this->mockStripe);
$result = $action->execute($this->team);
diff --git a/tests/Feature/Subscription/SubscriptionActionsTest.php b/tests/Feature/Subscription/SubscriptionActionsTest.php
new file mode 100644
index 000000000..698c84d95
--- /dev/null
+++ b/tests/Feature/Subscription/SubscriptionActionsTest.php
@@ -0,0 +1,70 @@
+set('constants.coolify.self_hosted', false);
+ config()->set('subscription.provider', 'stripe');
+ config()->set('subscription.stripe_api_key', 'sk_test_fake');
+
+ InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
+
+ $this->team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ Subscription::create([
+ 'team_id' => $this->team->id,
+ 'stripe_subscription_id' => 'sub_test_123',
+ 'stripe_customer_id' => 'cus_test_123',
+ 'stripe_invoice_paid' => true,
+ 'stripe_plan_id' => 'price_test_123',
+ 'stripe_cancel_at_period_end' => false,
+ 'stripe_past_due' => false,
+ ]);
+
+ $this->actingAs($this->user);
+ session(['currentTeam' => $this->team]);
+});
+
+describe('cancelImmediately with refund option', function () {
+ test('refunds and cancels via RefundSubscription when refund checkbox is selected', function () {
+ $mock = Mockery::mock(RefundSubscription::class);
+ $mock->shouldReceive('execute')->once()->andReturn(['success' => true, 'error' => null]);
+ $this->instance(RefundSubscription::class, $mock);
+
+ Livewire::test(Actions::class)
+ ->call('cancelImmediately', 'password', ['refundLatestPayment'])
+ ->assertDispatched('success')
+ ->assertRedirect(route('subscription.index'));
+ });
+
+ test('dispatches error when refund fails', function () {
+ $mock = Mockery::mock(RefundSubscription::class);
+ $mock->shouldReceive('execute')->once()->andReturn(['success' => false, 'error' => 'No paid invoice found to refund.']);
+ $this->instance(RefundSubscription::class, $mock);
+
+ Livewire::test(Actions::class)
+ ->call('cancelImmediately', 'password', ['refundLatestPayment'])
+ ->assertDispatched('error');
+ });
+
+ test('rejects invalid password before refunding', function () {
+ $mock = Mockery::mock(RefundSubscription::class);
+ $mock->shouldNotReceive('execute');
+ $this->instance(RefundSubscription::class, $mock);
+
+ Livewire::test(Actions::class)
+ ->call('cancelImmediately', 'wrong-password', ['refundLatestPayment'])
+ ->assertReturned('Invalid password.');
+ });
+});
From 03d2fabc64d7380893dd71754633b6fa1d276fdb Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 12 Jun 2026 16:05:41 +0200
Subject: [PATCH 6/8] chore(release): bump Coolify to 4.2.0
---
config/constants.php | 2 +-
other/nightly/versions.json | 4 ++--
versions.json | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/config/constants.php b/config/constants.php
index a01669673..4a956b31e 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.1.2',
+ 'version' => '4.2.0',
'helper_version' => '1.0.14',
'realtime_version' => '1.0.16',
'railpack_version' => '0.23.0',
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index 9c9a405aa..751db0754 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.1.2"
+ "version": "4.2.0"
},
"nightly": {
- "version": "4.2.0"
+ "version": "4.2.1"
},
"helper": {
"version": "1.0.14"
diff --git a/versions.json b/versions.json
index 9c9a405aa..751db0754 100644
--- a/versions.json
+++ b/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.1.2"
+ "version": "4.2.0"
},
"nightly": {
- "version": "4.2.0"
+ "version": "4.2.1"
},
"helper": {
"version": "1.0.14"
From 1aca7ead8cc0eb588a01e3efe5da4f034bda3ca5 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 12 Jun 2026 16:06:20 +0200
Subject: [PATCH 7/8] fix(logs): constrain activity monitor output width
---
.../views/livewire/activity-monitor.blade.php | 4 ++--
tests/Feature/LogFontStylingTest.php | 17 ++++++++++++++++-
2 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/resources/views/livewire/activity-monitor.blade.php b/resources/views/livewire/activity-monitor.blade.php
index 72b68edd0..f3e79db0e 100644
--- a/resources/views/livewire/activity-monitor.blade.php
+++ b/resources/views/livewire/activity-monitor.blade.php
@@ -48,11 +48,11 @@
characterData: true
});" x-destroy="observer && observer.disconnect()"
@class([
- 'flex flex-col w-full px-4 py-2 overflow-y-auto bg-white border border-solid rounded-sm dark:text-white dark:bg-coolgray-100 scrollbar border-neutral-300 dark:border-coolgray-300',
+ 'flex flex-col w-full min-w-0 max-w-full px-4 py-2 overflow-y-auto bg-white border border-solid rounded-sm dark:text-white dark:bg-coolgray-100 scrollbar border-neutral-300 dark:border-coolgray-300',
'flex-1 min-h-0' => $fullHeight,
'max-h-96' => !$fullHeight,
])>
-
{{ RunRemoteProcess::decodeOutput($activity) }}
+
{{ RunRemoteProcess::decodeOutput($activity) }}
@else
@if ($showWaiting)
diff --git a/tests/Feature/LogFontStylingTest.php b/tests/Feature/LogFontStylingTest.php
index c7903fb45..0696c3e2c 100644
--- a/tests/Feature/LogFontStylingTest.php
+++ b/tests/Feature/LogFontStylingTest.php
@@ -36,10 +36,25 @@
->toContain('class="flex flex-col font-logs"')
->toContain('class="font-logs text-neutral-400 mb-2"')
->and($activityMonitorView)
- ->toContain('toContain('and($dockerCleanupView)
->toContain('class="flex-1 text-sm font-logs text-gray-700 dark:text-gray-300"')
->toContain('class="font-logs text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap"')
->and($terminalClient)
->toContain('"Geist Mono"');
});
+
+it('constrains activity monitor logs inside the available viewport', function () {
+ $activityMonitorView = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php'));
+
+ expect($activityMonitorView)
+ ->toContain('flex flex-col w-full min-w-0 max-w-full')
+ ->toContain('toContain('');
+});
From 442ed9816990861c6bebf06c5535a77161c1d2aa Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 12 Jun 2026 16:09:26 +0200
Subject: [PATCH 8/8] feat(project): add mobile action controls for resources
Show restart, stop, redeploy, and update actions directly in the mobile
headers for applications, databases, and services, with coverage for the
mobile resource menu behavior.
---
.../project/application/heading.blade.php | 155 ++++++++----
.../project/database/heading.blade.php | 69 ++++--
.../project/service/heading.blade.php | 228 +++++++++++++-----
tests/Feature/MobileResourceMenuTest.php | 66 +++++
4 files changed, 387 insertions(+), 131 deletions(-)
diff --git a/resources/views/livewire/project/application/heading.blade.php b/resources/views/livewire/project/application/heading.blade.php
index fd37d869a..18e2e6db0 100644
--- a/resources/views/livewire/project/application/heading.blade.php
+++ b/resources/views/livewire/project/application/heading.blade.php
@@ -166,7 +166,113 @@
-
+ @if (!($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)))
+
+
Actions
+
+ @if (!str($application->status)->startsWith('exited'))
+ @if (!$application->destination->server->isSwarm())
+
+ @endif
+ @if ($application->build_pack !== 'dockercompose')
+ @if ($application->destination->server->isSwarm())
+
+ @else
+
+ @endif
+ @endif
+
+
+ Stop
+
+ @else
+
+ @endif
+ @if (!$application->destination->server->isSwarm())
+ @if ($application->status === 'running')
+
+ @else
+
+ @endif
+ @endif
+
+
+ @endif
+
- @endif
Database Startup
-
+
-
+ @if ($database->destination->server->isFunctional())
+
+
Actions
+
+ @if (!str($database->status)->startsWith('exited'))
+
+
+
+ Stop
+
+ @else
+
+ @endif
+
+
+ @endif
+
- @endif