Skip to content

Commit 345a4c1

Browse files
simonhampclaude
andauthored
Fix free plugin claim expiry date to match extended offer (#271)
* Fix free plugin claim expiry date to match extended offer period The claimFreePlugins controller action still had the old Feb 28 expiry while shouldSeeFreePluginsOffer had already been updated to May 31. This caused users to see the offer banner but get "expired" when claiming. Also adds tests covering the claim flow and expiry boundaries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix trailing newline formatting (pint) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix malformed UTF-8 in docs search snippet extraction Use mb_strcut() instead of substr() to avoid cutting multi-byte UTF-8 characters (em dashes, emoji, etc.) in half when extracting search snippets, which caused JSON encoding failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3355326 commit 345a4c1

4 files changed

Lines changed: 129 additions & 4 deletions

File tree

app/Http/Controllers/CustomerLicenseController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public function claimFreePlugins(): RedirectResponse
134134
$user = Auth::user();
135135

136136
// Check if offer has expired
137-
if (now()->gt('2026-02-28 23:59:59')) {
137+
if (now()->gt('2026-05-31 23:59:59')) {
138138
return to_route('dashboard')
139139
->with('error', 'This offer has expired.');
140140
}

app/Services/DocsSearchService.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,12 @@ protected function extractSnippet(string $content, array $queryTerms, int $lengt
301301
$pos = strpos($contentLower, $term);
302302
if ($pos !== false) {
303303
$start = max(0, $pos - 50);
304-
$snippet = substr($content, $start, $length);
304+
$snippet = mb_strcut($content, $start, $length, 'UTF-8');
305305

306306
if ($start > 0) {
307307
$snippet = '...'.$snippet;
308308
}
309-
if ($start + $length < strlen($content)) {
309+
if ($start + $length < mb_strlen($content, '8bit')) {
310310
$snippet .= '...';
311311
}
312312

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Models\License;
6+
use App\Models\Plugin;
7+
use App\Models\PluginLicense;
8+
use App\Models\User;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use Illuminate\Support\Carbon;
11+
use Tests\TestCase;
12+
13+
class ClaimFreePluginsTest extends TestCase
14+
{
15+
use RefreshDatabase;
16+
17+
public function test_eligible_user_can_claim_free_plugins(): void
18+
{
19+
$this->travelTo(Carbon::parse('2026-03-15'));
20+
21+
$user = User::factory()->create();
22+
License::factory()->create([
23+
'user_id' => $user->id,
24+
'created_at' => '2025-12-01',
25+
]);
26+
27+
foreach (User::FREE_PLUGINS_OFFER as $name) {
28+
Plugin::factory()->approved()->create(['name' => $name]);
29+
}
30+
31+
$response = $this->actingAs($user)
32+
->post(route('customer.claim-free-plugins'));
33+
34+
$response->assertRedirectToRoute('dashboard');
35+
$response->assertSessionHas('success');
36+
37+
$this->assertDatabaseCount('plugin_licenses', count(User::FREE_PLUGINS_OFFER));
38+
}
39+
40+
public function test_claim_is_rejected_after_offer_expires(): void
41+
{
42+
$this->travelTo(Carbon::parse('2026-06-01'));
43+
44+
$user = User::factory()->create();
45+
License::factory()->create([
46+
'user_id' => $user->id,
47+
'created_at' => '2025-12-01',
48+
]);
49+
50+
foreach (User::FREE_PLUGINS_OFFER as $name) {
51+
Plugin::factory()->approved()->create(['name' => $name]);
52+
}
53+
54+
$response = $this->actingAs($user)
55+
->post(route('customer.claim-free-plugins'));
56+
57+
$response->assertRedirectToRoute('dashboard');
58+
$response->assertSessionHas('error', 'This offer has expired.');
59+
60+
$this->assertDatabaseCount('plugin_licenses', 0);
61+
}
62+
63+
public function test_claim_is_allowed_on_last_day_of_offer(): void
64+
{
65+
$this->travelTo(Carbon::parse('2026-05-31 23:00:00'));
66+
67+
$user = User::factory()->create();
68+
License::factory()->create([
69+
'user_id' => $user->id,
70+
'created_at' => '2025-12-01',
71+
]);
72+
73+
foreach (User::FREE_PLUGINS_OFFER as $name) {
74+
Plugin::factory()->approved()->create(['name' => $name]);
75+
}
76+
77+
$response = $this->actingAs($user)
78+
->post(route('customer.claim-free-plugins'));
79+
80+
$response->assertRedirectToRoute('dashboard');
81+
$response->assertSessionHas('success');
82+
}
83+
84+
public function test_ineligible_user_cannot_claim_free_plugins(): void
85+
{
86+
$this->travelTo(Carbon::parse('2026-03-15'));
87+
88+
$user = User::factory()->create();
89+
90+
foreach (User::FREE_PLUGINS_OFFER as $name) {
91+
Plugin::factory()->approved()->create(['name' => $name]);
92+
}
93+
94+
$response = $this->actingAs($user)
95+
->post(route('customer.claim-free-plugins'));
96+
97+
$response->assertRedirectToRoute('dashboard');
98+
$response->assertSessionHas('error', 'You are not eligible for this offer.');
99+
}
100+
101+
public function test_user_cannot_claim_plugins_twice(): void
102+
{
103+
$this->travelTo(Carbon::parse('2026-03-15'));
104+
105+
$user = User::factory()->create();
106+
License::factory()->create([
107+
'user_id' => $user->id,
108+
'created_at' => '2025-12-01',
109+
]);
110+
111+
foreach (User::FREE_PLUGINS_OFFER as $name) {
112+
$plugin = Plugin::factory()->approved()->create(['name' => $name]);
113+
PluginLicense::factory()->create([
114+
'user_id' => $user->id,
115+
'plugin_id' => $plugin->id,
116+
]);
117+
}
118+
119+
$response = $this->actingAs($user)
120+
->post(route('customer.claim-free-plugins'));
121+
122+
$response->assertRedirectToRoute('dashboard');
123+
$response->assertSessionHas('message', 'You have already claimed all the free plugins.');
124+
}
125+
}

tests/Feature/Filament/ResyncPluginActionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@ public function test_resync_action_hidden_when_no_repository_url(): void
6464
->test(EditPlugin::class, ['record' => $plugin->getRouteKey()])
6565
->assertActionHidden('resync');
6666
}
67-
}
67+
}

0 commit comments

Comments
 (0)