Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

299 changed files with 440 additions and 43046 deletions

View File

@ -1,4 +1,4 @@
APP_NAME=DealHub
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
@ -63,28 +63,3 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
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}"

1
.gitignore vendored
View File

@ -22,4 +22,3 @@
Homestead.json
Homestead.yaml
Thumbs.db
laradumps.yaml

File diff suppressed because it is too large Load Diff

147
README.md
View File

@ -1,128 +1,59 @@
# 🚀 DealHub
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
**DelHub** is a **Recommendation & Deal Discovery Platform** built with **Laravel**, where verified brokers share deals and recommendations, and users discover trusted content through likes, favorites, search, and trending logic.
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
---
## About Laravel
## 📌 Features
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
### 🌐 Public / User
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
* View all approved deals and listings
* Like ❤️, Favorite ⭐, and Report 🚩 deals
* Search deals with category filters
* Discover trending, most liked, and recent deals
* View broker contact information
Laravel is accessible, powerful, and provides tools required for large, robust applications.
### 🧑‍💼 Broker
## Learning Laravel
* Create and manage deal posts
* Upload images and external links
* Public broker profile with listings
* View analytics (views, likes, clicks)
* Broker dashboard with performance insights
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
### 👑 Admin
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
* Manage users and brokers (CRUD)
* Login as User/Broker (for support/debugging)
* Approve or reject broker listings
* Feature or block deals
* Admin analytics dashboard
## Laravel Sponsors
---
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
## 🧱 Tech Stack
### Premium Partners
* **Backend:** Laravel
* **Frontend:** Blade + Tailwind CSS
* **Authentication:** Laravel Breeze
* **Database:** MySQL
* **Version Control:** Git & GitHub
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
---
## Contributing
## 🗂️ Core Modules
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
* Authentication & Role Management
* Listings (Deals)
* User Interactions (Like, Favorite, Report)
* Tracking (Views & Clicks)
* Search & Discovery
* Broker Profiles & Analytics
* Admin Dashboard & Controls
* Landing Page (Home, About, Contact)
## Code of Conduct
---
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## 🖥️ Landing Page Includes
## Security Vulnerabilities
* Header with navigation
* Hero banner
* About section
* How it works
* Contact form
* Footer with links
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
---
Used for:
* Trending deals
* Homepage highlights
* Category top listings
---
## ⚙️ Installation
```bash
git clone https://git.sentientgeeks.us/joydip.manna/dealhub.git
cd dealhub
# Install dependencies
composer install
npm install
# Set VAPID for webpush
php artisan webpush:vapid
# Install Reverb
php artisan install:broadcasting
npm run dev
php artisan migrate
php artisan serve
# Start Reverb Server ( Another terminal )
php artisan reverb:start
```
---
## 🔐 Roles
* **Admin**
* **Broker**
* **User**
Role-based access is handled via middleware.
---
## 📈 Future Enhancements
* Notifications
* Deal expiry & auto-hide
* API support
* Mobile app integration
* Advanced analytics charts
---
## 👨‍💻 Author
**DealHub**
Built with ❤️ using Laravel
---
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

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

