From 00e031a00924244706b19f6d927ffd1d87c59e67 Mon Sep 17 00:00:00 2001 From: mohanmd Date: Mon, 6 Jul 2026 17:36:25 +0530 Subject: [PATCH] charity event contact updated --- .env.example | 1 + app/Console/Commands/SendReminders.php | 26 +++- .../Api/EmployerAnnouncementController.php | 85 ++++++++++--- .../Api/EmployerProfileController.php | 9 +- .../Api/EmployerReviewController.php | 23 +++- .../Api/WorkerReviewController.php | 24 ++-- config/reminders.php | 1 + phpunit.xml | 1 + public/swagger.json | 17 ++- tests/Feature/AnnouncementApiTest.php | 86 +++++++++++++ tests/Feature/NewRemindersAndSettingsTest.php | 115 ++++++++++++++++++ 11 files changed, 346 insertions(+), 42 deletions(-) diff --git a/.env.example b/.env.example index f7d5f88..d51e328 100644 --- a/.env.example +++ b/.env.example @@ -73,6 +73,7 @@ VITE_APP_NAME="${APP_NAME}" # Notification Reminder Settings WORKER_NO_RESPONSE_REMINDER_DAYS=14 REVIEW_ELIGIBILITY_MONTHS=3 +REVIEW_ELIGIBILITY_MINUTES=10 REVIEW_EDIT_PERIOD_DAYS=7 EMPLOYER_HIRE_CONFIRM_REMINDER_DAYS=3 WORKER_JOIN_CONFIRM_REMINDER_DAYS=3 diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php index 692ad8c..879bc01 100644 --- a/app/Console/Commands/SendReminders.php +++ b/app/Console/Commands/SendReminders.php @@ -374,7 +374,9 @@ private function checkWorkerNoResponse() private function checkReviewReminders() { $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); - $level = "{$eligibilityMonths}_months"; + $eligibilityMinutes = config('reminders.review_eligibility_minutes'); + $isMinutes = ($eligibilityMinutes !== null && $eligibilityMinutes > 0); + $level = $isMinutes ? "{$eligibilityMinutes}_minutes" : "{$eligibilityMonths}_months"; // 1. Standard Job Applications with status 'hired' $applications = \App\Models\JobApplication::where('status', 'hired') @@ -390,9 +392,16 @@ private function checkReviewReminders() if (!$employer || !$worker) continue; $joiningDate = Carbon::parse($app->joining_confirmed_at); - $months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false); + + $eligible = false; + if ($isMinutes) { + $eligible = $joiningDate->diffInMinutes(now(), false) >= $eligibilityMinutes; + } else { + $months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false); + $eligible = $months >= $eligibilityMonths; + } - if ($months >= $eligibilityMonths) { + if ($eligible) { // Remind worker to review employer if (!$this->alreadySent($worker, 'review_employer_reminder', $app, $level)) { $this->info("Sending review reminder to worker {$worker->name} to review employer {$employer->name}."); @@ -428,9 +437,16 @@ private function checkReviewReminders() if (!$worker || !$employer) continue; $joiningDate = $offer->work_date ? Carbon::parse($offer->work_date) : $offer->updated_at; - $months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false); + + $eligible = false; + if ($isMinutes) { + $eligible = $joiningDate->diffInMinutes(now(), false) >= $eligibilityMinutes; + } else { + $months = $joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false); + $eligible = $months >= $eligibilityMonths; + } - if ($months >= $eligibilityMonths) { + if ($eligible) { // Remind worker to review employer if (!$this->alreadySent($worker, 'review_employer_reminder', $offer, $level)) { $this->info("Sending review reminder to worker {$worker->name} to review employer {$employer->name} (direct offer)."); diff --git a/app/Http/Controllers/Api/EmployerAnnouncementController.php b/app/Http/Controllers/Api/EmployerAnnouncementController.php index 751ec5b..97c9375 100644 --- a/app/Http/Controllers/Api/EmployerAnnouncementController.php +++ b/app/Http/Controllers/Api/EmployerAnnouncementController.php @@ -108,15 +108,26 @@ public function getMyAnnouncements(Request $request) $announcements = $query->skip($offset)->take($perPage)->get() ->map(function ($announcement) { + $charityDetails = null; + $content = $announcement->body; + if (strpos($announcement->body, '{"type":"Charity"') === 0) { + $decoded = json_decode($announcement->body, true); + if ($decoded) { + $charityDetails = $decoded; + $content = $decoded['content'] ?? $announcement->body; + } + } + return [ 'id' => $announcement->id, 'title' => $announcement->title, - 'body' => $announcement->body, + 'body' => $content, 'type' => $announcement->type, 'status' => $announcement->status ?? 'pending', 'remarks' => $announcement->remarks, 'created_at' => $announcement->created_at->toISOString(), 'time_ago' => $announcement->created_at->diffForHumans(), + 'charity_details' => $charityDetails, ]; }); @@ -155,12 +166,33 @@ public function createAnnouncement(Request $request) /** @var User $employer */ $employer = $request->attributes->get('employer'); - $validator = Validator::make($request->all(), [ + $isCharity = $request->type === 'charity' || + $request->has('event_date') || + $request->has('location_details') || + $request->has('provided_items') || + $request->has('contact_person_name'); + + $rules = [ 'title' => 'required|string|max:255', 'body' => 'required_without:content|string|max:5000', 'content' => 'required_without:body|string|max:5000', - 'type' => 'nullable|string|in:info,warning,success', - ]); + 'type' => 'nullable|string|in:info,warning,success,charity', + ]; + + if ($isCharity) { + $rules['event_date'] = 'required|string'; + $rules['event_time'] = 'required_without_all:start_time,end_time|nullable|string'; + $rules['start_time'] = 'required_without:event_time|nullable|string'; + $rules['end_time'] = 'required_without:event_time|nullable|string'; + $rules['provided_items'] = 'required|string'; + $rules['location_details'] = 'required|string'; + $rules['location_pin'] = 'required|string|url'; + $rules['contact_person_name'] = 'required|string|max:255'; + $rules['contact_number'] = 'required|string|regex:/^\d{7,15}$/'; + $rules['country_code'] = 'required|string|regex:/^\+\d{1,4}$/'; + } + + $validator = Validator::make($request->all(), $rules); if ($validator->fails()) { return response()->json([ @@ -172,27 +204,45 @@ public function createAnnouncement(Request $request) try { $bodyText = $request->body ?? $request->content; - - // Append extra event details if they are provided - $extras = []; - if ($request->event_date) $extras[] = "Date: " . $request->event_date; - if ($request->event_time) $extras[] = "Time: " . $request->event_time; - if ($request->location_details) $extras[] = "Location: " . $request->location_details; - if ($request->location_pin) $extras[] = "Map Pin: " . $request->location_pin; - if ($request->provided_items) $extras[] = "Provided: " . $request->provided_items; - - if (!empty($extras)) { - $bodyText .= "\n\n" . implode("\n", $extras); + + if ($isCharity) { + $eventTime = $request->event_time; + if (empty($eventTime)) { + $eventTime = $request->start_time . ' - ' . $request->end_time; + } + + $bodyText = json_encode([ + 'type' => 'Charity', + 'provided_items' => $request->provided_items, + 'event_date' => $request->event_date, + 'event_time' => $eventTime, + 'location_details' => $request->location_details, + 'location_pin' => $request->location_pin, + 'content' => $request->body ?? $request->content, + 'contact_person_name' => $request->contact_person_name, + 'contact_number' => $request->contact_number, + 'country_code' => $request->country_code, + ]); } $announcement = Announcement::create([ 'title' => $request->title, 'body' => $bodyText, - 'type' => $request->type ?? 'info', + 'type' => $request->type ?? ($isCharity ? 'charity' : 'info'), 'employer_id' => $employer->id, 'status' => 'pending', ]); + $charityDetails = null; + $content = $announcement->body; + if (strpos($announcement->body, '{"type":"Charity"') === 0) { + $decoded = json_decode($announcement->body, true); + if ($decoded) { + $charityDetails = $decoded; + $content = $decoded['content'] ?? $announcement->body; + } + } + return response()->json([ 'success' => true, 'message' => 'Announcement posted successfully.', @@ -200,12 +250,13 @@ public function createAnnouncement(Request $request) 'announcement' => [ 'id' => $announcement->id, 'title' => $announcement->title, - 'body' => $announcement->body, + 'body' => $content, 'type' => $announcement->type, 'status' => $announcement->status ?? 'pending', 'remarks' => $announcement->remarks, 'created_at' => $announcement->created_at->toISOString(), 'time_ago' => $announcement->created_at->diffForHumans(), + 'charity_details' => $charityDetails, ] ] ], 201); diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index fa38aa6..258d6d2 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -400,12 +400,7 @@ public function getDashboard(Request $request) // Recent Announcements / Events $dbAnnouncements = \App\Models\Announcement::where('status', 'approved')->latest()->limit(2)->get(); $recentAnnouncements = $dbAnnouncements->map(function ($ann) { - $body = $ann->body; - $eventDate = null; - $eventTime = null; - $locationDetails = null; - $locationPin = null; - $providedItems = null; + $charityDetails = null; if (strpos($ann->body, '{"type":"Charity"') === 0) { $decoded = json_decode($ann->body, true); @@ -416,6 +411,7 @@ public function getDashboard(Request $request) $locationDetails = $decoded['location_details'] ?? null; $locationPin = $decoded['location_pin'] ?? null; $providedItems = $decoded['provided_items'] ?? null; + $charityDetails = $decoded; } } else { $body = $ann->body; @@ -438,6 +434,7 @@ public function getDashboard(Request $request) 'provided_items' => $providedItems, 'created_at' => $ann->created_at->toISOString(), 'time_ago' => $ann->created_at->diffForHumans(), + 'charity_details' => $charityDetails, ]; }); diff --git a/app/Http/Controllers/Api/EmployerReviewController.php b/app/Http/Controllers/Api/EmployerReviewController.php index 4ac983d..079c91c 100644 --- a/app/Http/Controllers/Api/EmployerReviewController.php +++ b/app/Http/Controllers/Api/EmployerReviewController.php @@ -71,12 +71,23 @@ public function addReview(Request $request) ], 403); } - $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); - if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) { - return response()->json([ - 'success' => false, - 'message' => "You can write a review only after {$eligibilityMonths} months from the joining date." - ], 403); + // Verify if completed configured period from joining date + $eligibilityMinutes = config('reminders.review_eligibility_minutes'); + if ($eligibilityMinutes !== null && $eligibilityMinutes > 0) { + if ($joiningDate->diffInMinutes(now(), false) < $eligibilityMinutes) { + return response()->json([ + 'success' => false, + 'message' => "You can write a review only after {$eligibilityMinutes} minutes from the joining date." + ], 403); + } + } else { + $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); + if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) { + return response()->json([ + 'success' => false, + 'message' => "You can write a review only after {$eligibilityMonths} months from the joining date." + ], 403); + } } // Check if employer has already reviewed this worker diff --git a/app/Http/Controllers/Api/WorkerReviewController.php b/app/Http/Controllers/Api/WorkerReviewController.php index 9d6eda9..a047c74 100644 --- a/app/Http/Controllers/Api/WorkerReviewController.php +++ b/app/Http/Controllers/Api/WorkerReviewController.php @@ -90,13 +90,23 @@ public function addReview(Request $request) ], 403); } - // Verify if completed configured months from joining date - $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); - if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) { - return response()->json([ - 'success' => false, - 'message' => "You can write a review only after {$eligibilityMonths} months from the joining date." - ], 403); + // Verify if completed configured period from joining date + $eligibilityMinutes = config('reminders.review_eligibility_minutes'); + if ($eligibilityMinutes !== null && $eligibilityMinutes > 0) { + if ($joiningDate->diffInMinutes(now(), false) < $eligibilityMinutes) { + return response()->json([ + 'success' => false, + 'message' => "You can write a review only after {$eligibilityMinutes} minutes from the joining date." + ], 403); + } + } else { + $eligibilityMonths = (int) config('reminders.review_eligibility_months', 3); + if ($joiningDate->startOfDay()->diffInMonths(now()->startOfDay(), false) < $eligibilityMonths) { + return response()->json([ + 'success' => false, + 'message' => "You can write a review only after {$eligibilityMonths} months from the joining date." + ], 403); + } } // 2. Prevent duplicate reviews for the same job/employer combination diff --git a/config/reminders.php b/config/reminders.php index cdd7dc1..b06754a 100644 --- a/config/reminders.php +++ b/config/reminders.php @@ -3,6 +3,7 @@ return [ 'worker_no_response_reminder_days' => env('WORKER_NO_RESPONSE_REMINDER_DAYS', 14), 'review_eligibility_months' => env('REVIEW_ELIGIBILITY_MONTHS', 3), + 'review_eligibility_minutes' => env('REVIEW_ELIGIBILITY_MINUTES'), 'review_edit_period_days' => env('REVIEW_EDIT_PERIOD_DAYS', 7), 'employer_hire_confirm_reminder_days' => env('EMPLOYER_HIRE_CONFIRM_REMINDER_DAYS', '3,7'), 'worker_join_confirm_reminder_days' => env('WORKER_JOIN_CONFIRM_REMINDER_DAYS', '3,7,1'), diff --git a/phpunit.xml b/phpunit.xml index f0048c7..abb2081 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -36,6 +36,7 @@ + diff --git a/public/swagger.json b/public/swagger.json index 7279854..ac10af7 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -4730,7 +4730,10 @@ "event_date", "event_time", "location_details", - "location_pin" + "location_pin", + "contact_person_name", + "contact_number", + "country_code" ], "properties": { "title": { @@ -4762,6 +4765,18 @@ "type": "string", "format": "uri", "example": "https://maps.app.goo.gl/xyz" + }, + "contact_person_name": { + "type": "string", + "example": "Jane Doe" + }, + "contact_number": { + "type": "string", + "example": "551234567" + }, + "country_code": { + "type": "string", + "example": "+971" } } } diff --git a/tests/Feature/AnnouncementApiTest.php b/tests/Feature/AnnouncementApiTest.php index 4f18472..0a3e0ec 100644 --- a/tests/Feature/AnnouncementApiTest.php +++ b/tests/Feature/AnnouncementApiTest.php @@ -347,4 +347,90 @@ public function test_admin_can_reject_announcement_with_remarks() 'remarks' => 'Inappropriate content or location details.', ]); } + + /** + * Test employer can post and get charity announcements with contact details. + */ + public function test_employer_can_post_and_get_charity_announcement_with_contact_details() + { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->postJson('/api/employers/announcements', [ + 'title' => 'Free Dental Checkup Camp', + 'content' => 'Emirates Charity is providing free professional dental screening, cleanings, and educational packages for domestic helpers.', + 'provided_items' => 'Free Dental screening, cleanings, and wellness kits', + 'event_date' => '2026-06-15', + 'event_time' => '9:00 AM - 4:00 PM', + 'location_details' => 'Al Quoz Community Hall, Dubai', + 'location_pin' => 'https://maps.app.goo.gl/xyz', + 'contact_person_name' => 'Jane Doe', + 'contact_number' => '551234567', + 'country_code' => '+971', + ]); + + $response->assertStatus(201) + ->assertJson([ + 'success' => true, + 'message' => 'Announcement posted successfully.', + ]) + ->assertJsonStructure([ + 'data' => [ + 'announcement' => [ + 'id', + 'title', + 'body', + 'type', + 'status', + 'charity_details' => [ + 'type', + 'provided_items', + 'event_date', + 'event_time', + 'location_details', + 'location_pin', + 'content', + 'contact_person_name', + 'contact_number', + 'country_code', + ] + ] + ] + ]); + + $announcementId = $response->json('data.announcement.id'); + + // Approve it so it shows up in global get endpoint + Announcement::find($announcementId)->update(['status' => 'approved']); + + // Test GET /api/employers/announcements + $getResponse = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->getJson('/api/employers/announcements'); + + $getResponse->assertStatus(200); + $announcements = $getResponse->json('data.announcements'); + $postedAnn = collect($announcements)->firstWhere('id', $announcementId); + + $this->assertNotNull($postedAnn); + $this->assertEquals('Free Dental Checkup Camp', $postedAnn['title']); + $this->assertEquals('Jane Doe', $postedAnn['charity_details']['contact_person_name']); + $this->assertEquals('551234567', $postedAnn['charity_details']['contact_number']); + $this->assertEquals('+971', $postedAnn['charity_details']['country_code']); + + // Test GET /api/employers/my-announcements + $getMyResponse = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->employerToken, + ])->getJson('/api/employers/my-announcements'); + + $getMyResponse->assertStatus(200); + $myAnnouncements = $getMyResponse->json('data.announcements'); + $myPostedAnn = collect($myAnnouncements)->firstWhere('id', $announcementId); + + $this->assertNotNull($myPostedAnn); + $this->assertEquals('Free Dental Checkup Camp', $myPostedAnn['title']); + $this->assertEquals('Jane Doe', $myPostedAnn['charity_details']['contact_person_name']); + $this->assertEquals('551234567', $myPostedAnn['charity_details']['contact_number']); + $this->assertEquals('+971', $myPostedAnn['charity_details']['country_code']); + } } + diff --git a/tests/Feature/NewRemindersAndSettingsTest.php b/tests/Feature/NewRemindersAndSettingsTest.php index d143764..86f439d 100644 --- a/tests/Feature/NewRemindersAndSettingsTest.php +++ b/tests/Feature/NewRemindersAndSettingsTest.php @@ -366,4 +366,119 @@ public function test_employer_review_controller_eligibility_and_edit_window() $response->assertJsonPath('success', false); $response->assertJsonPath('message', 'The 7-day edit window for this review has expired.'); } + + public function test_minute_based_review_eligibility() + { + config(['reminders.review_eligibility_minutes' => 10]); + + $worker = $this->createTestWorker(); + $employer = $this->createTestEmployer(); + + $job = JobPost::create([ + 'employer_id' => $employer->id, + 'title' => 'Nanny', + 'location' => 'Dubai', + 'salary' => 2500, + 'workers_needed' => 1, + 'job_type' => 'Full Time', + 'start_date' => now()->subDays(5)->toDateString(), + 'description' => 'Test job', + 'status' => 'active', + ]); + + // JobApplication with joining_confirmed_at exactly 9 minutes ago + $app = JobApplication::create([ + 'job_id' => $job->id, + 'worker_id' => $worker->id, + 'status' => 'hired', + 'joining_confirmed_at' => now()->subMinutes(9), + ]); + + // 1. Worker review submission fails at 9 minutes + $response = $this->postJson('/api/workers/reviews', [ + 'employer_id' => $employer->id, + 'job_id' => $job->id, + 'rating' => 5, + 'comment' => 'Great employer!', + ], [ + 'Authorization' => 'Bearer worker_api_token' + ]); + $response->assertStatus(403); + $response->assertJsonPath('message', 'You can write a review only after 10 minutes from the joining date.'); + + // 2. Employer review submission fails at 9 minutes + $response = $this->postJson('/api/employers/reviews', [ + 'worker_id' => $worker->id, + 'rating' => 5, + 'comment' => 'Great worker!', + ], [ + 'Authorization' => 'Bearer employer_api_token' + ]); + $response->assertStatus(403); + $response->assertJsonPath('message', 'You can write a review only after 10 minutes from the joining date.'); + + // 3. Scheduler does NOT send reminder at 9 minutes + Notification::fake(); + Artisan::call('app:send-reminders'); + Notification::assertNothingSent(); + + // 4. Update joining date to 10 minutes ago + $app->joining_confirmed_at = now()->subMinutes(10); + $app->save(); + + // 5. Worker review submission succeeds at 10 minutes + $response = $this->postJson('/api/workers/reviews', [ + 'employer_id' => $employer->id, + 'job_id' => $job->id, + 'rating' => 5, + 'comment' => 'Great employer!', + ], [ + 'Authorization' => 'Bearer worker_api_token' + ]); + $response->assertStatus(201); + + // 6. Employer review submission succeeds at 10 minutes + $response = $this->postJson('/api/employers/reviews', [ + 'worker_id' => $worker->id, + 'rating' => 5, + 'comment' => 'Great worker!', + ], [ + 'Authorization' => 'Bearer employer_api_token' + ]); + $response->assertStatus(201); + + // 7. Scheduler sends reminders when updated to 10 minutes ago + $worker2 = $this->createTestWorker([ + 'email' => 'worker2@example.com', + 'phone' => '971500000002', + 'api_token' => 'worker2_api_token' + ]); + $app2 = JobApplication::create([ + 'job_id' => $job->id, + 'worker_id' => $worker2->id, + 'status' => 'hired', + 'joining_confirmed_at' => now()->subMinutes(10), + ]); + + Notification::fake(); + Artisan::call('app:send-reminders'); + + Notification::assertSentTo( + $worker2, + WorkerReviewReminderNotification::class, + function ($notification) use ($app2) { + return $notification->applicationId === $app2->id && + $notification->reviewLevel === '10_minutes'; + } + ); + + Notification::assertSentTo( + $employer, + EmployerReviewReminderNotification::class, + function ($notification) use ($app2) { + return $notification->applicationId === $app2->id && + $notification->reviewLevel === '10_minutes'; + } + ); + } }