Skip to content

Commit 7290e53

Browse files
committed
Add CleanupTrialAssetsTest and refactor Cleanup job to handle trial asset cleanup
1 parent d68603d commit 7290e53

2 files changed

Lines changed: 201 additions & 54 deletions

File tree

app/Jobs/Cleanup.php

Lines changed: 88 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -45,58 +45,12 @@ public function __construct()
4545

4646
public function handle()
4747
{
48-
Log::debug("Cleaning up non-paying customers...");
49-
50-
$this->cleanupTenants();
51-
52-
Log::debug("Non-paying customers cleaned.");
53-
Log::debug("Cleaning up empty tenants...");
54-
55-
$this->deleteEmptyTenants();
56-
57-
Log::debug("Empty tenants cleaned.");
58-
Log::debug("Cleaning up trials...");
59-
60-
Trial::whereNull('created_by')
61-
->where('updated_at', '<', now()->subDays(10))
62-
->delete();
63-
64-
Log::debug("Trials cleaned up.");
65-
Log::debug("Removing events associated to disabled osquery rules...");
66-
67-
// When a rule is disabled, cleanup the history
68-
$rules = YnhOsqueryRule::where('enabled', false)->get()->pluck('name');
69-
YnhOsquery::whereIn('name', $rules)->limit(10000)->delete();
70-
YnhOsqueryLatestEvent::whereIn('event_name', $rules)->delete();
71-
72-
Log::debug("Events removed.");
73-
Log::debug("Finding overflowing events...");
74-
75-
// When the list of cached events "overflow" for a given (server, rule), remove the oldest events
76-
$threshold = 1000;
77-
78-
$overflowingEvents = DB::table('ynh_osquery_latest_events')
79-
->select('ynh_server_id', 'server_name', 'event_name', DB::raw('COUNT(*) as event_count'))
80-
->whereNotIn('event_name', $rules)
81-
->groupBy('ynh_server_id', 'server_name', 'event_name')
82-
->having('event_count', '>', $threshold)
83-
->get();
84-
85-
Log::debug("{$overflowingEvents->count()} overflowing events found.");
86-
Log::debug("Removing overflowing events...");
87-
88-
foreach ($overflowingEvents as $event) {
89-
Log::debug("Compacting events {$event->event_name} for server {$event->server_name}...");
90-
DB::table('ynh_osquery_latest_events')
91-
->where('ynh_server_id', $event->ynh_server_id)
92-
->where('event_name', $event->event_name)
93-
->orderBy('calendar_time')
94-
->limit($event->event_count - $threshold)
95-
->delete();
96-
Log::debug("Events {$event->event_name} for server {$event->server_name} compacted.");
97-
}
98-
99-
Log::debug("Overflowing events removed.");
48+
$this->deleteTenantsWithoutUsers();
49+
$this->removeTrialsWithoutDomains();
50+
$this->deleteAssetsOfTenantsWithoutSubscription();
51+
$this->upgradeTrialAssetsToAssets();
52+
$this->removeEventsAssociatedWithDisabledOsqueryRules();
53+
$this->removeOverflowingEvents();
10054

10155
User::all()->each(function (User $user) {
10256

@@ -181,7 +135,87 @@ public function handle()
181135
});
182136
}
183137

184-
private function deleteEmptyTenants(): void
138+
private function removeEventsAssociatedWithDisabledOsqueryRules(): void
139+
{
140+
// When a rule is disabled, cleanup the history
141+
$rules = YnhOsqueryRule::where('enabled', false)->get()->pluck('name');
142+
YnhOsquery::whereIn('name', $rules)->limit(10000)->delete();
143+
YnhOsqueryLatestEvent::whereIn('event_name', $rules)->delete();
144+
}
145+
146+
private function removeOverflowingEvents(): void
147+
{
148+
Log::debug("Finding overflowing events...");
149+
150+
// When the list of cached events "overflow" for a given (server, rule), remove the oldest events
151+
$threshold = 1000;
152+
$rules = YnhOsqueryRule::where('enabled', false)->get()->pluck('name');
153+
$overflowingEvents = DB::table('ynh_osquery_latest_events')
154+
->select('ynh_server_id', 'server_name', 'event_name', DB::raw('COUNT(*) as event_count'))
155+
->whereNotIn('event_name', $rules)
156+
->groupBy('ynh_server_id', 'server_name', 'event_name')
157+
->having('event_count', '>', $threshold)
158+
->get();
159+
160+
Log::debug("{$overflowingEvents->count()} overflowing events found.");
161+
Log::debug("Removing overflowing events...");
162+
163+
foreach ($overflowingEvents as $event) {
164+
Log::debug("Compacting events {$event->event_name} for server {$event->server_name}...");
165+
DB::table('ynh_osquery_latest_events')
166+
->where('ynh_server_id', $event->ynh_server_id)
167+
->where('event_name', $event->event_name)
168+
->orderBy('calendar_time')
169+
->limit($event->event_count - $threshold)
170+
->delete();
171+
Log::debug("Events {$event->event_name} for server {$event->server_name} compacted.");
172+
}
173+
174+
Log::debug("Overflowing events removed.");
175+
}
176+
177+
private function removeTrialsWithoutDomains(): void
178+
{
179+
Trial::whereNull('domain')
180+
->where('updated_at', '<', now()->subDays(5))
181+
->delete();
182+
}
183+
184+
private function upgradeTrialAssetsToAssets(): void
185+
{
186+
Asset::withoutGlobalScope('tenant_scope')
187+
->whereNotNull('ynh_trial_id')
188+
->chunkById(100, function ($assets) {
189+
$assets->each(function (Asset $asset) {
190+
191+
/** @var ?Trial $trial */
192+
$trial = Trial::withoutGlobalScope('tenant_scope')->find($asset->ynh_trial_id);
193+
194+
if (!$trial || $trial->created_at->lt(now()->subDays(15))) {
195+
196+
/** @var ?User $user */
197+
$user = User::withoutGlobalScope('tenant_scope')->find($asset->created_by);
198+
$tenantId = $user?->tenant_id;
199+
200+
if (!$tenantId) {
201+
$asset->delete();
202+
return;
203+
}
204+
205+
$users = User::withoutGlobalScope('tenant_scope')->where('tenant_id', $tenantId)->get();
206+
$hasPayingUser = $users->contains(fn(User $user) => $user->subscriber());
207+
208+
// If the tenant has a subscription, the trial asset is now an asset
209+
if ($hasPayingUser) {
210+
$asset->ynh_trial_id = null;
211+
$asset->save();
212+
}
213+
}
214+
});
215+
});
216+
}
217+
218+
private function deleteTenantsWithoutUsers(): void
185219
{
186220
Tenant::where('created_at', '<=', now()->subDays(15))
187221
->get()
@@ -197,7 +231,7 @@ private function deleteEmptyTenants(): void
197231
});
198232
}
199233

