89 - Add article cleanup job with 30-day retention policy
This commit is contained in:
parent
bab2557e85
commit
cc94ba8e55
3 changed files with 143 additions and 0 deletions
25
app/Jobs/CleanupArticlesJob.php
Normal file
25
app/Jobs/CleanupArticlesJob.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ApprovalStatusEnum;
|
||||
use App\Models\Article;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class CleanupArticlesJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
private const RETENTION_DAYS = 30;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
Article::where('created_at', '<', now()->subDays(self::RETENTION_DAYS))
|
||||
->whereDoesntHave('routeArticles', fn ($q) => $q->whereIn('approval_status', [
|
||||
ApprovalStatusEnum::PENDING,
|
||||
ApprovalStatusEnum::APPROVED,
|
||||
]))
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use App\Jobs\ArticleDiscoveryJob;
|
||||
use App\Jobs\CheckFeedStalenessJob;
|
||||
use App\Jobs\CleanupArticlesJob;
|
||||
use App\Jobs\PublishNextArticleJob;
|
||||
use App\Jobs\SyncChannelPostsJob;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
|
@ -27,3 +28,9 @@
|
|||
->name('check-feed-staleness')
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
|
||||
Schedule::job(new CleanupArticlesJob)
|
||||
->daily()
|
||||
->name('cleanup-old-articles')
|
||||
->withoutOverlapping()
|
||||
->onOneServer();
|
||||
|
|
|
|||
111
tests/Unit/Jobs/CleanupArticlesJobTest.php
Normal file
111
tests/Unit/Jobs/CleanupArticlesJobTest.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Jobs;
|
||||
|
||||
use App\Enums\ApprovalStatusEnum;
|
||||
use App\Jobs\CleanupArticlesJob;
|
||||
use App\Models\Article;
|
||||
use App\Models\Feed;
|
||||
use App\Models\Route;
|
||||
use App\Models\RouteArticle;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CleanupArticlesJobTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_deletes_old_articles_with_no_route_articles(): void
|
||||
{
|
||||
$old = Article::factory()->for(Feed::factory())->create([
|
||||
'created_at' => now()->subDays(31),
|
||||
]);
|
||||
$recent = Article::factory()->for(Feed::factory())->create([
|
||||
'created_at' => now()->subDays(10),
|
||||
]);
|
||||
|
||||
(new CleanupArticlesJob)->handle();
|
||||
|
||||
$this->assertDatabaseMissing('articles', ['id' => $old->id]);
|
||||
$this->assertDatabaseHas('articles', ['id' => $recent->id]);
|
||||
}
|
||||
|
||||
public function test_preserves_old_articles_with_pending_route_articles(): void
|
||||
{
|
||||
$route = Route::factory()->create();
|
||||
$article = Article::factory()->for($route->feed)->create([
|
||||
'created_at' => now()->subDays(31),
|
||||
]);
|
||||
RouteArticle::factory()->for($article)->create([
|
||||
'feed_id' => $route->feed_id,
|
||||
'platform_channel_id' => $route->platform_channel_id,
|
||||
'approval_status' => ApprovalStatusEnum::PENDING,
|
||||
]);
|
||||
|
||||
(new CleanupArticlesJob)->handle();
|
||||
|
||||
$this->assertDatabaseHas('articles', ['id' => $article->id]);
|
||||
}
|
||||
|
||||
public function test_preserves_old_articles_with_approved_route_articles(): void
|
||||
{
|
||||
$route = Route::factory()->create();
|
||||
$article = Article::factory()->for($route->feed)->create([
|
||||
'created_at' => now()->subDays(31),
|
||||
]);
|
||||
RouteArticle::factory()->for($article)->create([
|
||||
'feed_id' => $route->feed_id,
|
||||
'platform_channel_id' => $route->platform_channel_id,
|
||||
'approval_status' => ApprovalStatusEnum::APPROVED,
|
||||
]);
|
||||
|
||||
(new CleanupArticlesJob)->handle();
|
||||
|
||||
$this->assertDatabaseHas('articles', ['id' => $article->id]);
|
||||
}
|
||||
|
||||
public function test_deletes_old_articles_with_only_rejected_route_articles(): void
|
||||
{
|
||||
$route = Route::factory()->create();
|
||||
$article = Article::factory()->for($route->feed)->create([
|
||||
'created_at' => now()->subDays(31),
|
||||
]);
|
||||
RouteArticle::factory()->for($article)->create([
|
||||
'feed_id' => $route->feed_id,
|
||||
'platform_channel_id' => $route->platform_channel_id,
|
||||
'approval_status' => ApprovalStatusEnum::REJECTED,
|
||||
]);
|
||||
|
||||
(new CleanupArticlesJob)->handle();
|
||||
|
||||
$this->assertDatabaseMissing('articles', ['id' => $article->id]);
|
||||
}
|
||||
|
||||
public function test_cascade_deletes_route_articles(): void
|
||||
{
|
||||
$route = Route::factory()->create();
|
||||
$article = Article::factory()->for($route->feed)->create([
|
||||
'created_at' => now()->subDays(31),
|
||||
]);
|
||||
$routeArticle = RouteArticle::factory()->for($article)->create([
|
||||
'feed_id' => $route->feed_id,
|
||||
'platform_channel_id' => $route->platform_channel_id,
|
||||
'approval_status' => ApprovalStatusEnum::REJECTED,
|
||||
]);
|
||||
|
||||
(new CleanupArticlesJob)->handle();
|
||||
|
||||
$this->assertDatabaseMissing('route_articles', ['id' => $routeArticle->id]);
|
||||
}
|
||||
|
||||
public function test_preserves_article_at_exact_retention_boundary(): void
|
||||
{
|
||||
$boundary = Article::factory()->for(Feed::factory())->create([
|
||||
'created_at' => now()->subDays(30),
|
||||
]);
|
||||
|
||||
(new CleanupArticlesJob)->handle();
|
||||
|
||||
$this->assertDatabaseHas('articles', ['id' => $boundary->id]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue