feat: engagment notifications
Laravel CI-CD / Tests Unitaires (push) Failing after 1m39s Details
Laravel CI-CD / Deploy with Kamal (push) Has been skipped Details

This commit is contained in:
Leon Morival 2026-05-26 11:55:39 +02:00
parent df94a23a19
commit f716a205ce
13 changed files with 65 additions and 24 deletions

View File

@ -3,6 +3,7 @@ APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
APP_MOBILE_SCHEME=bowly
APP_PORT=8003 APP_PORT=8003
APP_LOCALE=fr APP_LOCALE=fr
APP_FALLBACK_LOCALE=en APP_FALLBACK_LOCALE=en

View File

@ -7,6 +7,7 @@ use App\Http\Requests\StravaCallbackRequest;
use App\Http\Requests\StravaSyncRequest; use App\Http\Requests\StravaSyncRequest;
use App\Models\StravaConnection; use App\Models\StravaConnection;
use App\Models\WorkoutSessions; use App\Models\WorkoutSessions;
use App\Services\MobileDeepLink;
use App\Services\StravaClient; use App\Services\StravaClient;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\RequestException;
@ -40,10 +41,7 @@ class StravaController extends Controller
->filter(fn ($value): bool => $value !== null && $value !== '') ->filter(fn ($value): bool => $value !== null && $value !== '')
->all(); ->all();
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986); return redirect()->away(MobileDeepLink::to('strava/callback', $params));
$url = 'bowly://strava/callback'.($query ? "?{$query}" : '');
return redirect()->away($url);
} }
public function callback(StravaCallbackRequest $request, StravaClient $strava): JsonResponse public function callback(StravaCallbackRequest $request, StravaClient $strava): JsonResponse

View File

@ -3,6 +3,7 @@
namespace App\Notifications; namespace App\Notifications;
use App\Models\PostReviews; use App\Models\PostReviews;
use App\Services\MobileDeepLink;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -42,7 +43,7 @@ class MealPostCommentedNotification extends Notification
'sound' => 'default', 'sound' => 'default',
'channelId' => 'default', 'channelId' => 'default',
'data' => [ 'data' => [
'url' => "bowly://meals/{$mealPostId}", 'url' => MobileDeepLink::to("meals/{$mealPostId}"),
], ],
]; ];
} }

View File