@ -1,15 +0,0 @@
<?php
namespace App\Enums;
use App\Traits\EnumAsArray;
enum InteractionType: string
{
use EnumAsArray;
case Like = 'like';
case Favorite = 'favorite';
case Redirection = 'redirect';
case View = 'view';
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Enums;
use App\Traits\EnumAsArray;
enum ReportStatus: string
{
use EnumAsArray;
case Pending = 'pending';
case Resolved = 'resolved';
case Rejected = 'rejected';
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Enums;
use App\Traits\EnumAsArray;
enum ReportType: string
{
use EnumAsArray;
case Spam = 'spam';
case Harmful = 'harmful';
case Nudity = 'nudity';
case Misinformation = 'misinformation';
case Unauthorized = 'unauthorized';
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Enums;
use App\Traits\EnumAsArray;
enum UserStatus: string
{
use EnumAsArray;
case Active = 'active';
case Blocked = 'blocked';
case Pending = 'pending';
}

View File

@ -1,34 +0,0 @@
<?php
namespace App\Enums;
use App\Traits\EnumAsArray;
enum UserTypes: string
{
use EnumAsArray;
case Admin = 'admin';
case User = 'user';
case Broker = 'broker';
public function label(): string
{
return match ($this) {
UserTypes::Broker => 'Broker (Posts deals)',
UserTypes::User => 'User (Browse deals)',
self::Admin => 'Admin',
};
}
public static function labels(): array
{
$labels = array_map(function ($enum) {
return ['value' => $enum->value, 'label' => $enum->label()];
}, self::cases());
return array_filter($labels, function ($kv) {
return $kv['value'] !== self::Admin->value;
});
}
}

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,61 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Actions\RecordUserPageVisitAction;
use App\Enums\UserStatus;
use App\Enums\UserTypes;
use App\Http\Controllers\Controller;
use App\Http\Requests\AuthenticateUserRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthenticatedUserController extends Controller
{
public function create()
{
return view('auth.login');
}
public function store(AuthenticateUserRequest $request, RecordUserPageVisitAction $action)
{
$data = $request->validated();
if (Auth::attempt($data, $data['remember_me'] ?? false)) {
$user = Auth::user();
if ($user->status !== UserStatus::Active->value) {
Auth::logout();
return back()->with('error', 'Your account is not active.');
}
$request->session()->regenerate();
$route = match ($user->role) {
UserTypes::Admin->value => 'admin.dashboard',
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);
} else {
return back()
->withInput()
->with('error', 'Invalid Credentials');
}
}
public function destroy(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route('home');
}
}

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,58 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\UserStatus;
use App\Enums\UserTypes;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreRegisterdUser;
use App\Models\Broker;
use App\Models\Customer;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class RegisteredUserController extends Controller
{
public function create()
{
return view('auth.register');
}
public function store(StoreRegisterdUser $request)
{
$data = $request->validated();
try {
DB::transaction(function () use ($data) {
switch ($data['role']) {
case UserTypes::Broker->value:
$data['status'] = UserStatus::Pending->value;
// Create Broker first, then link the user
$broker = Broker::create();
$broker->user()->create($data);
break;
case UserTypes::User->value:
$data['status'] = UserStatus::Active->value;
$customer = Customer::create();
$customer->user()->create($data);
break;
}
});
return to_route('login.create')
->with('success', 'User registered successfully.');
} catch (\Throwable $e) {
Log::error('Registration Failed: '.$e->getMessage());
return back()
->withInput()
->with('error', 'Something went wrong during registration.');
}
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Http\Controllers\Broker;
use App\Actions\GetBrokerStatsAction;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
class BrokerDashboardController extends Controller
{
public function index(GetBrokerStatsAction $getBrokerStatsAction)
{
return view('dashboards.broker.index')
->with('deals', $this->deals())
->with('stats', $getBrokerStatsAction->execute(Auth::user()));
}
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()->take(3)->get();
}
}

View File

@ -1,144 +0,0 @@
<?php
namespace App\Http\Controllers\Broker;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBrokerDeal;
use App\Models\Deal;
use App\Models\DealCategory;
use App\Services\FileService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class BrokerDealController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return view('dashboards.broker.deals.index')
->with('deals', $this->deals());
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('dashboards.broker.deals.create')
->with('categories', DealCategory::all('id', 'name'));
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreBrokerDeal $request, FileService $fileService)
{
$data = $request->validated();
$data['slug'] = Str::slug($data['title']);
$data['user_id'] = $request->user()->id;
if ($request->hasFile('image')) {
$image = $request->file('image');
$data['image'] = $fileService->upload($image, 'images/deals',
$data['slug'].'.'.$image->extension());
}
Deal::unguard();
Deal::create($data);
Deal::reguard();
return to_route('broker.deals.index')->with('success', 'Deal has been created.');
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Deal $deal)
{
return view('dashboards.broker.deals.edit')
->with('deal', $deal)
->with('categories', DealCategory::all('id', 'name'));
}
/**
* Update the specified resource in storage.
*/
public function update(StoreBrokerDeal $request, Deal $deal, FileService $fileService)
{
$data = $request->validated();
try {
DB::transaction(function () use ($deal, $data, $fileService, $request) {
$data['slug'] = Str::slug($data['title']);
if ($request->hasFile('image')) {
$fileService->delete($deal->image);
$image = $request->file('image');
$data['image'] = $fileService->upload($image, 'images/deals',
$data['slug'].'.'.$image->extension());
}
Deal::unguard();
$deal->update($data);
Deal::reguard();
});
} catch (\Throwable $exception) {
Log::error($exception->getMessage(), $exception->getTrace());
return back()->with('error', 'Something gone wrong.');
}
return back()->with('success', 'Deal has been updated.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Deal $deal, FileService $fileService)
{
// remove the image of deal
try {
DB::transaction(function () use ($deal, $fileService) {
$fileService->delete($deal->image);
$deal->delete();
});
} catch (\Throwable $exception) {
Log::error($exception->getMessage(), $exception->getTrace());
return back()->with('error', 'Something gone wrong.');
}
return back()->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,90 +0,0 @@
<?php
namespace App\Http\Controllers\Broker;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBrokerProfileRequest;
use App\Models\Broker;
use App\Models\User;
use App\Services\ProfileInitialsService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class BrokerProfileController extends Controller
{
/**
* Display the specified resource.
*/
public function show(User $profile, ProfileInitialsService $service)
{
// Get the broker profile
$broker = $profile->type;
// TODO: move this to middleware
if (! $broker instanceof Broker) {
abort(403, 'This user is not a broker.');
}
$initials = $service->create($profile->name);
return view('dashboards.broker.profile.show')
->with('name', $profile->name)
->with('joinDate', $profile->created_at->format('F Y'))
->with('email', $profile->email)
->with('initials', $initials)
->with('verified', $broker->verified)
->with('location', $broker->location)
->with('bio', $broker->bio)
->with('phone', $broker->phone);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(User $profile)
{
return view('dashboards.broker.profile.edit')
->with('profile', $profile)
->with('broker', $profile->type);
}
/**
* Update the specified resource in storage.
*/
public function update(StoreBrokerProfileRequest $request, User $profile)
{
/**
* Separate the user fields from the broker fields
*/
$userFields = ['name', 'email'];
$data = collect($request->validated());
$userData = $data->only($userFields)->toArray();
$brokerData = $data->except($userFields)->toArray();
try {
DB::transaction(function () use ($profile, $userData, $brokerData) {
$profile->update($userData);
$broker = $profile->type;
Broker::unguard();
$broker->update($brokerData);
Broker::reguard();
});
return to_route('broker.profile.show', $profile)
->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.');
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

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

@ -1,91 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Actions\AddRecentSearchAction;
use App\Enums\ExplorePageFilters;
use App\Enums\UserTypes;
use App\Http\Requests\ExploreSearchSortRequest;
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;
class ExplorePageController extends Controller
{
public function __invoke(
ExploreSearchSortRequest $request,
ExplorePageDealsQuery $query,
AddRecentSearchAction $addRecentSearchAction
) {
return view('explore')
->with('profileLink', $this->profileLink())
->with('categories', $this->categories())
->with('recentSearches', $this->recentSearches())
->with('deals', $this->deals($request, $query->builder(), $addRecentSearchAction));
}
protected function deals(FormRequest $request, Builder $query, AddRecentSearchAction $action): LengthAwarePaginator
{
$query->tap(fn ($q) => (new Deal)->withActiveDeals($q));
// Add a search query
if ($request->has('search') && $request->get('search') !== null) {
$query->tap(fn ($q) => (new Deal)->search($q, $request->search));
\Illuminate\Support\defer(function () use ($action, $request) {
$action->execute($request->user(), ['query' => $request->search]);
});
}
// Add category sorting filter
if ($request->has('category') && $request->get('category') !== null) {
$query->tap(fn ($q) => (new Deal)->filterByCategory($q, $request->category));
}
// Add sorting filters
$query = match (ExplorePageFilters::tryFrom($request->sortBy)) {
ExplorePageFilters::Like => $query->orderBy('total_likes', 'desc'),
ExplorePageFilters::Click => $query->orderBy('total_redirection', 'desc'),
default => $query->orderByRaw(
'((COALESCE(total_likes, 0) * 70.0) / 100.0) + ((COALESCE(total_redirection, 0) * 30.0) / 100.0) DESC'
)
};
return $query->latest()->paginate();
}
/**
* Determines the link to the user's profile dashboard
* based on the user's role.
*
* @return string The URL for the user's dashboard.
*/
protected function profileLink(): string
{
$user = Auth::user();
return match ($user->role ?? null) {
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

@ -1,98 +0,0 @@
<?php
namespace App\Http\Controllers\Interaction;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreReportRequest;
use App\Models\Deal;
use App\Models\Report;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ReportController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreReportRequest $request, Deal $deal)
{
$data = $request->validated();
$data['user_id'] = Auth::id();
// Check if the user already reported the deal
$alreadyReported = $deal->reports()->where('user_id', Auth::id())->exists();
if ($alreadyReported) {
return response()->json(['message' => 'You had already reported this deal'], 200);
}
try {
DB::transaction(function () use ($data, $deal) {
Report::unguard();
Report::create($data)->deals()->attach($deal);
Report::reguard();
});
return response()->json(['message' => 'Report submitted. Thank you for keeping DealHub safe'], 201);
} catch (\Throwable $exception) {
Log::error('Error creating report', [
'user_id' => Auth::id(),
'deal_id' => $deal->id,
'error' => $exception->getMessage(),
'trace' => $exception->getTraceAsString(),
]);
return response()->json(['message' => 'Error creating report'], 500);
}
}
/**
* Display the specified resource.
*/
public function show(Report $report)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Report $report)
{
//
}
/**
* Remove the specified resource from storage.
*/
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,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,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,24 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class HasRole
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, ...$roles): Response
{
if (in_array($request->user()->role, $roles)) {
return $next($request);
} else {
abort('401');
}
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class AuthenticateUserRequest 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, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'email', 'max:255'],
'password' => 'required',
'remember_me' => 'nullable|boolean',
];
}
}

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

@ -1,40 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreBrokerDeal extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()->isBroker();
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => 'required|min:10|max:255',
'description' => 'required|min:10|max:300',
'image' => [$this->isMethod('post') ? 'required' : 'nullable', 'image', 'mimes:jpeg,png,jpg', 'max:10240'],
'link' => 'nullable|url',
'deal_category_id' => 'required|exists:deal_categories,id',
];
}
public function messages(): array
{
return [
'category_id.required' => 'The category field is required.',
'category_id.exists' => 'The category does not exist.',
];
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Http\Requests;
use AllowDynamicProperties;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
#[AllowDynamicProperties]
class StoreBrokerProfileRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// If this request is by a broker 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->broker->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,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,35 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\UserTypes;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
class StoreRegisterdUser 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, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'password' => ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()],
'email' => 'required|string|email|max:255|unique:users',
'role' => ['required', Rule::in(UserTypes::values()), Rule::notIn(UserTypes::Admin->value)],
];
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Enums\ReportType;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreReportRequest 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 [
'type' => ['required', Rule::in(ReportType::values())],
'description' => 'required|string|min:10|max:500',
];
}
}

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

@ -1,60 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
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
{
protected $fillable = ['bio', 'location', 'phone'];
protected function casts(): array
{
return [
'verified' => 'boolean',
];
}
public function user(): MorphOne
{
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

@ -1,199 +0,0 @@
<?php
namespace App\Models;
use App\Enums\InteractionType;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
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
{
public function broker(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function category(): BelongsTo
{
return $this->belongsTo(DealCategory::class, 'deal_category_id');
}
public function interactions(): HasMany
{
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
*/
#[Scope]
public function WithActiveDeals(Builder $query): Builder
{
return $query->where('active', true);
}
/**
* Scope a query to determine if the current user has liked and favorite a deal
*/
#[Scope]
public function WithCurrentUserInteractions(Builder $query): Builder
{
return $query->withExists([
'interactions as is_liked' => function ($query) {
$query->where('user_id', Auth::id())
->where('type', InteractionType::Like);
},
'interactions as is_favorite' => function ($query) {
$query->where('user_id', Auth::id())
->where('type', InteractionType::Favorite);
},
]);
}
/**
* Scope a query to get total like count per deal
*/
#[Scope]
public function WithLikePerDeal(Builder $query): Builder
{
return $query->withCount([
'interactions as total_likes' => function ($query) {
$query->where('type', InteractionType::Like);
},
]);
}
/**
* Scope a query to get click count per deal
*/
#[Scope]
public function WithRedirectionPerDeal(Builder $query): Builder
{
return $query->withSum([
'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

@ -1,42 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
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
{
protected $fillable = ['name', 'description', 'slug', 'active', 'order'];
public function deals(): HasMany
{
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

@ -1,56 +0,0 @@
<?php
namespace App\Models;
use App\Enums\InteractionType;
use Illuminate\Database\Eloquent\Model;
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
{
protected $fillable = [
'type',
'user_id',
];
public function user(): BelongsTo
{
return $this->belongsTo(Deal::class);
}
public function deal(): BelongsTo
{
return $this->belongsTo(Deal::class);
}
protected function casts(): array
{
return [
'type' => InteractionType::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

@ -1,69 +0,0 @@
<?php
namespace App\Models;
use App\Enums\ReportStatus;
use App\Enums\ReportType;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
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
{
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function deals(): BelongsToMany
{
return $this->belongsToMany(Deal::class);
}
protected function casts(): array
{
return [
'type' => ReportType::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

@ -4,75 +4,13 @@
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
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
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, HasPushSubscriptions, Notifiable;
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
@ -83,8 +21,6 @@ class User extends Authenticatable
'name',
'email',
'password',
'status',
'role',
];
/**
@ -109,72 +45,4 @@ protected function casts(): array
'password' => 'hashed',
];
}
public function isBroker(): bool
{
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
{
return $this->hasMany(Deal::class);
}
/**
* Returns model of User's role type
*/
public function type(): MorphTo
{
return $this->morphTo('role');
}
public function interactions(): HasMany
{
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,24 +0,0 @@
<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class FileService
{
public function upload(UploadedFile $file, string $folder, string $filename): string
{
// This prevents overriding of same image name
$filename = time().$filename.'.'.$file->extension();
return $file->storeAs($folder, $filename, 'public');
}
public function delete(?string $path): void
{
if ($path && Storage::disk('public')->exists($path)) {
Storage::disk('public')->delete($path);
}
}
}

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('');
}
}

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