Release v1.4.0 #146
8 changed files with 258 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ public function index(): JsonResponse
|
|||
'article_processing_enabled' => Setting::isArticleProcessingEnabled(),
|
||||
'publishing_approvals_enabled' => Setting::isPublishingApprovalsEnabled(),
|
||||
'article_publishing_interval' => Setting::getArticlePublishingInterval(),
|
||||
'daily_publish_cap' => Setting::getDailyPublishCap(),
|
||||
];
|
||||
|
||||
return $this->sendResponse($settings, 'Settings retrieved successfully.');
|
||||
|
|
@ -37,6 +38,7 @@ public function update(Request $request): JsonResponse
|
|||
'article_processing_enabled' => 'boolean',
|
||||
'publishing_approvals_enabled' => 'boolean',
|
||||
'article_publishing_interval' => 'integer|min:0',
|
||||
'daily_publish_cap' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
if (isset($validated['article_processing_enabled'])) {
|
||||
|
|
@ -51,10 +53,15 @@ public function update(Request $request): JsonResponse
|
|||
Setting::setArticlePublishingInterval($validated['article_publishing_interval']);
|
||||
}
|
||||
|
||||
if (isset($validated['daily_publish_cap'])) {
|
||||
Setting::setDailyPublishCap($validated['daily_publish_cap']);
|
||||
}
|
||||
|
||||
$updatedSettings = [
|
||||
'article_processing_enabled' => Setting::isArticleProcessingEnabled(),
|
||||
'publishing_approvals_enabled' => Setting::isPublishingApprovalsEnabled(),
|
||||
'article_publishing_interval' => Setting::getArticlePublishingInterval(),
|
||||
'daily_publish_cap' => Setting::getDailyPublishCap(),
|
||||
];
|
||||
|
||||
return $this->sendResponse(
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ public function __construct()
|
|||
*/
|
||||
public function handle(PublishRouteArticleAction $publishRouteArticle): void
|
||||
{
|
||||
if ($this->dailyCapReached()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$interval = Setting::getArticlePublishingInterval();
|
||||
|
||||
if ($interval > 0) {
|
||||
|
|
@ -70,4 +74,15 @@ public function handle(PublishRouteArticleAction $publishRouteArticle): void
|
|||
|
||||
$publishRouteArticle->execute($routeArticle);
|
||||
}
|
||||
|
||||
private function dailyCapReached(): bool
|
||||
{
|
||||
$cap = Setting::getDailyPublishCap();
|
||||
|
||||
if ($cap <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ArticlePublication::where('published_at', '>=', now()->startOfDay())->count() >= $cap;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ class Settings extends Component
|
|||
|
||||
public int $feedStalenessThreshold = 48;
|
||||
|
||||
public int $dailyPublishCap = 0;
|
||||
|
||||
public ?string $successMessage = null;
|
||||
|
||||
public ?string $errorMessage = null;
|
||||
|
|
@ -26,6 +28,7 @@ public function mount(): void
|
|||
$this->publishingApprovalsEnabled = Setting::isPublishingApprovalsEnabled();
|
||||
$this->articlePublishingInterval = Setting::getArticlePublishingInterval();
|
||||
$this->feedStalenessThreshold = Setting::getFeedStalenessThreshold();
|
||||
$this->dailyPublishCap = Setting::getDailyPublishCap();
|
||||
}
|
||||
|
||||
public function toggleArticleProcessing(): void
|
||||
|
|
@ -62,6 +65,16 @@ public function updateFeedStalenessThreshold(): void
|
|||
$this->showSuccess();
|
||||
}
|
||||
|
||||
public function updateDailyPublishCap(): void
|
||||
{
|
||||
$this->validate([
|
||||
'dailyPublishCap' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
Setting::setDailyPublishCap($this->dailyPublishCap);
|
||||
$this->showSuccess();
|
||||
}
|
||||
|
||||
protected function showSuccess(): void
|
||||
{
|
||||
$this->successMessage = 'Settings updated successfully!';
|
||||
|
|
|
|||
|
|
@ -81,4 +81,14 @@ public static function setFeedStalenessThreshold(int $hours): void
|
|||
{
|
||||
static::set('feed_staleness_threshold', (string) $hours);
|
||||
}
|
||||
|
||||
public static function getDailyPublishCap(): int
|
||||
{
|
||||
return (int) static::get('daily_publish_cap', 0);
|
||||
}
|
||||
|
||||
public static function setDailyPublishCap(int $articles): void
|
||||
{
|
||||
static::set('daily_publish_cap', (string) $articles);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,36 @@ class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-
|
|||
<p class="text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Daily Publish Cap
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Maximum posts per UTC day, counted across all channels. An article sent to
|
||||
three channels counts as three. Set to 0 for no limit.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
wire:model="dailyPublishCap"
|
||||
min="0"
|
||||
step="1"
|
||||
class="w-24 rounded-md border border-gray-300 px-3 py-2 shadow-xs focus:border-indigo-500 focus:ring-indigo-500 text-sm dark:border-gray-600"
|
||||
/>
|
||||
<button
|
||||
wire:click="updateDailyPublishCap"
|
||||
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@error('dailyPublishCap')
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
|
|
|
|||
|
|
@ -114,6 +114,41 @@ public function test_index_returns_article_publishing_interval(): void
|
|||
->assertJsonPath('data.article_publishing_interval', 5);
|
||||
}
|
||||
|
||||
public function test_index_returns_daily_publish_cap(): void
|
||||
{
|
||||
$response = $this->getJson('/api/v1/settings');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'daily_publish_cap',
|
||||
],
|
||||
])
|
||||
->assertJsonPath('data.daily_publish_cap', 0);
|
||||
}
|
||||
|
||||
public function test_update_accepts_valid_daily_publish_cap(): void
|
||||
{
|
||||
$response = $this->putJson('/api/v1/settings', [
|
||||
'daily_publish_cap' => 30,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonPath('data.daily_publish_cap', 30);
|
||||
|
||||
$this->assertSame(30, Setting::getDailyPublishCap());
|
||||
}
|
||||
|
||||
public function test_update_rejects_negative_daily_publish_cap(): void
|
||||
{
|
||||
$response = $this->putJson('/api/v1/settings', [
|
||||
'daily_publish_cap' => -1,
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['daily_publish_cap']);
|
||||
}
|
||||
|
||||
public function test_update_accepts_valid_article_publishing_interval(): void
|
||||
{
|
||||
$response = $this->putJson('/api/v1/settings', [
|
||||
|
|
|
|||
|
|
@ -51,4 +51,36 @@ public function test_update_feed_staleness_threshold_shows_success_message(): vo
|
|||
->call('updateFeedStalenessThreshold')
|
||||
->assertSet('successMessage', 'Settings updated successfully!');
|
||||
}
|
||||
|
||||
public function test_mount_loads_daily_publish_cap(): void
|
||||
{
|
||||
Setting::setDailyPublishCap(25);
|
||||
|
||||
Livewire::test(Settings::class)
|
||||
->assertSet('dailyPublishCap', 25);
|
||||
}
|
||||
|
||||
public function test_mount_defaults_daily_publish_cap_to_unlimited(): void
|
||||
{
|
||||
Livewire::test(Settings::class)
|
||||
->assertSet('dailyPublishCap', 0);
|
||||
}
|
||||
|
||||
public function test_update_daily_publish_cap_saves_value(): void
|
||||
{
|
||||
Livewire::test(Settings::class)
|
||||
->set('dailyPublishCap', 40)
|
||||
->call('updateDailyPublishCap')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSame(40, Setting::getDailyPublishCap());
|
||||
}
|
||||
|
||||
public function test_update_daily_publish_cap_validates_minimum(): void
|
||||
{
|
||||
Livewire::test(Settings::class)
|
||||
->set('dailyPublishCap', -1)
|
||||
->call('updateDailyPublishCap')
|
||||
->assertHasErrors(['dailyPublishCap' => 'min']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,6 +227,122 @@ public function test_handle_skips_publishing_when_last_publication_within_interv
|
|||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_handle_skips_publishing_when_daily_cap_reached(): void
|
||||
{
|
||||
$this->createApprovedRouteArticle();
|
||||
|
||||
ArticlePublication::factory()->count(3)->create(['published_at' => now()->subHours(2)]);
|
||||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
$job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService));
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_handle_publishes_when_below_daily_cap(): void
|
||||
{
|
||||
$this->createApprovedRouteArticle();
|
||||
|
||||
ArticlePublication::factory()->count(2)->create(['published_at' => now()->subHours(2)]);
|
||||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
->once()
|
||||
->andReturn(['title' => 'Test Article', 'description' => 'Test description']);
|
||||
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
||||
->once()
|
||||
->andReturn(PublishOutcome::published($this->makePublication()));
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
$job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService));
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_handle_publishes_when_daily_cap_is_zero(): void
|
||||
{
|
||||
$this->createApprovedRouteArticle();
|
||||
|
||||
ArticlePublication::factory()->count(50)->create(['published_at' => now()->subHours(2)]);
|
||||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(0);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
->once()
|
||||
->andReturn(['title' => 'Test Article', 'description' => 'Test description']);
|
||||
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
||||
->once()
|
||||
->andReturn(PublishOutcome::published($this->makePublication()));
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
$job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService));
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_daily_cap_counts_each_channel_publication_separately(): void
|
||||
{
|
||||
$this->createApprovedRouteArticle();
|
||||
|
||||
$article = Article::factory()->create();
|
||||
ArticlePublication::factory()->count(3)->create([
|
||||
'article_id' => $article->id,
|
||||
'published_at' => now()->subHours(2),
|
||||
]);
|
||||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
|
||||
$articleFetcherMock->shouldNotReceive('fetchArticleData');
|
||||
$publishingServiceMock->shouldNotReceive('publishRouteArticle');
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
$job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService));
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_handle_ignores_publications_from_previous_days(): void
|
||||
{
|
||||
$this->createApprovedRouteArticle();
|
||||
|
||||
ArticlePublication::factory()->count(5)->create(['published_at' => now()->subDay()]);
|
||||
Setting::setArticlePublishingInterval(0);
|
||||
Setting::setDailyPublishCap(3);
|
||||
|
||||
$articleFetcherMock = Mockery::mock(ArticleFetcher::class);
|
||||
$articleFetcherMock->shouldReceive('fetchArticleData')
|
||||
->once()
|
||||
->andReturn(['title' => 'Test Article', 'description' => 'Test description']);
|
||||
|
||||
$publishingServiceMock = Mockery::mock(ArticlePublishingService::class);
|
||||
$publishingServiceMock->shouldReceive('publishRouteArticle')
|
||||
->once()
|
||||
->andReturn(PublishOutcome::published($this->makePublication()));
|
||||
|
||||
$job = new PublishNextArticleJob;
|
||||
$job->handle(new PublishRouteArticleAction($articleFetcherMock, $publishingServiceMock, $this->notificationService));
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_handle_publishes_when_last_publication_beyond_interval(): void
|
||||
{
|
||||
$this->createApprovedRouteArticle();
|
||||
|
|
|
|||
Loading…
Reference in a new issue