Skip to content

Commit 98a7afd

Browse files
committed
feat: LAR 10 Send a Telegram notification for articles that are submitted but neither approved nor declined.
1 parent 4f3c3b9 commit 98a7afd

File tree

9 files changed

+157
-15
lines changed

9 files changed

+157
-15
lines changed

.phpunit.cache/test-results

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":"pest_2.35.0","defects":[],"times":{"P\\Tests\\Feature\\NotifyPendingArticlesTest::__pest_evaluable_it_will_send_a_notification_when_there_are_pending_articles":0.207,"P\\Tests\\Feature\\NotifyPendingArticlesTest::__pest_evaluable_it_will_not_send_a_notification_when_there_are_no_pending_articles":0.061,"P\\Tests\\Integration\\ChannelTest::__pest_evaluable_channel_can_have_children":0.137,"P\\Tests\\Integration\\ChannelTest::__pest_evaluable_child_channel_can_be_a_parent":0.015,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_can_find_by_slug":0.046,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_can_give_an_excerpt_of_its_body":0.057,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_html_in_excerpts_is_markdown_converted":0.016,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_can_have_many_tags":0.024,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_records_activity_when_a_discussion_is_created":0.025,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_generates_a_slug_when_valid_url_characters_provided":0.007,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_generates_a_unique_slug_when_valid_url_characters_provided":0.017,"P\\Tests\\Integration\\DiscussionTest::__pest_evaluable_it_generates_a_slug_when_invalid_url_characters_provided":0.011,"P\\Tests\\Integration\\ReplyTest::__pest_evaluable_it_records_activity_when_a_reply_is_send":0.004,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_can_find_by_slug":0.013,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_can_give_an_excerpt_of_its_body":0.006,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_html_in_excerpts_is_markdown_converted":0.017,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_can_have_many_channels":0.003,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_records_activity_when_a_thread_is_created":0.003,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_its_conversation_is_old_when_the_oldest_reply_was_six_months_ago":0.049,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_its_conversation_is_old_when_there_are_no_replies_but_the_creation_date_was_six_months_ago":0.021,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_we_can_mark_and_unmark_a_reply_as_the_solution":0.065,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_can_retrieve_the_latest_threads_in_a_correct_order":0.053,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_can_retrieve_only_resolved_threads":0.056,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_can_retrieve_only_active_threads":0.052,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_generates_a_slug_when_valid_url_characters_provided":0.008,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_generates_a_unique_slug_when_valid_url_characters_provided":0.019,"P\\Tests\\Integration\\ThreadTest::__pest_evaluable_it_generates_a_slug_when_invalid_url_characters_provided":0.012,"P\\Tests\\Feature\\Cleanup\\DeleteOldUnverifiedUsersTest::__pest_evaluable_it_will_delete_unverified_users_after_some_days":0.387,"P\\Tests\\Feature\\Cleanup\\DeleteOldUnverifiedUsersTest::__pest_evaluable_it_will_not_delete_verified_users":0.014,"P\\Tests\\Feature\\UserActivitiesTest::__pest_evaluable_it_records_activity_when_an_article_is_created":0.002,"P\\Tests\\Feature\\UserActivitiesTest::__pest_evaluable_it_get_feed_from_any_user":0.002}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Console\Commands;
6+
7+
use App\Models\Article;
8+
use Illuminate\Console\Command;
9+
use Illuminate\Notifications\AnonymousNotifiable;
10+
use App\Notifications\PendingArticlesNotification;
11+
12+
final class NotifyPendingArticles extends Command
13+
{
14+
protected $signature = 'lcm:notify-pending-articles';
15+
16+
protected $description = 'Send a Telegram notification for articles that are submitted but neither approved nor declined';
17+
18+
public function handle(AnonymousNotifiable $notifiable): void
19+
{
20+
$pendingArticles = Article::awaitingApproval()->get();
21+
22+
if ($pendingArticles->isNotEmpty()) {
23+
$notifiable->notify(new PendingArticlesNotification($pendingArticles));
24+
}
25+
}
26+
}

app/Console/Kernel.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ protected function schedule(Schedule $schedule): void
2323
$schedule->command('lcm:post-article-to-telegram')->everyFourHours();
2424
$schedule->command('lcm:send-unverified-mails')->weeklyOn(1, '8:00');
2525
$schedule->command('sitemap:generate')->daily();
26+
$schedule->command('lcm:notify-pending-articles')->everyTwoDays();
2627
}
2728
}
2829

2930
protected function commands(): void
3031
{
3132
$this->load(__DIR__.'/Commands');
3233
}
33-
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Notifications;
6+
7+
use Illuminate\Bus\Queueable;
8+
use Illuminate\Notifications\Notification;
9+
use Illuminate\Contracts\Queue\ShouldQueue;
10+
use Illuminate\Database\Eloquent\Collection;
11+
use Illuminate\Notifications\Messages\MailMessage;
12+
use NotificationChannels\Telegram\TelegramChannel;
13+
use NotificationChannels\Telegram\TelegramMessage;
14+
15+
final class PendingArticlesNotification extends Notification
16+
{
17+
use Queueable;
18+
19+
public function __construct(public Collection $pendingArticles) {}
20+
21+
public function via(mixed $notifiable): array
22+
{
23+
return [TelegramChannel::class];
24+
}
25+
26+
public function toTelegram(): TelegramMessage
27+
{
28+
$message = $this->content();
29+
30+
return TelegramMessage::create()
31+
->to(config('services.telegram-bot-api.channel'))
32+
->content($message);
33+
}
34+
35+
private function content(): string
36+
{
37+
$message = __("Pending approval articles:\n");
38+
foreach ($this->pendingArticles as $article) {
39+
$url = route('articles.show', $article->slug);
40+
41+
$message .= __("• Title: [:title](:url)\n", [
42+
'title' => $article->title,
43+
'url' => $url,
44+
]);
45+
46+
$message .= __("• By: [@:username](:profile_url)\n", [
47+
'username' => $article->user?->username,
48+
'profile_url' => route('profile', $article->user?->username),
49+
]);
50+
51+
$message .= __("• Submitted on: :date\n\n", [
52+
'date' => $article->submitted_at->translatedFormat('d/m/Y'),
53+
]);
54+
}
55+
56+
return $message;
57+
}
58+
}

