Compare commits

...

84 Commits

Author SHA1 Message Date
8be89d350f change show province 2026-05-23 16:36:40 +03:30
d31c641216 Merge pull request 'create seeder for city and province' (#25) from feature/ProvinceAndCitySeeder into develop
Reviewed-on: #25
2026-05-23 11:54:56 +00:00
aa363d1695 Merge pull request 'feature/CreateSeedrStatus' (#24) from feature/CreateSeedrStatus into develop
Reviewed-on: #24
2026-05-23 11:51:20 +00:00
ca2ec66fb5 resolve the conflicts 2026-05-23 15:21:15 +03:30
5c757e1823 create seeder for city and province 2026-05-23 15:13:12 +03:30
d9f895c382 create seeder for city and province 2026-05-23 14:55:00 +03:30
9ad3d6cea0 create show for city and province 2026-05-23 14:03:10 +03:30
8554f4cc55 create list for city and province 2026-05-23 13:49:09 +03:30
5e48777e1c create list for city and province 2026-05-23 13:45:05 +03:30
01bae8474b Merge pull request 'add id card to profile' (#23) from feature/TransactionServices into develop
Reviewed-on: #23
2026-05-20 11:52:39 +00:00
463ca3f006 add id card to profile 2026-05-20 15:22:13 +03:30
1ba141e715 Merge pull request 'change edit method' (#22) from feature/EditUserProfile into develop
Reviewed-on: #22
2026-05-20 06:23:34 +00:00
76e6ab4e0c change edit method 2026-05-20 09:53:05 +03:30
fb35ed58b1 Merge pull request 'feature/EditProfile' (#18) from feature/EditProfile into develop
Reviewed-on: #18
2026-05-20 05:50:25 +00:00
ffb4179e0d Merge pull request 'create user gateway controller' (#21) from feature/GatewayManagement into develop
Reviewed-on: #21
2026-05-20 05:44:09 +00:00
a7fa1097b7 create user gateway controller 2026-05-20 09:14:06 +03:30
f2f9d38907 Merge pull request 'create Enum and seeder for BusinessType and payment_statuses' (#20) from feature/CreateSeedrStatus into develop
Reviewed-on: #20
2026-05-19 10:49:50 +00:00
5b79cefe28 create Enum and seeder for BusinessType and payment_statuses 2026-05-18 16:28:58 +03:30
41b7ead69e Merge pull request 'separate any gateways' (#19) from feature/PaymentModel into develop
Reviewed-on: #19
2026-05-18 11:10:23 +00:00
5ea986db40 separate any gateways 2026-05-18 14:34:28 +03:30
d04637b1ef fix code 2026-05-18 14:10:45 +03:30
3c61e279ec create profile for expert 2026-05-18 13:59:05 +03:30
e9f6ee71fa create profile for expert 2026-05-18 13:58:20 +03:30
60129f5abb create profile for expert 2026-05-18 13:57:03 +03:30
2d8f2dbcc5 create profile for expert 2026-05-18 13:50:37 +03:30
86e353749a create profile for expert 2026-05-18 13:49:33 +03:30
f3a9b5e788 Merge pull request 'create payment and gateway resources' (#17) from feature/PaymentModel into develop
Reviewed-on: #17
2026-05-17 10:31:08 +00:00
d04277ece5 add gaurded values 2026-05-17 14:00:33 +03:30
e53a89016f create payment and gateway resources 2026-05-17 13:57:39 +03:30
53f2303253 Merge pull request 'upload files' (#16) from feature/UploadDoc into develop
Reviewed-on: #16
2026-05-17 05:26:22 +00:00
c6dc5256e6 resolve the conflicts 2026-05-17 08:56:02 +03:30
19abb28acf upload files 2026-05-17 08:49:28 +03:30
431acd9279 Merge pull request 'feature/FixManagment' (#14) from feature/FixManagment into develop
Reviewed-on: #14
2026-05-16 11:31:59 +00:00
9fcd8045db fix code3 2026-05-16 14:57:34 +03:30
7c82556d93 fix code2 2026-05-16 14:38:39 +03:30
581e7af1fd fix code 2026-05-16 14:36:09 +03:30
b433d2ff52 Merge pull request 'create expert login resources' (#13) from feature/ExpertLogin into develop
Reviewed-on: #13
2026-05-13 10:16:51 +00:00
6f06218cb2 Merge pull request 'feature/CreateDocumention' (#12) from feature/CreateDocumention into develop
Reviewed-on: #12
Reviewed-by: amirghasempoor <amirghasempoor79@gmail.com>
2026-05-13 10:16:38 +00:00
af23df5527 fix routs 2026-05-13 13:45:58 +03:30
1a2077a164 create expert login resources 2026-05-13 13:41:12 +03:30
e33aac7eed fix code 2026-05-13 13:33:58 +03:30
7b0507bdda fix code 2026-05-13 10:33:59 +03:30
ae8ca057fd change some item in controller and request 2026-05-13 09:27:03 +03:30
32c14bd6c4 save for get develop 2026-05-13 08:56:02 +03:30
363c7005d0 Merge pull request 'add level to users table' (#11) from feature/docConfirm into develop
Reviewed-on: #11
2026-05-12 11:03:44 +00:00
f29510d76e add level to users table 2026-05-12 14:32:18 +03:30
5596943b70 Merge branch 'develop' into feature/CreateDocumention
# Conflicts:
#	backend/app/User/Controllers/Dashboard/ConfirmController.php
#	backend/app/User/Controllers/Dashboard/DocumentationController.php
2026-05-12 14:05:15 +03:30
5831a5594a Merge remote-tracking branch 'origin' into feature/CreateDocumention 2026-05-12 14:02:17 +03:30
12736d53c5 create base thing such as Roler permission Expert and user Management and their dependency for Admin 2026-05-12 13:58:40 +03:30
fda129875e Merge pull request 'create new route for expert' (#10) from feature/docConfirm into develop
Reviewed-on: #10
2026-05-12 10:23:05 +00:00
ca71547657 create new route for expert 2026-05-12 13:50:48 +03:30
2d31eccc4a Merge pull request 'create user documents' (#9) from feature/docConfirm into develop
Reviewed-on: #9
2026-05-12 09:35:37 +00:00
abb92b4e22 create user documents 2026-05-12 13:04:15 +03:30
ea68943354 Merge pull request 'create city and province resources' (#8) from feature/docConfirm into develop
Reviewed-on: #8
2026-05-12 07:39:30 +00:00
e3831e98c9 create city and province resources 2026-05-12 11:08:47 +03:30
9794aa1f54 Merge pull request 'create EmailEvent for login' (#7) from feature/UploadData into develop
Reviewed-on: #7
2026-05-10 11:01:41 +00:00
4afcd5c81e create EmailEvent for login 2026-05-10 14:29:52 +03:30
74bc255d82 Merge pull request 'make auth service global' (#6) from feature/expertPage into develop
Reviewed-on: #6
2026-05-10 10:56:03 +00:00
ca094cff9f prevent request forgery 2026-05-10 14:25:12 +03:30
c2328ec666 make auth service global 2026-05-09 09:09:49 +03:30
e4f7b96b92 Merge pull request 'create loggin and register whit their form requst' (#4) from feature/AuthPasport into develop
Reviewed-on: #4
2026-05-06 05:51:11 +00:00
f072bd1ee8 delete lan 2026-05-06 09:16:35 +03:30
f01992368e create user info 2026-05-06 09:12:29 +03:30
161fa3ae1e Merge pull request 'add languages files' (#5) from feature/MultiLang into develop
Reviewed-on: #5
2026-05-06 05:21:31 +00:00
05c54fb2d7 add languages files 2026-05-06 08:49:57 +03:30
b32ee444ca write for user 2026-05-05 17:33:34 +03:30
264ed1ecb9 install data table 2026-05-04 14:52:54 +03:30
cee480fff5 improve the code in some part of the project 2026-05-03 15:29:57 +03:30
5024a242d2 fix code 2026-05-03 15:28:50 +03:30
0bba710b7d fix code 2026-05-03 15:11:29 +03:30
f788f9a7ea improve code and fix their bugs 2026-05-02 17:07:40 +03:30
e74619d0ab reviwe my form request in login 2026-05-02 15:29:09 +03:30
5b42e23afc improve form requst 2026-05-02 09:09:34 +03:30
51aa3b157c create loggin and register whit their form requst 2026-04-29 16:52:18 +03:30
b24b61c754 Merge pull request 'feature/AuthorizationFIB' (#3) from feature/AuthorizationFIB into develop
Reviewed-on: #3
2026-04-29 11:12:59 +00:00
5d2501c71c create config file for service 2026-04-29 14:42:02 +03:30
7fb9e345a2 create all FTB services 2026-04-29 14:34:01 +03:30
9d86e329fb Merge pull request 'use nginx and php-fpm for docker' (#2) from feature/Dockerize into develop
Reviewed-on: #2
2026-04-27 12:48:55 +00:00
4596025ba3 install passport 2026-04-27 16:12:08 +03:30
1dbd816da2 add dockerfile for production 2026-04-27 15:18:35 +03:30
52ed10f831 remove production yaml files 2026-04-27 14:35:04 +03:30
e99aa9b8f1 use nginx and php-fpm for docker 2026-04-26 16:25:59 +03:30
ccd2de26df Merge pull request 'install fresh laravel 13' (#1) from develop into main
Reviewed-on: #1
2026-04-26 11:55:45 +00:00
9cdf4c129b install fresh laravel 13 2026-04-26 15:23:49 +03:30
211 changed files with 18770 additions and 0 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
PROJECT_NAME=""
BACKEND_PATH="./Backend"
FRONTEND_PATH=""
POSTGRES_USER=""
POSTGRES_DB=""
POSTGRES_PASSWORD=""

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
.env
front

1
README.md Normal file
View File

@@ -0,0 +1 @@
# payment gateway

18
backend/.dockerignore Executable file
View File

@@ -0,0 +1,18 @@
#.git
#.gitignore
#README.md
#.env
#.env.backup
#node_modules
#npm-debug.log
#yarn-error.log
#vendor/
#storage/logs/*
#storage/framework/cache/*
#storage/framework/sessions/*
#storage/framework/views/*
#bootstrap/cache/*
#.phpunit.result.cache
#Homestead.json
#Homestead.yaml
#auth.json

18
backend/.editorconfig Executable file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[{compose,docker-compose}.{yml,yaml}]
indent_size = 4

77
backend/.env.example Executable file
View File

@@ -0,0 +1,77 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=backend
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
FIB_AUTHORIZATION_URL=
FIB_CLIENT_ID=
FIB_CLIENT_SECRET=
FIB_CREATE_PAYMENT_URL
FIB_CHECK_PAYMENT_STATUS_URL
FIB_CANCEL_PAYMENT_URL
FIB_REFUND_URL

11
backend/.gitattributes vendored Executable file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

26
backend/.gitignore vendored Executable file
View File

@@ -0,0 +1,26 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.codex
/.cursor/
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
_ide_helper.php
Homestead.json
Homestead.yaml
Thumbs.db

2
backend/.npmrc Executable file
View File

@@ -0,0 +1,2 @@
ignore-scripts=true
audit=true

35
backend/Dockerfile Executable file
View File

@@ -0,0 +1,35 @@
FROM php:8.5.3-fpm-alpine3.23
# Install common PHP extension dependencies
RUN apk update && apk add --no-cache \
bash \
curl \
libpng \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libwebp-dev \
libxml2-dev \
unzip \
libzip-dev \
libpq-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install pdo_pgsql gd zip
# Set the working directory
WORKDIR /var/www/payment
COPY . .
RUN chmod -R 775 /var/www/payment/storage /var/www/payment/bootstrap/cache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
ENV COMPOSER_ALLOW_SUPERUSER=1
# Default command
CMD composer install ;\
composer dump-autoload --optimize --classmap-authoritative ;\
php artisan migrate ;\
php artisan db:seed ;\
php-fpm

41
backend/Dockerfile.production Executable file
View File

@@ -0,0 +1,41 @@
FROM docker.arvancloud.ir/composer:latest AS builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative
FROM php:8.5.3-fpm-alpine3.23 AS application
RUN apk update && apk add --no-cache \
bash \
curl \
libpng \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libwebp-dev \
libxml2-dev \
unzip \
libzip-dev \
libpq-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install pdo_pgsql gd zip
WORKDIR /var/www/payment
COPY --from=builder /app /var/www/payment
RUN chown -R www-data:www-data /var/www/payment \
&& chmod -R 665 storage bootstrap/cache
CMD ["php-fpm"]

58
backend/README.md Executable file
View File

@@ -0,0 +1,58 @@
<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>
<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
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:
- [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).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
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.
In addition, [Laracasts](https://laracasts.com) 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.
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
## Agentic Development
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
```bash
composer require laravel/boost --dev
php artisan boost:install
```
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## 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).
## Security Vulnerabilities
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.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\Auth\LoginRequest;
use App\Admin\Requests\Auth\RegisterRequest;
use App\Http\Controllers\Controller;
use App\Models\Expert;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
use ApiResponse;
public function register(RegisterRequest $request): JsonResponse
{
Expert::query()->create([
'username' => $request->username,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$request->session()->regenerate();
return $this->successResponse();
}
public function login(LoginRequest $request): JsonResponse
{
$credentials = ['password' => $request->password];
if (filter_var($request->login, FILTER_VALIDATE_EMAIL)) {
$credentials['email'] = $request->login;
} else {
$credentials['username'] = $request->login;
}
if (Auth::attempt($credentials))
{
$request->session()->regenerate();
return $this->successResponse();
}
return $this->errorResponse(__('messages.incorrect_or_username_password'));
}
public function logout(Request $request): JsonResponse
{
Auth::guard('expert')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return $this->successResponse();
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\Documentation\RejectRequest;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DocumentationController extends Controller
{
use ApiResponse;
public function index(Request $request): array
{
return DataTableFacade::run(
User::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
allowedSelects: [
'id', 'first_name', 'last_name', 'national_id',
'phone_number', 'postal_code', 'city_id', 'province_id',
'province_name', 'city_name', 'address'
]
);
}
public function files(User $user): JsonResponse
{
return $this->successResponse($user->load('documents'));
}
public function confirm(User $user): JsonResponse
{
$user->update([
'authority_level' => 1
]);
return $this->successResponse();
}
public function reject(RejectRequest $request, User $user): JsonResponse
{
$user->update([
'kyc_status' => 0,
'kyc_reject_reason' => $request->kyc_reject_reason
]);
return $this->successResponse();
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\ExpertManagement\StoreRequest;
use App\Admin\Requests\ExpertManagement\UpdateRequest;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Models\City;
use App\Models\Expert;
use App\Models\Province;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Throwable;
class ExpertManagementController extends Controller
{
use ApiResponse;
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$data = DataTableFacade::run(
Expert::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
allowedSelects: ['id', 'username', 'first_name', 'last_name', 'phone_number', 'email', 'national_id'],
);
return response()->json($data);
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreRequest $request): JsonResponse
{
$province = Province::query()->find($request->province_id);
$city = City::query()->find($request->city_id);
Expert::query()->create([
'username' => $request->username,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'national_id' => $request->national_id,
'password' => Hash::make($request->password),
'email' => $request->email,
'phone_number' => $request->phone_number,
'province_id' => $request->province_id,
'province_name' => $province->name,
'city_id' => $request->city_id,
'city_name' => $city->name,
]);
return $this->successResponse();
}
/**
* Display the specified resource.
*/
public function show(Expert $expert): JsonResponse
{
return $this->successResponse($expert->load(['roles','permissions'])
);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateRequest $request, Expert $expert): JsonResponse
{
$province = Province::query()->find($request->province_id);
$city = City::query()->find($request->city_id);
$expert->update([
'username' => $request->username,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'national_id' => $request->national_id,
'password' => Hash::make($request->password),
'email' => $request->email,
'phone_number' => $request->phone_number,
'province_id' => $request->province_id,
'province_name' => $province->name,
'city_id' => $request->city_id,
'city_name' => $city->name,
]);
return $this->successResponse();
}
/**
* Remove the specified resource from storage.
* @throws Throwable
*/
public function destroy(Expert $expert): JsonResponse
{
DB::transaction(function () use ($expert) {
$expert->roles()->detach();
$expert->permissions()->detach();
$expert->delete();
});
return $this->successResponse();
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\PermissionManagement\StoreRequest;
use App\Admin\Requests\PermissionManagement\UpdateRequest;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Permission;
class PermissionManagementController extends Controller
{
/**
* Display a listing of the resource.
*/
use ApiResponse;
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
return response()->json(
DataTableFacade::run(
Permission::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*']
)
);
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreRequest $request): JsonResponse
{
Permission::create([
'name' => $request->name,
'guard_name' => 'web',
]);
return $this->successResponse();
}
/**
* Display the specified resource.
*/
public function show(Permission $permission): JsonResponse
{
return $this->successResponse($permission);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateRequest $request, Permission $permission): JsonResponse
{
$permission->update([
'name' => $request->name,
]);
return $this->successResponse();
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Permission $permission): JsonResponse
{
$permission->delete();
return $this->successResponse();
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\Profile\ChangePasswordRequest;
use App\Admin\Requests\Profile\EditRequest;
use App\Admin\Resources\ExpertResource;
use App\Http\Controllers\Controller;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class ProfileController extends Controller
{
use ApiResponse;
public function info(): JsonResponse
{
return $this->successResponse(new ExpertResource(Auth::guard('expert')->user()));
}
public function edit(EditRequest $request): JsonResponse
{
$expert = $request->user('expert');
$expert->update([
'username' => $request->username,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'email' => $expert->email,
'national_id' => $request->national_id,
'phone_number' => $request->phone_number,
'province_id' => $request->province_id,
'city_id' => $request->city_id,
]);
return $this->successResponse();
}
public function changePassword(ChangePasswordRequest $request): JsonResponse
{
$expert = Auth::guard('expert')->user();
if (! Hash::check($request->current_password, $expert->password))
{
return $this->errorResponse(__('messages.incorrect_current_password'));
}
$expert->update([
'password' => Hash::make($request->new_password)
]);
return $this->successResponse();
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\RoleManagement\StoreRequest;
use App\Admin\Requests\RoleManagement\UpdateRequest;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\Models\Role;
use Throwable;
class RoleManagementController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$data = DataTableFacade::run(
Role::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
allowedSelects: ['id', 'name']
);
return response()->json($data);
}
/**
* @throws Throwable
*/
public function store(StoreRequest $request): JsonResponse
{
DB::transaction(function () use ($request) {
$role = Role::create([
'name' => $request->name,
'guard_name' => 'web',
]);
$role->givePermissionTo($request->permissions);
});
return $this->successResponse();
}
/**
* @throws Throwable
*/
public function update(UpdateRequest $request, Role $role): JsonResponse
{
DB::transaction(function () use ($request, $role) {
$role->update([
'name' => $request->name,
'guard_name' => 'web',
]);
$role->syncPermissions($request->permissions);
});
return $this->successResponse();
}
public function show(Role $role): JsonResponse
{
return $this->successResponse($role);
}
public function destroy(Role $role): JsonResponse
{
$role->delete();
return $this->successResponse();
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Requests\UserManagement\StoreRequest;
use App\Admin\Requests\UserManagement\UpdateRequest;
use App\Facades\DataTable\DataTableFacade;
use App\Http\Controllers\Controller;
use App\Models\City;
use App\Models\Province;
use App\Models\User;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Throwable;
class UserManagementController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$data = DataTableFacade::run(
User::query(),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
allowedSelects: ['id', 'username','first_name', 'last_name','kyc_status','national_id',
'phone_number','postal_code','province_id','province_name', 'city_id', 'city_name',
'birth_date','tax_id', 'password', 'email'],
);
return response()->json($data);
}
/**
* Store a newly created resource in storage.
*/
public function store(StoreRequest $request): JsonResponse
{
$province = Province::query()->find($request->province_id);
$city = City::query()->find($request->city_id);
User::query()->create([
'username' => $request->username,
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'national_id' => $request->national_id,
'password' => Hash::make($request->password),
'email' => $request->email,
'birth_date' => $request->birth_date,
'phone_number' => $request->phone_number,
'postal_code' => $request->postal_code,
'address' => $request->address,
'province_id' => $request->province_id,
'province_name' => $province->name,
'city_id' => $request->city_id,
'city_name' => $city->name,
]);
return $this->successResponse();
}
/**
* Display the specified resource.
*/
public function show(User $user): JsonResponse
{
return $this->successResponse($user);
}
/**
* Update the specified resource in storage.
*/
public function update(UpdateRequest $request, User $user): JsonResponse
{
$province = Province::query()->find($request->province_id);
$city = City::query()->find($request->city_id);
$user->update([
'first_name' => $request->first_name,
'username' => $request->username,
'last_name' => $request->last_name,
'national_id' => $request->national_id,
'password' => Hash::make($request->password),
'email' => $request->email,
'birth_date' => $request->birth_date,
'phone_number' => $request->phone_number,
'postal_code' => $request->postal_code,
'address' => $request->address,
'province_id' => $request->province_id,
'province_name' => $province->name,
'city_id' => $request->city_id,
'city_name' => $city->name,
]);
return $this->successResponse();
}
/**
* Remove the specified resource from storage.
* @throws Throwable
*/
public function destroy(User $user): JsonResponse
{
$user->delete();
return $this->successResponse();
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Admin\Requests\Auth;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest 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|string>
*/
public function rules(): array
{
return [
'login' => 'required|string',
'password' => 'required|string',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Admin\Requests\Auth;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest 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|string>
*/
public function rules(): array
{
return [
'username' => 'required|unique:experts,username',
'email' => 'required|email|unique:experts,email',
'password' => 'required|string|regex:/^(?=.*[a-zA-Z])(?=.*\d).+$/|min:6|confirmed',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Admin\Requests\Documentation;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class RejectRequest 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|string>
*/
public function rules(): array
{
return [
'kyc_reject_reason' => 'required|string',
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Admin\Requests\ExpertManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest 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|string>
*/
public function rules(): array
{
return [
'username' => 'required|string|max:255|unique:experts,username',
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'national_id' => 'required|string|max:20|unique:experts,national_id',
'password' => 'required|string|min:8|confirmed',
'email' => 'required|email|max:255|unique:experts,email',
'phone_number' => 'required|string|max:20|unique:experts,phone_number',
'province_id' => 'required|integer|exists:provinces,id',
'city_id' => 'required|integer|exists:cities,id',
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Admin\Requests\ExpertManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* @property mixed $expert
*/
class UpdateRequest 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|string>
*/
public function rules(): array
{
return [
'username' => ['string', 'max:255',
Rule::unique('experts', 'username')->ignore($this->expert->id),],
'first_name' => 'string|max:255',
'last_name' => 'string|max:255',
'national_id' => ['string', 'max:20',
Rule::unique('experts', 'national_id')->ignore($this->expert->id),],
'password' => 'string|min:8|confirmed',
'email' => ['email', 'max:255',
Rule::unique('experts', 'email')->ignore($this->expert->id),],
'phone_number' => ['string', 'max:20',
Rule::unique('experts', 'phone_number')->ignore($this->expert->id),],
'province_id' => 'required|integer|exists:provinces,id',
'city_id' => 'required|integer|exists:cities,id',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Admin\Requests\PermissionManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest 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|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|unique:permissions,name',
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Admin\Requests\PermissionManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* @property mixed $permission
*/
class UpdateRequest 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|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255', Rule::unique('permissions', 'name')->ignore($this->permission->id)],
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Admin\Requests\Profile;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class ChangePasswordRequest 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|string>
*/
public function rules(): array
{
return [
'current_password' => 'required|string|regex:/^(?=.*[a-zA-Z])(?=.*\d).+$/|min:8',
'new_password' => 'required|string|regex:/^(?=.*[a-zA-Z])(?=.*\d).+$/|min:8',
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Admin\Requests\Profile;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class EditRequest 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|string>
*/
public function rules(): array
{
return [
'username' => 'string|max:255',
'first_name' => 'required|string',
'last_name' => 'required|string',
'national_id' => 'required',
'email' => 'string|email|max:255',
'phone_number' => 'string',
'province_id' => 'string',
'city_id' => 'string',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Admin\Requests\RoleManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest 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|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|unique:roles,name',
'permissions' => 'array',
'permissions.*' => 'integer|exists:permissions,id',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Admin\Requests\RoleManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* @property mixed $role
*/
class UpdateRequest 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|string>
*/
public function rules(): array
{
return [
'name' => ['string', 'max:255', Rule::unique('roles', 'name')->ignore($this->role->id)],
'permissions' => 'array',
'permissions.*' => 'integer|exists:permissions,id',
];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Admin\Requests\UserManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest 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|string>
*/
public function rules(): array
{
return [
'username' => 'required|string|max:255|unique:users,username',
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'national_id' => 'required|string|max:20|unique:users,national_id',
'password' => 'required|string|min:6|confirmed',
'email' => 'required|email|max:255|unique:users,email',
'birth_date' => 'nullable|date',
'phone_number' => 'required|string|max:20|unique:users,phone_number',
'postal_code' => 'nullable|string|max:20',
'address' => 'nullable|string|max:1000',
'province_id' => 'required|integer|exists:provinces,id',
'city_id' => 'required|integer|exists:cities,id',
];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Admin\Requests\UserManagement;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* @property mixed $user
*/
class UpdateRequest 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|string>
*/
public function rules(): array
{
return [
'username' => ['string', 'max:255',
Rule::unique('users', 'username')->ignore($this->user->id),],
'first_name' => 'string|max:255',
'last_name' => 'string|max:255',
'national_id' => ['string', 'max:20',
Rule::unique('users', 'national_id')->ignore($this->user->id),],
'password' => 'string|min:8|confirmed',
'email' => ['email', 'max:255',
Rule::unique('users', 'email')->ignore($this->user->id),],
'birth_date' => 'nullable|date',
'phone_number' => ['string', 'max:20',
Rule::unique('users', 'phone_number')->ignore($this->user->id),],
'postal_code' => 'nullable|string|max:20',
'address' => 'nullable|string|max:1000',
'province_id' => 'integer|exists:provinces,id',
'city_id' => 'integer|exists:cities,id',
];
}
}

View File

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

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Enums;
enum BusinessTypeEnum: int
{
case MEDICAL = 1;
case LEGAL = 2;
case ENGINEERING = 3;
case EDUCATION = 4;
case CONSULTING = 5;
case BEAUTY = 6;
case TECHNICAL_SERVICES = 7;
case FINANCIAL = 8;
case REAL_ESTATE = 9;
case RESTAURANT = 10;
case SHOP = 11;
case TRANSPORTATION = 12;
case ART = 13;
case SPORT = 14;
case OTHER = 15;
public function label(): string
{
return match ($this) {
self::MEDICAL => 'medical',
self::LEGAL => 'legal',
self::ENGINEERING => 'engineering',
self::EDUCATION => 'education',
self::CONSULTING => 'consulting',
self::BEAUTY => 'beauty',
self::TECHNICAL_SERVICES => 'technical_services',
self::FINANCIAL => 'financial',
self::REAL_ESTATE => 'real_estate',
self::RESTAURANT => 'restaurant',
self::SHOP => 'shop',
self::TRANSPORTATION => 'transportation',
self::ART => 'art',
self::SPORT => 'sport',
self::OTHER => 'other',
};
}
public function name($id): string
{
$mapArray = [
1 => 'medical',
2 => 'legal',
3 => 'engineering',
4 => 'education',
5 => 'consulting',
6 => 'beauty',
7 => 'technical_services',
8 => 'financial',
9 => 'real_estate',
10 => 'restaurant',
11 => 'shop',
12 => 'transportation',
13 => 'art',
14 => 'sport',
15 => 'other',
];
return $mapArray[$id];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Enums;
enum PaymentStatusEnum:int
{
case Unpaid = 0;
case Cancelled = 1;
case Expired = 2;
case Paid = 3;
case Refunded = 4;
public function label(): string
{
return match ($this) {
self::Unpaid => 'unpaid',
self::Cancelled => 'cancelled',
self::Expired => 'expired',
self::Paid => 'paid',
self::Refunded => 'refunded',
};
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Facades\DataTable;
use App\Services\DataTable\DataTableInput;
use App\Services\DataTable\DataTableService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Database\Query\Builder;
use Illuminate\Http\Request;
use App\Services\DataTable\Exceptions\InvalidParameterInterface;
class DataTable
{
/**
* Extracts data from request, passes to datatable service and prepares data for response.
* @param Model|Builder $mixed
* @param Request $request
* @param array $allowedFilters
* @param array $allowedRelations
* @param array $allowedSortings
* @param array $allowedSelects
* @return array
* @throws InvalidParameterInterface if input parameters are invalid.
*/
public function run(
Model|Builder $mixed,
Request $request,
array $allowedFilters = [],
array $allowedRelations = [],
array $allowedSortings = [],
array $allowedSelects = [],
array $allowedGroupBy = []
): array
{
$filters = json_decode($request->filters);
$sorting = json_decode($request->sorting);
$rels = $request->rels ?: array();
$dataTableInput = new DataTableInput(
$request->start,
$request->size,
$filters,
$sorting,
$rels,
$allowedFilters,
$allowedSortings,
$allowedGroupBy
);
$query = $this->makeQueryFromModel($mixed);
$dataTableService = (new DataTableService($query, $dataTableInput))
->setAllowedFilters($allowedFilters)
->setAllowedRelations($allowedRelations)
->setAllowedSortings($allowedSortings)
->setAllowedSelects($allowedSelects)
->setAllowedGroupBy($allowedGroupBy);
return $dataTableService->getData();
}
protected function makeQueryFromModel(Model|Builder $mixed): Builder
{
return ($mixed instanceof Model) ? $mixed->query() : $mixed;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Facades\DataTable;
use Illuminate\Support\Facades\Facade;
class DataTableFacade extends Facade
{
protected static function getFacadeAccessor(): string
{
return DataTable::class;
}
}

113
backend/app/Facades/File/File.php Executable file
View File

@@ -0,0 +1,113 @@
<?php
namespace App\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\File as HttpFile;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;
use InvalidArgumentException;
class File
{
public function save(HttpFile|UploadedFile $file, string $path, string $name = null, string $disk = 'public'): string
{
return Storage::disk($disk)->putFileAs(
$path,
$file,
$name ?? Str::uuid() . '.' . $file->extension()
);
}
/**
* @param string $filePath
* @param bool $url_is_absolute
* @param string $disk
* @return void
* @throws InvalidArgumentException when filePath is empty.
*/
public function delete(string $filePath, bool $url_is_absolute = false, string $disk = 'public'): void
{
if (!$filePath){
throw new InvalidArgumentException('File path is invalid.');
}
if ($url_is_absolute) {
$needle = 'storage/';
$pos = strpos($filePath, $needle);
$filePath = substr($filePath, $pos + strlen($needle));
}
if (Storage::disk($disk)->exists($filePath)) {
Storage::disk($disk)->delete($filePath);
}
}
public function deleteContent($file_path): bool
{
if (file_exists($file_path)) {
file_put_contents($file_path, '');
return true;
}
return false;
}
public function tail($file_path, $lines = 100, $buffer = 2048): bool|string
{
// Open file
$file = @fopen($file_path, "rb");
if ($file === false) {
return false;
}
// Jump to last character
fseek($file, -1, SEEK_END);
// Read it and adjust line number if necessary
// (Otherwise the result would be wrong if file doesn't end with a blank line)
if (fread($file, 1) != "\n") {
$lines -= 1;
}
// Start reading
$output = '';
$chunk = '';
// While we would like more
while (ftell($file) > 0 && $lines >= 0) {
// Figure out how far back we should jump
$seek = min(ftell($file), $buffer);
// Do the jump (backwards, relative to where we are)
fseek($file, -$seek, SEEK_CUR);
// Read a chunk and prepend it to our output
$output = ($chunk = fread($file, $seek)) . $output;
// Jump back to where we started reading
fseek($file, -mb_strlen($chunk, '8bit'), SEEK_CUR);
// Decrease our line counter
$lines -= substr_count($chunk, "\n");
}
// While we have too many lines
// (Because of buffer size we might have read too many)
while ($lines++ < 0) {
// Find first newline and remove all text before that
$output = substr($output, strpos($output, "\n") + 1);
}
// Close file and return
fclose($file);
return trim($output);
}
public function deleteDirectory(string $path, string $disk = 'public'): void
{
Storage::disk($disk)->deleteDirectory($path);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Facades\File;
use Illuminate\Support\Facades\Facade;
class FileFacade extends Facade
{
protected static function getFacadeAccessor(): string
{
return File::class;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class LoginNotificationMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public User $user
) {
//
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Login Notification Mail',
);
}
public function content(): Content
{
return new Content(
view: 'emails.login-notification',
);
}
public function attachments(): array
{
return [];
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\BusinessTypeFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BusinessType extends Model
{
/** @use HasFactory<BusinessTypeFactory> */
use HasFactory;
protected $guarded = ['id'];
}

15
backend/app/Models/City.php Executable file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\CityFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class City extends Model
{
/** @use HasFactory<CityFactory> */
use HasFactory;
protected $guarded = ['id'];
}

30
backend/app/Models/Document.php Executable file
View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Database\Factories\DocumentFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Facades\Storage;
class Document extends Model
{
/** @use HasFactory<DocumentFactory> */
use HasFactory;
protected $guarded = ['id'];
public function documentable(): MorphTo
{
return $this->morphTo();
}
protected function url(): Attribute
{
return Attribute::make(
get: fn($value) => Storage::disk('public')->url($value)
);
}
}

22
backend/app/Models/Expert.php Executable file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Database\Factories\ExpertFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Traits\HasRoles;
/**
* @method roles()
* @method permissions()
*/
class Expert extends Model
{
/** @use HasFactory<ExpertFactory> */
use HasFactory;
use HasRoles;
protected $guarded = ['id'];
protected string $guard_name = 'web';
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class FibTransaction extends Model
{
/** @use HasFactory<\Database\Factories\FibTransactionFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class FibTransactionStatus extends Model
{
/** @use HasFactory<\Database\Factories\FibTransactionStatusFactory> */
use HasFactory;
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\PaymentFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model
{
/** @use HasFactory<PaymentFactory> */
use HasFactory;
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\PaymentStatusFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PaymentStatus extends Model
{
/** @use HasFactory<PaymentStatusFactory> */
use HasFactory;
protected $guarded = ['id'];
}

15
backend/app/Models/Province.php Executable file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\ProvinceFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Province extends Model
{
/** @use HasFactory<ProvinceFactory> */
use HasFactory;
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\TransactionFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model
{
/** @use HasFactory<TransactionFactory> */
use HasFactory;
protected $guarded = ['id'];
}

39
backend/app/Models/User.php Executable file
View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\Bridge\Client;
use Laravel\Passport\HasApiTokens;
#[Guarded(['id'])]
#[Hidden(['password'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, HasApiTokens;
protected $guarded = ['id'];
public function documents(): MorphToMany
{
return $this->morphToMany(Document::class, 'documentable');
}
public function validateForPassportPasswordGrant(string $password): bool
{
return Hash::check($password, $this->password);
}
public function findForPassport(string $username, Client $client): User
{
return $this->where('username', $username)->first();
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Database\Factories\UserGatewayFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UserGateway extends Model
{
/** @use HasFactory<UserGatewayFactory> */
use HasFactory;
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;
use Carbon\CarbonInterval;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Passport::enablePasswordGrant();
Passport::tokensExpireIn(CarbonInterval::minutes(1));
Passport::refreshTokensExpireIn(CarbonInterval::minutes(5));
Passport::personalAccessTokensExpireIn(CarbonInterval::minutes(1));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Providers;
use App\Facades\DataTable\DataTable;
use Illuminate\Support\ServiceProvider;
class DataTableServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->bind('datatable', function () {
return new DataTable();
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Providers;
use App\Facades\File\File;
use Illuminate\Support\ServiceProvider;
class FileServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->bind('file', function () {
return new File();
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Services\DataTable;
use App\Services\DataTable\Filter\Filter;
use App\Services\DataTable\Sort\Sort;
class DataTableInput
{
/**
* @param int $start
* @param int $size
* @param array $filters
* @param array $sorting
* @param array $rels
*/
public function __construct(
private ?int $start,
private ?int $size,
private array $filters,
private ?array $sorting,
private array $rels,
private array $allowedFilters,
private array $allowedSortings,
private ?array $GroupBy,
)
{
}
public function getStart(): ?int
{
return $this->start;
}
public function getSize(): ?int
{
return $this->size;
}
/**
* @return array returns an array of Filter objects
*/
public function getFilters(): array
{
$filters = array();
foreach ($this->filters as $filter) {
$filters[] = new Filter(
$filter->id,
$filter->value,
$filter->fn,
$filter->datatype,
$this->allowedFilters
);
}
return $filters;
}
/**
* @return array
*/
public function getSorting(): ?array
{
$sorts = [];
if (!empty($this->sorting)){
foreach ($this->sorting as $sort) {
$sorts[] = new Sort($sort->id, $sort->desc, $this->allowedSortings);
}
}
return $sorts;
}
public function getRelations(): array
{
return $this->rels;
}
public function getGroupBy(): ?array
{
return $this->GroupBy;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Services\DataTable;
use App\Services\DataTable\Filter\ApplyFilter;
use App\Services\DataTable\Sort\ApplySort;
use Illuminate\Contracts\Database\Query\Builder;
class DataTableService
{
protected array $allowedFilters;
protected array $allowedRelations;
protected array $allowedSortings;
protected array $allowedSelects;
protected array $allowedGroupBy;
private int $totalRowCount;
public function __construct(
protected Builder $query,
private DataTableInput $dataTableInput
)
{
}
public function setAllowedFilters(array $allowedFilters): DataTableService
{
$this->allowedFilters = $allowedFilters;
return $this;
}
public function setAllowedRelations(array $allowedRelations): DataTableService
{
$this->allowedRelations = $allowedRelations;
return $this;
}
public function setAllowedSortings(array $allowedSortings): DataTableService
{
$this->allowedSortings = $allowedSortings;
return $this;
}
public function setAllowedSelects(array $allowedSelects): DataTableService
{
$this->allowedSelects = $allowedSelects;
return $this;
}
public function setAllowedGroupBy(array $allowedGroupBy): DataTableService
{
$this->allowedGroupBy = $allowedGroupBy;
return $this;
}
/**
* Handle 'getData' operations
* @return array
*/
public function getData(): array
{
$query = $this->buildQuery();
$data = $query->get();
return array(
'data' => $data,
'meta' => [
'totalRowCount' => $this->totalRowCount
]
);
}
protected function buildQuery(): Builder
{
$query = $this->query;
foreach ($this->dataTableInput->getFilters() as $filter) {
$query = (new ApplyFilter($query, $filter))->apply();
}
$query = $this->applySelect($query, $this->allowedSelects);
$query = $this->includeRelationsInQuery($query, $this->allowedRelations);
$query = $this->applyGroupBy($query, $this->allowedGroupBy);
$this->totalRowCount = $query->count();
if (!is_null($this->dataTableInput->getStart())) {
$query->offset($this->dataTableInput->getStart());
}
if(!is_null($this->dataTableInput->getSize())){
$query->limit($this->dataTableInput->getSize());
}
$sorting = $this->dataTableInput->getSorting();
foreach ($sorting as $sort){
$query = (new ApplySort($query, $sort))->apply();
}
return $query;
}
protected function applySelect(Builder $query, array $selectedFields): Builder
{
if (!empty($selectedFields)) {
$query->select($selectedFields);
}
return $query;
}
protected function applyGroupBy(Builder $query, array $groupBy): Builder
{
return empty($groupBy) ? $query : $query->groupBy($groupBy);
}
protected function includeRelationsInQuery(Builder $query, array $rels): Builder
{
if (!empty($rels)) {
$query->with($rels);
}
return $query;
}
// (later) define mapping of relation names to prevent relation name expose.
// (later) define mapping of column names to prevent column name expose.
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Services\DataTable\Enums;
enum DataType: string
{
case NUMERIC = 'numeric';
case TEXT = 'text';
case DATE = 'date';
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Services\DataTable\Enums;
enum SearchType: string
{
case CONTAINS = 'contains';
case EQUALS = 'equals';
case NOT_EQUALS = 'notEquals';
case BETWEEN = 'between';
case GREATER_THAN = 'greaterThan';
case LESS_THAN = 'lessThan';
case FUZZY = 'fuzzy';
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Services\DataTable\Exceptions;
use Exception;
class InvalidFilterException extends Exception implements InvalidParameterInterface
{
protected $fieldName;
public function __construct($fieldName, $message = "", $code = 400, \Throwable $previous = null)
{
$this->fieldName = $fieldName;
parent::__construct($message, $code, $previous);
}
public function getFieldName()
{
return $this->fieldName;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Services\DataTable\Exceptions;
interface InvalidParameterInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Services\DataTable\Exceptions;
use Exception;
class InvalidRelationException extends Exception implements InvalidParameterInterface
{
protected $fieldName;
public function __construct($fieldName, $message = "", $code = 400, \Throwable $previous = null)
{
$this->fieldName = $fieldName;
parent::__construct($message, $code, $previous);
}
public function getFieldName()
{
return $this->fieldName;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Services\DataTable\Exceptions;
use Exception;
class InvalidSortingException extends Exception implements InvalidParameterInterface
{
protected $fieldName;
public function __construct($fieldName, $message = "", $code = 400, \Throwable $previous = null)
{
$this->fieldName = $fieldName;
parent::__construct($message, $code, $previous);
}
public function getFieldName()
{
return $this->fieldName;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Services\DataTable\Filter;
use App\Services\DataTable\Enums\SearchType;
use App\Services\DataTable\Exceptions\InvalidFilterException;
use App\Services\DataTable\Filter\SearchFunctions\FilterBetween;
use App\Services\DataTable\Filter\SearchFunctions\FilterContains;
use App\Services\DataTable\Filter\SearchFunctions\FilterEquals;
use App\Services\DataTable\Filter\SearchFunctions\FilterGreaterThan;
use App\Services\DataTable\Filter\SearchFunctions\FilterLessThan;
use App\Services\DataTable\Filter\SearchFunctions\FilterNotEquals;
use App\Services\DataTable\Filter\SearchFunctions\SearchFilter;
use Illuminate\Contracts\Database\Query\Builder;
class ApplyFilter
{
private SearchFilter $searchFilter;
/**
* @param Builder $query
* @param Filter $filter
*/
public function __construct(
private Builder $query,
private Filter $filter,
)
{
}
public function apply(): Builder
{
$filter = $this->filter;
$query = $this->query;
$searchType = SearchType::from($filter->getFn());
switch ($searchType) {
case SearchType::CONTAINS:
$this->searchFilter = new FilterContains($query, $filter);
break;
case SearchType::EQUALS:
$this->searchFilter = new FilterEquals($query, $filter);
break;
case SearchType::NOT_EQUALS:
$this->searchFilter = new FilterNotEquals($query, $filter);
break;
case SearchType::BETWEEN:
$this->searchFilter = new FilterBetween($query, $filter);
break;
case SearchType::GREATER_THAN:
$this->searchFilter = new FilterGreaterThan($query, $filter);
break;
case SearchType::LESS_THAN:
$this->searchFilter = new FilterLessThan($query, $filter);
break;
default:
$searchFunction = $filter->getFn();
throw new InvalidFilterException($searchFunction, "search function `$searchFunction` is invalid.");
}
$relation = $this->filter->getRelation();
return method_exists($this->query->getModel(), $relation) ? $this->applyFilterToRelation($relation) : $this->searchFilter->apply();
}
protected function applyFilterToRelation(string $relation): Builder
{
return $this->query->whereHas($relation, function (Builder $query) {
$this->filter->removeRelationFromId();
$this->applyFilter($query, $this->filter);
});
}
private function applyFilter(Builder $query, Filter $filter): Builder
{
return (new ApplyFilter($query, $filter))->apply();
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Services\DataTable\Filter;
use App\Services\DataTable\Validators\FilterValidator;
class Filter
{
private static FilterValidator $filterValidator;
/**
* @param string $id
* @param string|int|array $value
* @param string $fn
* @param string $datatype
* @param array $allowedFilters
* @throws \App\Services\DataTable\Exceptions\InvalidFilterException
*/
public function __construct(
private string $id,
private string|int|array $value,
private string $fn,
private string $datatype,
private array $allowedFilters
)
{
self::$filterValidator = FilterValidator::getInstance();
self::$filterValidator->isValid($this, $this->allowedFilters);
}
public function getId(): string
{
return $this->id;
}
public function getValue(): array|int|string
{
return $this->value;
}
public function getFn(): string
{
return $this->fn;
}
public function getDatatype(): string
{
return $this->datatype;
}
public function getRelation(): string
{
$fieldArray = explode('.', $this->id);
return count($fieldArray) > 1 ? $fieldArray[0] : '';
}
public function getColumn(): string
{
$fieldArray = explode('.', $this->id);
return array_pop($fieldArray);
}
public function removeRelationFromId(): void
{
$this->id = $this->getColumn();
}
public function setValue(int|array|string $value): void
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use Illuminate\Contracts\Database\Query\Builder;
class FilterBetween extends SearchFilter
{
public function apply(): Builder
{
$query = $this->query;
[$minVal, $maxVal] = $this->filter->getValue();
if ($minVal) {
if ($this->filter->getDatatype() == "date") {
$minVal = $minVal . " 00:00:00";
}
$this->filter->setValue($minVal);
$query = (new FilterGreaterThanOrEqual($this->query, $this->filter))->apply();
}
if ($maxVal) {
if ($this->filter->getDatatype() == "date") {
$maxVal = $maxVal . " 23:59:59";
}
$this->filter->setValue($maxVal);
$query = (new FilterLessThanOrEqual($this->query, $this->filter))->apply();
}
return $query;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Enums\DataType;
use Illuminate\Contracts\Database\Query\Builder;
class FilterContains extends SearchFilter
{
public function apply(): Builder
{
$column = $this->filter->getId();
$value = '%' . $this->filter->getValue() . '%';
if ($this->filter->getDatatype() == DataType::TEXT->value) {
$query = $this->searchIgnoreCase($column, $value);
} else {
$query = $this->query->where($column, 'LIKE', $value);
}
return $query;
}
private function searchIgnoreCase(string $column, string $value): Builder
{
$value = strtolower($value);
return $this->query->whereRaw("LOWER($column) LIKE ?", [$value]);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Enums\DataType;
use Illuminate\Contracts\Database\Query\Builder;
class FilterEquals extends SearchFilter
{
public function apply(): Builder
{
$column = $this->filter->getId();
$value = $this->filter->getValue();
if ($this->filter->getDatatype() == DataType::TEXT->value) {
$query = $this->searchIgnoreCase($column, $value);
} else {
$query = $this->query->where($column, $value);
}
return $query;
}
private function searchIgnoreCase(string $column, string $value): Builder
{
$value = strtolower($value);
return $this->query->whereRaw("LOWER($column) = ?", [$value]);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Enums\DataType;
use Illuminate\Contracts\Database\Query\Builder;
class FilterGreaterThan extends SearchFilter
{
public function apply(): Builder
{
$value = ($this->filter->getDatatype() == DataType::NUMERIC) ?
(float)$this->filter->getValue() : $this->filter->getValue();
return $this->query->where($this->filter->getId(), '>', $value);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Enums\DataType;
use Illuminate\Contracts\Database\Query\Builder;
class FilterGreaterThanOrEqual extends SearchFilter
{
public function apply(): Builder
{
$value = ($this->filter->getDatatype() == DataType::NUMERIC) ?
(float)$this->filter->getValue() : $this->filter->getValue();
return $this->query->where($this->filter->getId(), '>=', $value);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Enums\DataType;
use Illuminate\Contracts\Database\Query\Builder;
class FilterLessThan extends SearchFilter
{
public function apply(): Builder
{
$value = ($this->filter->getDatatype() == DataType::NUMERIC) ?
(float)$this->filter->getValue() : $this->filter->getValue();
return $this->query->where($this->filter->getId(), '<', $value);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Enums\DataType;
use Illuminate\Contracts\Database\Query\Builder;
class FilterLessThanOrEqual extends SearchFilter
{
public function apply(): Builder
{
$value = ($this->filter->getDatatype() == DataType::NUMERIC) ?
(float)$this->filter->getValue() : $this->filter->getValue();
return $this->query->where($this->filter->getId(), '<=', $value);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use Illuminate\Contracts\Database\Query\Builder;
class FilterNotEquals extends SearchFilter
{
public function apply(): Builder
{
$column = $this->filter->getId();
$value = $this->filter->getValue();
return $this->query->whereNot($column, $value);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Services\DataTable\Filter\SearchFunctions;
use App\Services\DataTable\Filter\Filter;
use Illuminate\Contracts\Database\Query\Builder;
abstract class SearchFilter
{
/**
* @param Builder $query
* @param Filter $filter
*/
public function __construct(
protected Builder $query,
protected Filter $filter,
)
{
}
abstract public function apply(): Builder;
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Services\DataTable\Sort;
use Illuminate\Contracts\Database\Query\Builder;
class ApplySort
{
/**
* @param Builder $query
* @param ?Sort $sort
*/
public function __construct(
private Builder $query,
private ?Sort $sort,
)
{
}
public function apply(): Builder
{
$query = $this->query;
if (!is_null($this->sort)) {
$query->orderBy($this->sort->getId(), $this->sort->getDirection());
}
return $query;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Services\DataTable\Sort;
use App\Services\DataTable\Validators\SortingValidator;
class Sort
{
private static SortingValidator $sortingValidator;
/**
* @param string $id
* @param bool $desc
* @param array $allowedSortings
* @throws \App\Services\DataTable\Exceptions\InvalidSortingException
*/
public function __construct(
private string $id,
private bool $desc,
private array $allowedSortings,
)
{
self::$sortingValidator = SortingValidator::getInstance();
self::$sortingValidator->isValid($this, $this->allowedSortings);
}
/**
* @return string
*/
public function getId(): string
{
return $this->id;
}
public function getDirection(): string
{
return $this->desc === true ? 'desc' : 'asc';
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Services\DataTable\Validators;
use App\Services\DataTable\Enums\SearchType;
use App\Services\DataTable\Enums\DataType;
use App\Services\DataTable\Exceptions\InvalidFilterException;
use App\Services\DataTable\Filter\Filter;
class FilterValidator
{
private static $instance;
private function __construct()
{
}
public static function getInstance(): FilterValidator
{
if (!isset(self::$instance)) {
self::$instance = new static();
}
return self::$instance;
}
public function isValid(Filter $filter, array $allowedFilters): bool
{
if (!$this->isAllowed($filter, $allowedFilters)) {
$filterId = $filter->getId();
throw new InvalidFilterException($filter->getId(), "filtering field `$filterId` is not allowed.");
}
if (!$this->isValidSearchFunction($filter)) {
$searchFunction = $filter->getFn();
throw new InvalidFilterException($searchFunction, "search function `$searchFunction` is invalid.");
}
if ($this->isValidDataType($filter) == -1) {
throw new InvalidFilterException(null, "datatype property is not set in `filters` array.");
}
if (!$this->isValidDataType($filter)) {
$datatype = $filter->getDatatype();
throw new InvalidFilterException($datatype, "datatype `$datatype` is invalid.");
}
return true;
}
protected function isAllowed(Filter $filter, array $allowedFilters): bool
{
return $allowedFilters == ['*'] || in_array($filter->getId(), $allowedFilters);
}
protected function isValidSearchFunction(Filter $filter): bool
{
$searchFunction = $filter->getFn();
return isset($searchFunction) && in_array($searchFunction, SearchType::values());
}
protected function isValidDataType(Filter $filter): int
{
if (!property_exists($filter, 'datatype'))
return -1;
return $filter->getDatatype() && in_array($filter->getDatatype(), DataType::values()) ? 1 : 0;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Services\DataTable\Validators;
use App\Services\DataTable\Exceptions\InvalidRelationException;
class RelationValidator
{
public static function validate(?array $rels, array $allowedRelations): bool
{
foreach ($rels as $relation) {
if (!self::isAllowed($relation, $allowedRelations)) {
throw new InvalidRelationException($relation, "relation `$relation` is not allowed.");
}
}
return true;
}
public static function isAllowed(string $relation, array $allowedRelations): bool
{
return !$relation || in_array($relation, $allowedRelations);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Services\DataTable\Validators;
use App\Services\DataTable\Exceptions\InvalidSortingException;
use App\Services\DataTable\Sort\Sort;
class SortingValidator
{
private static $instance;
private function __construct()
{
}
public static function getInstance(): SortingValidator
{
if (!isset(self::$instance)) {
self::$instance = new static();
}
return self::$instance;
}
public function isValid(Sort $sorting, array $allowedSortings): bool
{
if (!$this->isAllowed($sorting, $allowedSortings)) {
$sortId = $sorting->getId();
throw new InvalidSortingException($sortId, "sorting field `$sortId` is not allowed.");
}
return true;
}
protected function isAllowed(Sort $sorting, array $allowedSortings): bool
{
return $allowedSortings == ['*'] || in_array($sorting->getId(), $allowedSortings);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Services\FIB;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class AuthorizationService
{
protected string $url;
protected string $channelName;
protected array $inputParameters;
public function __construct()
{
$this->url = config('fib.authorization.url');
$this->channelName = 'fib_authorization';
$this->inputParameters = [
'client_id' => config('fib.authorization.client_id'),
'client_secret' => config('fib.authorization.client_secret'),
'grant_type' => 'client_credentials',
];
}
/**
* @throws Exception
*/
public function run(): array
{
try {
return $this->sendRequest();
}
catch (Exception $e) {
Log::channel($this->channelName)
->error(get_class($this),
[
'message' => $e->getMessage(),
]
);
throw $e;
}
}
/**
* @throws ConnectionException
*/
public function sendRequest(): array
{
return Http::asForm()
->throw()
->post($this->url, $this->inputParameters)
->json();
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Services\FIB;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CancelPaymentService
{
protected string $url;
protected string $channelName;
protected string $accessToken;
protected array $headers;
public function __construct()
{
$this->url = config('fib.cancelPayment.url');
$this->channelName = '';
}
public function setHeaders(): void
{
$this->headers = [
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
];
}
/**
* @throws Exception
*/
public function run(): array
{
try {
return $this->sendRequest();
}
catch (Exception $e) {
Log::channel($this->channelName)
->error(get_class($this),
[
'message' => $e->getMessage(),
]
);
throw $e;
}
}
/**
* @throws ConnectionException
*/
public function sendRequest(): array
{
return Http::withHeaders($this->headers)
->throw()
->withoutVerifying()
->post($this->url)
->json();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Services\FIB;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CheckPaymentStatusService
{
protected string $url;
protected string $channelName;
public function __construct()
{
$this->url = config('fib.checkPaymentStatus.url');
$this->channelName = '';
}
/**
* @throws Exception
*/
public function run(): array
{
try {
return $this->sendRequest();
}
catch (Exception $e) {
Log::channel($this->channelName)
->error(get_class($this),
[
'message' => $e->getMessage(),
]
);
throw $e;
}
}
/**
* @throws ConnectionException
*/
public function sendRequest(): array
{
return Http::throw()
->withoutVerifying()
->post($this->url)
->json();
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Services\FIB;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CreatePaymentService
{
protected string $url;
protected string $channelName;
protected string $accessToken;
protected array $headers;
protected array $inputParameters;
public function __construct()
{
$this->url = config('fib.createPayment.url');
$this->channelName = '';
}
public function setAccessToken(string $accessToken): void
{
$this->accessToken = $accessToken;
}
public function setHeaders(): void
{
$this->headers = [
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
];
}
public function setInputParameters(array $inputParameters): void
{
$this->inputParameters = $inputParameters;
}
/**
* @throws Exception
*/
public function run(): array
{
try {
return $this->sendRequest();
}
catch (Exception $e) {
Log::channel($this->channelName)
->error(get_class($this),
[
'message' => $e->getMessage(),
]
);
throw $e;
}
}
/**
* @throws ConnectionException
*/
public function sendRequest(): array
{
return Http::withHeaders($this->headers)
->throw()
->withoutVerifying()
->post($this->url, $this->inputParameters)
->json();
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Services\FIB;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class RefundService
{
protected string $url;
protected string $channelName;
protected string $accessToken;
protected array $headers;
public function __construct()
{
$this->url = config('fib.refund.url');
$this->channelName = '';
}
public function setHeaders(): void
{
$this->headers = [
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
];
}
/**
* @throws Exception
*/
public function run(): array
{
try {
return $this->sendRequest();
}
catch (Exception $e) {
Log::channel($this->channelName)
->error(get_class($this),
[
'message' => $e->getMessage(),
]
);
throw $e;
}
}
/**
* @throws ConnectionException
*/
public function sendRequest(): array
{
return Http::withHeaders($this->headers)
->throw()
->withoutVerifying()
->post($this->url)
->json();
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Traits;
use Illuminate\Http\JsonResponse;
trait ApiResponse
{
public function successResponse(mixed $data = null, string $message = null, int $statusCode = 200): JsonResponse
{
if (!is_null($data)) {
return response()->json([
'data' => $data
], $statusCode);
}
$message = $message ?? __('messages.successful');
return response()->json([
'message' => $message
], $statusCode);
}
public function errorResponse(string $message, string $type = 'logical_exception', int $statusCode = 422): JsonResponse
{
return response()->json([
'type' => $type,
'message' => $message
], $statusCode);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\User\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Traits\ApiResponse;
use App\User\Requests\Auth\LoginRequest;
use App\User\Requests\Auth\RegisterRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
use ApiResponse;
public function register(RegisterRequest $request): JsonResponse
{
$user = User::query()->create([
'username' => $request->username,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
Auth::guard('web')->login($user);
$request->session()->regenerate();
return $this->successResponse();
}
public function login(LoginRequest $request): JsonResponse
{
$credentials = ['password' => $request->password,];
if (filter_var($request->login, FILTER_VALIDATE_EMAIL)) {
$credentials['email'] = $request->login;
} else {
$credentials['username'] = $request->login;
}
if (Auth::attempt($credentials))
{
$request->session()->regenerate();
return $this->successResponse();
}
return $this->errorResponse(__('messages.incorrect_or_username_password'));
}
public function logout(Request $request): JsonResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return $this->successResponse();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\User\Controllers;
use App\Http\Controllers\Controller;
use App\Models\City;
use App\Models\Province;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CityController extends Controller
{
use ApiResponse;
public function list(Request $request): JsonResponse
{
$cities = City::query()
->where('province_id', '=', $request->query('province_id'))
->get(['id', 'name']);
return $this->successResponse($cities);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\User\Controllers;
use App\Facades\File\FileFacade;
use App\Http\Controllers\Controller;
use App\Traits\ApiResponse;
use App\User\Requests\Documentation\EditRequest;
use App\User\Requests\Documentation\UploadRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class DocumentationController extends Controller
{
use ApiResponse;
public function upload(UploadRequest $request): jsonResponse
{
$user = Auth::guard('web')->user();
foreach ($request->documents as $document) {
$user->documents()->create([
'title' => $document['title'],
'url' => FileFacade::save($document['file'], "/{$user->id}/documents", $document['title']),
]);
}
return $this->successResponse();
}
public function edit(EditRequest $request): JsonResponse
{
$user = Auth::guard('web')->user();
foreach ($request->documents as $document) {
$user->documents()->sync();
}
return $this->successResponse();
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\User\Controllers;
use App\Enums\BusinessTypeEnum;
use App\Facades\DataTable\DataTableFacade;
use App\Facades\File\FileFacade;
use App\Http\Controllers\Controller;
use App\Models\UserGateway;
use App\Traits\ApiResponse;
use App\User\Requests\Gateway\StoreRequest;
use App\User\Requests\Gateway\UpdateRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Throwable;
class GatewayManagementController extends Controller
{
use ApiResponse;
public function index(Request $request)
{
return DataTableFacade::run(
UserGateway::query()->where('user_id', '=', Auth::guard('web')->id()),
$request,
allowedFilters: ['*'],
allowedSortings: ['*'],
allowedSelects: ['id', 'name', 'domain', 'tax_id', 'bank_name', 'account_holder', 'iban', 'business_type_id', 'business_type_name'],
);
}
/**
* @throws Throwable
*/
public function store(StoreRequest $request): JsonResponse
{
$user = Auth::guard('web')->user();
$logo = $request->has('logo') ? FileFacade::save($request->file('logo'), "{$user->id}") : null;
DB::transaction(function () use ($user, $logo, $request) {
UserGateway::query()->create([
'user_id' => $user->id,
'username' => $user->username,
'name' => $request->name,
'domain' => $request->domain,
'tax_id' => $request->tax_id,
'bank_name' => $request->bank_name,
'account_holder' => $request->account_holder,
'iban' => $request->iban,
'business_type_id' => $request->business_type_id,
'business_type_name' => BusinessTypeEnum::name($request->business_type_id),
'logo' => $logo,
]);
$user->update(['authority_level' => 1]);
});
return $this->successResponse();
}
public function show(UserGateway $gateway): JsonResponse
{
return $this->successResponse($gateway);
}
public function update(UpdateRequest $request, UserGateway $gateway): JsonResponse
{
$logo = null;
$user = Auth::guard('web')->user();
if ($request->has('logo'))
{
FileFacade::delete($gateway->logo, true);
$logo = FileFacade::save(request()->file('logo'), "{$user->id}");
}
$gateway->update([
'name' => $request->name,
'domain' => $request->domain,
'business_type_id' => $request->business_type_id,
'business_type_name' => BusinessTypeEnum::name($request->business_type_id),
'logo' => $logo,
]);
return $this->successResponse();
}
public function destroy(UserGateway $gateway): JsonResponse
{
$gateway->delete();
return $this->successResponse();
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\User\Controllers;
use App\Facades\File\FileFacade;
use App\Http\Controllers\Controller;
use App\Models\City;
use App\Models\Province;
use App\Traits\ApiResponse;
use App\User\Requests\Profile\ChangePasswordRequest;
use App\User\Requests\Profile\EditRequest;
use App\User\Resources\UserResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Throwable;
class ProfileController extends Controller
{
use ApiResponse;
public function info(): JsonResponse
{
return $this->successResponse(new UserResource(Auth::user()));
}
/**
* @throws Throwable
*/
public function edit(EditRequest $request): JsonResponse
{
$user = Auth::user();
$province = Province::query()->find($request->province_id);
$city = City::query()->find($request->city_id);
DB::transaction(function () use ($user, $province, $city, $request) {
if ($user->kyc_status == 0)
{
$user->documents()->create([
'title' => 'id_card',
'url' => FileFacade::save($request->file('id_card'), "{$user->id}/"),
]);
}
$user->update([
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'national_id' => $request->national_id,
'address' => $request->address,
'postal_code' => $request->postal_code,
'phone_number' => $request->phone_number,
'province_id' => $province->id,
'province_name' => $province->name,
'city_id' => $city->id,
'city_name' => $city->name,
'birth_date' => $request->birth_date,
'email' => $request->email,
'kyc_status' => 1,
]);
});
return $this->successResponse();
}
public function changePassword(ChangePasswordRequest $request): JsonResponse
{
$user = Auth::user();
if (! Hash::check($request->current_password, $user->password)) {
return $this->errorResponse(__('messages.incorrect_current_password'));
}
$user->update([
'password' => Hash::make($request->new_password),
]);
return $this->successResponse();
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\User\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Province;
use App\Traits\ApiResponse;
use Illuminate\Http\JsonResponse;
class ProvinceController extends Controller
{
use ApiResponse;
public function list(): JsonResponse
{
return $this->successResponse(Province::all(['id', 'name']));
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\User\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LoginEmailEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(public User $user)
{
//
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\User\Listeners;
use App\Mail\LoginNotificationMail;
use App\User\Events\LoginEmailEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
class SendEmailListener implements ShouldQueue{
public string $queue = 'emails';
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(LoginEmailEvent $event): void
{
$user = $event->user;
Mail::to($user->email)->send(
new LoginNotificationMail($user)
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\User\Requests\Auth;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest 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|string>
*/
public function rules(): array
{
return [
'login' => 'required|string',
'password' => 'required|string',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\User\Requests\Auth;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest 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|string>
*/
public function rules(): array
{
return [
'username' => 'required|unique:users,username',
'email' => 'required|email|unique:users,email',
'password' => 'required|string|regex:/^(?=.*[a-zA-Z])(?=.*\d).+$/|min:6|confirmed',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\User\Requests\Documentation;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class EditRequest 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|string>
*/
public function rules(): array
{
return [
'documents' => 'required|array',
'documents.*.file' => 'required|file|mimes:pdf,jpg,jpeg,png|max:2048',
'documents.*.title' => 'required|string',
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\User\Requests\Documentation;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class UploadRequest 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|string>
*/
public function rules(): array
{
return [
'documents' => 'required|array',
'documents.*.file' => 'required|file|mimes:pdf,jpg,jpeg,png|max:2048',
'documents.*.title' => 'required|string',
];
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\User\Requests\Gateway;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class StoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Auth::guard('web')->user()->kyc_status;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'domain' => 'required|string|max:255',
'tax_id' => 'required|string',
'bank_name' => 'required|string|max:255',
'account_holder' => 'required|string',
'iban' => 'required|string|max:255',
'logo' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048',
'business_type_id' => 'required|string|exists:business_types,id',
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\User\Requests\Gateway;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class UpdateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Auth::guard('web')->user()->kyc_status;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'domain' => 'required|string|max:255',
'business_type_id' => 'required|string|exists:business_types,id',
'logo' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048',
];
}
}

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