@ -54,6 +54,8 @@ return [
'url' => env('APP_URL', 'http://localhost'), 'url' => env('APP_URL', 'http://localhost'),
'mobile_scheme' => env('APP_MOBILE_SCHEME', 'bowly'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Timezone | Application Timezone

View File

@ -60,6 +60,7 @@ env:
APP_DEBUG: "false" APP_DEBUG: "false"
APP_NAME: "Bowly" APP_NAME: "Bowly"
APP_URL: https://bemeal.leonmorival.com APP_URL: https://bemeal.leonmorival.com
APP_MOBILE_SCHEME: bowly
SERVER_NAME: ":80" SERVER_NAME: ":80"
APP_RUNTIME: "Laravel\\FrankenPHP\\Runtime" APP_RUNTIME: "Laravel\\FrankenPHP\\Runtime"
FILESYSTEM_DISK: s3 FILESYSTEM_DISK: s3

View File

@ -37,17 +37,6 @@ services:
networks: networks:
- internal - internal
meilisearch:
image: getmeili/meilisearch:latest
container_name: bemeal-meilisearch
restart: unless-stopped
environment:
MEILI_NO_ANALYTICS: "true"
MEILI_MASTER_KEY: "${MEILISEARCH_KEY:?MEILISEARCH_KEY is required}"
volumes:
- meilisearch-data:/meili_data
networks:
- internal
volumes: volumes:
pgsql-data: pgsql-data:
@ -57,9 +46,6 @@ volumes:
redis-data: redis-data:
name: bemeal_redis_data name: bemeal_redis_data
meilisearch-data:
name: bemeal_meilisearch_data
networks: networks:
internal: internal:
external: true external: true

View File

@ -38,6 +38,28 @@ return [
'your_meal' => 'your meal', 'your_meal' => 'your meal',
'moderation_threshold_title' => 'Reports need review', 'moderation_threshold_title' => 'Reports need review',
'moderation_threshold_body' => ':count reports received for :type.', 'moderation_threshold_body' => ':count reports received for :type.',
'engagement_reminders' => [
'publish_meal' => [
'title' => 'Your next meal is waiting',
'body' => "Do not hesitate to publish today's meal on Bowli.",
],
'meal_photo' => [
'title' => 'A plate worth sharing?',
'body' => 'Take a quick photo and keep track of your meal.',
],
'evening_checkin' => [
'title' => 'Small Bowli reminder',
'body' => "Add today's meal to keep your habits on track.",
],
'community_inspiration' => [
'title' => 'Inspire the community',
'body' => 'Publish your meal and give other users new ideas.',
],
'workout_checkin' => [
'title' => 'Meal or workout?',
'body' => 'If you moved today, add your workout in Bowli too.',
],
],
], ],
'reports' => [ 'reports' => [
'created' => 'Report sent. Thanks, our team will review this content.', 'created' => 'Report sent. Thanks, our team will review this content.',

View File

@ -38,6 +38,28 @@ return [
'your_meal' => 'votre plat', 'your_meal' => 'votre plat',
'moderation_threshold_title' => 'Signalements à vérifier', 'moderation_threshold_title' => 'Signalements à vérifier',
'moderation_threshold_body' => ':count signalements reçus pour :type.', 'moderation_threshold_body' => ':count signalements reçus pour :type.',
'engagement_reminders' => [
'publish_meal' => [
'title' => 'Ton prochain plat attend',
'body' => 'Nhésite pas à publier ton repas du jour sur Bowli.',
],
'meal_photo' => [
'title' => 'Une assiette à partager ?',
'body' => 'Prends une photo rapide et garde une trace de ton repas.',
],
'evening_checkin' => [
'title' => 'Petit rappel Bowli',
'body' => 'Ajoute ton plat du jour pour suivre tes habitudes sans perdre le fil.',
],
'community_inspiration' => [
'title' => 'Inspire la communauté',
'body' => 'Publie ton plat et donne des idées aux autres utilisateurs.',
],
'workout_checkin' => [
'title' => 'Repas ou workout ?',
'body' => 'Si tu as bougé aujourdhui, ajoute aussi ta séance dans Bowli.',
],
],
], ],
'reports' => [ 'reports' => [
'created' => 'Signalement envoyé. Merci, notre équipe va examiner ce contenu.', 'created' => 'Signalement envoyé. Merci, notre équipe va examiner ce contenu.',

View File

@ -53,7 +53,6 @@ Route::prefix('legal-documents')->group(function (): void {
Route::middleware(['auth:sanctum', 'verified', 'not_suspended'])->group(function (): void { Route::middleware(['auth:sanctum', 'verified', 'not_suspended'])->group(function (): void {
Route::post('device-tokens', [DeviceTokenController::class, 'store'])->middleware('throttle:device-token'); Route::post('device-tokens', [DeviceTokenController::class, 'store'])->middleware('throttle:device-token');
Route::delete('device-tokens', [DeviceTokenController::class, 'destroy'])->middleware('throttle:device-token'); Route::delete('device-tokens', [DeviceTokenController::class, 'destroy'])->middleware('throttle:device-token');
Route::get('test-notifs', [DeviceTokenController::class, 'notifs'])->middleware('throttle:notifications');
}); });
// Workouts // Workouts

View File

@ -25,3 +25,7 @@ Schedule::command('meal-posts:generate-ai')
->daily() ->daily()
->withoutOverlapping(55) ->withoutOverlapping(55)
->onOneServer(); ->onOneServer();
Schedule::command('notifications:send-engagement-reminders')
->dailyAt('12:30')
->withoutOverlapping(30)
->onOneServer();

View File

@ -43,6 +43,8 @@ it('creates one review per user for a meal post', function () {
}); });
it('sends a push notification to the meal owner when another user comments on their meal', function () { it('sends a push notification to the meal owner when another user comments on their meal', function () {
config()->set('app.mobile_scheme', 'dailymeal');
Http::fake([ Http::fake([
'https://exp.host/*' => Http::response([ 'https://exp.host/*' => Http::response([
'data' => [ 'data' => [
@ -77,7 +79,7 @@ it('sends a push notification to the meal owner when another user comments on th
'sound' => 'default', 'sound' => 'default',
'channelId' => 'default', 'channelId' => 'default',
'data' => [ 'data' => [
'url' => "bowly://meals/{$mealPost->id}", 'url' => "dailymeal://meals/{$mealPost->id}",
], ],
], ],
]); ]);

View File

@ -2,7 +2,7 @@
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
it('schedules expired auth and stale device token cleanup commands', function () { it('schedules maintenance and engagement commands', function () {
Artisan::call('schedule:list', [ Artisan::call('schedule:list', [
'--json' => true, '--json' => true,
]); ]);
@ -13,5 +13,6 @@ it('schedules expired auth and stale device token cleanup commands', function ()
expect($commands) expect($commands)
->toContain('php artisan sanctum:prune-expired --hours=24') ->toContain('php artisan sanctum:prune-expired --hours=24')
->toContain('php artisan auth:clear-resets') ->toContain('php artisan auth:clear-resets')
->toContain('php artisan device-tokens:prune-stale --days=180'); ->toContain('php artisan device-tokens:prune-stale --days=180')
->toContain('php artisan notifications:send-engagement-reminders');
}); });

View File

@ -38,6 +38,8 @@ it('returns a strava authorization url for the api callback', function () {
}); });
it('redirects the strava web callback to the mobile app', function () { it('redirects the strava web callback to the mobile app', function () {
config()->set('app.mobile_scheme', 'dailymeal');
$query = http_build_query([ $query = http_build_query([
'code' => 'authorization-code', 'code' => 'authorization-code',
'scope' => 'read activity:read', 'scope' => 'read activity:read',
@ -46,7 +48,7 @@ it('redirects the strava web callback to the mobile app', function () {
$this $this
->get("/strava/callback?{$query}") ->get("/strava/callback?{$query}")
->assertRedirect("bowly://strava/callback?{$query}"); ->assertRedirect("dailymeal://strava/callback?{$query}");
}); });
it('stores strava tokens from an authorization code', function () { it('stores strava tokens from an authorization code', function () {