200-
private function cleanupTenants(): void
234+
private function deleteAssetsOfTenantsWithoutSubscription(): void
201235
{
202236
Tenant::where('cleanup', true)
203237
->where('created_at', '<=', now()->subDays(15))
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Jobs\Cleanup;
6+
use App\Models\Asset;
7+
use App\Models\Tenant;
8+
use App\Models\Trial;
9+
use App\Models\User;
10+
use Carbon\Carbon;
11+
use Illuminate\Support\Facades\DB;
12+
use Tests\TestCaseWithDb;
13+
use Wave\Subscription;
14+
15+
class CleanupTrialAssetsTest extends TestCaseWithDb
16+
{
17+
public function test_cleanup_deletes_old_trial_assets_without_subscription()
18+
{
19+
// Disable foreign key checks for this test or create necessary relations
20+
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
21+
22+
$tenant = Tenant::create(['name' => 'Test Tenant']);
23+
$user = User::factory()->create(['tenant_id' => $tenant->id]);
24+
25+
$oldTrial = Trial::create([
26+
'hash' => 'old_trial',
27+
'created_by' => $user->id
28+
]);
29+
$oldTrial->created_at = Carbon::now()->subDays(16);
30+
$oldTrial->save();
31+
32+
$asset = Asset::create([
33+
'asset' => 'old-trial-asset.com',
34+
'type' => \App\Enums\AssetTypesEnum::DNS,
35+
'ynh_trial_id' => $oldTrial->id,
36+
'created_by' => $user->id
37+
]);
38+
39+
// No subscription for this user/tenant
40+
(new Cleanup())->handle();
41+
42+
$this->assertDatabaseMissing('am_assets', ['id' => $asset->id]);
43+
}
44+
45+
public function test_cleanup_sets_trial_id_to_null_if_subscription_exists()
46+
{
47+
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
48+
49+
$tenant = Tenant::create(['name' => 'Subscribed Tenant']);
50+
$user = User::factory()->create(['tenant_id' => $tenant->id]);
51+
52+
// Create a subscription for the user
53+
Subscription::create([
54+
'billable_type' => get_class($user),
55+
'billable_id' => $user->id,
56+
'plan_id' => 1, // Assume plan 1 exists or use a real ID
57+
'status' => 'active',
58+
'vendor_slug' => 'paddle',
59+
'vendor_product_id' => 'prod_123',
60+
'vendor_transaction_id' => 'trans_123',
61+
'vendor_customer_id' => 'cust_123',
62+
'vendor_subscription_id' => 'sub_123',
63+
'cycle' => 'month',
64+
]);
65+
66+
$oldTrial = Trial::create([
67+
'hash' => 'subscribed_trial',
68+
'created_by' => $user->id
69+
]);
70+
$oldTrial->created_at = Carbon::now()->subDays(16);
71+
$oldTrial->save();
72+
73+
$asset = Asset::create([
74+
'asset' => 'subscribed-asset.com',
75+
'type' => \App\Enums\AssetTypesEnum::DNS,
76+
'ynh_trial_id' => $oldTrial->id,
77+
'created_by' => $user->id
78+
]);
79+
80+
(new Cleanup())->handle();
81+
82+
$this->assertDatabaseHas('am_assets', [
83+
'id' => $asset->id,
84+
'ynh_trial_id' => null
85+
]);
86+
}
87+
88+
public function test_cleanup_does_not_touch_recent_trial_assets()
89+
{
90+
$tenant = Tenant::create(['name' => 'Recent Trial Tenant']);
91+
$user = User::factory()->create(['tenant_id' => $tenant->id]);
92+
93+
$recentTrial = Trial::create([
94+
'hash' => 'recent_trial',
95+
'created_at' => Carbon::now()->subDays(10),
96+
'created_by' => $user->id
97+
]);
98+
99+
$asset = Asset::create([
100+
'asset' => 'recent-asset.com',
101+
'type' => \App\Enums\AssetTypesEnum::DNS,
102+
'ynh_trial_id' => $recentTrial->id,
103+
'created_by' => $user->id
104+
]);
105+
106+
(new Cleanup())->handle();
107+
108+
$this->assertDatabaseHas('am_assets', [
109+
'id' => $asset->id,
110+
'ynh_trial_id' => $recentTrial->id
111+
]);
112+
}
113+
}

0 commit comments

Comments
 (0)