Compare commits

..

No commits in common. "main" and "feature/user-interactions" have entirely different histories.

266 changed files with 997 additions and 40321 deletions

View File

@ -1,4 +1,4 @@
APP_NAME=DealHub APP_NAME=Laravel
APP_ENV=local APP_ENV=local
APP_KEY= APP_KEY=
APP_DEBUG=true APP_DEBUG=true
@ -63,28 +63,3 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
TWILIO_SID=
TWILIO_AUTH_TOKEN=
TWILIO_NUMBER=
# OTP valid time in minutes
OTP_LIFESPAN=10
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
# Same as the VAPID_PUBLIC_KEY
VITE_VAPID_PUBLIC_KEY="${VAPID_PUBLIC_KEY}"
REVERB_APP_ID=
REVERB_APP_KEY=
REVERB_APP_SECRET=
REVERB_HOST=
REVERB_PORT=
REVERB_SCHEME=
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

File diff suppressed because it is too large Load Diff

View File

@ -79,22 +79,11 @@ ## ⚙️ Installation
```bash ```bash
git clone https://git.sentientgeeks.us/joydip.manna/dealhub.git git clone https://git.sentientgeeks.us/joydip.manna/dealhub.git
cd dealhub cd dealhub
# Install dependencies
composer install composer install
npm install npm install
# Set VAPID for webpush
php artisan webpush:vapid
# Install Reverb
php artisan install:broadcasting
npm run dev npm run dev
php artisan migrate php artisan migrate
php artisan serve php artisan serve
# Start Reverb Server ( Another terminal )
php artisan reverb:start
``` ```
--- ---

View File