lang/en.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@
33
"The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute.",
44
"Verify Email Address": "Verify Email Address",
55
"Please click the button below to verify your email address.": "Please click the button below to verify your email address.",
6-
"If you did not create an account, no further action is required.": "If you did not create an account, no further action is required."
6+
"If you did not create an account, no further action is required.": "If you did not create an account, no further action is required.",
7+
"Pending approval articles:" : "Pending approval articles:",
8+
"• Title: [:title](:url)" : "• Title: [:title](:url)",
9+
"• By: [@:username](:profile_url)" : "• By: [@:username](:profile_url)",
10+
"• Submitted on: :date" : "• Submitted on: :date"
711
}

lang/fr.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,9 @@
33
"The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Le champ :attribute donné est apparu dans une fuite de données. Veuillez choisir un autre :attribute.",
44
"Verify Email Address": "Vérifier l'adresse e-mail",
55
"Please click the button below to verify your email address.": "Veuillez cliquer sur le bouton ci-dessous pour vérifier votre adresse email.",
6-
"If you did not create an account, no further action is required.": "Si vous n'avez pas créé de compte, aucune autre action n'est requise."
6+
"If you did not create an account, no further action is required.": "Si vous n'avez pas créé de compte, aucune autre action n'est requise.",
7+
"Pending approval articles:" : "Articles soumis en attente d'approbation:",
8+
"• Title: [:title](:url)" : "• Titre: [:title](:url)",
9+
"• By: [@:username](:profile_url)" : "• Par: [@:username](:profile_url)",
10+
"• Submitted on: :date" : "• Soumis le: :date"
711
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Console\Commands\NotifyPendingArticles;
6+
use App\Models\Article;
7+
use App\Notifications\PendingArticlesNotification;
8+
use Illuminate\Notifications\AnonymousNotifiable;
9+
use Illuminate\Support\Facades\Notification;
10+
11+
beforeEach(fn() => Notification::fake());
12+
13+
it('will send a notification when there are pending articles', function (): void {
14+
Article::factory()->createMany([
15+
[
16+
'submitted_at' => now(),
17+
],
18+
[
19+
'submitted_at' => now()->subDay(),
20+
'approved_at' => now(),
21+
],
22+
[
23+
'submitted_at' => now()->subDay(),
24+
'declined_at' => now(),
25+
],
26+
]);
27+
28+
$this->assertDatabaseCount('articles', 3);
29+
30+
$this->artisan(NotifyPendingArticles::class)->assertExitCode(0);
31+
32+
Notification::assertSentTo(
33+
new AnonymousNotifiable(),
34+
PendingArticlesNotification::class,
35+
fn($notification) => $notification->pendingArticles->count() === 1
36+
);
37+
38+
Notification::assertCount(1);
39+
});
40+
41+
it('will not send a notification when there are no pending articles', function (): void {
42+
Article::factory()->createMany([
43+
[
44+
'submitted_at' => now()->subDay(),
45+
'approved_at' => now(),
46+
],
47+
[
48+
'submitted_at' => now()->subDay(),
49+
'declined_at' => now(),
50+
],
51+
]);
52+
53+
$this->assertDatabaseCount('articles', 2);
54+
$this->artisan(NotifyPendingArticles::class)->assertExitCode(0);
55+
56+
Notification::assertNothingSent();
57+
Notification::assertCount(0);
58+
});

tests/Integration/DiscussionTest.php

+1-6
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@
55
use App\Models\Activity;
66
use App\Models\Discussion;
77
use App\Models\Tag;
8-
use Illuminate\Foundation\Testing\DatabaseMigrations;
9-
use Illuminate\Foundation\Testing\RefreshDatabase;
10-
11-
uses(RefreshDatabase::class);
12-
uses(DatabaseMigrations::class);
138

149
it('can find by slug', function (): void {
1510
Discussion::factory()->create(['slug' => 'foo']);
@@ -74,4 +69,4 @@
7469

7570
// When providing a slug with invalid url characters, a random 5 character string is returned.
7671
expect($discussion->slug())->toMatch('/\w{5}/');
77-
});
72+
});

tests/Integration/ThreadTest.php

+1-6
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88
use App\Models\Thread;
99
use App\Models\User;
1010
use Carbon\Carbon;
11-
use Illuminate\Foundation\Testing\DatabaseMigrations;
12-
use Illuminate\Foundation\Testing\RefreshDatabase;
13-
14-
uses(RefreshDatabase::class);
15-
uses(DatabaseMigrations::class);
1611

1712
it('can find by slug', function (): void {
1813
Thread::factory()->create(['slug' => 'foo']);
@@ -192,4 +187,4 @@ function createActiveThread(): Thread
192187
$reply->save();
193188

194189
return $thread;
195-
}
190+
}

0 commit comments

Comments
 (0)