diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc index 81d3c7e1..1128ebac 100644 --- a/.cursor/rules/laravel-boost.mdc +++ b/.cursor/rules/laravel-boost.mdc @@ -11,7 +11,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.22 +- php - 8.4.23 - filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 48ac46a6..d90aed87 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.22 +- php - 8.4.23 - filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 48ac46a6..d90aed87 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.22 +- php - 8.4.23 - filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 diff --git a/AGENTS.md b/AGENTS.md index 48ac46a6..d90aed87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.22 +- php - 8.4.23 - filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 diff --git a/CLAUDE.md b/CLAUDE.md index 48ac46a6..d90aed87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.22 +- php - 8.4.23 - filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 diff --git a/app/Livewire/Customer/Plugins/Show.php b/app/Livewire/Customer/Plugins/Show.php index b6ccc56e..304ff020 100644 --- a/app/Livewire/Customer/Plugins/Show.php +++ b/app/Livewire/Customer/Plugins/Show.php @@ -6,9 +6,11 @@ use App\Enums\PluginType; use App\Jobs\ReviewPluginRepository; use App\Models\Plugin; +use App\Notifications\PluginPendingReview; use App\Notifications\PluginSubmitted; use App\Services\GitHubUserService; use Flux\Flux; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\ValidationException; use Livewire\Attributes\Computed; @@ -164,6 +166,9 @@ public function submitForReview(): void // Notify $user->notify(new PluginSubmitted($this->plugin)); + Notification::route('mail', 'support@nativephp.com') + ->notify(new PluginPendingReview($this->plugin)); + Flux::toast(variant: 'success', text: 'Your plugin has been submitted for review!'); } diff --git a/app/Livewire/ShowcaseSubmissionForm.php b/app/Livewire/ShowcaseSubmissionForm.php index 999ee530..454da2c7 100644 --- a/app/Livewire/ShowcaseSubmissionForm.php +++ b/app/Livewire/ShowcaseSubmissionForm.php @@ -3,6 +3,8 @@ namespace App\Livewire; use App\Models\Showcase; +use App\Notifications\ShowcaseSubmitted; +use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Storage; use Livewire\Attributes\Validate; use Livewire\Component; @@ -167,6 +169,9 @@ public function submit(): mixed 'approved_by' => null, ]); + Notification::route('mail', 'support@nativephp.com') + ->notify(new ShowcaseSubmitted($this->showcase, resubmitted: true)); + return to_route('customer.showcase.index') ->with('warning', 'Your submission has been updated and sent back for review.'); } @@ -175,11 +180,14 @@ public function submit(): mixed ->with('success', 'Your submission has been updated.'); } - Showcase::create([ + $showcase = Showcase::create([ 'user_id' => auth()->id(), ...$data, ]); + Notification::route('mail', 'support@nativephp.com') + ->notify(new ShowcaseSubmitted($showcase)); + return to_route('customer.showcase.index') ->with('success', 'Thank you! Your app has been submitted for review.'); } diff --git a/app/Livewire/WallOfLoveSubmissionForm.php b/app/Livewire/WallOfLoveSubmissionForm.php index 10ab9aca..f4348a9a 100644 --- a/app/Livewire/WallOfLoveSubmissionForm.php +++ b/app/Livewire/WallOfLoveSubmissionForm.php @@ -3,6 +3,8 @@ namespace App\Livewire; use App\Models\WallOfLoveSubmission; +use App\Notifications\WallOfLoveSubmitted; +use Illuminate\Support\Facades\Notification; use Livewire\Component; use Livewire\Features\SupportFileUploads\WithFileUploads; @@ -92,7 +94,7 @@ public function submit(): mixed $photoPath = $this->photo->store('wall-of-love-photos', 'public'); } - WallOfLoveSubmission::create([ + $submission = WallOfLoveSubmission::create([ 'user_id' => auth()->id(), 'name' => $this->name, 'company' => $this->company ?: null, @@ -101,6 +103,9 @@ public function submit(): mixed 'testimonial' => $this->testimonial ?: null, ]); + Notification::route('mail', 'support@nativephp.com') + ->notify(new WallOfLoveSubmitted($submission)); + return to_route('dashboard')->with('success', 'Thank you! Your submission has been received and is awaiting review.'); } diff --git a/app/Notifications/PluginPendingReview.php b/app/Notifications/PluginPendingReview.php new file mode 100644 index 00000000..143052d9 --- /dev/null +++ b/app/Notifications/PluginPendingReview.php @@ -0,0 +1,43 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + $plugin = $this->plugin->loadMissing('user'); + $user = $plugin->user; + + return (new MailMessage) + ->subject('New Plugin Submission: '.$plugin->name) + ->greeting('A plugin has been submitted for review!') + ->line("**Plugin:** {$plugin->name}") + ->when($plugin->display_name, fn (MailMessage $message) => $message->line("**Display name:** {$plugin->display_name}")) + ->line('**Submitted by:** '.($user?->name ?? 'Unknown')) + ->line('**Type:** '.$plugin->type->label()) + ->when($plugin->tier, fn (MailMessage $message) => $message->line('**Tier:** '.$plugin->tier->label())) + ->action('Review Plugin', PluginResource::getUrl('edit', ['record' => $plugin])); + } +} diff --git a/app/Notifications/ShowcaseSubmitted.php b/app/Notifications/ShowcaseSubmitted.php new file mode 100644 index 00000000..b2366b5b --- /dev/null +++ b/app/Notifications/ShowcaseSubmitted.php @@ -0,0 +1,58 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + $showcase = $this->showcase->loadMissing('user'); + $user = $showcase->user; + + $platforms = collect([ + $showcase->has_mobile ? 'Mobile' : null, + $showcase->has_desktop ? 'Desktop' : null, + ])->filter()->implode(', '); + + $subject = $this->resubmitted + ? 'Showcase Re-submitted for Review: '.$showcase->title + : 'New Showcase Submission: '.$showcase->title; + + $greeting = $this->resubmitted + ? 'An approved showcase has been updated and sent back for review.' + : 'A new app has been submitted to the Showcase!'; + + return (new MailMessage) + ->subject($subject) + ->greeting($greeting) + ->line("**Title:** {$showcase->title}") + ->line('**Submitted by:** '.($user?->name ?? 'Unknown')) + ->when($platforms !== '', fn (MailMessage $message) => $message->line("**Platforms:** {$platforms}")) + ->line('**Description:**') + ->line(Str::limit($showcase->description, 500)) + ->action('Review Showcase', ShowcaseResource::getUrl('edit', ['record' => $showcase])); + } +} diff --git a/app/Notifications/WallOfLoveSubmitted.php b/app/Notifications/WallOfLoveSubmitted.php new file mode 100644 index 00000000..617dc713 --- /dev/null +++ b/app/Notifications/WallOfLoveSubmitted.php @@ -0,0 +1,44 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + public function toMail(object $notifiable): MailMessage + { + $submission = $this->submission->loadMissing('user'); + $user = $submission->user; + + return (new MailMessage) + ->subject('New Wall of Love Submission from '.$submission->name) + ->greeting('A new Wall of Love submission has been received!') + ->line("**Name:** {$submission->name}") + ->line('**Submitted by:** '.($user?->name ?? 'Unknown')) + ->when($submission->company, fn (MailMessage $message) => $message->line("**Company:** {$submission->company}")) + ->when($submission->url, fn (MailMessage $message) => $message->line("**URL:** {$submission->url}")) + ->when($submission->testimonial, fn (MailMessage $message) => $message->line('**Testimonial:**')->line(Str::limit($submission->testimonial, 500))) + ->action('Review Submission', WallOfLoveSubmissionResource::getUrl('edit', ['record' => $submission])); + } +} diff --git a/tests/Feature/CustomerPluginReviewChecksTest.php b/tests/Feature/CustomerPluginReviewChecksTest.php index 6ef83f55..1f52edc3 100644 --- a/tests/Feature/CustomerPluginReviewChecksTest.php +++ b/tests/Feature/CustomerPluginReviewChecksTest.php @@ -6,6 +6,7 @@ use App\Livewire\Customer\Plugins\Show; use App\Models\DeveloperAccount; use App\Models\User; +use App\Notifications\PluginPendingReview; use App\Notifications\PluginSubmitted; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; @@ -129,6 +130,14 @@ public function submitting_a_plugin_for_review_runs_review_checks(): void Notification::assertSentTo($user, PluginSubmitted::class, function (PluginSubmitted $notification) use ($plugin) { return $notification->plugin->id === $plugin->id; }); + + Notification::assertSentOnDemand( + PluginPendingReview::class, + function (PluginPendingReview $notification, array $channels, object $notifiable) use ($plugin) { + return $notifiable->routes['mail'] === 'support@nativephp.com' + && $notification->plugin->id === $plugin->id; + } + ); } /** @test */ diff --git a/tests/Feature/ShowcaseSubmissionTest.php b/tests/Feature/ShowcaseSubmissionTest.php new file mode 100644 index 00000000..45ddc208 --- /dev/null +++ b/tests/Feature/ShowcaseSubmissionTest.php @@ -0,0 +1,126 @@ +create(); + + Livewire::actingAs($user) + ->test(ShowcaseSubmissionForm::class) + ->set('title', 'My Awesome App') + ->set('description', 'A great app built with NativePHP.') + ->set('hasMobile', true) + ->set('certifiedNativephp', true) + ->call('submit') + ->assertHasNoErrors() + ->assertRedirect(); + + $showcase = Showcase::where('title', 'My Awesome App')->first(); + $this->assertNotNull($showcase); + $this->assertEquals($user->id, $showcase->user_id); + + Notification::assertSentOnDemand( + ShowcaseSubmitted::class, + function (ShowcaseSubmitted $notification, array $channels, object $notifiable) use ($showcase) { + return $notifiable->routes['mail'] === 'support@nativephp.com' + && $notification->showcase->id === $showcase->id + && $notification->resubmitted === false; + } + ); + } + + public function test_editing_an_approved_showcase_sends_it_back_for_review_and_notifies_support(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $showcase = Showcase::factory()->approved()->mobile()->create([ + 'user_id' => $user->id, + 'title' => 'Existing App', + 'description' => 'The original description.', + ]); + + Livewire::actingAs($user) + ->test(ShowcaseSubmissionForm::class, ['showcase' => $showcase]) + ->assertSet('isEditing', true) + ->set('description', 'An updated description.') + ->call('submit') + ->assertHasNoErrors() + ->assertRedirect(); + + $showcase->refresh(); + $this->assertTrue($showcase->isPending()); + $this->assertNull($showcase->approved_at); + + Notification::assertSentOnDemand( + ShowcaseSubmitted::class, + function (ShowcaseSubmitted $notification, array $channels, object $notifiable) use ($showcase) { + return $notifiable->routes['mail'] === 'support@nativephp.com' + && $notification->showcase->id === $showcase->id + && $notification->resubmitted === true; + } + ); + } + + public function test_editing_a_still_pending_showcase_does_not_notify_support(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $showcase = Showcase::factory()->pending()->mobile()->create([ + 'user_id' => $user->id, + ]); + + Livewire::actingAs($user) + ->test(ShowcaseSubmissionForm::class, ['showcase' => $showcase]) + ->assertSet('isEditing', true) + ->set('description', 'A tweaked description.') + ->call('submit') + ->assertHasNoErrors(); + + Notification::assertNothingSent(); + } + + public function test_showcase_notification_email_includes_submission_details(): void + { + $user = User::factory()->create(['name' => 'Ada Lovelace']); + $showcase = Showcase::factory()->mobile()->create([ + 'user_id' => $user->id, + 'title' => 'Difference Engine', + 'description' => 'A calculating machine for the modern age.', + ]); + + $rendered = (new ShowcaseSubmitted($showcase)) + ->toMail(new AnonymousNotifiable) + ->render() + ->toHtml(); + + $this->assertStringContainsString('Difference Engine', $rendered); + $this->assertStringContainsString('Ada Lovelace', $rendered); + $this->assertStringContainsString('Mobile', $rendered); + + $resubmitted = (new ShowcaseSubmitted($showcase, resubmitted: true)) + ->toMail(new AnonymousNotifiable) + ->render() + ->toHtml(); + + $this->assertStringContainsString('sent back for review', $resubmitted); + } +} diff --git a/tests/Feature/WallOfLoveSubmissionTest.php b/tests/Feature/WallOfLoveSubmissionTest.php new file mode 100644 index 00000000..3bd0cd67 --- /dev/null +++ b/tests/Feature/WallOfLoveSubmissionTest.php @@ -0,0 +1,67 @@ +create(); + + Livewire::actingAs($user) + ->test(WallOfLoveSubmissionForm::class) + ->set('name', 'Grace Hopper') + ->set('company', 'US Navy') + ->set('url', 'https://example.com') + ->set('testimonial', 'NativePHP shipped my app in a weekend.') + ->call('submit') + ->assertHasNoErrors() + ->assertRedirect(); + + $submission = WallOfLoveSubmission::where('name', 'Grace Hopper')->first(); + $this->assertNotNull($submission); + $this->assertEquals($user->id, $submission->user_id); + + Notification::assertSentOnDemand( + WallOfLoveSubmitted::class, + function (WallOfLoveSubmitted $notification, array $channels, object $notifiable) use ($submission) { + return $notifiable->routes['mail'] === 'support@nativephp.com' + && $notification->submission->id === $submission->id; + } + ); + } + + public function test_wall_of_love_notification_email_includes_submission_details(): void + { + $user = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->create([ + 'user_id' => $user->id, + 'name' => 'Grace Hopper', + 'company' => 'US Navy', + 'testimonial' => 'NativePHP shipped my app in a weekend.', + ]); + + $rendered = (new WallOfLoveSubmitted($submission)) + ->toMail(new AnonymousNotifiable) + ->render() + ->toHtml(); + + $this->assertStringContainsString('Grace Hopper', $rendered); + $this->assertStringContainsString('US Navy', $rendered); + $this->assertStringContainsString('NativePHP shipped my app in a weekend.', $rendered); + } +}