@ -1,4 +0,0 @@
- [ ] Live Support.
- [ ] Add an Audit Log table to save all actions.
- [ ] Link foreign keys properly with related tables in the database.
- [ ] Add debouncing to the search feature and create proper database indexes to improve search performance.

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +0,0 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
final readonly class AddRecentSearchAction
{
public function execute(User $user, array $data): void
{
try {
DB::transaction(function () use ($user, $data) {
$user->recentSearches()->updateOrcreate($data);
$recentSearchCount = $user->recentSearches()->count();
if ($recentSearchCount > 5) {
$user->recentSearches()->oldest()->limit(1)->delete();
}
});
} catch (\Throwable $e) {
Log::error('Error adding recent search',
[
'user_id' => $user->id,
'error' => $e->getMessage(),
'trace' => $e->getTrace(),
]);
}
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Actions;
use App\Models\Inbox;
use App\Models\User;
use DB;
final readonly class CreateOrGetInboxAction
{
/**
* @throws \Throwable
*/
public function execute(User $recipient, User $sender): Inbox
{
$existingInbox = Inbox::whereHas('users', fn ($q) => $q->where('users.id', $sender->id))
->whereHas('users', fn ($q) => $q->where('users.id', $recipient->id))
->first();
if ($existingInbox) {
return $existingInbox;
}
return DB::transaction(function () use ($sender, $recipient) {
$inbox = Inbox::create();
$inbox->users()->attach([$sender->id, $recipient->id]);
return $inbox;
}, 2);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Actions;
use App\Enums\UserTypes;
use App\Models\Deal;
use App\Models\User;
final readonly class GetAdminStatsAction
{
/**
* @return array<string, int>
*/
public function execute(): array
{
return [
'listings' => Deal::count(),
'customers' => User::where('role', UserTypes::User->value)->count(),
'brokers' => User::where('role', UserTypes::Broker->value)->count(),
];
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Actions;
use App\Enums\InteractionType;
use App\Models\User;
final readonly class GetBrokerStatsAction
{
/**
* @return array<string, int>
*/
public function execute(User $user): array
{
return [
'listings' => $user->deals()->count(),
'likes' => $user->dealsInteractions()->where('type', InteractionType::Like)->count(),
'views' => $user->dealsInteractions()->where('type', InteractionType::View)->sum('count'),
'clicks' => $user->dealsInteractions()->where('type', InteractionType::Redirection)->sum('count'),
];
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Actions;
use App\Enums\InteractionType;
use App\Models\Deal;
use App\Models\User;
use Illuminate\Support\Collection;
final readonly class GetUserFavoritesAction
{
public function execute(User $user): Collection
{
return $user->interactedDeals()
->where('interactions.type', InteractionType::Favorite)
->tap(fn ($q) => (new Deal)->withActiveDeals($q))
->select('deals.id', 'deals.title')
->get();
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Collection;
final readonly class GetUserReportedDealsAction
{
public function execute(User $user): Collection
{
return $user->reports()
->select('reports.id')
->with('deals:id,title')
->get();
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Actions\PasswordReset;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Services\OTPService;
final readonly class ResendOTPAction
{
public function __construct(
private OTPService $otpService,
private SendOTPToUserAction $otpToUserAction
) {}
/**
* @throws \Throwable
*/
public function execute(): void
{
$user = \Session::get('otp_user_id') ? User::find(\Session::get('otp_user_id')) : null;
throw_if(! $user, new UserNotFoundException('User not found'));
$otp = $this->otpService->generate($user);
$this->otpToUserAction->execute(['user' => $user, 'otp' => $otp]);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Actions\PasswordReset;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Services\OTPService;
final readonly class SendOTPAction
{
public function __construct(
private OTPService $otpService,
private SendOTPToUserAction $otpToUserAction
) {}
/**
* @throws \Throwable
*/
public function execute(array $data): void
{
$user = User::where('email', $data['email'])->first();
throw_if(! $user, new UserNotFoundException('User not found'));
\Session::put('otp_user_id', $user->id);
$otp = $this->otpService->generate($user);
$this->otpToUserAction->execute(['user' => $user, 'otp' => $otp]);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Actions\PasswordReset;
use App\Actions\SendPasswordResetMailAction;
use App\Services\TwilioService;
use Twilio\Exceptions\TwilioException;
final readonly class SendOTPToUserAction
{
public function __construct(
private SendPasswordResetMailAction $mailAction,
private TwilioService $twilioService
) {}
/**
* @param array<string, mixed> $data
*
* @throws \Throwable
*/
public function execute(array $data): void
{
['user' => $user,'otp' => $otp] = $data;
$this->mailAction->execute($user->email, $otp);
if ($user?->type->phone !== null) {
try {
$this->twilioService->sendSms($user->type->phone, "Your OTP is $otp");
} catch (TwilioException $e) {
\Log::error('SMS send failed', [$e->getMessage()]);
}
}
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Actions\PasswordReset;
use App\Exceptions\UserNotFoundException;
use App\Models\User;
use App\Services\OTPService;
final readonly class VerifyOTPAction
{
public function __construct(
private OTPService $otpService,
) {}
/**
* @throws \Throwable
*/
public function execute(array $data): bool
{
$user = \Session::get('otp_user_id') ? User::find(\Session::get('otp_user_id')) : null;
throw_if(! $user, new UserNotFoundException('User not found'));
return $this->otpService->verify($user, $data['otp']);
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Actions;
use App\Models\PageVisit;
use App\Models\User;
final readonly class RecordUserPageVisitAction
{
public function execute(?User $user, string $page): void
{
PageVisit::create(['user_id' => $user?->id, 'page' => $page, 'user_type' => $user?->role ?? null, 'created_at' => now()]);
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Actions;
use App\Models\Broker;
use App\Models\Customer;
use App\Models\Deal;
use App\Notifications\NewDealNotification;
final readonly class SendDealCreatedNotificationCustomerAction
{
public function execute(Deal $deal): void
{
/**
* @var Broker $broker
*/
$broker = $deal->broker->type;
$followers = $broker->followers()->with('user')->get();
$followers->map(function (Customer $follower) use ($deal) {
$user = $follower->user;
\Log::info("Sending notification to {$follower->user->name}", [$deal, $follower->user]);
$user->notifyNow(new NewDealNotification($deal));
});
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Actions;
use App\Events\MessageSent;
use App\Exceptions\MessageNotSendException;
use App\Models\User;
use DB;
final readonly class SendMessageAction
{
public function __construct(private CreateOrGetInboxAction $inboxAction) {}
/**
* @throws \Throwable
*/
public function execute(User $sender, User $recipient, array $data): void
{
try {
// find the inbox between the two users
DB::beginTransaction();
$inbox = $this->inboxAction->execute($recipient, $sender);
// update the inbox with the last message and last user as current user
$inbox->last_message = $data['message'];
$inbox->last_user_id = $sender->id;
$inbox->save();
// create a new message in the inbox
$message = $inbox->messages()->create([
'message' => $data['message'],
'user_id' => $sender->id,
]);
// Send the message to all other users in the inbox
broadcast(new MessageSent($message))->toOthers();
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw new MessageNotSendException('Message not sent.', previous: $e);
}
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Actions;
use App\Mail\PasswordResetMail;
final readonly class SendPasswordResetMailAction
{
/**
* @throws \Throwable
*/
public function execute(string $email, string $otp): void
{
try {
$message = \Mail::to($email)->send(new PasswordResetMail($otp));
\Log::info('Mail sent successfully', ['message' => $message]);
} catch (\Throwable $e) {
\Log::info('Mail send failed', ['message' => $e->getMessage()]);
}
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Throwable;
final readonly class UpdateBrokerAction
{
/**
* @throws Throwable
*/
public function execute(array $data, User $profile): void
{
/**
* Separate the user fields from the broker fields
*/
$userFields = ['name', 'email'];
$data = collect($data);
$profileData = $data->only($userFields)->toArray();
$userData = $data->except($userFields)->toArray();
DB::transaction(function () use ($profileData, $profile, $userData) {
$profile->update($profileData);
$user = $profile->type;
$user->update($userData);
});
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Actions;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Throwable;
final readonly class UpdateCustomerAction
{
/**
* @throws Throwable
*/
public function execute(array $data, User $profile): void
{
/**
* Separate the user fields from the broker fields
*/
$userFields = ['name', 'email'];
$data = collect($data);
$profileData = $data->only($userFields)->toArray();
$userData = $data->except($userFields)->toArray();
DB::transaction(function () use ($profileData, $profile, $userData) {
$profile->update($profileData);
$user = $profile->type;
$user->update($userData);
});
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Enums;
use App\Traits\EnumAsArray;
enum ExplorePageFilters: string
{
use EnumAsArray;
case Like = 'like';
case Click = 'click';
}

View File

@ -11,5 +11,4 @@ enum InteractionType: string
case Like = 'like'; case Like = 'like';
case Favorite = 'favorite'; case Favorite = 'favorite';
case Redirection = 'redirect'; case Redirection = 'redirect';
case View = 'view';
} }

View File

@ -1,38 +0,0 @@
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public Message $message)
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
$users = $this->message->inbox->users->pluck('id')->toArray();
sort($users);
return [
new PrivateChannel("chat.{$users[0]}.{$users[1]}"),
];
}
}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class MessageNotSendException extends Exception {}

View File

@ -1,7 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class UserNotFoundException extends Exception {}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\GetAdminStatsAction;
use App\Http\Controllers\Controller;
class AdminDashboardController extends Controller
{
public function __invoke(GetAdminStatsAction $action)
{
return view('dashboards.admin.index')
->with('stats', $action->execute());
}
}

View File

@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\UpdateBrokerAction;
use App\Enums\UserStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBrokerProfileRequest;
use App\Models\Broker;
use Illuminate\Support\Facades\Log;
class BrokerController extends Controller
{
public function index()
{
return view('dashboards.admin.brokers.index')
->with('activeBrokers', Broker::select(['id', 'location'])
->whereRelation('user', 'status', UserStatus::Active->value)
->with('user:id,name,email,role_id,role_type')
->get()
)
->with('pendingBrokers', Broker::select(['id', 'location'])
->whereRelation('user', 'status', UserStatus::Pending->value)
->with('user:id,name,email,role_id,role_type')
->get()
);
}
public function edit(Broker $broker)
{
return view('dashboards.admin.brokers.edit')
->with('profile', $broker->user)
->with('backLink', route('admin.brokers.index'))
->with('actionLink', route('admin.brokers.update', $broker));
}
public function update(StoreBrokerProfileRequest $request, Broker $broker, UpdateBrokerAction $action)
{
try {
$action->execute($request->validated(), $broker->user);
return to_route('admin.brokers.index')
->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) {
Log::error('Broker Profile Update Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->withInput()->with('error', 'Something went wrong.');
}
}
public function destroy(Broker $broker)
{
try {
\DB::transaction(function () use ($broker) {
$broker->user->delete();
$broker->delete();
});
return back()->with('success', 'Broker deleted successfully.');
} catch (\Throwable $e) {
Log::error('Broker Delete Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
public function approve(Broker $broker)
{
try {
$broker->user->update(['status' => UserStatus::Active->value]);
return to_route('admin.brokers.index')->with('success', 'Broker approved successfully.');
} catch (\Throwable $e) {
Log::error('Broker Approval Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -1,59 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\UpdateCustomerAction;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreCustomerProfileRequest;
use App\Models\Customer;
use Illuminate\Support\Facades\Log;
class CustomerController extends Controller
{
public function index()
{
return view('dashboards.admin.customers.index')
->with('customers', Customer::select(['id', 'location'])
->with('user:id,name,email,role_id,role_type')
->get()
);
}
public function edit(Customer $customer)
{
return view('dashboards.admin.customers.edit')
->with('profile', $customer->user)
->with('backLink', route('admin.customers.index'))
->with('actionLink', route('admin.customers.update', $customer));
}
public function update(StoreCustomerProfileRequest $request, Customer $customer, UpdateCustomerAction $action)
{
try {
$action->execute($request->validated(), $customer->user);
return to_route('admin.customers.index')
->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) {
Log::error('Customer Profile Update Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->withInput()->with('error', 'Something went wrong.');
}
}
public function destroy(Customer $customer)
{
try {
\DB::transaction(function () use ($customer) {
$customer->user->delete();
$customer->delete();
});
return back()->with('success', 'Customer deleted successfully.');
} catch (\Throwable $e) {
Log::error('Customer Delete Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\SendDealCreatedNotificationCustomerAction;
use App\Http\Controllers\Controller;
use App\Models\Deal;
use Illuminate\Support\Facades\Log;
class DealController extends Controller
{
public function index()
{
return view('dashboards.admin.deals.index')
->with('pendingDeals', $this->pendingDeals())
->with('activeDeals', $this->activeDeals());
}
public function approve(Deal $deal, SendDealCreatedNotificationCustomerAction $notificationCustomerAction)
{
try {
\DB::transaction(function () use ($deal) {
$deal->active = true;
$deal->save();
});
$notificationCustomerAction->execute($deal);
return back()->with('success', 'Deal activated successfully.');
} catch (\Throwable $e) {
Log::error('Deal activation Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
public function reject(Deal $deal)
{
try {
$deal->delete();
return back()->with('success', 'Deal deleted successfully.');
} catch (\Throwable $e) {
Log::error('Deal deletion Failed: ', [$e->getMessage(), $e->getTrace()]);
return back()->with('error', 'Something went wrong.');
}
}
private function pendingDeals()
{
return Deal::where('active', false)->get();
}
private function activeDeals()
{
return Deal::where('active', true)->get();
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Enums\ReportStatus;
use App\Http\Controllers\Controller;
use App\Models\Report;
use App\Notifications\ReportRejectedNotificationToUser;
use App\Notifications\ReportResolvedNotificationToBroker;
use Illuminate\Support\Facades\DB;
class ReportController extends Controller
{
public function index()
{
$reports = Report::query()
->with(['user:id,name', 'deals:id,title'])
->tap(fn ($q) => (new Report)->orderByStatus($q,
[ReportStatus::Pending, ReportStatus::Rejected, ReportStatus::Resolved]))
->get();
return view('dashboards.admin.reports.index')
->with('reports', $reports);
}
public function resolve(Report $report)
{
try {
$report->status = ReportStatus::Resolved;
$report->save();
$report->user->notify(new ReportRejectedNotificationToUser($report->deals()->first()->title, false));
$report->deals()->first()->broker->notify(new ReportResolvedNotificationToBroker($report->deals()->first()->title,
false));
return back()->with('success', 'Report resolved successfully.');
} catch (\Throwable $e) {
\Log::error('Error resolving report', [$report->id, $e->getMessage()]);
return back()->with('error', 'Something went wrong.');
}
}
public function reject(Report $report)
{
try {
$report->status = ReportStatus::Rejected;
$report->save();
$report->user->notify(new ReportRejectedNotificationToUser($report->deals()->first()->title));
return back()->with('success', 'Report Rejected successfully.');
} catch (\Throwable $e) {
\Log::error('Error rejecting report', [$report->id, $e->getMessage()]);
return back()->with('error', 'Something went wrong.');
}
}
/**
* Make the attached deal inactive and mark the report resolved.
*/
public function removeContent(Report $report)
{
try {
DB::transaction(function () use ($report) {
$report->user->notify(new ReportRejectedNotificationToUser($report->deals()->first()->title, true));
$report->deals()->first()->broker->notify(new ReportResolvedNotificationToBroker($report->deals()->first()->title,
true));
$deal = $report->deals()->first();
$deal->active = false;
$report->status = ReportStatus::Resolved;
$deal->save();
$report->save();
});
return back()->with('error', 'Deal has been deactivated.');
} catch (\Throwable $e) {
\Log::error('Error removing content of report', [$report->id, $e->getMessage(), $e->getTraceAsString()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Auth;
class ImpersonatedUserController extends Controller
{
public function store(User $user)
{
Session()->put('impersonate', Auth::id());
Session()->put('impersonate_name', $user->name);
Auth::login($user);
return to_route('explore');
}
public function destroy()
{
$adminId = Session()->get('impersonate');
Auth::loginUsingId($adminId);
Session()->forget('impersonate');
Session()->forget('impersonate_name');
return to_route('admin.dashboard');
}
}

View File

@ -1,91 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Actions\PasswordReset\ResendOTPAction;
use App\Actions\PasswordReset\SendOTPAction;
use App\Actions\PasswordReset\VerifyOTPAction;
use App\Exceptions\UserNotFoundException;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Validation\Rules\Password;
class PasswordResetController extends Controller
{
public function show()
{
return view('auth.passwords.reset');
}
public function sendCode(Request $request, SendOTPAction $action)
{
$data = $request->validate([
'email' => 'required|email',
]);
try {
$action->execute($data);
return to_route('password.reset.show.verify')
->with('success', 'Password reset code is sent');
} catch (UserNotFoundException $e) {
return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent');
}
}
public function showVerify()
{
return view('auth.passwords.verify')
->with('expiryMinutes', 3);
}
public function verify(Request $request, VerifyOTPAction $otpAction)
{
$data = $request->validate(['otp' => 'required|string:min:5:max:6']);
try {
$isVerified = $otpAction->execute($data);
if (! $isVerified) {
return back()->with('error', 'Invalid OTP');
}
return to_route('password.reset.show.update')->with('success', 'OTP Verified');
} catch (UserNotFoundException $e) {
return back()->with('error', 'Session Expired');
}
}
public function showUpdate()
{
return view('auth.passwords.update');
}
public function update(Request $request)
{
$data = $request->validate([
'password' => 'required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols(),
]);
$user = User::find(Session::get('otp_user_id'));
if (! $user) {
return back()->with('error', 'Session Expired');
}
$user->update(['password' => $data['password']]);
\Session::forget('otp_user_id');
return to_route('login.create')->with('success', 'Password updated successfully');
}
public function resend(ResendOTPAction $otpAction)
{
try {
$otpAction->execute();
return to_route('password.reset.show.verify')
->with('success', 'Password reset code is sent');
} catch (UserNotFoundException $e) {
return to_route('password.reset.show.verify')->with('success', 'Password reset code is sent');
}
}
}

View File

@ -1,11 +1,9 @@
<?php <?php
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers;
use App\Actions\RecordUserPageVisitAction;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use App\Enums\UserTypes; use App\Enums\UserTypes;
use App\Http\Controllers\Controller;
use App\Http\Requests\AuthenticateUserRequest; use App\Http\Requests\AuthenticateUserRequest;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -17,7 +15,7 @@ public function create()
return view('auth.login'); return view('auth.login');
} }
public function store(AuthenticateUserRequest $request, RecordUserPageVisitAction $action) public function store(AuthenticateUserRequest $request)
{ {
$data = $request->validated(); $data = $request->validated();
if (Auth::attempt($data, $data['remember_me'] ?? false)) { if (Auth::attempt($data, $data['remember_me'] ?? false)) {
@ -36,12 +34,6 @@ public function store(AuthenticateUserRequest $request, RecordUserPageVisitActio
UserTypes::Broker->value, UserTypes::User->value => 'explore', UserTypes::Broker->value, UserTypes::User->value => 'explore',
}; };
try {
$action->execute($user, $route);
} catch (\Throwable $e) {
\Log::error('Error recording user page visit', [$e->getMessage()]);
}
return to_route($route); return to_route($route);
} else { } else {
return back() return back()

View File

@ -2,17 +2,15 @@
namespace App\Http\Controllers\Broker; namespace App\Http\Controllers\Broker;
use App\Actions\GetBrokerStatsAction;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class BrokerDashboardController extends Controller class BrokerDashboardController extends Controller
{ {
public function index(GetBrokerStatsAction $getBrokerStatsAction) public function index()
{ {
return view('dashboards.broker.index') return view('dashboards.broker.index')
->with('deals', $this->deals()) ->with('deals', $this->deals());
->with('stats', $getBrokerStatsAction->execute(Auth::user()));
} }
protected function deals() protected function deals()
@ -30,9 +28,7 @@ protected function deals()
'deal_category_id', 'deal_category_id',
]) ])
->with('category:id,name') ->with('category:id,name')
->WithLikePerDeal() ->latest()
->WithRedirectionPerDeal() ->paginate();
->withViewPerDeal()
->latest()->take(3)->get();
} }
} }

View File

@ -6,16 +6,16 @@
use App\Http\Requests\StoreBrokerProfileRequest; use App\Http\Requests\StoreBrokerProfileRequest;
use App\Models\Broker; use App\Models\Broker;
use App\Models\User; use App\Models\User;
use App\Services\ProfileInitialsService;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class BrokerProfileController extends Controller class BrokerProfileController extends Controller
{ {
/** /**
* Display the specified resource. * Display the specified resource.
*/ */
public function show(User $profile, ProfileInitialsService $service) public function show(User $profile)
{ {
// Get the broker profile // Get the broker profile
$broker = $profile->type; $broker = $profile->type;
@ -25,7 +25,14 @@ public function show(User $profile, ProfileInitialsService $service)
abort(403, 'This user is not a broker.'); abort(403, 'This user is not a broker.');
} }
$initials = $service->create($profile->name); /**
* Create the initials from a full name (e.g. John Doe, Alex Mark, jane clerk)
* to display on profile page (e.g. JD, AM, JC).
*/
$initials = Str::of($profile->name)
->explode(' ')
->map(fn ($word) => Str::substr(ucfirst($word), 0, 1))
->join('');
return view('dashboards.broker.profile.show') return view('dashboards.broker.profile.show')
->with('name', $profile->name) ->with('name', $profile->name)

View File

@ -1,15 +1,14 @@
<?php <?php
namespace App\Http\Controllers\Broker; namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBrokerDeal; use App\Http\Requests\StoreBrokerDeal;
use App\Models\Deal; use App\Models\Deal;
use App\Models\DealCategory; use App\Models\DealCategory;
use App\Services\FileService; use App\Services\FileService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class BrokerDealController extends Controller class BrokerDealController extends Controller
@ -19,8 +18,7 @@ class BrokerDealController extends Controller
*/ */
public function index() public function index()
{ {
return view('dashboards.broker.deals.index') //
->with('deals', $this->deals());
} }
/** /**
@ -51,7 +49,7 @@ public function store(StoreBrokerDeal $request, FileService $fileService)
Deal::create($data); Deal::create($data);
Deal::reguard(); Deal::reguard();
return to_route('broker.deals.index')->with('success', 'Deal has been created.'); return to_route('broker.dashboard')->with('success', 'Deal has been created.');
} }
/** /**
@ -91,10 +89,10 @@ public function update(StoreBrokerDeal $request, Deal $deal, FileService $fileSe
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
Log::error($exception->getMessage(), $exception->getTrace()); Log::error($exception->getMessage(), $exception->getTrace());
return back()->with('error', 'Something gone wrong.'); return to_route('broker.dashboard')->with('error', 'Something gone wrong.');
} }
return back()->with('success', 'Deal has been updated.'); return to_route('broker.dashboard')->with('success', 'Deal has been updated.');
} }
/** /**
@ -114,31 +112,9 @@ public function destroy(Deal $deal, FileService $fileService)
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
Log::error($exception->getMessage(), $exception->getTrace()); Log::error($exception->getMessage(), $exception->getTrace());
return back()->with('error', 'Something gone wrong.'); return to_route('broker.dashboard')->with('error', 'Something gone wrong.');
} }
return back()->with('success', 'Deal has been deleted.'); return to_route('broker.dashboard')->with('success', 'Deal has been deleted.');
}
protected function deals()
{
return Auth::user()
->deals()
->select([
'id',
'title',
'description',
'image',
'active',
'slug',
'link',
'deal_category_id',
])
->with('category:id,name')
->WithLikePerDeal()
->WithRedirectionPerDeal()
->withViewPerDeal()
->latest()
->paginate(15);
} }
} }

View File

@ -1,59 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\CreateOrGetInboxAction;
use App\Actions\SendMessageAction;
use App\Exceptions\MessageNotSendException;
use App\Http\Requests\SendMessageRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Log;
use Throwable;
class ChatController extends Controller
{
//
public function index(#[CurrentUser] User $user)
{
return view('dashboards.user.chat')
->with('inboxes', $user->inboxes);
}
public function show(#[CurrentUser] User $sender, User $recipient, CreateOrGetInboxAction $action)
{
try {
$inbox = $action->execute($recipient, $sender);
} catch (Throwable $e) {
Log::error('Inbox instantiation Failed: ', [$e->getMessage()]);
abort(500);
}
return view('dashboards.user.chat')
->with('recipient', $recipient)
->with('inboxes', $sender->inboxes)
->with('messages', $inbox->messages()->latest()->get());
}
/**
* @throws Throwable
*/
public function store(
#[CurrentUser] User $sender,
User $recipient,
SendMessageRequest $request,
SendMessageAction $action
) {
try {
$action->execute($sender, $recipient, $request->validated());
return response()->json(['message' => 'Message sent successfully.']);
} catch (MessageNotSendException $e) {
Log::error('Message send failed', [$e->getMessage()]);
return response()->json(['message' => 'Message sent failed.'], 500);
}
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CommentRequest;
use App\Models\Comment;
use App\Models\Deal;
class CommentController extends Controller
{
public function index(Deal $deal)
{
$comments = $deal->comments()->with('user')->latest()->get();
$html = view('components.dashboard.user.deal-comment.index', compact('comments'))->render();
return response()->json(['html' => $html]);
}
public function store(Deal $deal, CommentRequest $request)
{
$data = $request->validated();
$data['user_id'] = $request->user()->id;
$data['deal_id'] = $deal->id;
Comment::create($data);
return response()->json(['message' => 'Comment created successfully.'], 201);
}
public function show(Comment $comment) {}
public function update(CommentRequest $request, Comment $comment)
{
$comment->update($request->validated());
}
public function destroy(Comment $comment)
{
$comment->delete();
return response()->json();
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContactRequest;
use App\Models\Admin;
use App\Notifications\NewContactNotification;
class ContactController extends Controller
{
public function __invoke(ContactRequest $request)
{
try {
$data = $request->validated();
$admin = Admin::first();
$admin->user->notify(new NewContactNotification($data['name'], $data['email'], $data['message']));
return back()->with('success', 'Your message has been sent successfully.');
} catch (\Throwable $e) {
\Log::error('Error sending contact message', [$e->getMessage()]);
return back()->with('error', 'Something went wrong.');
}
}
}

View File

@ -2,60 +2,41 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\AddRecentSearchAction;
use App\Enums\ExplorePageFilters;
use App\Enums\UserTypes; use App\Enums\UserTypes;
use App\Http\Requests\ExploreSearchSortRequest;
use App\Models\Deal; use App\Models\Deal;
use App\Models\DealCategory;
use App\Queries\ExplorePageDealsQuery;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class ExplorePageController extends Controller class ExplorePageController extends Controller
{ {
public function __invoke( public function __invoke()
ExploreSearchSortRequest $request, {
ExplorePageDealsQuery $query,
AddRecentSearchAction $addRecentSearchAction
) {
return view('explore') return view('explore')
->with('profileLink', $this->profileLink()) ->with('profileLink', $this->profileLink())
->with('categories', $this->categories()) ->with('deals', $this->deals());
->with('recentSearches', $this->recentSearches())
->with('deals', $this->deals($request, $query->builder(), $addRecentSearchAction));
} }
protected function deals(FormRequest $request, Builder $query, AddRecentSearchAction $action): LengthAwarePaginator protected function deals()
{ {
$query->tap(fn ($q) => (new Deal)->withActiveDeals($q)); return Deal::query()
// Add a search query ->select([
if ($request->has('search') && $request->get('search') !== null) { 'id', 'title', 'description', 'image', 'active', 'slug', 'link',
$query->tap(fn ($q) => (new Deal)->search($q, $request->search)); 'deal_category_id', 'user_id',
])
\Illuminate\Support\defer(function () use ($action, $request) { // Select additional details
$action->execute($request->user(), ['query' => $request->search]); ->with([
}); 'category:id,name',
} 'broker' => function ($query) {
$query->select('id', 'name', 'email', 'role_type', 'role_id')
// Add category sorting filter ->with('type');
if ($request->has('category') && $request->get('category') !== null) { },
$query->tap(fn ($q) => (new Deal)->filterByCategory($q, $request->category)); ])
} // Select only admin-approved deals
->withActiveDeals()
// Add sorting filters // Check if the current user interacted with the deal
$query = match (ExplorePageFilters::tryFrom($request->sortBy)) { ->withCurrentUserInteractions()
ExplorePageFilters::Like => $query->orderBy('total_likes', 'desc'), ->withLikes()
ExplorePageFilters::Click => $query->orderBy('total_redirection', 'desc'), ->latest()
default => $query->orderByRaw( ->paginate();
'((COALESCE(total_likes, 0) * 70.0) / 100.0) + ((COALESCE(total_redirection, 0) * 30.0) / 100.0) DESC'
)
};
return $query->latest()->paginate();
} }
/** /**
@ -64,28 +45,11 @@ protected function deals(FormRequest $request, Builder $query, AddRecentSearchAc
* *
* @return string The URL for the user's dashboard. * @return string The URL for the user's dashboard.
*/ */
protected function profileLink(): string protected function profileLink()
{ {
$user = Auth::user(); $user = Auth::user();
if ($user->role === UserTypes::Broker->value) {
return match ($user->role ?? null) { return route('broker.profile.show', $user);
UserTypes::Broker->value => route('broker.profile.show', $user),
UserTypes::User->value => route('customer.profile.show', $user),
default => route('login.create')
};
} }
protected function categories(): Collection
{
return DealCategory::all(['id', 'name']);
}
protected function recentSearches(): Collection
{
if (! Auth::check()) {
return collect();
}
return Auth::user()->recentSearches()->latest()->select(['id', 'query'])->get();
} }
} }

View File

@ -1,43 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Broker;
use App\Models\Follow;
class FollowController extends Controller
{
public function __invoke(Broker $broker)
{
$follow = $this->checkFollow($broker);
if ($follow === null) {
return $this->store($broker);
} else {
return $this->destroy($follow);
}
}
public function store(Broker $broker)
{
Follow::create([
'broker_id' => $broker->id,
'customer_id' => auth()->id(),
]);
return response()->json(['message' => 'Followed successfully.']);
}
public function destroy(Follow $follow)
{
$follow->delete();
return response()->json(['message' => 'Unfollowed successfully.']);
}
protected function checkFollow(Broker $broker)
{
return Follow::where('broker_id', $broker->id)
->where('customer_id', auth()->id())
->first();
}
}

View File

@ -1,136 +0,0 @@
<?php
namespace App\Http\Controllers\Interaction;
use App\Enums\InteractionType;
use App\Http\Controllers\Controller;
use App\Models\Deal;
use App\Models\Interaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class InteractionController extends Controller
{
/**
* Interact to a deal by Like or Favorite state
*
* @param InteractionType $type [InteractionType::Like, InteractionType::Favorite]
*/
public function togglesState(Deal $deal, InteractionType $type, Request $request)
{
if (! in_array($type, [InteractionType::Like, InteractionType::Favorite])) {
return response()->json(['error' => 'This interaction is not supported'], 400);
}
try {
// Check for existing like of the user with deal
$existingInteraction = $deal->interactions()
->where('user_id', Auth::id())
->where('type', $type)
->first();
// Delete the existing like if exists, else add the like
$message = '';
if ($existingInteraction) {
$existingInteraction->delete();
$message = ucfirst($type->value).' removed from deal';
} else {
$data = [
'type' => $type,
'user_id' => Auth::id(),
];
Interaction::unguard();
$deal->interactions()->create($data);
Interaction::reguard();
$message = ucfirst($type->value).' added to deal';
}
if ($request->expectsJson()) {
return response()->json(['message' => $message]);
} else {
return back()->with('success', $message);
}
} catch (\Throwable $e) {
Log::error('Error when liked a deal',
[
'deal_id' => $deal->id,
'user_id' => Auth::id(),
'type' => $type,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
if ($request->expectsJson()) {
return response()->json(['error' => 'Something went wrong.'], 500);
}
return back()->with('error', 'Something went wrong.');
}
}
public function redirect(Deal $deal)
{
if (blank($deal->link)) {
abort(404);
}
\Illuminate\Support\defer(function () use ($deal) {
try {
$interaction = $deal->interactions()->firstOrCreate([
'type' => InteractionType::Redirection,
'user_id' => Auth::id(),
]);
if (! $interaction->wasRecentlyCreated) {
$interaction->increment('count');
}
} catch (\Throwable $e) {
Log::error('Error when redirecting a deal external link',
[
'deal_id' => $deal->id,
'user_id' => Auth::id(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
abort(500);
}
});
return redirect()->away($deal->link);
}
public function view(Deal $deal)
{
try {
$interaction = $deal->interactions()->firstOrCreate([
'type' => InteractionType::View,
'user_id' => Auth::id(),
]);
if (! $interaction->wasRecentlyCreated) {
$interaction->increment('count');
}
} catch (\Throwable $e) {
Log::error('Error when view a deal',
[
'deal_id' => $deal->id,
'user_id' => Auth::id(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
return response()->json(['error' => 'Something went wrong.'], 500);
}
return response()->json([
'message' => 'View counted',
]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Enums\InteractionType;
use App\Models\Deal;
use App\Models\Interaction;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class InteractionController extends Controller
{
/**
* Interact to a deal by Like or Favorite state
*
* @param InteractionType $type [InteractionType::Like, InteractionType::Favorite]
* @return \Illuminate\Http\JsonResponse
*/
public function togglesState(Deal $deal, InteractionType $type)
{
if (! in_array($type, [InteractionType::Like, InteractionType::Favorite])) {
return response()->json(['error' => 'This interaction is not supported'], 400);
}
try {
// Check for existing like of the user with deal
$existingInteraction = $deal->interactions()
->where('user_id', Auth::id())
->where('type', $type)
->first();
// Delete the existing like if exists, else add the like
$message = '';
if ($existingInteraction) {
$existingInteraction->delete();
$message = "{$type->value} removed from deal";
} else {
$data = [
'type' => $type,
'user_id' => Auth::id(),
];
Interaction::unguard();
$deal->interactions()->create($data);
Interaction::reguard();
$message = "{$type->value} added to deal";
}
return response()->json(['message' => $message]);
} catch (\Throwable $e) {
Log::error('Error when liked a deal',
[
'deal_id' => $deal->id,
'use_id' => Auth::id(),
'type' => $type,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
return response()->json(['error' => 'Something went wrong.'], 500);
}
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\PushSubscriptionRequest;
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\JsonResponse;
class PushSubscriptionController extends Controller
{
public function __invoke(#[CurrentUser] User $user, PushSubscriptionRequest $request): JsonResponse
{
$data = $request->validated();
$user->updatePushSubscription($data['endpoint'], $data['keys']['p256dh'], $data['keys']['auth']);
return response()->json(['message' => 'Push subscription updated successfully.']);
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\RecentSearch;
class RecentSearchController extends Controller
{
public function __invoke(RecentSearch $recentSearch)
{
$recentSearch->delete();
return response()->json(['message' => 'Search deleted successfully.']);
}
}

View File

@ -1,13 +1,12 @@
<?php <?php
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use App\Enums\UserTypes; use App\Enums\UserTypes;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreRegisterdUser; use App\Http\Requests\StoreRegisterdUser;
use App\Models\Broker; use App\Models\Broker;
use App\Models\Customer; use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -23,29 +22,21 @@ public function store(StoreRegisterdUser $request)
$data = $request->validated(); $data = $request->validated();
try { try {
DB::transaction(function () use ($data) { DB::transaction(function () use ($data) {
switch ($data['role']) { if ($data['role'] === UserTypes::Broker->value) {
case UserTypes::Broker->value:
$data['status'] = UserStatus::Pending->value; $data['status'] = UserStatus::Pending->value;
// Create Broker first, then link the user // Create Broker first, then the User linked to it
$broker = Broker::create(); $broker = Broker::create();
$broker->user()->create($data); $broker->user()->create($data);
break; } else {
case UserTypes::User->value:
$data['status'] = UserStatus::Active->value; $data['status'] = UserStatus::Active->value;
$customer = Customer::create(); User::create($data);
$customer->user()->create($data);
break;
} }
}); });
return to_route('login.create') return to_route('login.create')
->with('success', 'User registered successfully.'); ->with('userRegistered', 'User registered successfully.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error('Registration Failed: '.$e->getMessage()); Log::error('Registration Failed: '.$e->getMessage());

View File

@ -1,8 +1,7 @@
<?php <?php
namespace App\Http\Controllers\Interaction; namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreReportRequest; use App\Http\Requests\StoreReportRequest;
use App\Models\Deal; use App\Models\Deal;
use App\Models\Report; use App\Models\Report;
@ -30,9 +29,9 @@ public function store(StoreReportRequest $request, Deal $deal)
$data['user_id'] = Auth::id(); $data['user_id'] = Auth::id();
// Check if the user already reported the deal // Check if the user already reported the deal
$alreadyReported = $deal->reports()->where('user_id', Auth::id())->exists(); $alreadyReported = $deal->reports()->where('user_id', Auth::id())->first();
if ($alreadyReported) { if ($alreadyReported) {
return response()->json(['message' => 'You had already reported this deal'], 200); return response()->json(['message' => 'You already reported this report'], 405);
} }
try { try {
@ -42,7 +41,7 @@ public function store(StoreReportRequest $request, Deal $deal)
Report::reguard(); Report::reguard();
}); });
return response()->json(['message' => 'Report submitted. Thank you for keeping DealHub safe'], 201); return response()->json(['message' => 'Report created'], 201);
} catch (\Throwable $exception) { } catch (\Throwable $exception) {
Log::error('Error creating report', [ Log::error('Error creating report', [
@ -78,21 +77,6 @@ public function update(Request $request, Report $report)
*/ */
public function destroy(Report $report) public function destroy(Report $report)
{ {
try { //
DB::transaction(function () use ($report) {
$report->deals()->detach();
$report->delete();
});
return back()->with('success', 'Report deleted successfully.');
} catch (\Throwable $e) {
Log::error('Error deleting report', [
'user_id' => Auth::id(),
'report_id' => $report->id,
'error' => $e->getMessage(),
]);
}
return back()->with('error', 'Something went wrong.');
} }
} }

View File

@ -1,36 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Enums\UserTypes;
use App\Http\Resources\ActiveUsersStatsCollection;
use App\Http\Resources\DealsCountByCategoryCollection;
use App\Models\DealCategory;
use App\Queries\PageVisitStatsQuery;
use Illuminate\Http\Request;
class StatsController extends Controller
{
public function getActiveUsers(Request $request, PageVisitStatsQuery $baseQuery)
{
$startDay = $request->from ?? now()->subDays(30);
$endDay = $request->to ?? now();
$activeCustomers = $baseQuery->builder(UserTypes::User, $startDay, $endDay)->get();
$activeBrokers = $baseQuery->builder(UserTypes::Broker, $startDay, $endDay)->get();
return response()->json([
'activeCustomers' => new ActiveUsersStatsCollection($activeCustomers),
'activeBrokers' => new ActiveUsersStatsCollection($activeBrokers),
]);
}
public function getDealsByCategory()
{
return new DealsCountByCategoryCollection(
DealCategory::select(['id', 'name'])
->withCount('deals')
->get()
);
}
}

View File

@ -1,73 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Actions\GetUserFavoritesAction;
use App\Actions\GetUserReportedDealsAction;
use App\Actions\UpdateCustomerAction;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreCustomerProfileRequest;
use App\Models\User;
use App\Services\ProfileInitialsService;
use Illuminate\Support\Facades\Log;
class UserProfileController extends Controller
{
/**
* Display the specified resource.
*/
public function show(
User $profile,
ProfileInitialsService $service,
GetUserFavoritesAction $favoritesAction,
GetUserReportedDealsAction $reportedDealsAction
) {
// Get the user profile
$user = $profile->type;
$initials = $service->create($profile->name);
return view('dashboards.user.profile.show')
->with('user', $profile)
->with('name', $profile->name)
->with('joinDate', $profile->created_at->format('F Y'))
->with('email', $profile->email)
->with('initials', $initials)
->with('location', $user->location)
->with('bio', $user->bio)
->with('phone', $user->phone)
->with('favorites', $favoritesAction->execute($profile))
->with('reported', $reportedDealsAction->execute($profile));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(User $profile)
{
return view('dashboards.user.profile.edit')
->with('profile', $profile)
->with('pageTitle', 'Edit Profile')
->with('title', 'Edit Your Profile')
->with('description', 'Update your profile information.')
->with('backLink', route('customer.profile.show', $profile))
->with('actionLink', route('customer.profile.update', $profile));
}
/**
* Update the specified resource in storage.
*/
public function update(StoreCustomerProfileRequest $request, User $profile, UpdateCustomerAction $action)
{
try {
$action->execute($request->validated(), $profile);
return to_route('customer.profile.show', $profile)
->with('success', 'Profile updated successfully.');
} catch (\Throwable $e) {
Log::error('Customer Profile Update Failed: '.$e->getMessage(), $e->getTrace());
return back()->withInput()->with('error', 'Something went wrong.');
}
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserFollowedBroker
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$sender = \Auth::user();
if ($sender->isBroker()) {
return $next($request);
}
$recipientUser = $request->route('recipient');
if ($recipientUser->isBroker()) {
$recipient = $recipientUser->type;
$isFollowing = $recipient->followers->contains($sender->id);
if ($isFollowing) {
return $next($request);
}
abort(403, 'You are not following this broker.');
}
abort('404', 'Broker not found.');
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CommentRequest extends FormRequest
{
public function rules(): array
{
return [
'text' => ['required', 'string', 'max:255'],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ContactRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'email' => 'required|email|max:255',
'message' => 'required|string|min:10|max:255',
];
}
protected function getRedirectUrl(): string
{
return parent::getRedirectUrl().'#contact';
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class DealOutboundRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\ExplorePageFilters;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ExploreSearchSortRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'sortBy' => ['nullable', 'string', Rule::in(ExplorePageFilters::values())],
'search' => ['nullable', 'string', 'min:1', 'max:255'],
'category' => ['nullable', 'exists:deal_categories,id'],
];
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PushSubscriptionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'endpoint' => 'required|string|max:500',
'keys.auth' => 'required|string|max:255',
'keys.p256dh' => 'required|string|max:255',
];
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendMessageRequest extends FormRequest
{
public function rules(): array
{
return [
'message' => 'required|string',
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -2,11 +2,9 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use AllowDynamicProperties;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
#[AllowDynamicProperties]
class StoreBrokerProfileRequest extends FormRequest class StoreBrokerProfileRequest extends FormRequest
{ {
/** /**
@ -14,17 +12,7 @@ class StoreBrokerProfileRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
// If this request is by a broker profile, then only allow the owner to update it. return $this->user()->isBroker();
if (isset($this->profile)) {
$this->user = $this->profile;
return $this->user()->id === $this->profile->id;
}
// If this request is by an admin, then allow them to update any profile.
$this->user = $this->broker->user;
return $this->user()->isAdmin();
} }
/** /**
@ -37,7 +25,7 @@ public function rules(): array
return [ return [
'name' => 'required|string|min:3|max:255', 'name' => 'required|string|min:3|max:255',
'bio' => 'required|string|min:10|max:255', 'bio' => 'required|string|min:10|max:255',
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->user->id)], 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->user()->id)],
'phone' => 'required|string|min:10|max:255', 'phone' => 'required|string|min:10|max:255',
'location' => 'required|string|min:3|max:255', 'location' => 'required|string|min:3|max:255',
]; ];

View File

@ -1,45 +0,0 @@
<?php
namespace App\Http\Requests;
use AllowDynamicProperties;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
#[AllowDynamicProperties]
class StoreCustomerProfileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// If this request is by a customer profile, then only allow the owner to update it.
if (isset($this->profile)) {
$this->user = $this->profile;
return $this->user()->id === $this->profile->id;
}
// If this request is by an admin, then allow them to update any profile.
$this->user = $this->customer->user;
return $this->user()->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3|max:255',
'bio' => 'required|string|min:10|max:255',
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->user->id)],
'phone' => 'required|string|min:10|max:255',
'location' => 'required|string|min:3|max:255',
];
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ActiveUsersStatsCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ActiveUsersStatsResource extends JsonResource
{
/**
* @return array{data: string, userCount: int}
*/
public function toArray(Request $request): array
{
return [
'date' => $this->date,
'userCount' => (int) $this->user_count,
];
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class BrokerRoleResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'bio' => $this->bio,
'location' => $this->location,
'phone' => $this->phone,
];
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\URL;
class DealResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'image' => asset('storage/'.$this->image),
'link' => $this->link !== null ? URL::signedRoute('redirect', $this->id) : null,
'category' => $this->whenLoaded('category'),
'broker' => new UserResource($this->whenLoaded('broker')),
'totalLikes' => $this->total_likes,
'totalRedirection' => $this->total_redirection,
'isLiked' => $this->is_liked,
'isFavorite' => $this->is_favorite,
'isFollowed' => $this->is_followed,
];
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class DealsCountByCategoryCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
];
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DealsCountByCategoryResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'name' => $this->name,
'dealsCount' => $this->deals_count,
];
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'role' => new BrokerRoleResource($this->whenLoaded('role')),
];
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PasswordResetMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(private readonly string $otp) {}
public function envelope(): Envelope
{
return new Envelope(
from: new Address(config('mail.from.address'), config('mail.from.name')),
subject: 'Password Reset',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.password-reset',
with: ['otp' => $this->otp]
);
}
public function attachments(): array
{
return [];
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
/**
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Admin whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Admin extends Model
{
public function user(): MorphOne
{
return $this->morphOne(User::class, 'role');
}
}

View File

@ -3,39 +3,10 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\MorphOne;
/**
* @property int $id
* @property string|null $bio
* @property string|null $location
* @property string|null $phone
* @property bool $verified
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Follow|null $pivot
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Customer> $followers
* @property-read int|null $followers_count
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereBio($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereLocation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker wherePhone($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Broker whereVerified($value)
*
* @mixin \Eloquent
*/
class Broker extends Model class Broker extends Model
{ {
protected $fillable = ['bio', 'location', 'phone'];
protected function casts(): array protected function casts(): array
{ {
return [ return [
@ -47,14 +18,4 @@ public function user(): MorphOne
{ {
return $this->morphOne(User::class, 'role'); return $this->morphOne(User::class, 'role');
} }
public function followers(): BelongsToMany
{
return $this->belongsToMany(
Customer::class,
'follows',
'broker_id',
'customer_id'
)->using(Follow::class);
}
} }

View File

@ -1,47 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property string $text
* @property int $deal_id
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Deal|null $deal
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereDealId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereText($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Comment whereUserId($value)
*
* @mixin \Eloquent
*/
class Comment extends Model
{
protected $fillable = [
'text',
'deal_id',
'user_id',
];
public function deal(): BelongsTo
{
return $this->belongsTo(Deal::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
/**
* @property int $id
* @property string|null $bio
* @property string|null $location
* @property string|null $phone
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Follow> $followings
* @property-read int|null $followings_count
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereBio($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereLocation($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer wherePhone($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Customer whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Customer extends Model
{
protected $fillable = ['bio', 'location', 'phone'];
public function user(): MorphOne
{
return $this->morphOne(User::class, 'role');
}
public function followings(): HasMany
{
return $this->hasMany(Follow::class, 'customer_id');
}
}

View File

@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use App\Enums\InteractionType; use App\Enums\InteractionType;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -11,52 +10,6 @@
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
/**
* @property int $id
* @property string $title
* @property string $slug
* @property string $description
* @property string|null $image
* @property string|null $link
* @property int $active
* @property int $deal_category_id
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $broker
* @property-read \App\Models\DealCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Interaction> $interactions
* @property-read int|null $interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Report> $reports
* @property-read int|null $reports_count
*
* @method static Builder<static>|Deal WithActiveDeals()
* @method static Builder<static>|Deal WithCurrentUserInteractions()
* @method static Builder<static>|Deal WithLikePerDeal()
* @method static Builder<static>|Deal WithRedirectionPerDeal()
* @method static Builder<static>|Deal filterByCategory(string $category)
* @method static Builder<static>|Deal newModelQuery()
* @method static Builder<static>|Deal newQuery()
* @method static Builder<static>|Deal query()
* @method static Builder<static>|Deal search(string $search)
* @method static Builder<static>|Deal whereActive($value)
* @method static Builder<static>|Deal whereCreatedAt($value)
* @method static Builder<static>|Deal whereDealCategoryId($value)
* @method static Builder<static>|Deal whereDescription($value)
* @method static Builder<static>|Deal whereId($value)
* @method static Builder<static>|Deal whereImage($value)
* @method static Builder<static>|Deal whereLink($value)
* @method static Builder<static>|Deal whereSlug($value)
* @method static Builder<static>|Deal whereTitle($value)
* @method static Builder<static>|Deal whereUpdatedAt($value)
* @method static Builder<static>|Deal whereUserId($value)
* @method static Builder<static>|Deal withIsFollowedByCurrentUser()
* @method static Builder<static>|Deal withViewPerDeal()
*
* @mixin \Eloquent
*/
class Deal extends Model class Deal extends Model
{ {
public function broker(): BelongsTo public function broker(): BelongsTo
@ -74,30 +27,18 @@ public function interactions(): HasMany
return $this->hasMany(Interaction::class); return $this->hasMany(Interaction::class);
} }
public function reports(): BelongsToMany
{
return $this->belongsToMany(Report::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
/** /**
* Scope a query to only include active deals * Get deals that are active
*/ */
#[Scope] public function scopeWithActiveDeals(Builder $query): Builder
public function WithActiveDeals(Builder $query): Builder
{ {
return $query->where('active', true); return $query->where('active', true);
} }
/** /**
* Scope a query to determine if the current user has liked and favorite a deal * Get if the current user has liked or favorite the deal
*/ */
#[Scope] public function scopeWithCurrentUserInteractions(Builder $query): Builder
public function WithCurrentUserInteractions(Builder $query): Builder
{ {
return $query->withExists([ return $query->withExists([
'interactions as is_liked' => function ($query) { 'interactions as is_liked' => function ($query) {
@ -111,11 +52,7 @@ public function WithCurrentUserInteractions(Builder $query): Builder
]); ]);
} }
/** public function scopeWithLikes(Builder $query): Builder
* Scope a query to get total like count per deal
*/
#[Scope]
public function WithLikePerDeal(Builder $query): Builder
{ {
return $query->withCount([ return $query->withCount([
'interactions as total_likes' => function ($query) { 'interactions as total_likes' => function ($query) {
@ -124,76 +61,8 @@ public function WithLikePerDeal(Builder $query): Builder
]); ]);
} }
/** public function reports(): BelongsToMany
* Scope a query to get click count per deal
*/
#[Scope]
public function WithRedirectionPerDeal(Builder $query): Builder
{ {
return $query->withSum([ return $this->belongsToMany(Report::class);
'interactions as total_redirection' => function ($query) {
$query->where('type', InteractionType::Redirection);
},
], 'count');
}
/**
* Scope a query to get a view count per deal
*/
#[Scope]
public function withViewPerDeal(Builder $query): Builder
{
return $query->withSum([
'interactions as total_views' => function ($query) {
$query->where('type', InteractionType::View);
},
], 'count');
}
/**
* Scope a search in a query
*/
#[Scope]
public function search(Builder $query, string $search): Builder
{
return $query->where(function (Builder $query) use ($search) {
$query->where('title', 'LIKE', "%$search%")
->orWhereRelation('broker', 'name', 'LIKE', "%$search%");
});
}
/**
* Scope a category filter in a query
*/
#[Scope]
public function filterByCategory(Builder $query, string $category): Builder
{
return $query->where('deal_category_id', $category);
}
// Add this to App\Models\Deal.php
/**
* Scope a query to check if the current user follows the deal's broker
*/
#[Scope]
public function withIsFollowedByCurrentUser(Builder $query): Builder
{
$user = Auth::user();
if (! $user || $user->role_type !== \App\Models\Customer::class) {
return $query->withExists(['broker as is_followed' => fn ($q) => $q->whereRaw('1 = 0')]);
}
return $query->withExists([
'broker as is_followed' => function ($query) use ($user) {
$query->where('role_type', \App\Models\Broker::class)
->whereHasMorph('type', [\App\Models\Broker::class], function ($query) use ($user) {
$query->whereHas('followers', function ($query) use ($user) {
$query->where('customer_id', $user->id);
});
});
},
]);
} }
} }

View File

@ -5,36 +5,8 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $name
* @property string|null $description
* @property string $slug
* @property int $active
* @property int $order
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereActive($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereOrder($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereSlug($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|DealCategory whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class DealCategory extends Model class DealCategory extends Model
{ {
protected $fillable = ['name', 'description', 'slug', 'active', 'order'];
public function deals(): HasMany public function deals(): HasMany
{ {
return $this->hasMany(Deal::class); return $this->hasMany(Deal::class);

View File

@ -1,46 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
/**
* @property int $id
* @property int $customer_id
* @property int $broker_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Broker|null $broker
* @property-read \App\Models\Customer|null $customer
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereBrokerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereCustomerId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Follow whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Follow extends Pivot
{
protected $table = 'follows';
protected $fillable = [
'customer_id',
'broker_id',
];
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function broker(): BelongsTo
{
return $this->belongsTo(Broker::class);
}
}

View File

@ -1,55 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string|null $last_message
* @property int|null $last_user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $lastUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
* @property-read int|null $users_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastMessage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereLastUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Inbox whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class Inbox extends Model
{
protected $fillable = ['last_user_id', 'last_message'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
public function lastUser(): BelongsTo
{
return $this->belongsTo(User::class, 'id', 'last_user_id');
}
public function messages(): HasMany
{
return $this->hasMany(Message::class, 'inbox_id');
}
public function getRecipientAttribute(): User
{
// first user in the relationship that is NOT the authenticated user
return $this->users->where('id', '!=', auth()->id())->first();
}
}

View File

@ -6,37 +6,8 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $user_id
* @property int $deal_id
* @property InteractionType $type
* @property int $count
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Deal|null $deal
* @property-read \App\Models\Deal|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereCount($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereDealId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Interaction whereUserId($value)
*
* @mixin \Eloquent
*/
class Interaction extends Model class Interaction extends Model
{ {
protected $fillable = [
'type',
'user_id',
];
public function user(): BelongsTo public function user(): BelongsTo
{ {
return $this->belongsTo(Deal::class); return $this->belongsTo(Deal::class);

View File

@ -1,62 +0,0 @@
<?php
namespace App\Models;
use Eloquent;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
/**
* @property int $id
* @property int $user_id
* @property int $inbox_id
* @property string $message
* @property string|null $read_at
* @property string|null $delivered_at
* @property string|null $deleted_at
* @property string|null $failed_at
* @property Carbon $created_at
* @property-read Inbox|null $inbox
* @property-read User|null $user
*
* @method static Builder<static>|Message newModelQuery()
* @method static Builder<static>|Message newQuery()
* @method static Builder<static>|Message query()
* @method static Builder<static>|Message whereCreatedAt($value)
* @method static Builder<static>|Message whereDeletedAt($value)
* @method static Builder<static>|Message whereDeliveredAt($value)
* @method static Builder<static>|Message whereFailedAt($value)
* @method static Builder<static>|Message whereId($value)
* @method static Builder<static>|Message whereInboxId($value)
* @method static Builder<static>|Message whereMessage($value)
* @method static Builder<static>|Message whereReadAt($value)
* @method static Builder<static>|Message whereUserId($value)
*
* @mixin Eloquent
*/
class Message extends Model
{
public $timestamps = false;
protected $fillable = [
'inbox_id', 'user_id', 'message',
'read_at', 'deleted_at', 'failed_at',
'created_at', 'delivered_at',
];
protected $casts = [
'created_at' => 'datetime',
];
public function inbox(): BelongsTo
{
return $this->belongsTo(Inbox::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Models;
use App\Enums\UserTypes;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int|null $user_id
* @property string $page
* @property UserTypes|null $user_type
* @property string $created_at
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit wherePage($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|PageVisit whereUserType($value)
*
* @mixin \Eloquent
*/
class PageVisit extends Model
{
protected $fillable = [
'user_id', 'page', 'user_type',
'created_at',
];
public $timestamps = false;
protected function casts(): array
{
return [
'user_type' => UserTypes::class,
];
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $user_id
* @property string $query
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\User|null $user
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereQuery($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|RecentSearch whereUserId($value)
*
* @mixin \Eloquent
*/
class RecentSearch extends Model
{
protected $fillable = ['query'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -4,38 +4,10 @@
use App\Enums\ReportStatus; use App\Enums\ReportStatus;
use App\Enums\ReportType; use App\Enums\ReportType;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property ReportType $type
* @property ReportStatus $status
* @property string $description
* @property int $user_id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
* @property-read \App\Models\User|null $user
*
* @method static Builder<static>|Report newModelQuery()
* @method static Builder<static>|Report newQuery()
* @method static Builder<static>|Report orderByStatus(array $statusOrder)
* @method static Builder<static>|Report query()
* @method static Builder<static>|Report whereCreatedAt($value)
* @method static Builder<static>|Report whereDescription($value)
* @method static Builder<static>|Report whereId($value)
* @method static Builder<static>|Report whereStatus($value)
* @method static Builder<static>|Report whereType($value)
* @method static Builder<static>|Report whereUpdatedAt($value)
* @method static Builder<static>|Report whereUserId($value)
*
* @mixin \Eloquent
*/
class Report extends Model class Report extends Model
{ {
public function user(): BelongsTo public function user(): BelongsTo
@ -55,15 +27,4 @@ protected function casts(): array
'status' => ReportStatus::class, 'status' => ReportStatus::class,
]; ];
} }
#[Scope]
public function orderByStatus(Builder $query, array $statusOrder): Builder
{
$values = array_map(fn ($enum) => $enum->value, $statusOrder);
// Create placeholders for each value: FIELD(status, ?, ?, ?)
$placeholders = implode(',', array_fill(0, count($values), '?'));
return $query->orderByRaw("FIELD(status, $placeholders)", $values);
}
} }

View File

@ -5,74 +5,15 @@
// use Illuminate\Contracts\Auth\MustVerifyEmail; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use NotificationChannels\WebPush\HasPushSubscriptions;
/**
* @property int $id
* @property string $name
* @property string $email
* @property \Illuminate\Support\Carbon|null $email_verified_at
* @property string $password
* @property string|null $remember_token
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string $status
* @property string $role
* @property string|null $role_type
* @property int|null $role_id
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $deals
* @property-read int|null $deals_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Interaction> $dealsInteractions
* @property-read int|null $deals_interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Inbox> $inboxes
* @property-read int|null $inboxes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Deal> $interactedDeals
* @property-read int|null $interacted_deals_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, User> $interactions
* @property-read int|null $interactions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \NotificationChannels\WebPush\PushSubscription> $pushSubscriptions
* @property-read int|null $push_subscriptions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\RecentSearch> $recentSearches
* @property-read int|null $recent_searches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Report> $reports
* @property-read int|null $reports_count
* @property-read Model|\Eloquent|null $type
*
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRole($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRoleId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRoleType($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
*
* @mixin \Eloquent
*/
class User extends Authenticatable class User extends Authenticatable
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */ /** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, HasPushSubscriptions, Notifiable; use HasFactory, Notifiable;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -115,16 +56,6 @@ public function isBroker(): bool
return $this->type instanceof Broker; return $this->type instanceof Broker;
} }
public function isAdmin(): bool
{
return $this->type instanceof Admin;
}
public function isCustomer(): bool
{
return $this->type instanceof Customer;
}
public function deals(): HasMany public function deals(): HasMany
{ {
return $this->hasMany(Deal::class); return $this->hasMany(Deal::class);
@ -142,39 +73,4 @@ public function interactions(): HasMany
{ {
return $this->hasMany(User::class); return $this->hasMany(User::class);
} }
public function recentSearches(): HasMany
{
return $this->hasMany(RecentSearch::class);
}
public function dealsInteractions(): HasManyThrough
{
return $this->hasManyThrough(Interaction::class, Deal::class);
}
public function interactedDeals(): HasManyThrough
{
return $this->hasManyThrough(Deal::class, Interaction::class, 'user_id', 'id', 'id', 'deal_id');
}
public function reports(): HasMany
{
return $this->hasMany(Report::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function inboxes(): BelongsToMany
{
return $this->belongsToMany(Inbox::class);
}
public function messages(): HasMany
{
return $this->hasMany(Message::class);
}
} }

View File

@ -1,37 +0,0 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class NewContactNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $customerName,
private readonly string $customerEmail,
private readonly string $customerMessage
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('New Contact Submission: '.$this->customerName)
->greeting('Hello Admin,') // Or keep it empty if you prefer
->line('You have received a new message from your contact form:')
->line("**Name:** {$this->customerName}")
->line("**Email:** {$this->customerEmail}")
->line('**Message:**')
->line($this->customerMessage)
->action('Reply via Email', 'mailto:'.$this->customerEmail);
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace App\Notifications;
use App\Models\Deal;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
use NotificationChannels\WebPush\WebPushChannel;
use NotificationChannels\WebPush\WebPushMessage;
class NewDealNotification extends Notification
{
/**
* Create a new notification instance.
*/
public function __construct(private readonly Deal $deal)
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return [WebPushChannel::class];
}
public function toWebPush($notifiable, $notification): WebPushMessage
{
\Log::info('Building WebPush for user: '.$notifiable->id);
return (new WebPushMessage)
->title("New deal from {$this->deal->broker->name}")
->body('Check out this deal:'.Str::limit($this->deal->title, 30, '...'))
->action('View deal', route('explore', ['show' => $this->deal->id]));
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ReportRejectedNotificationToUser extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $dealTitle,
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
return (new MailMessage)
->subject('Update on Your Recent Report: '.$this->dealTitle)
->greeting('Hello!')
->line('Thank you for helping us maintain the integrity of our marketplace.')
->line("We have completed our review of the deal you reported: **{$this->dealTitle}**.")
->line('Based on our moderation policy, we have rejected your report.')
->action('View Marketplace', route('explore'))
->line('Your feedback helps make our community a safer place for everyone.');
}
public function toArray($notifiable): array
{
return [
'report_outcome' => $this->isContentRemoved ? 'violation_confirmed' : 'no_violation_found',
'deal_title' => $this->dealTitle,
];
}
}

View File

@ -1,46 +0,0 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ReportResolvedNotificationToBroker extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $dealTitle,
private readonly bool $isContentRemoved
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
$status = $this->isContentRemoved
? 'has been removed following a policy review.'
: 'has been reviewed and remains active.';
return (new MailMessage)
->subject('Update Regarding Your Reported Deal: '.$this->dealTitle)
->greeting('Hello!')
->line("We are writing to inform you that the report regarding your deal, **{$this->dealTitle}**, has been resolved.")
->line("Our moderation team has completed their review, and the content {$status}")
->action('View My Deals', route('broker.dashboard')) // Adjusted for your UMS/Project structure
->line('Thank you for being a part of our marketplace.');
}
public function toArray($notifiable): array
{
return [
'deal_title' => $this->dealTitle,
'action_taken' => $this->isContentRemoved ? 'removed' : 'kept',
];
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ReportResolvedNotificationToUser extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
private readonly string $dealTitle,
private readonly bool $isContentRemoved
) {}
public function via($notifiable): array
{
return ['mail'];
}
public function toMail($notifiable): MailMessage
{
$outcome = $this->isContentRemoved
? 'has been removed following our investigation.'
: 'will remain active as it was found to be in compliance with our guidelines.';
return (new MailMessage)
->subject('Update on Your Recent Report: '.$this->dealTitle)
->greeting('Hello!')
->line('Thank you for helping us maintain the integrity of our marketplace.')
->line("We have completed our review of the deal you reported: **{$this->dealTitle}**.")
->line("Based on our moderation policy, the content {$outcome}")
->action('View Marketplace', route('explore'))
->line('Your feedback helps make our community a safer place for everyone.');
}
public function toArray($notifiable): array
{
return [
'report_outcome' => $this->isContentRemoved ? 'violation_confirmed' : 'no_violation_found',
'deal_title' => $this->dealTitle,
];
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Queries;
use App\Models\Deal;
use Illuminate\Database\Eloquent\Builder;
final readonly class ExplorePageDealsQuery
{
/**
* @return Builder<Deal>
*/
public function builder(): Builder
{
return Deal::query()
->select([
'id', 'title', 'description', 'image', 'active', 'slug', 'link',
'deal_category_id', 'user_id',
])
// Select additional details
->with([
'category:id,name',
'broker' => function ($query) {
$query->select('id', 'name', 'email', 'role_type', 'role_id')
->with('type');
},
])
// Check if the current user interacted with the deal
->tap(fn ($q) => (new Deal)->withCurrentUserInteractions($q))
->tap(fn ($q) => (new Deal)->withLikePerDeal($q))
->tap(fn ($q) => (new Deal)->withIsFollowedByCurrentUser($q))
->tap(fn ($q) => (new Deal)->withRedirectionPerDeal($q));
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Queries;
use App\Enums\UserTypes;
use App\Models\PageVisit;
use Illuminate\Database\Eloquent\Builder;
final readonly class PageVisitStatsQuery
{
/**
* @return Builder<PageVisit>
*/
public function builder(UserTypes $userType, string $startDay, string $endDay): Builder
{
return PageVisit::query()
->selectRaw('count(distinct user_id) as user_count, created_at as date')
->where('user_type', $userType)
->whereBetween('created_at', [$startDay.' 00:00:00', $endDay.' 23:59:59'])
->groupBy('date');
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class OTPService
{
public function generate(User $user, int $length = 6): string
{
$code = \Str::random($length);
Cache::put("otp_$user->id", $code, now()->addMinutes((int) config('auth.otp_lifespan', '10')));
return $code;
}
public function verify(User $user, string $otp): bool
{
$code = Cache::get("otp_$user->id");
if ($code === $otp) {
Cache::forget("otp_$user->id");
return true;
}
return false;
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Services;
use Illuminate\Support\Str;
class ProfileInitialsService
{
/**
* Create the initials from a full name (e.g. John Doe, Alex Mark, jane clerk)
* to display on the profile page (e.g. JD, AM, JC).
*/
public function create(string $fullname)
{
return Str::of($fullname)
->explode(' ')
->map(fn ($word) => Str::substr(ucfirst($word), 0, 1))
->join('');
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Services;
use Twilio\Exceptions\TwilioException;
use Twilio\Rest\Client;
class TwilioService
{
private Client $client;
public function __construct()
{
$this->client = new Client(config('services.twilio.sid'), config('services.twilio.token'));
}
/**
* @throws TwilioException
*/
public function sendSms(string $to, string $message): void
{
$this->client->messages->create($to, ['from' => config('services.twilio.from'), 'body' => $message]);
}
}

View File

@ -8,7 +8,6 @@
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {

2
bootstrap/cache/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,105 +0,0 @@
<?php return array (
'barryvdh/laravel-ide-helper' =>
array (
'providers' =>
array (
0 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
),
),
'blade-ui-kit/blade-heroicons' =>
array (
'providers' =>
array (
0 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
),
),
'blade-ui-kit/blade-icons' =>
array (
'providers' =>
array (
0 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
),
),
'laradumps/laradumps' =>
array (
'providers' =>
array (
0 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
),
),
'laravel-notification-channels/webpush' =>
array (
'providers' =>
array (
0 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
),
),
'laravel/pail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Pail\\PailServiceProvider',
),
),
'laravel/reverb' =>
array (
'providers' =>
array (
0 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
1 => 'Laravel\\Reverb\\ReverbServiceProvider',
),
),
'laravel/sail' =>
array (
'providers' =>
array (
0 => 'Laravel\\Sail\\SailServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'nesbot/carbon' =>
array (
'providers' =>
array (
0 => 'Carbon\\Laravel\\ServiceProvider',
),
),
'nunomaduro/collision' =>
array (
'providers' =>
array (
0 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
),
),
'nunomaduro/termwind' =>
array (
'providers' =>
array (
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
),
),
'opcodesio/log-viewer' =>
array (
'aliases' =>
array (
'LogViewer' => 'Opcodes\\LogViewer\\Facades\\LogViewer',
),
'providers' =>
array (
0 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
),
),
'pestphp/pest-plugin-laravel' =>
array (
'providers' =>
array (
0 => 'Pest\\Laravel\\PestServiceProvider',
),
),
);

View File

@ -1,289 +0,0 @@
<?php return array (
'providers' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
2 => 'Illuminate\\Bus\\BusServiceProvider',
3 => 'Illuminate\\Cache\\CacheServiceProvider',
4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
5 => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
6 => 'Illuminate\\Cookie\\CookieServiceProvider',
7 => 'Illuminate\\Database\\DatabaseServiceProvider',
8 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
9 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
10 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
11 => 'Illuminate\\Hashing\\HashServiceProvider',
12 => 'Illuminate\\Mail\\MailServiceProvider',
13 => 'Illuminate\\Notifications\\NotificationServiceProvider',
14 => 'Illuminate\\Pagination\\PaginationServiceProvider',
15 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
16 => 'Illuminate\\Pipeline\\PipelineServiceProvider',
17 => 'Illuminate\\Queue\\QueueServiceProvider',
18 => 'Illuminate\\Redis\\RedisServiceProvider',
19 => 'Illuminate\\Session\\SessionServiceProvider',
20 => 'Illuminate\\Translation\\TranslationServiceProvider',
21 => 'Illuminate\\Validation\\ValidationServiceProvider',
22 => 'Illuminate\\View\\ViewServiceProvider',
23 => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
24 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
25 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
26 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
27 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
28 => 'Laravel\\Pail\\PailServiceProvider',
29 => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
30 => 'Laravel\\Reverb\\ReverbServiceProvider',
31 => 'Laravel\\Sail\\SailServiceProvider',
32 => 'Laravel\\Tinker\\TinkerServiceProvider',
33 => 'Carbon\\Laravel\\ServiceProvider',
34 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
35 => 'Termwind\\Laravel\\TermwindServiceProvider',
36 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
37 => 'Pest\\Laravel\\PestServiceProvider',
38 => 'App\\Providers\\AppServiceProvider',
),
'eager' =>
array (
0 => 'Illuminate\\Auth\\AuthServiceProvider',
1 => 'Illuminate\\Cookie\\CookieServiceProvider',
2 => 'Illuminate\\Database\\DatabaseServiceProvider',
3 => 'Illuminate\\Encryption\\EncryptionServiceProvider',
4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider',
5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider',
6 => 'Illuminate\\Notifications\\NotificationServiceProvider',
7 => 'Illuminate\\Pagination\\PaginationServiceProvider',
8 => 'Illuminate\\Session\\SessionServiceProvider',
9 => 'Illuminate\\View\\ViewServiceProvider',
10 => 'BladeUI\\Heroicons\\BladeHeroiconsServiceProvider',
11 => 'BladeUI\\Icons\\BladeIconsServiceProvider',
12 => 'LaraDumps\\LaraDumps\\LaraDumpsServiceProvider',
13 => 'NotificationChannels\\WebPush\\WebPushServiceProvider',
14 => 'Laravel\\Pail\\PailServiceProvider',
15 => 'Laravel\\Reverb\\ReverbServiceProvider',
16 => 'Carbon\\Laravel\\ServiceProvider',
17 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
18 => 'Termwind\\Laravel\\TermwindServiceProvider',
19 => 'Opcodes\\LogViewer\\LogViewerServiceProvider',
20 => 'Pest\\Laravel\\PestServiceProvider',
21 => 'App\\Providers\\AppServiceProvider',
),
'deferred' =>
array (
'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider',
'cache' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider',
'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider',
'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider',
'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\Console\\InvokeSerializedClosureCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PauseCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\ResumeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ReloadCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ApiInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\BroadcastingInstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ClassMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConfigPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EnumMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\InterfaceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\JobMiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\TraitMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Migrations\\Migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider',
'Illuminate\\Concurrency\\ConcurrencyManager' => 'Illuminate\\Concurrency\\ConcurrencyServiceProvider',
'hash' => 'Illuminate\\Hashing\\HashServiceProvider',
'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider',
'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider',
'mailer' => 'Illuminate\\Mail\\MailServiceProvider',
'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider',
'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider',
'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider',
'queue' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider',
'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider',
'redis' => 'Illuminate\\Redis\\RedisServiceProvider',
'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider',
'translator' => 'Illuminate\\Translation\\TranslationServiceProvider',
'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider',
'validator' => 'Illuminate\\Validation\\ValidationServiceProvider',
'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\GeneratorCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\ModelsCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\MetaCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Barryvdh\\LaravelIdeHelper\\Console\\EloquentCommand' => 'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider',
'Laravel\\Reverb\\ApplicationManager' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
'Laravel\\Reverb\\Contracts\\ApplicationProvider' => 'Laravel\\Reverb\\ApplicationManagerServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
),
'when' =>
array (
'Illuminate\\Broadcasting\\BroadcastServiceProvider' =>
array (
),
'Illuminate\\Bus\\BusServiceProvider' =>
array (
),
'Illuminate\\Cache\\CacheServiceProvider' =>
array (
),
'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' =>
array (
),
'Illuminate\\Concurrency\\ConcurrencyServiceProvider' =>
array (
),
'Illuminate\\Hashing\\HashServiceProvider' =>
array (
),
'Illuminate\\Mail\\MailServiceProvider' =>
array (
),
'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' =>
array (
),
'Illuminate\\Pipeline\\PipelineServiceProvider' =>
array (
),
'Illuminate\\Queue\\QueueServiceProvider' =>
array (
),
'Illuminate\\Redis\\RedisServiceProvider' =>
array (
),
'Illuminate\\Translation\\TranslationServiceProvider' =>
array (
),
'Illuminate\\Validation\\ValidationServiceProvider' =>
array (
),
'Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider' =>
array (
),
'Laravel\\Reverb\\ApplicationManagerServiceProvider' =>
array (
),
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);

View File

@ -11,14 +11,10 @@
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"blade-ui-kit/blade-heroicons": "^2.6", "blade-ui-kit/blade-heroicons": "^2.6",
"laravel-notification-channels/webpush": "^10.4",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/reverb": "^1.0", "laravel/tinker": "^2.10.1"
"laravel/tinker": "^2.10.1",
"twilio/sdk": "^8.10"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "^3.6",
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"laradumps/laradumps": "^5.0", "laradumps/laradumps": "^5.0",
"laravel/pail": "^1.2.2", "laravel/pail": "^1.2.2",
@ -26,7 +22,6 @@
"laravel/sail": "^1.41", "laravel/sail": "^1.41",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6", "nunomaduro/collision": "^8.6",
"opcodesio/log-viewer": "^3.21",
"pestphp/pest": "^4.3", "pestphp/pest": "^4.3",
"pestphp/pest-plugin-laravel": "^4.0" "pestphp/pest-plugin-laravel": "^4.0"
}, },

2467
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -111,6 +111,5 @@
*/ */
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
'otp_lifespan' => env('OTP_LIFESPAN', 10),
]; ];

View File

@ -1,82 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View File

@ -1,95 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];

Some files were not shown because too many files have changed in this diff Show More