Compare commits
14 Commits
2d96978c38
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d16d8f512 | |||
| 6e92ebaada | |||
| e987a528ef | |||
| f77ff527e7 | |||
| fd0d445b6e | |||
| 9718fb8c7c | |||
| 792387804c | |||
| b4b1fdc288 | |||
| 45da0103c0 | |||
| 5f19f82f86 | |||
| 1e599b917b | |||
| 51b13631a3 | |||
| 4ce663c669 | |||
| aeef95339d |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev"
|
||||
],
|
||||
"port": 5173
|
||||
},
|
||||
{
|
||||
"name": "serve",
|
||||
"runtimeExecutable": "php",
|
||||
"runtimeArgs": [
|
||||
"artisan",
|
||||
"serve"
|
||||
],
|
||||
"port": 8000
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.git
|
||||
node_modules
|
||||
vendor
|
||||
public/build
|
||||
public/hot
|
||||
public/storage
|
||||
storage/logs/*
|
||||
storage/framework/cache/*
|
||||
storage/framework/sessions/*
|
||||
storage/framework/views/*
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
graphify-out
|
||||
@@ -11,4 +11,15 @@ enum DocumentModalityEnum: string
|
||||
case Sonography = 'sonography';
|
||||
case Echo = 'echo';
|
||||
case Radiology = 'radiology';
|
||||
|
||||
public function price(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::CtScan => 1000000,
|
||||
self::Mri => 1200000,
|
||||
self::Sonography => 500000,
|
||||
self::Echo => 700000,
|
||||
self::Radiology => 400000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,13 @@ enum DocumentPriorityEnum: string
|
||||
|
||||
case Medium = 'عادی';
|
||||
case High = 'اورژانسی';
|
||||
|
||||
public function processingHours(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::Medium => 24,
|
||||
self::High => 3,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum InvoicePaymentMethodEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Wallet = 'کیف پول';
|
||||
case Gateway = 'درگاه پرداخت';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum InvoiceStatusEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Pending = 'در انتظار پرداخت';
|
||||
case Paid = 'پرداخت شده';
|
||||
case Canceled = 'لغو شده';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PaymentPurposeEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Invoice = 'پرداخت فاکتور';
|
||||
case WalletTopUp = 'شارژ کیف پول';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PaymentStatusEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Pending = 'در انتظار پرداخت';
|
||||
case Paid = 'پرداخت شده';
|
||||
case Failed = 'ناموفق';
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
enum ShiftDayEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Saturday = 'شنبه';
|
||||
case Sunday = 'یکشنبه';
|
||||
case Monday = 'دوشنبه';
|
||||
case Tuesday = 'سهشنبه';
|
||||
case Wednesday = 'چهارشنبه';
|
||||
case Thursday = 'پنجشنبه';
|
||||
case Friday = 'جمعه';
|
||||
|
||||
public function carbonDay(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::Saturday => Carbon::SATURDAY,
|
||||
self::Sunday => Carbon::SUNDAY,
|
||||
self::Monday => Carbon::MONDAY,
|
||||
self::Tuesday => Carbon::TUESDAY,
|
||||
self::Wednesday => Carbon::WEDNESDAY,
|
||||
self::Thursday => Carbon::THURSDAY,
|
||||
self::Friday => Carbon::FRIDAY,
|
||||
};
|
||||
}
|
||||
|
||||
public static function fromDate(Carbon $date): self
|
||||
{
|
||||
return match ($date->dayOfWeek) {
|
||||
Carbon::SATURDAY => self::Saturday,
|
||||
Carbon::SUNDAY => self::Sunday,
|
||||
Carbon::MONDAY => self::Monday,
|
||||
Carbon::TUESDAY => self::Tuesday,
|
||||
Carbon::WEDNESDAY => self::Wednesday,
|
||||
Carbon::THURSDAY => self::Thursday,
|
||||
Carbon::FRIDAY => self::Friday,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum WalletTransactionReasonEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case TopUp = 'شارژ کیف پول';
|
||||
case Commission = 'کمیسیون پزشک';
|
||||
case InvoicePayment = 'پرداخت فاکتور';
|
||||
case Refund = 'بازگشت وجه';
|
||||
case Withdraw = 'برداشت وجه';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum WalletTransactionTypeEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Deposit = 'واریز';
|
||||
case Withdrawal = 'برداشت';
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum WithdrawRequestStatusEnum: string
|
||||
{
|
||||
use EnumHasOptions;
|
||||
|
||||
case Pending = 'در انتظار بررسی';
|
||||
case Approved = 'تایید شده';
|
||||
case Rejected = 'رد شده';
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Shift;
|
||||
use App\Http\Resources\Admin\ShiftResource;
|
||||
use App\Http\Requests\Admin\ShiftRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShiftController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('shifts.index');
|
||||
|
||||
$lists = Shift::with(['doctor'])
|
||||
->when(isset($request->search) , function ($query) use ($request){
|
||||
$query->where(function ($q) use ($request) {
|
||||
$q->whereHas('doctor', function ($doctorQuery) use ($request) {
|
||||
$doctorQuery->where('fullname', 'LIKE', "%{$request->search}%");
|
||||
})->orWhere('id', $request->search);
|
||||
});
|
||||
})
|
||||
->when(isset($request->day_of_week) && $request->day_of_week != 'همه روزها', function ($query) use ($request) {
|
||||
$query->whereIn('day_of_week' , (array) $request->day_of_week);
|
||||
})
|
||||
->when(isset($request->status) && $request->status != 'همه وضعیت ها', function ($query) use ($request) {
|
||||
$query->where('is_active' , $request->status == 'فعال');
|
||||
})
|
||||
->when(isset($request->occupancy) && $request->occupancy != 'همه شیفت ها', function ($query) use ($request) {
|
||||
$request->occupancy == 'آزاد'
|
||||
? $query->whereNull('user_id')
|
||||
: $query->whereNotNull('user_id');
|
||||
})
|
||||
->latest()
|
||||
->paginate(20)
|
||||
->withQueryString()
|
||||
->onEachSide(1);
|
||||
|
||||
$shifts = ShiftResource::collection($lists);
|
||||
|
||||
return Inertia::admin('Shifts/index', [
|
||||
'shifts' => $shifts,
|
||||
'search' => $request->search,
|
||||
'day_of_week' => $request->day_of_week,
|
||||
'status' => $request->status,
|
||||
'occupancy' => $request->occupancy,
|
||||
'permissions_edit' => auth()->user()->can('shifts.edit') ? false : true,
|
||||
'permissions_delete' => auth()->user()->can('shifts.delete') ? false : true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function create()
|
||||
{
|
||||
$this->authorize('shifts.create');
|
||||
return Inertia::admin('Shifts/create_and_edit');
|
||||
}
|
||||
|
||||
|
||||
public function store(ShiftRequest $request)
|
||||
{
|
||||
$this->authorize('shifts.create');
|
||||
Shift::create([
|
||||
'user_id' => $request->user_id,
|
||||
'day_of_week' => $request->day_of_week,
|
||||
'date' => $request->date,
|
||||
'start_time' => $request->start_time,
|
||||
'end_time' => $request->end_time,
|
||||
'is_active' => $request->is_active,
|
||||
'created_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
session()->flash('success', 'شیفت با موفقیت ثبت شد');
|
||||
return response(null , 200);
|
||||
}
|
||||
|
||||
|
||||
public function show(Shift $shift)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
public function edit(Shift $shift)
|
||||
{
|
||||
$this->authorize('shifts.edit');
|
||||
return Inertia::admin('Shifts/create_and_edit', [
|
||||
'shift' => ShiftResource::make($shift->load('doctor'))->resolve(),
|
||||
'is_edit' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function update(ShiftRequest $request, Shift $shift)
|
||||
{
|
||||
$this->authorize('shifts.edit');
|
||||
$shift->update([
|
||||
'user_id' => $request->user_id,
|
||||
'day_of_week' => $request->day_of_week,
|
||||
'date' => $request->date,
|
||||
'start_time' => $request->start_time,
|
||||
'end_time' => $request->end_time,
|
||||
'is_active' => $request->is_active,
|
||||
]);
|
||||
|
||||
session()->flash('success', 'شیفت با موفقیت بروزرسانی شد');
|
||||
return response(null , 200);
|
||||
}
|
||||
|
||||
|
||||
public function destroy(Shift $shift)
|
||||
{
|
||||
$this->authorize('shifts.delete');
|
||||
$shift->delete();
|
||||
|
||||
session()->flash('success', 'شیفت با موفقیت حذف شد');
|
||||
return response(null , 200);
|
||||
}
|
||||
|
||||
public function calendarData()
|
||||
{
|
||||
$this->authorize('shifts.index');
|
||||
|
||||
$lists = Shift::with('doctor')->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => ShiftResource::collection($lists),
|
||||
]);
|
||||
}
|
||||
|
||||
public function bulkDelete(Request $request)
|
||||
{
|
||||
$this->authorize('shifts.delete');
|
||||
Shift::destroy($request->ids);
|
||||
session()->flash('success', 'شیفت های مورد نظر با موفقیت حذف شدند.');
|
||||
return response(null , 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\WithdrawRequestStatusEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\WithdrawRejectRequest;
|
||||
use App\Http\Resources\Admin\WithdrawRequestResource;
|
||||
use App\Models\WithdrawRequest;
|
||||
use App\Services\Wallet\WalletService;
|
||||
use App\Services\Wallet\WithdrawRequestService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WithdrawRequestController extends Controller
|
||||
{
|
||||
protected WithdrawRequestService $withdrawRequestService;
|
||||
|
||||
protected WalletService $walletService;
|
||||
|
||||
public function __construct(WithdrawRequestService $withdrawRequestService, WalletService $walletService)
|
||||
{
|
||||
$this->withdrawRequestService = $withdrawRequestService;
|
||||
$this->walletService = $walletService;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$lists = WithdrawRequest::with(['user', 'bankCard', 'reviewedBy'])
|
||||
->when(isset($request->search), function ($query) use ($request) {
|
||||
$query->whereHas('user', function ($userQuery) use ($request) {
|
||||
$userQuery->where('fullname', 'LIKE', "%{$request->search}%")
|
||||
->orWhere('phone', 'LIKE', "%{$request->search}%")
|
||||
->orWhere('email', 'LIKE', "%{$request->search}%");
|
||||
});
|
||||
})
|
||||
->when(isset($request->status) && $request->status != 'همه وضعیت ها', function ($query) use ($request) {
|
||||
$query->where('status', $request->status);
|
||||
})
|
||||
->latest()
|
||||
->paginate(20)
|
||||
->withQueryString()
|
||||
->onEachSide(1);
|
||||
|
||||
$pending = WithdrawRequest::where('status', WithdrawRequestStatusEnum::Pending->value);
|
||||
|
||||
return Inertia::admin('WithdrawRequests/index', [
|
||||
'withdrawRequests' => WithdrawRequestResource::collection($lists),
|
||||
'stats' => [
|
||||
'pending_count' => (clone $pending)->count(),
|
||||
'pending_amount' => (int) (clone $pending)->sum('amount'),
|
||||
'system_wallet_balance' => $this->walletService->system()->balance,
|
||||
],
|
||||
'search' => $request->search,
|
||||
'status' => $request->status,
|
||||
]);
|
||||
}
|
||||
|
||||
public function approve(WithdrawRequest $withdrawRequest)
|
||||
{
|
||||
$this->withdrawRequestService->approve($withdrawRequest, Auth::user());
|
||||
|
||||
session()->flash('success', 'درخواست برداشت تایید شد و مبلغ به ولت سایت منتقل شد.');
|
||||
|
||||
return response(null, 200);
|
||||
}
|
||||
|
||||
public function reject(WithdrawRejectRequest $request, WithdrawRequest $withdrawRequest)
|
||||
{
|
||||
$this->withdrawRequestService->reject($withdrawRequest, Auth::user(), $request->validated('note'));
|
||||
|
||||
session()->flash('success', 'درخواست برداشت رد شد.');
|
||||
|
||||
return response(null, 200);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@
|
||||
namespace App\Http\Controllers\Client\Doctor;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Client\CaseRejectRequest;
|
||||
use App\Http\Resources\Client\CaseDetailResource;
|
||||
use App\Http\Resources\Client\CaseResource;
|
||||
use App\Models\Document;
|
||||
use App\Services\Document\CaseService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
@@ -28,4 +31,25 @@ class CaseController extends Controller
|
||||
'cases' => CaseResource::collection($cases),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Document $case)
|
||||
{
|
||||
abort_unless($this->caseService->canAccess($case, Auth::user()), 403);
|
||||
|
||||
$case->load(['pet', 'requester', 'media', 'reports']);
|
||||
|
||||
return Inertia::client('Dashboard/Cases/show', [
|
||||
'case' => (new CaseDetailResource($case))->resolve(),
|
||||
'canReport' => $this->caseService->canReport($case, Auth::user()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function reject(CaseRejectRequest $request, Document $case)
|
||||
{
|
||||
$this->caseService->reject($case, Auth::user(), $request->validated('reason'));
|
||||
|
||||
session()->flash('success', 'درخواست رد شد و مبلغ فاکتور به کیف پول درخواستکننده بازگردانده شد.');
|
||||
|
||||
return redirect()->route('dashboard.cases.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class DashboardController extends Controller
|
||||
|
||||
return Inertia::client('Dashboard/Dashboard', [
|
||||
'cases' => CaseResource::collection($cases),
|
||||
'stats' => $this->caseService->statsForUser(Auth::user()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ class DocumentController extends Controller
|
||||
|
||||
public function show(Document $document)
|
||||
{
|
||||
return Inertia::client('Dashboard/Documents/show', [
|
||||
'document' => $document,
|
||||
]);
|
||||
return redirect()->route('dashboard.cases.show', $document);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,59 @@
|
||||
namespace App\Http\Controllers\Client\Doctor;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\Client\ReportSubmitRequest;
|
||||
use App\Http\Resources\Client\CaseResource;
|
||||
use App\Http\Resources\Client\ReportResource;
|
||||
use App\Models\Document;
|
||||
use App\Models\Report;
|
||||
use App\Services\Document\CaseService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
protected CaseService $caseService;
|
||||
|
||||
public function __construct(CaseService $caseService)
|
||||
{
|
||||
$this->caseService = $caseService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return Inertia::client('Dashboard/Reports/Index');
|
||||
$reports = Report::with(['document', 'doctor'])
|
||||
->where(function ($query) {
|
||||
$query->where('doctor_id', Auth::id())
|
||||
->orWhereHas('document', fn ($documentQuery) => $documentQuery->where('requester_id', Auth::id()));
|
||||
})
|
||||
->latest('submitted_at')
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->onEachSide(1);
|
||||
|
||||
$pendingCases = $this->caseService->isDoctor(Auth::user())
|
||||
? CaseResource::collection($this->caseService->pendingForUser(Auth::user())->get())
|
||||
: null;
|
||||
|
||||
return Inertia::client('Dashboard/Reports/Index', [
|
||||
'reports' => ReportResource::collection($reports),
|
||||
'pendingCases' => $pendingCases,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(ReportSubmitRequest $request)
|
||||
{
|
||||
$document = Document::findOrFail($request->validated('document_id'));
|
||||
|
||||
$this->caseService->submitReport(
|
||||
$document,
|
||||
Auth::user(),
|
||||
$request->validated(),
|
||||
$request->file('files', [])
|
||||
);
|
||||
|
||||
session()->flash('success', 'گزارش با موفقیت ثبت شد.');
|
||||
|
||||
return redirect()->route('dashboard.cases.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,77 @@
|
||||
|
||||
namespace App\Http\Controllers\Client\Doctor;
|
||||
|
||||
use App\Enums\ShiftDayEnum;
|
||||
use App\Enums\UserEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Resources\Client\ShiftResource;
|
||||
use App\Models\Shift;
|
||||
use App\Services\Shift\ShiftScheduleService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ShiftController extends Controller
|
||||
{
|
||||
protected ShiftScheduleService $shiftScheduleService;
|
||||
|
||||
public function __construct(ShiftScheduleService $shiftScheduleService)
|
||||
{
|
||||
$this->shiftScheduleService = $shiftScheduleService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return Inertia::client('Dashboard/Shifts/Index');
|
||||
$this->ensureDoctor();
|
||||
|
||||
$myShifts = Shift::where('user_id', Auth::id())->get();
|
||||
|
||||
$openShifts = Shift::active()
|
||||
->open()
|
||||
->where(function ($query) {
|
||||
$query->whereNull('date')
|
||||
->orWhereDate('date', '>=', now()->toDateString());
|
||||
})
|
||||
->get();
|
||||
|
||||
return Inertia::client('Dashboard/Shifts/Index', [
|
||||
'myShifts' => ShiftResource::collection($this->sortSchedule($myShifts)),
|
||||
'openShifts' => ShiftResource::collection($this->sortSchedule($openShifts)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Shift $shift)
|
||||
{
|
||||
$this->ensureDoctor();
|
||||
|
||||
$this->shiftScheduleService->claim($shift->id, Auth::user());
|
||||
|
||||
return response(null, 200);
|
||||
}
|
||||
|
||||
public function destroy(Shift $shift)
|
||||
{
|
||||
$this->ensureDoctor();
|
||||
|
||||
$this->shiftScheduleService->release($shift, Auth::user());
|
||||
|
||||
return response(null, 200);
|
||||
}
|
||||
|
||||
protected function ensureDoctor(): void
|
||||
{
|
||||
abort_unless(Auth::user()->level === UserEnum::Doctor->value, 403);
|
||||
}
|
||||
|
||||
protected function sortSchedule($shifts)
|
||||
{
|
||||
$dayOrder = array_flip(ShiftDayEnum::values());
|
||||
|
||||
return $shifts
|
||||
->sortBy([
|
||||
fn ($a, $b) => strcmp($a->date?->toDateString() ?? '', $b->date?->toDateString() ?? ''),
|
||||
fn ($a, $b) => ($dayOrder[$a->day_of_week] ?? 0) <=> ($dayOrder[$b->day_of_week] ?? 0),
|
||||
fn ($a, $b) => strcmp($a->start_time, $b->start_time),
|
||||
])
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,32 @@
|
||||
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Enums\UserEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Client\FastRequestRequest;
|
||||
use App\Http\Resources\Client\DoctorResource;
|
||||
use App\Http\Resources\Client\FastRequestResource;
|
||||
use App\Http\Resources\Client\PetArchiveResource;
|
||||
use App\Models\User;
|
||||
use App\Services\Document\FastRequestService;
|
||||
use App\Services\Shift\ShiftScheduleService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class FastRequestController extends Controller
|
||||
{
|
||||
protected FastRequestService $fastRequestService;
|
||||
|
||||
public function __construct(FastRequestService $fastRequestService)
|
||||
protected ShiftScheduleService $shiftScheduleService;
|
||||
|
||||
public function __construct(FastRequestService $fastRequestService, ShiftScheduleService $shiftScheduleService)
|
||||
{
|
||||
$this->fastRequestService = $fastRequestService;
|
||||
$this->shiftScheduleService = $shiftScheduleService;
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$pets = Auth::user()->pets()->latest()->get();
|
||||
|
||||
$doctors = User::where('level', UserEnum::Doctor->value)
|
||||
->latest()
|
||||
->get();
|
||||
$doctors = $this->shiftScheduleService->availableDoctorsAt(now());
|
||||
|
||||
return response()->json([
|
||||
'pets' => PetArchiveResource::collection($pets),
|
||||
@@ -45,7 +45,7 @@ class FastRequestController extends Controller
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'document' => new FastRequestResource($document->load('pet')),
|
||||
'document' => new FastRequestResource($document->refresh()->load(['pet', 'invoice'])),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Client\InvoiceResource;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\Wallet\WalletService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
protected WalletService $walletService;
|
||||
|
||||
public function __construct(WalletService $walletService)
|
||||
{
|
||||
$this->walletService = $walletService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$invoices = Auth::user()->invoices()
|
||||
->with('document')
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->onEachSide(1);
|
||||
|
||||
return Inertia::client('Dashboard/Invoices/Index', [
|
||||
'invoices' => InvoiceResource::collection($invoices),
|
||||
]);
|
||||
}
|
||||
|
||||
public function payWithWallet(Invoice $invoice)
|
||||
{
|
||||
abort_unless($invoice->user_id === Auth::id(), 403);
|
||||
|
||||
$invoice = $this->walletService->payInvoice(Auth::user(), $invoice);
|
||||
|
||||
return response()->json([
|
||||
'invoice' => new InvoiceResource($invoice),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Medium;
|
||||
use App\Services\Document\CaseService;
|
||||
use App\Services\FileStorageService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
protected CaseService $caseService;
|
||||
protected FileStorageService $fileStorageService;
|
||||
|
||||
public function __construct(CaseService $caseService, FileStorageService $fileStorageService)
|
||||
{
|
||||
$this->caseService = $caseService;
|
||||
$this->fileStorageService = $fileStorageService;
|
||||
}
|
||||
|
||||
public function download(Medium $medium)
|
||||
{
|
||||
abort_unless($this->caseService->canAccessMedium($medium, Auth::user()), 403);
|
||||
|
||||
return $this->fileStorageService->downloadFile($medium);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Client\PaymentRequest;
|
||||
use App\Http\Resources\Client\PaymentResource;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Services\Invoice\InvoiceService;
|
||||
use App\Services\Payment\ZibalPaymentService;
|
||||
use App\Services\Wallet\WalletService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
protected ZibalPaymentService $zibalPaymentService;
|
||||
|
||||
protected WalletService $walletService;
|
||||
|
||||
protected InvoiceService $invoiceService;
|
||||
|
||||
public function __construct(ZibalPaymentService $zibalPaymentService, WalletService $walletService, InvoiceService $invoiceService)
|
||||
{
|
||||
$this->zibalPaymentService = $zibalPaymentService;
|
||||
$this->walletService = $walletService;
|
||||
$this->invoiceService = $invoiceService;
|
||||
}
|
||||
|
||||
public function store(PaymentRequest $request)
|
||||
{
|
||||
$invoice = Invoice::where('id', $request->invoice_id)
|
||||
->where('user_id', Auth::id())
|
||||
->where('status', InvoiceStatusEnum::Pending->value)
|
||||
->firstOrFail();
|
||||
|
||||
$payment = $this->zibalPaymentService->request(Auth::user(), $invoice->amount, $invoice->number, [
|
||||
'purpose' => PaymentPurposeEnum::Invoice->value,
|
||||
'invoice_id' => $invoice->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'payment' => new PaymentResource($payment),
|
||||
'url' => $this->zibalPaymentService->paymentUrl($payment),
|
||||
]);
|
||||
}
|
||||
|
||||
public function callback(Request $request)
|
||||
{
|
||||
$payment = Payment::where('track_id', $request->trackId)->first();
|
||||
|
||||
if (!$payment) {
|
||||
return redirect()->route('dashboard.index');
|
||||
}
|
||||
|
||||
$wasPending = $payment->status === PaymentStatusEnum::Pending;
|
||||
|
||||
if ($request->success == 1) {
|
||||
$payment = $this->zibalPaymentService->verify($payment);
|
||||
} else {
|
||||
$payment = $this->zibalPaymentService->markFailed($payment);
|
||||
}
|
||||
|
||||
if ($wasPending && $payment->status === PaymentStatusEnum::Paid) {
|
||||
DB::transaction(fn () => $this->settle($payment));
|
||||
}
|
||||
|
||||
session()->flash('payment_result', [
|
||||
'status' => $payment->status === PaymentStatusEnum::Paid ? 'paid' : 'failed',
|
||||
'track_id' => $payment->track_id,
|
||||
'purpose' => $payment->purpose->name,
|
||||
]);
|
||||
|
||||
return redirect()->route('dashboard.index');
|
||||
}
|
||||
|
||||
protected function settle(Payment $payment): void
|
||||
{
|
||||
if ($payment->purpose === PaymentPurposeEnum::WalletTopUp) {
|
||||
$this->walletService->deposit($payment->payer, $payment->amount, WalletTransactionReasonEnum::TopUp, $payment);
|
||||
return;
|
||||
}
|
||||
|
||||
$invoice = $payment->invoice;
|
||||
|
||||
if ($invoice && $invoice->status === InvoiceStatusEnum::Pending) {
|
||||
$this->invoiceService->markPaid($invoice, InvoicePaymentMethodEnum::Gateway);
|
||||
$this->walletService->recordGatewayInvoicePayment($invoice, $payment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Client\ProfileUpdateRequest;
|
||||
use App\Models\Medium;
|
||||
use App\Services\FileStorageService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
protected FileStorageService $fileStorageService;
|
||||
|
||||
public function __construct(FileStorageService $fileStorageService)
|
||||
{
|
||||
$this->fileStorageService = $fileStorageService;
|
||||
}
|
||||
|
||||
public function update(ProfileUpdateRequest $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
$data = $request->safe()->except('avatar');
|
||||
|
||||
if ($request->hasFile('avatar')) {
|
||||
if (!empty($user->profile_url)) {
|
||||
if ($medium = Medium::where('url', $user->profile_url)->first()) {
|
||||
$this->fileStorageService->deleteMedium($medium);
|
||||
}
|
||||
}
|
||||
|
||||
$medium = $this->fileStorageService->storeFile($request->file('avatar'), 'public', 'profile');
|
||||
$data['profile_url'] = $medium->url;
|
||||
}
|
||||
|
||||
$user->update($data);
|
||||
|
||||
session()->flash('success', 'حساب کاربری شما با موفقیت ویرایش شد.');
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Client\BankCardStoreRequest;
|
||||
use App\Http\Requests\Client\WalletDepositRequest;
|
||||
use App\Http\Requests\Client\WithdrawRequestStoreRequest;
|
||||
use App\Http\Resources\Client\BankCardResource;
|
||||
use App\Http\Resources\Client\PaymentResource;
|
||||
use App\Http\Resources\Client\WalletTransactionResource;
|
||||
use App\Http\Resources\Client\WithdrawRequestResource;
|
||||
use App\Models\BankCard;
|
||||
use App\Services\Payment\ZibalPaymentService;
|
||||
use App\Services\Wallet\WalletService;
|
||||
use App\Services\Wallet\WithdrawRequestService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class WalletController extends Controller
|
||||
{
|
||||
protected WalletService $walletService;
|
||||
|
||||
protected ZibalPaymentService $zibalPaymentService;
|
||||
|
||||
protected WithdrawRequestService $withdrawRequestService;
|
||||
|
||||
public function __construct(WalletService $walletService, ZibalPaymentService $zibalPaymentService, WithdrawRequestService $withdrawRequestService)
|
||||
{
|
||||
$this->walletService = $walletService;
|
||||
$this->zibalPaymentService = $zibalPaymentService;
|
||||
$this->withdrawRequestService = $withdrawRequestService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$wallet = $this->walletService->forUser(Auth::user());
|
||||
|
||||
$transactions = $wallet->transactions()
|
||||
->with('invoice')
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->withQueryString()
|
||||
->onEachSide(1);
|
||||
|
||||
$withdrawRequests = Auth::user()->withdrawRequests()
|
||||
->with('bankCard')
|
||||
->latest()
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
return Inertia::client('Dashboard/Wallet/Index', [
|
||||
'transactions' => WalletTransactionResource::collection($transactions),
|
||||
'withdrawRequests' => WithdrawRequestResource::collection($withdrawRequests)->resolve(),
|
||||
'bankCards' => BankCardResource::collection(Auth::user()->bankCards()->latest()->get())->resolve(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deposit(WalletDepositRequest $request)
|
||||
{
|
||||
$payment = $this->zibalPaymentService->request(
|
||||
Auth::user(),
|
||||
$request->validated('amount'),
|
||||
'wallet-' . Auth::id() . '-' . now()->timestamp,
|
||||
['purpose' => PaymentPurposeEnum::WalletTopUp->value]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'payment' => new PaymentResource($payment),
|
||||
'url' => $this->zibalPaymentService->paymentUrl($payment),
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeWithdrawRequest(WithdrawRequestStoreRequest $request)
|
||||
{
|
||||
$bankCard = BankCard::findOrFail($request->validated('bank_card_id'));
|
||||
|
||||
$this->withdrawRequestService->request(Auth::user(), $request->validated('amount'), $bankCard);
|
||||
|
||||
session()->flash('success', 'درخواست برداشت شما ثبت شد و در انتظار بررسی است.');
|
||||
|
||||
return redirect()->route('dashboard.wallet.index');
|
||||
}
|
||||
|
||||
public function storeBankCard(BankCardStoreRequest $request)
|
||||
{
|
||||
Auth::user()->bankCards()->create($request->validated());
|
||||
|
||||
session()->flash('success', 'کارت بانکی با موفقیت ثبت شد.');
|
||||
|
||||
return redirect()->route('dashboard.wallet.index');
|
||||
}
|
||||
|
||||
public function destroyBankCard(BankCard $bankCard)
|
||||
{
|
||||
abort_unless($bankCard->user_id === Auth::id(), 403);
|
||||
|
||||
$bankCard->delete();
|
||||
|
||||
return redirect()->route('dashboard.wallet.index');
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,10 @@ class PetRequest extends FormRequest
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$this->merge([
|
||||
'weight' => PersianToEnglish::toEnglish($this->weight),
|
||||
]);
|
||||
if ($this->filled('weight')) {
|
||||
$this->merge([
|
||||
'weight' => PersianToEnglish::toEnglish($this->weight),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Enums\ShiftDayEnum;
|
||||
use App\Enums\UserEnum;
|
||||
use App\Services\Shift\ShiftScheduleService;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class ShiftRequest 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 [
|
||||
'user_id' => ['nullable', Rule::exists('users', 'id')->where('level', UserEnum::Doctor->value)],
|
||||
'day_of_week' => ['nullable', 'required_without:date', 'string', Rule::in(ShiftDayEnum::values())],
|
||||
'date' => ['nullable', 'required_without:day_of_week', 'date'],
|
||||
'start_time' => ['required', 'date_format:H:i'],
|
||||
'end_time' => ['required', 'date_format:H:i', 'different:start_time'],
|
||||
'is_active' => ['required', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function prepareForValidation()
|
||||
{
|
||||
if ($this->filled('date')) {
|
||||
$this->merge(['day_of_week' => null]);
|
||||
}
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) {
|
||||
if ($validator->errors()->isNotEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scheduleService = app(ShiftScheduleService::class);
|
||||
$ignoreId = $this->route('shift')?->id;
|
||||
|
||||
if ($scheduleService->hasConflict($this->validated(), $ignoreId)) {
|
||||
$validator->errors()->add('start_time', 'این شیفت با شیفت دیگری از این پزشک تداخل دارد.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class UserRequest extends FormRequest
|
||||
'phone' => ['required', 'max:255', new PhoneNumber],
|
||||
'fullname' => 'nullable | max:255',
|
||||
'role' => ['nullable', 'array', 'exists:roles,id'],
|
||||
'profile_url' => ['nullable', 'exists:medium,url'],
|
||||
'profile_url' => ['nullable', 'exists:media,url'],
|
||||
'email' => ['nullable', 'email', 'max:255',
|
||||
isset($toBeEditedUser)
|
||||
? Rule::unique('users')->ignore($toBeEditedUser->id)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class WithdrawRejectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BankCardStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'card_number' => ['required', 'digits:16'],
|
||||
'holder_name' => ['required', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CaseRejectRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'reason' => ['required', 'string', 'max:1000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use App\Enums\DocumentModalityEnum;
|
||||
use App\Enums\DocumentPriorityEnum;
|
||||
use App\Enums\PetGenderEnum;
|
||||
use App\Enums\PetTypeEnum;
|
||||
use App\Enums\UserEnum;
|
||||
|
||||
class FastRequestRequest extends FormRequest
|
||||
{
|
||||
@@ -22,7 +23,7 @@ class FastRequestRequest extends FormRequest
|
||||
'modality' => ['required', 'string', Rule::in(DocumentModalityEnum::values())],
|
||||
'priority' => ['required', 'string', Rule::in(DocumentPriorityEnum::keys())],
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
'pet_id' => ['nullable', 'exists:pets,id'],
|
||||
'pet_id' => ['nullable', Rule::exists('pets', 'id')->where('owner_id', $this->user()->id)],
|
||||
'pet' => ['required_without:pet_id', 'array'],
|
||||
'pet.name' => ['required_without:pet_id', 'nullable', 'string', 'max:255'],
|
||||
'pet.type' => ['required_without:pet_id', 'nullable', 'string', Rule::in(PetTypeEnum::keys())],
|
||||
@@ -31,7 +32,7 @@ class FastRequestRequest extends FormRequest
|
||||
'pet.age' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'pet.weight' => ['nullable', 'numeric', 'min:0', 'max:1000'],
|
||||
'pet.sterilized' => ['nullable', 'boolean'],
|
||||
'doctor_id' => ['nullable', 'exists:users,id'],
|
||||
'doctor_id' => ['nullable', Rule::exists('users', 'id')->where('level', UserEnum::Doctor->value)],
|
||||
'history_files' => ['nullable', 'array'],
|
||||
'history_files.*' => ['file', 'image', 'max:10240'],
|
||||
'scan_files' => ['nullable', 'array'],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PaymentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'invoice_id' => ['required', 'integer', 'exists:invoices,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ProfileUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'fullname' => ['required', 'string', 'max:255'],
|
||||
'birthdate' => ['nullable', 'date'],
|
||||
'national_code' => ['required', 'string', 'max:20'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'province' => ['nullable', 'string', 'max:255'],
|
||||
'city' => ['nullable', 'string', 'max:255'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'avatar' => ['nullable', 'image', 'mimes:png,jpg,jpeg', 'max:10240'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ReportSubmitRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'document_id' => ['required', 'exists:documents,id'],
|
||||
'technique' => ['nullable', 'string', 'max:10000'],
|
||||
'finding' => ['required', 'string', 'max:10000'],
|
||||
'diagnose' => ['nullable', 'string', 'max:10000'],
|
||||
'recommendation' => ['nullable', 'string', 'max:10000'],
|
||||
'files' => ['nullable', 'array'],
|
||||
'files.*' => ['file', 'image', 'max:10240'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class WalletDepositRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'amount' => ['required', 'integer', 'min:10000', 'max:500000000'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Client;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class WithdrawRequestStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'amount' => ['required', 'integer', 'min:10000', 'max:500000000'],
|
||||
'bank_card_id' => ['required', 'exists:bank_cards,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ class ReportResource extends JsonResource
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'document' => $this->relationLoaded('document') ? $this->document->only(['id','code']) : null,
|
||||
'document' => $this->relationLoaded('document') ? optional($this->document)->only(['id','code']) : null,
|
||||
'status' => $this->status,
|
||||
'body' => $this->body,
|
||||
'doctor' => $this->relationLoaded('doctor') ? $this->doctor->only(['id','fullname']) : null,
|
||||
'doctor' => $this->relationLoaded('doctor') ? optional($this->doctor)->only(['id','fullname']) : null,
|
||||
'submitted_at' => verta($this->submitted_at)->format('%Y/%m/%d - H:i'),
|
||||
'submitted_at_form' => $this->submitted_at,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Admin;
|
||||
|
||||
use App\Enums\ShiftDayEnum;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ShiftResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'user_id' => $this->user_id,
|
||||
'doctor' => $this->relationLoaded('doctor') && $this->doctor ? $this->doctor->only(['id', 'fullname']) : null,
|
||||
'day_of_week' => $this->day_of_week,
|
||||
'day_index' => $this->day_of_week ? ShiftDayEnum::from($this->day_of_week)->carbonDay() : null,
|
||||
'date' => $this->date ? verta($this->date)->format('%Y/%m/%d') : null,
|
||||
'date_raw' => $this->date?->format('Y-m-d'),
|
||||
'start_time' => substr($this->start_time, 0, 5),
|
||||
'end_time' => substr($this->end_time, 0, 5),
|
||||
'is_active' => $this->is_active,
|
||||
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WithdrawRequestResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'amount' => $this->amount,
|
||||
'status' => $this->status,
|
||||
'admin_note' => $this->admin_note,
|
||||
'user' => $this->relationLoaded('user') ? $this->user->only(['id', 'fullname', 'level', 'phone']) : null,
|
||||
'bank_card' => $this->relationLoaded('bankCard') && $this->bankCard
|
||||
? $this->bankCard->only(['id', 'card_number', 'holder_name'])
|
||||
: null,
|
||||
'reviewed_by' => $this->relationLoaded('reviewedBy') && $this->reviewedBy ? $this->reviewedBy->only(['id', 'fullname']) : null,
|
||||
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||
'reviewed_at' => $this->reviewed_at ? verta($this->reviewed_at)->format('%Y/%m/%d - H:i') : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class BankCardResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'card_number' => $this->card_number,
|
||||
'holder_name' => $this->holder_name,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class CaseDetailResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$report = $this->reports->sortByDesc('submitted_at')->first();
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'code' => $this->code,
|
||||
'status' => $this->status,
|
||||
'modality' => $this->modality,
|
||||
'priority' => $this->priority,
|
||||
'note' => $this->note,
|
||||
'rejection_reason' => $this->rejection_reason,
|
||||
'submitted_at' => $this->submitted_at ? verta($this->submitted_at)->format('%Y/%m/%d - H:i') : null,
|
||||
'processing_deadline' => $this->processing_deadline?->toIso8601String(),
|
||||
'pet' => $this->whenLoaded('pet', fn () => [
|
||||
'name' => $this->pet->name,
|
||||
'type' => $this->pet->type,
|
||||
'gender' => $this->pet->gender,
|
||||
'breed' => $this->pet->breed,
|
||||
'age' => $this->pet->birthday ? (int) $this->pet->birthday->diffInYears(now()) : null,
|
||||
'weight' => $this->pet->weight,
|
||||
'sterilized' => $this->pet->sterilized,
|
||||
]),
|
||||
'requester' => $this->whenLoaded('requester', fn () => [
|
||||
'level' => $this->requester->level,
|
||||
'fullname' => $this->requester->fullname,
|
||||
'phone' => $this->requester->phone,
|
||||
'specialty' => $this->requester->specialty,
|
||||
'clinic_name' => $this->requester->clinic_name,
|
||||
'clinic_phone' => $this->requester->clinic_phone,
|
||||
'province' => $this->requester->province,
|
||||
'city' => $this->requester->city,
|
||||
]),
|
||||
'primary_files' => $this->mediaFiles('primary'),
|
||||
'history_files' => $this->mediaFiles('history'),
|
||||
'report' => $report ? [
|
||||
'id' => $report->id,
|
||||
'status' => $report->status,
|
||||
'technique' => $report->technique,
|
||||
'finding' => $report->finding,
|
||||
'diagnose' => $report->diagnose,
|
||||
'recommendation' => $report->recommendation,
|
||||
'submitted_at' => $report->submitted_at ? verta($report->submitted_at)->format('%Y/%m/%d - H:i') : null,
|
||||
'attachments' => $report->attachments->map(fn ($medium) => $this->mediumPayload($medium))->values(),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function mediaFiles(string $collection): array
|
||||
{
|
||||
return $this->media
|
||||
->filter(fn ($medium) => $medium->pivot->collection === $collection)
|
||||
->map(fn ($medium) => $this->mediumPayload($medium))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
protected function mediumPayload($medium): array
|
||||
{
|
||||
return [
|
||||
'id' => $medium->id,
|
||||
'filename' => $medium->filename,
|
||||
'url' => $medium->isPrivate() ? route('dashboard.media.download', $medium) : $medium->url,
|
||||
'mime_type' => $medium->mime_type,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class FastRequestResource extends JsonResource
|
||||
'status' => $this->status,
|
||||
'priority' => $this->priority,
|
||||
'pet' => new PetArchiveResource($this->whenLoaded('pet')),
|
||||
'invoice' => new InvoiceResource($this->whenLoaded('invoice')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class InvoiceResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'number' => $this->number,
|
||||
'amount' => $this->amount,
|
||||
'status' => $this->status,
|
||||
'payment_method' => $this->payment_method,
|
||||
'paid_at' => $this->paid_at,
|
||||
'created_at' => $this->created_at,
|
||||
'document' => $this->whenLoaded('document', fn () => [
|
||||
'id' => $this->document->id,
|
||||
'code' => $this->document->code,
|
||||
'modality' => $this->document->modality,
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PaymentResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'track_id' => $this->track_id,
|
||||
'amount' => $this->amount,
|
||||
'status' => $this->status,
|
||||
'ref_number' => $this->ref_number,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ReportResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'status' => $this->status,
|
||||
'submitted_at' => $this->submitted_at ? verta($this->submitted_at)->format('%Y/%m/%d - H:i') : null,
|
||||
'document' => $this->whenLoaded('document', fn () => [
|
||||
'id' => $this->document->id,
|
||||
'code' => $this->document->code,
|
||||
'modality' => $this->document->modality,
|
||||
'status' => $this->document->status,
|
||||
]),
|
||||
'doctor' => $this->whenLoaded('doctor', fn () => optional($this->doctor)->fullname),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use App\Enums\ShiftDayEnum;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ShiftResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'day_of_week' => $this->day_of_week,
|
||||
'day_index' => $this->day_of_week ? ShiftDayEnum::from($this->day_of_week)->carbonDay() : null,
|
||||
'date' => $this->date ? verta($this->date)->format('%Y/%m/%d') : null,
|
||||
'date_raw' => $this->date?->format('Y-m-d'),
|
||||
'start_time' => substr($this->start_time, 0, 5),
|
||||
'end_time' => substr($this->end_time, 0, 5),
|
||||
'is_active' => $this->is_active,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WalletTransactionResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'reason' => $this->reason,
|
||||
'amount' => $this->amount,
|
||||
'balance_after' => $this->balance_after,
|
||||
'description' => $this->description,
|
||||
'via_gateway' => $this->payment_id !== null,
|
||||
'invoice_number' => $this->whenLoaded('invoice', fn () => $this->invoice?->number),
|
||||
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources\Client;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class WithdrawRequestResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'amount' => $this->amount,
|
||||
'status' => $this->status,
|
||||
'admin_note' => $this->admin_note,
|
||||
'bank_card' => $this->relationLoaded('bankCard') && $this->bankCard
|
||||
? $this->bankCard->only(['id', 'card_number', 'holder_name'])
|
||||
: null,
|
||||
'created_at' => verta($this->created_at)->format('%Y/%m/%d - H:i'),
|
||||
'reviewed_at' => $this->reviewed_at ? verta($this->reviewed_at)->format('%Y/%m/%d - H:i') : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Services\Sms\IranPayamakService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
|
||||
class SendPatternSms implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public array $backoff = [60, 300, 900];
|
||||
|
||||
public function __construct(
|
||||
protected string $phone,
|
||||
protected string $patternKey,
|
||||
protected array $attributes
|
||||
) {
|
||||
$this->afterCommit();
|
||||
}
|
||||
|
||||
public function handle(IranPayamakService $iranPayamakService): void
|
||||
{
|
||||
$iranPayamakService->sendPattern($this->phone, $this->patternKey, $this->attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BankCard extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'card_number',
|
||||
'holder_name',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,11 @@ class Document extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->hasOne(Invoice::class);
|
||||
}
|
||||
|
||||
public function assignments()
|
||||
{
|
||||
return $this->hasMany(DocumentAssignment::class);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Invoice extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'number',
|
||||
'amount',
|
||||
'status',
|
||||
'payment_method',
|
||||
'paid_at',
|
||||
'document_id',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => InvoiceStatusEnum::class,
|
||||
'payment_method' => InvoicePaymentMethodEnum::class,
|
||||
'amount' => 'integer',
|
||||
'paid_at' => 'datetime',
|
||||
'document_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
public function document()
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function payments()
|
||||
{
|
||||
return $this->hasMany(Payment::class);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OtpCode extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'phone',
|
||||
'code',
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Payment extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'track_id',
|
||||
'amount',
|
||||
'status',
|
||||
'purpose',
|
||||
'ref_number',
|
||||
'verified_at',
|
||||
'invoice_id',
|
||||
'payer_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => PaymentStatusEnum::class,
|
||||
'purpose' => PaymentPurposeEnum::class,
|
||||
'amount' => 'integer',
|
||||
'verified_at' => 'datetime',
|
||||
'invoice_id' => 'integer',
|
||||
'payer_id' => 'integer',
|
||||
];
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function payer()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'payer_id');
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ class Report extends Model
|
||||
'medium_id',
|
||||
'status',
|
||||
'body',
|
||||
'technique',
|
||||
'finding',
|
||||
'diagnose',
|
||||
'recommendation',
|
||||
'submitted_at',
|
||||
];
|
||||
|
||||
@@ -43,7 +47,7 @@ class Report extends Model
|
||||
'media_assignments',
|
||||
'mediable_id',
|
||||
'media_id'
|
||||
)->withPivot('collection')->withTimestamps();
|
||||
)->withPivot('collection');
|
||||
}
|
||||
|
||||
public function medium()
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Shift extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'day_of_week',
|
||||
'date',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'is_active',
|
||||
'user_id',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'is_active' => 'boolean',
|
||||
'user_id' => 'integer',
|
||||
'created_by' => 'integer',
|
||||
];
|
||||
|
||||
public function doctor()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOpen($query)
|
||||
{
|
||||
return $query->whereNull('user_id');
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class User extends Authenticatable
|
||||
protected $fillable = [
|
||||
'username',
|
||||
'fullname',
|
||||
'birthdate',
|
||||
'national_code',
|
||||
'level',
|
||||
'email',
|
||||
@@ -56,6 +57,7 @@ class User extends Authenticatable
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'birthdate' => 'date:Y-m-d',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
@@ -71,4 +73,29 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->hasMany(Pet::class, 'owner_id');
|
||||
}
|
||||
|
||||
public function shifts()
|
||||
{
|
||||
return $this->hasMany(Shift::class);
|
||||
}
|
||||
|
||||
public function wallet()
|
||||
{
|
||||
return $this->hasOne(Wallet::class);
|
||||
}
|
||||
|
||||
public function invoices()
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
|
||||
public function bankCards()
|
||||
{
|
||||
return $this->hasMany(BankCard::class);
|
||||
}
|
||||
|
||||
public function withdrawRequests()
|
||||
{
|
||||
return $this->hasMany(WithdrawRequest::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Wallet extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'balance',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'balance' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(WalletTransaction::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Enums\WalletTransactionTypeEnum;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WalletTransaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'type',
|
||||
'reason',
|
||||
'amount',
|
||||
'balance_after',
|
||||
'description',
|
||||
'wallet_id',
|
||||
'invoice_id',
|
||||
'payment_id',
|
||||
'withdraw_request_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'type' => WalletTransactionTypeEnum::class,
|
||||
'reason' => WalletTransactionReasonEnum::class,
|
||||
'amount' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
'wallet_id' => 'integer',
|
||||
'invoice_id' => 'integer',
|
||||
'payment_id' => 'integer',
|
||||
'withdraw_request_id' => 'integer',
|
||||
];
|
||||
|
||||
public function wallet()
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
return $this->belongsTo(Payment::class);
|
||||
}
|
||||
|
||||
public function withdrawRequest()
|
||||
{
|
||||
return $this->belongsTo(WithdrawRequest::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\WithdrawRequestStatusEnum;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WithdrawRequest extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'amount',
|
||||
'status',
|
||||
'admin_note',
|
||||
'reviewed_at',
|
||||
'reviewed_by',
|
||||
'user_id',
|
||||
'bank_card_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => WithdrawRequestStatusEnum::class,
|
||||
'amount' => 'integer',
|
||||
'reviewed_at' => 'datetime',
|
||||
'reviewed_by' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'bank_card_id' => 'integer',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function bankCard()
|
||||
{
|
||||
return $this->belongsTo(BankCard::class);
|
||||
}
|
||||
|
||||
public function reviewedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reviewed_by');
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
return [
|
||||
'User' => $user,
|
||||
'permissions' => $permissions,
|
||||
'wallet_balance' => (int) ($user?->wallet?->balance ?? 0),
|
||||
];
|
||||
},
|
||||
]);
|
||||
@@ -52,6 +53,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
return [
|
||||
'success' => Session::get('success'),
|
||||
'error' => Session::get('error'),
|
||||
'payment_result' => Session::get('payment_result'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Enums\UserEnum;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function ($user = null) {
|
||||
return $user?->level === UserEnum::Admin->value;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class AuthService
|
||||
|
||||
Auth::login($user);
|
||||
session()->regenerate();
|
||||
session()->forget('register_phone' , 'verified_phone' , 'otp_verified' , 'login_phone' , 'login_level' , 'available_levels');
|
||||
session()->forget(['register_phone', 'verified_phone', 'otp_verified', 'login_phone', 'login_level', 'available_levels']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\OtpCode;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Services\Sms\IranPayamakService;
|
||||
|
||||
class OtpService
|
||||
{
|
||||
protected IranPayamakService $iranPayamakService;
|
||||
|
||||
public function __construct(IranPayamakService $iranPayamakService)
|
||||
{
|
||||
$this->iranPayamakService = $iranPayamakService;
|
||||
}
|
||||
|
||||
public function generateCode(): string
|
||||
{
|
||||
return (string) random_int(1000, 9999);
|
||||
@@ -45,51 +52,14 @@ class OtpService
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function sendSms(string $phone, string $code): array
|
||||
private function sendSms(string $phone, string $code): void
|
||||
{
|
||||
try {
|
||||
$formattedRecipient = $this->formatPhoneForSms($phone);
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
'Api-Key' => config('services.iranpayamak.api_key'),
|
||||
])
|
||||
->post(config('services.iranpayamak.api_url'), [
|
||||
'code' => config('services.iranpayamak.patterns.verify_code'),
|
||||
'attributes' => [
|
||||
'otp' => $code,
|
||||
'status' => 'بازنشانی رمز عبور'
|
||||
],
|
||||
'recipient' => $formattedRecipient,
|
||||
'line_number' => config('services.iranpayamak.line_number'),
|
||||
'number_format' => 'english',
|
||||
$this->iranPayamakService->sendPattern($phone, 'verify_code', [
|
||||
'otp' => $code,
|
||||
'status' => 'بازنشانی رمز عبور',
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json();
|
||||
} else {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'SMS sending failed: ' . $response->status(),
|
||||
'data' => $response->body()
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'SMS sending exception: ' . $e->getMessage(),
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function formatPhoneForSms(string $phone): string
|
||||
{
|
||||
if (str_starts_with($phone, '+98')) {
|
||||
return '0' . substr($phone, 3);
|
||||
}
|
||||
return $phone;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class RegistrationService
|
||||
|
||||
Auth::login($user);
|
||||
session()->regenerate();
|
||||
session()->forget('register_phone' , 'verified_phone' , 'otp_verified' , 'login_phone' , 'login_level' , 'available_levels');
|
||||
session()->forget(['register_phone', 'verified_phone', 'otp_verified', 'login_phone', 'login_level', 'available_levels']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -3,25 +3,183 @@
|
||||
namespace App\Services\Document;
|
||||
|
||||
use App\Enums\DocumentStatusEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Enums\MediaCollectionEnum;
|
||||
use App\Enums\ReportStatusEnum;
|
||||
use App\Enums\UserEnum;
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Models\Document;
|
||||
use App\Models\Medium;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use App\Services\FileStorageService;
|
||||
use App\Services\Shift\ShiftScheduleService;
|
||||
use App\Services\Sms\SmsNotificationService;
|
||||
use App\Services\Wallet\WalletService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class CaseService
|
||||
{
|
||||
protected FileStorageService $fileStorageService;
|
||||
|
||||
protected WalletService $walletService;
|
||||
|
||||
protected ShiftScheduleService $shiftScheduleService;
|
||||
|
||||
protected SmsNotificationService $smsNotificationService;
|
||||
|
||||
public function __construct(FileStorageService $fileStorageService, WalletService $walletService, ShiftScheduleService $shiftScheduleService, SmsNotificationService $smsNotificationService)
|
||||
{
|
||||
$this->fileStorageService = $fileStorageService;
|
||||
$this->walletService = $walletService;
|
||||
$this->shiftScheduleService = $shiftScheduleService;
|
||||
$this->smsNotificationService = $smsNotificationService;
|
||||
}
|
||||
|
||||
public function pendingForUser(User $user): Builder
|
||||
{
|
||||
$statuses = $this->isDoctor($user)
|
||||
? [DocumentStatusEnum::InProgress->value]
|
||||
: [DocumentStatusEnum::Registered->value, DocumentStatusEnum::InProgress->value];
|
||||
|
||||
return Document::with(['pet', 'requester'])
|
||||
->whereIn('status', [
|
||||
DocumentStatusEnum::Registered->value,
|
||||
DocumentStatusEnum::InProgress->value,
|
||||
])
|
||||
->where(function ($query) use ($user) {
|
||||
$query->where('requester_id', $user->id)
|
||||
->orWhereHas('assignments', function ($assignmentQuery) use ($user) {
|
||||
$assignmentQuery->where('user_id', $user->id);
|
||||
});
|
||||
})
|
||||
->whereIn('status', $statuses)
|
||||
->where(fn ($query) => $this->scopeVisibleTo($query, $user))
|
||||
->latest('submitted_at');
|
||||
}
|
||||
|
||||
public function canAccess(Document $document, User $user): bool
|
||||
{
|
||||
return $document->requester_id === $user->id
|
||||
|| $document->assignments()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function canAccessMedium(Medium $medium, User $user): bool
|
||||
{
|
||||
$documents = $medium->documents()->get()
|
||||
->merge($medium->reports()->with('document')->get()->pluck('document'));
|
||||
|
||||
return $documents->contains(fn ($document) => $this->canAccess($document, $user));
|
||||
}
|
||||
|
||||
public function canReport(Document $document, User $user): bool
|
||||
{
|
||||
return $this->isDoctor($user)
|
||||
&& $document->status === DocumentStatusEnum::InProgress
|
||||
&& $document->assignments()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
public function submitReport(Document $document, User $doctor, array $data, array $files = []): Report
|
||||
{
|
||||
return DB::transaction(function () use ($document, $doctor, $data, $files) {
|
||||
if (!$this->canReport($document, $doctor)) {
|
||||
throw ValidationException::withMessages(['document' => 'امکان ثبت گزارش برای این پرونده وجود ندارد.']);
|
||||
}
|
||||
|
||||
$report = $document->reports()->create([
|
||||
'doctor_id' => $doctor->id,
|
||||
'status' => ReportStatusEnum::Approved->value,
|
||||
'technique' => $data['technique'] ?? null,
|
||||
'finding' => $data['finding'] ?? null,
|
||||
'diagnose' => $data['diagnose'] ?? null,
|
||||
'recommendation' => $data['recommendation'] ?? null,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$medium = $this->fileStorageService->storeFile($file);
|
||||
$report->media()->attach($medium->id, ['collection' => MediaCollectionEnum::Attachments->value]);
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'status' => DocumentStatusEnum::Completed->value,
|
||||
'finalized_at' => now(),
|
||||
'finalized_by' => $doctor->id,
|
||||
]);
|
||||
|
||||
$this->smsNotificationService->reportReady($document);
|
||||
|
||||
return $report;
|
||||
});
|
||||
}
|
||||
|
||||
public function reject(Document $document, User $doctor, string $reason): Document
|
||||
{
|
||||
return DB::transaction(function () use ($document, $doctor, $reason) {
|
||||
if (!$this->canReport($document, $doctor)) {
|
||||
throw ValidationException::withMessages(['document' => 'امکان رد این پرونده وجود ندارد.']);
|
||||
}
|
||||
|
||||
$document->update([
|
||||
'status' => DocumentStatusEnum::Rejected->value,
|
||||
'finalized_at' => now(),
|
||||
'finalized_by' => $doctor->id,
|
||||
'rejection_reason' => $reason,
|
||||
]);
|
||||
|
||||
$invoice = $document->invoice;
|
||||
|
||||
if ($invoice && $invoice->status === InvoiceStatusEnum::Paid) {
|
||||
$invoice->update(['status' => InvoiceStatusEnum::Canceled->value]);
|
||||
$this->walletService->deposit($document->requester, $invoice->amount, WalletTransactionReasonEnum::Refund);
|
||||
}
|
||||
|
||||
return $document;
|
||||
});
|
||||
}
|
||||
|
||||
public function statsForUser(User $user): array
|
||||
{
|
||||
$monthStart = now()->startOfMonth();
|
||||
$isDoctor = $this->isDoctor($user);
|
||||
|
||||
$reviewedQuery = Document::where('status', DocumentStatusEnum::Completed->value)
|
||||
->where('finalized_at', '>=', $monthStart);
|
||||
|
||||
if ($isDoctor) {
|
||||
$reviewedQuery->where('finalized_by', $user->id);
|
||||
} else {
|
||||
$reviewedQuery->where('requester_id', $user->id);
|
||||
}
|
||||
|
||||
$amountQuery = $isDoctor
|
||||
? Document::where('documents.finalized_by', $user->id)
|
||||
->where('documents.status', DocumentStatusEnum::Completed->value)
|
||||
->where('documents.finalized_at', '>=', $monthStart)
|
||||
->join('invoices', 'invoices.document_id', '=', 'documents.id')
|
||||
->where('invoices.status', InvoiceStatusEnum::Paid->value)
|
||||
: Document::where('documents.requester_id', $user->id)
|
||||
->join('invoices', 'invoices.document_id', '=', 'documents.id')
|
||||
->where('invoices.status', InvoiceStatusEnum::Paid->value)
|
||||
->where('invoices.paid_at', '>=', $monthStart);
|
||||
|
||||
$nextShift = $isDoctor ? $this->shiftScheduleService->nextShiftStart($user, now()) : null;
|
||||
$onShift = $isDoctor && $this->shiftScheduleService->isOnShift($user, now());
|
||||
|
||||
return [
|
||||
'pending_cases' => $this->pendingForUser($user)->count(),
|
||||
'monthly_reviewed' => $reviewedQuery->count(),
|
||||
'monthly_amount' => (int) $amountQuery->sum('invoices.amount'),
|
||||
'next_shift_hours' => $onShift ? 0 : ($nextShift ? (int) ceil(now()->diffInMinutes($nextShift, true) / 60) : null),
|
||||
'is_doctor' => $isDoctor,
|
||||
];
|
||||
}
|
||||
|
||||
protected function scopeVisibleTo($query, User $user): void
|
||||
{
|
||||
if ($this->isDoctor($user)) {
|
||||
$query->whereHas('assignments', fn ($assignmentQuery) => $assignmentQuery->where('user_id', $user->id));
|
||||
return;
|
||||
}
|
||||
|
||||
$query->where('requester_id', $user->id)
|
||||
->orWhereHas('assignments', fn ($assignmentQuery) => $assignmentQuery->where('user_id', $user->id));
|
||||
}
|
||||
|
||||
public function isDoctor(User $user): bool
|
||||
{
|
||||
return $user->level === UserEnum::Doctor->value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,27 @@ use App\Models\Document;
|
||||
use App\Models\Pet;
|
||||
use App\Models\User;
|
||||
use App\Services\FileStorageService;
|
||||
use App\Services\Invoice\InvoiceService;
|
||||
use App\Services\Shift\ShiftScheduleService;
|
||||
use App\Services\Sms\SmsNotificationService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FastRequestService
|
||||
{
|
||||
protected FileStorageService $fileStorageService;
|
||||
|
||||
public function __construct(FileStorageService $fileStorageService)
|
||||
protected ShiftScheduleService $shiftScheduleService;
|
||||
|
||||
protected InvoiceService $invoiceService;
|
||||
|
||||
protected SmsNotificationService $smsNotificationService;
|
||||
|
||||
public function __construct(FileStorageService $fileStorageService, ShiftScheduleService $shiftScheduleService, InvoiceService $invoiceService, SmsNotificationService $smsNotificationService)
|
||||
{
|
||||
$this->fileStorageService = $fileStorageService;
|
||||
$this->shiftScheduleService = $shiftScheduleService;
|
||||
$this->invoiceService = $invoiceService;
|
||||
$this->smsNotificationService = $smsNotificationService;
|
||||
}
|
||||
|
||||
public function create(User $requester, array $data, array $historyFiles = [], array $scanFiles = []): Document
|
||||
@@ -35,16 +47,23 @@ class FastRequestService
|
||||
'requester_id' => $requester->id,
|
||||
]);
|
||||
|
||||
$this->invoiceService->createForDocument($document);
|
||||
|
||||
$this->attachFiles($document, $historyFiles, MediaCollectionEnum::History);
|
||||
$this->attachFiles($document, $scanFiles, MediaCollectionEnum::Primary);
|
||||
|
||||
if (!empty($data['doctor_id'])) {
|
||||
$doctorId = $data['doctor_id']
|
||||
?? $this->shiftScheduleService->autoSelectDoctor(now())?->id;
|
||||
|
||||
if (!empty($doctorId)) {
|
||||
$document->assignments()->create([
|
||||
'user_id' => $data['doctor_id'],
|
||||
'user_id' => $doctorId,
|
||||
'role' => 'رادیولوژیست',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->smsNotificationService->requestRegistered($document);
|
||||
|
||||
return $document;
|
||||
});
|
||||
}
|
||||
@@ -52,7 +71,7 @@ class FastRequestService
|
||||
protected function resolvePet(User $requester, array $data): Pet
|
||||
{
|
||||
if (!empty($data['pet_id'])) {
|
||||
return Pet::findOrFail($data['pet_id']);
|
||||
return $requester->pets()->findOrFail($data['pet_id']);
|
||||
}
|
||||
|
||||
$pet = $data['pet'];
|
||||
|
||||
@@ -49,7 +49,7 @@ class FileStorageService
|
||||
$new = Storage::disk($newDisk);
|
||||
|
||||
$stream = $old->readStream($medium->path);
|
||||
if ($stream === false) return false;
|
||||
if (! is_resource($stream)) return false;
|
||||
|
||||
$new->put($medium->path, $stream);
|
||||
if (is_resource($stream)) fclose($stream);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Invoice;
|
||||
|
||||
use App\Enums\DocumentModalityEnum;
|
||||
use App\Enums\DocumentStatusEnum;
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Models\Document;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\Sms\SmsNotificationService;
|
||||
|
||||
class InvoiceService
|
||||
{
|
||||
protected SmsNotificationService $smsNotificationService;
|
||||
|
||||
public function __construct(SmsNotificationService $smsNotificationService)
|
||||
{
|
||||
$this->smsNotificationService = $smsNotificationService;
|
||||
}
|
||||
|
||||
public function createForDocument(Document $document): Invoice
|
||||
{
|
||||
$invoice = Invoice::create([
|
||||
'number' => 'pending',
|
||||
'amount' => DocumentModalityEnum::from($document->modality)->price(),
|
||||
'status' => InvoiceStatusEnum::Pending->value,
|
||||
'document_id' => $document->id,
|
||||
'user_id' => $document->requester_id,
|
||||
]);
|
||||
|
||||
$invoice->update(['number' => str_pad((string) $invoice->id, 5, '0', STR_PAD_LEFT)]);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
public function markPaid(Invoice $invoice, InvoicePaymentMethodEnum $method): Invoice
|
||||
{
|
||||
$invoice->update([
|
||||
'status' => InvoiceStatusEnum::Paid->value,
|
||||
'payment_method' => $method->value,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
|
||||
$document = $invoice->document;
|
||||
|
||||
if ($document && $document->status === DocumentStatusEnum::Registered) {
|
||||
$document->update([
|
||||
'status' => DocumentStatusEnum::InProgress->value,
|
||||
'processing_deadline' => now()->addHours($document->priority->processingHours()),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->smsNotificationService->paymentConfirmed($invoice);
|
||||
|
||||
return $invoice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\Payment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
class ZibalPaymentService
|
||||
{
|
||||
protected const API_URL = 'https://gateway.zibal.ir/v1';
|
||||
protected const START_URL = 'https://gateway.zibal.ir/start';
|
||||
|
||||
public function request(User $payer, int $amount, string $orderId, array $attributes = []): Payment
|
||||
{
|
||||
$response = Http::post(self::API_URL . '/request', [
|
||||
'merchant' => $this->merchant(),
|
||||
'amount' => $amount * 10,
|
||||
'callbackUrl' => $this->callbackUrl(),
|
||||
'orderId' => $orderId,
|
||||
])->json();
|
||||
|
||||
if (($response['result'] ?? null) !== 100) {
|
||||
throw new RuntimeException('خطا در ایجاد درخواست پرداخت: ' . ($response['message'] ?? 'پاسخ نامعتبر از درگاه'));
|
||||
}
|
||||
|
||||
return Payment::create(array_merge([
|
||||
'track_id' => (string) $response['trackId'],
|
||||
'amount' => $amount,
|
||||
'status' => PaymentStatusEnum::Pending->value,
|
||||
'payer_id' => $payer->id,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
public function paymentUrl(Payment $payment): string
|
||||
{
|
||||
return self::START_URL . '/' . $payment->track_id;
|
||||
}
|
||||
|
||||
public function verify(Payment $payment): Payment
|
||||
{
|
||||
if ($payment->status !== PaymentStatusEnum::Pending) {
|
||||
return $payment;
|
||||
}
|
||||
|
||||
$response = Http::post(self::API_URL . '/verify', [
|
||||
'merchant' => $this->merchant(),
|
||||
'trackId' => $payment->track_id,
|
||||
])->json();
|
||||
|
||||
if (in_array($response['result'] ?? null, [100, 201], true)) {
|
||||
$payment->update([
|
||||
'status' => PaymentStatusEnum::Paid->value,
|
||||
'ref_number' => $response['refNumber'] ?? null,
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$payment->update(['status' => PaymentStatusEnum::Failed->value]);
|
||||
}
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
public function markFailed(Payment $payment): Payment
|
||||
{
|
||||
if ($payment->status === PaymentStatusEnum::Pending) {
|
||||
$payment->update(['status' => PaymentStatusEnum::Failed->value]);
|
||||
}
|
||||
|
||||
return $payment;
|
||||
}
|
||||
|
||||
protected function merchant(): string
|
||||
{
|
||||
return config('services.zibal.sandbox') ? 'zibal' : config('services.zibal.merchant');
|
||||
}
|
||||
|
||||
protected function callbackUrl(): string
|
||||
{
|
||||
return config('services.zibal.callback_url') ?: route('dashboard.payments.callback');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Shift;
|
||||
|
||||
use App\Enums\DocumentStatusEnum;
|
||||
use App\Enums\ShiftDayEnum;
|
||||
use App\Enums\UserEnum;
|
||||
use App\Models\Shift;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ShiftScheduleService
|
||||
{
|
||||
public function shiftsCoveringMoment(Carbon $moment): Collection
|
||||
{
|
||||
$minutes = $moment->hour * 60 + $moment->minute;
|
||||
$today = ShiftDayEnum::fromDate($moment)->value;
|
||||
$yesterday = ShiftDayEnum::fromDate($moment->copy()->subDay())->value;
|
||||
|
||||
return Shift::active()
|
||||
->where(function ($query) use ($moment, $today, $yesterday) {
|
||||
$query->whereDate('date', $moment->toDateString())
|
||||
->orWhereDate('date', $moment->copy()->subDay()->toDateString())
|
||||
->orWhere(function ($subQuery) use ($today, $yesterday) {
|
||||
$subQuery->whereNull('date')->whereIn('day_of_week', [$today, $yesterday]);
|
||||
});
|
||||
})
|
||||
->get()
|
||||
->filter(function (Shift $shift) use ($moment, $minutes) {
|
||||
$start = $this->toMinutes($shift->start_time);
|
||||
$end = $this->toMinutes($shift->end_time);
|
||||
|
||||
if ($end > $start) {
|
||||
return $this->coversDate($shift, $moment) && $minutes >= $start && $minutes < $end;
|
||||
}
|
||||
|
||||
// overnight shift: covers [start, 24:00) on its day and [00:00, end) on the next day
|
||||
if ($this->coversDate($shift, $moment) && $minutes >= $start) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->coversDate($shift, $moment->copy()->subDay()) && $minutes < $end;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
public function availableDoctorsAt(Carbon $moment): Collection
|
||||
{
|
||||
$doctorIds = $this->shiftsCoveringMoment($moment)->pluck('user_id')->filter()->unique();
|
||||
|
||||
if ($doctorIds->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return User::where('level', UserEnum::Doctor->value)
|
||||
->whereIn('id', $doctorIds)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function isOnShift(User $doctor, Carbon $moment): bool
|
||||
{
|
||||
return $this->shiftsCoveringMoment($moment)->contains('user_id', $doctor->id);
|
||||
}
|
||||
|
||||
public function autoSelectDoctor(Carbon $moment): ?User
|
||||
{
|
||||
$doctorIds = $this->shiftsCoveringMoment($moment)->pluck('user_id')->filter()->unique();
|
||||
|
||||
if ($doctorIds->isEmpty()) {
|
||||
return $this->doctorWithNearestShift($moment);
|
||||
}
|
||||
|
||||
return User::where('level', UserEnum::Doctor->value)
|
||||
->whereIn('id', $doctorIds)
|
||||
->withCount(['documents as open_documents_count' => function ($query) {
|
||||
$query->whereIn('status', [
|
||||
DocumentStatusEnum::Registered->value,
|
||||
DocumentStatusEnum::InProgress->value,
|
||||
]);
|
||||
}])
|
||||
->orderBy('open_documents_count')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
// fallback when nobody is on shift: the doctor whose next shift starts soonest
|
||||
public function doctorWithNearestShift(Carbon $from): ?User
|
||||
{
|
||||
$doctorIds = Shift::active()->whereNotNull('user_id')->distinct()->pluck('user_id');
|
||||
|
||||
return User::where('level', UserEnum::Doctor->value)
|
||||
->whereIn('id', $doctorIds)
|
||||
->get()
|
||||
->map(fn (User $doctor) => ['doctor' => $doctor, 'start' => $this->nextShiftStart($doctor, $from)])
|
||||
->filter(fn (array $item) => $item['start'] !== null)
|
||||
->sortBy('start')
|
||||
->first()['doctor'] ?? null;
|
||||
}
|
||||
|
||||
public function nextShiftStart(User $doctor, Carbon $from): ?Carbon
|
||||
{
|
||||
$shifts = Shift::active()->where('user_id', $doctor->id)->get();
|
||||
|
||||
return $shifts
|
||||
->map(function (Shift $shift) use ($from) {
|
||||
for ($offset = 0; $offset <= 7; $offset++) {
|
||||
$day = $from->copy()->addDays($offset)->startOfDay();
|
||||
|
||||
if (!$this->coversDate($shift, $day)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = $day->copy()->addMinutes($this->toMinutes($shift->start_time));
|
||||
|
||||
if ($start->greaterThanOrEqualTo($from)) {
|
||||
return $start;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
->filter()
|
||||
->sort()
|
||||
->first();
|
||||
}
|
||||
|
||||
public function hasConflict(array $data, ?int $ignoreId = null): bool
|
||||
{
|
||||
if (empty($data['user_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$candidate = new Shift($data);
|
||||
|
||||
return Shift::active()
|
||||
->where('user_id', $data['user_id'])
|
||||
->when($ignoreId, fn ($query) => $query->where('id', '!=', $ignoreId))
|
||||
->get()
|
||||
->contains(fn (Shift $existing) => $this->overlaps($existing, $candidate));
|
||||
}
|
||||
|
||||
public function claim(int $shiftId, User $doctor): Shift
|
||||
{
|
||||
return DB::transaction(function () use ($shiftId, $doctor) {
|
||||
$shift = Shift::whereKey($shiftId)->lockForUpdate()->firstOrFail();
|
||||
|
||||
if (!$shift->is_active || $shift->user_id !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'shift' => 'این شیفت قابل انتخاب نیست.',
|
||||
]);
|
||||
}
|
||||
|
||||
$data = array_merge(
|
||||
$shift->only(['day_of_week', 'date', 'start_time', 'end_time']),
|
||||
['user_id' => $doctor->id]
|
||||
);
|
||||
|
||||
if ($this->hasConflict($data, $shift->id)) {
|
||||
throw ValidationException::withMessages([
|
||||
'shift' => 'این شیفت با برنامه فعلی شما تداخل دارد.',
|
||||
]);
|
||||
}
|
||||
|
||||
$shift->update(['user_id' => $doctor->id]);
|
||||
|
||||
return $shift;
|
||||
});
|
||||
}
|
||||
|
||||
public function release(Shift $shift, User $doctor): Shift
|
||||
{
|
||||
if ($shift->user_id !== $doctor->id) {
|
||||
throw ValidationException::withMessages([
|
||||
'shift' => 'شما مجاز به تغییر این شیفت نیستید.',
|
||||
]);
|
||||
}
|
||||
|
||||
$shift->update(['user_id' => null]);
|
||||
|
||||
return $shift;
|
||||
}
|
||||
|
||||
public function overlaps(Shift $first, Shift $second): bool
|
||||
{
|
||||
$byWeekday = !$first->date || !$second->date;
|
||||
|
||||
foreach ($this->daySegments($first, $byWeekday) as [$dayA, $startA, $endA]) {
|
||||
foreach ($this->daySegments($second, $byWeekday) as [$dayB, $startB, $endB]) {
|
||||
if ($dayA === $dayB && $startA < $endB && $startB < $endA) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function coversDate(Shift $shift, Carbon $date): bool
|
||||
{
|
||||
if ($shift->date) {
|
||||
return $shift->date->isSameDay($date);
|
||||
}
|
||||
|
||||
return $shift->day_of_week === ShiftDayEnum::fromDate($date)->value;
|
||||
}
|
||||
|
||||
protected function daySegments(Shift $shift, bool $byWeekday): array
|
||||
{
|
||||
$start = $this->toMinutes($shift->start_time);
|
||||
$end = $this->toMinutes($shift->end_time);
|
||||
|
||||
if ($shift->date) {
|
||||
$day = $byWeekday ? ShiftDayEnum::fromDate($shift->date)->value : $shift->date->toDateString();
|
||||
$nextDay = $byWeekday
|
||||
? ShiftDayEnum::fromDate($shift->date->copy()->addDay())->value
|
||||
: $shift->date->copy()->addDay()->toDateString();
|
||||
} else {
|
||||
$day = $shift->day_of_week;
|
||||
$nextDay = $this->nextDay($shift->day_of_week);
|
||||
}
|
||||
|
||||
if ($end > $start) {
|
||||
return [[$day, $start, $end]];
|
||||
}
|
||||
|
||||
return [[$day, $start, 1440], [$nextDay, 0, $end]];
|
||||
}
|
||||
|
||||
protected function nextDay(string $day): string
|
||||
{
|
||||
$days = ShiftDayEnum::values();
|
||||
$index = array_search($day, $days);
|
||||
|
||||
return $days[($index + 1) % count($days)];
|
||||
}
|
||||
|
||||
protected function toMinutes(string $time): int
|
||||
{
|
||||
[$hours, $minutes] = explode(':', $time);
|
||||
|
||||
return ((int) $hours) * 60 + ((int) $minutes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Sms;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
class IranPayamakService
|
||||
{
|
||||
public function sendPattern(string $phone, string $patternKey, array $attributes): array
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
'Api-Key' => config('services.iranpayamak.api_key'),
|
||||
])->post(config('services.iranpayamak.api_url'), [
|
||||
'code' => config("services.iranpayamak.patterns.{$patternKey}"),
|
||||
'attributes' => $attributes,
|
||||
'recipient' => $this->formatRecipient($phone),
|
||||
'line_number' => config('services.iranpayamak.line_number'),
|
||||
'number_format' => 'english',
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new RuntimeException('SMS sending failed: ' . $response->status() . ' ' . $response->body());
|
||||
}
|
||||
|
||||
return $response->json() ?? [];
|
||||
}
|
||||
|
||||
protected function formatRecipient(string $phone): string
|
||||
{
|
||||
if (str_starts_with($phone, '+98')) {
|
||||
return '0' . substr($phone, 3);
|
||||
}
|
||||
|
||||
return $phone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Sms;
|
||||
|
||||
use App\Jobs\SendPatternSms;
|
||||
use App\Models\Document;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\User;
|
||||
|
||||
class SmsNotificationService
|
||||
{
|
||||
public function requestRegistered(Document $document): void
|
||||
{
|
||||
$this->dispatch($document->requester, 'request_registered', [
|
||||
'code' => $document->code,
|
||||
]);
|
||||
}
|
||||
|
||||
public function paymentConfirmed(Invoice $invoice): void
|
||||
{
|
||||
$this->dispatch($invoice->user, 'payment_confirmed', [
|
||||
'invoice' => $invoice->number,
|
||||
'amount' => number_format($invoice->amount),
|
||||
]);
|
||||
}
|
||||
|
||||
public function reportReady(Document $document): void
|
||||
{
|
||||
$this->dispatch($document->requester, 'report_ready', [
|
||||
'code' => $document->code,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function dispatch(?User $user, string $patternKey, array $attributes): void
|
||||
{
|
||||
if (empty($user?->phone) || empty(config("services.iranpayamak.patterns.{$patternKey}"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
SendPatternSms::dispatch($user->phone, $patternKey, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Wallet;
|
||||
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Enums\WalletTransactionTypeEnum;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use App\Models\WithdrawRequest;
|
||||
use App\Services\Invoice\InvoiceService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
protected InvoiceService $invoiceService;
|
||||
|
||||
public function __construct(InvoiceService $invoiceService)
|
||||
{
|
||||
$this->invoiceService = $invoiceService;
|
||||
}
|
||||
|
||||
public function forUser(User $user): Wallet
|
||||
{
|
||||
return Wallet::firstOrCreate(['user_id' => $user->id], ['balance' => 0]);
|
||||
}
|
||||
|
||||
public function system(): Wallet
|
||||
{
|
||||
return Wallet::firstOrCreate(['user_id' => null], ['balance' => 0]);
|
||||
}
|
||||
|
||||
public function deposit(User $user, int $amount, WalletTransactionReasonEnum $reason, ?Payment $payment = null): WalletTransaction
|
||||
{
|
||||
return $this->depositToWallet($this->forUser($user), $amount, $reason, $payment);
|
||||
}
|
||||
|
||||
public function depositToWallet(Wallet $wallet, int $amount, WalletTransactionReasonEnum $reason, ?Payment $payment = null, ?WithdrawRequest $withdrawRequest = null): WalletTransaction
|
||||
{
|
||||
return DB::transaction(function () use ($wallet, $amount, $reason, $payment, $withdrawRequest) {
|
||||
$wallet = $this->lockedWallet($wallet);
|
||||
$wallet->update(['balance' => $wallet->balance + $amount]);
|
||||
|
||||
return $wallet->transactions()->create([
|
||||
'type' => WalletTransactionTypeEnum::Deposit->value,
|
||||
'reason' => $reason->value,
|
||||
'amount' => $amount,
|
||||
'balance_after' => $wallet->balance,
|
||||
'payment_id' => $payment?->id,
|
||||
'withdraw_request_id' => $withdrawRequest?->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function withdraw(User $user, int $amount, WalletTransactionReasonEnum $reason, ?Invoice $invoice = null, ?WithdrawRequest $withdrawRequest = null): WalletTransaction
|
||||
{
|
||||
return DB::transaction(function () use ($user, $amount, $reason, $invoice, $withdrawRequest) {
|
||||
$wallet = $this->lockedWallet($this->forUser($user));
|
||||
|
||||
if ($wallet->balance < $amount) {
|
||||
throw ValidationException::withMessages(['amount' => 'موجودی کیف پول کافی نیست']);
|
||||
}
|
||||
|
||||
$wallet->update(['balance' => $wallet->balance - $amount]);
|
||||
|
||||
return $wallet->transactions()->create([
|
||||
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||
'reason' => $reason->value,
|
||||
'amount' => $amount,
|
||||
'balance_after' => $wallet->balance,
|
||||
'invoice_id' => $invoice?->id,
|
||||
'withdraw_request_id' => $withdrawRequest?->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function payInvoice(User $user, Invoice $invoice): Invoice
|
||||
{
|
||||
return DB::transaction(function () use ($user, $invoice) {
|
||||
if ($invoice->status !== InvoiceStatusEnum::Pending) {
|
||||
throw ValidationException::withMessages(['invoice' => 'این فاکتور قابل پرداخت نیست']);
|
||||
}
|
||||
|
||||
$this->withdraw($user, $invoice->amount, WalletTransactionReasonEnum::InvoicePayment, $invoice);
|
||||
|
||||
return $this->invoiceService->markPaid($invoice, InvoicePaymentMethodEnum::Wallet);
|
||||
});
|
||||
}
|
||||
|
||||
public function recordGatewayInvoicePayment(Invoice $invoice, Payment $payment): WalletTransaction
|
||||
{
|
||||
$wallet = $this->forUser($invoice->user);
|
||||
|
||||
return $wallet->transactions()->create([
|
||||
'type' => WalletTransactionTypeEnum::Withdrawal->value,
|
||||
'reason' => WalletTransactionReasonEnum::InvoicePayment->value,
|
||||
'amount' => $invoice->amount,
|
||||
'balance_after' => $wallet->balance,
|
||||
'invoice_id' => $invoice->id,
|
||||
'payment_id' => $payment->id,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function lockedWallet(Wallet $wallet): Wallet
|
||||
{
|
||||
return Wallet::whereKey($wallet->id)->lockForUpdate()->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Wallet;
|
||||
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Enums\WithdrawRequestStatusEnum;
|
||||
use App\Models\BankCard;
|
||||
use App\Models\User;
|
||||
use App\Models\WithdrawRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WithdrawRequestService
|
||||
{
|
||||
protected WalletService $walletService;
|
||||
|
||||
public function __construct(WalletService $walletService)
|
||||
{
|
||||
$this->walletService = $walletService;
|
||||
}
|
||||
|
||||
public function request(User $user, int $amount, BankCard $bankCard): WithdrawRequest
|
||||
{
|
||||
if ($bankCard->user_id !== $user->id) {
|
||||
throw ValidationException::withMessages(['bank_card_id' => 'کارت بانکی انتخابشده معتبر نیست.']);
|
||||
}
|
||||
|
||||
$balance = $this->walletService->forUser($user)->balance;
|
||||
|
||||
$pendingAmount = WithdrawRequest::where('user_id', $user->id)
|
||||
->where('status', WithdrawRequestStatusEnum::Pending->value)
|
||||
->sum('amount');
|
||||
|
||||
if ($amount + $pendingAmount > $balance) {
|
||||
throw ValidationException::withMessages(['amount' => 'مجموع مبلغ درخواستهای برداشت از موجودی کیف پول شما بیشتر است.']);
|
||||
}
|
||||
|
||||
return WithdrawRequest::create([
|
||||
'amount' => $amount,
|
||||
'status' => WithdrawRequestStatusEnum::Pending->value,
|
||||
'user_id' => $user->id,
|
||||
'bank_card_id' => $bankCard->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function approve(WithdrawRequest $withdrawRequest, User $admin): WithdrawRequest
|
||||
{
|
||||
return DB::transaction(function () use ($withdrawRequest, $admin) {
|
||||
$this->ensurePending($withdrawRequest);
|
||||
|
||||
$this->walletService->withdraw(
|
||||
$withdrawRequest->user,
|
||||
$withdrawRequest->amount,
|
||||
WalletTransactionReasonEnum::Withdraw,
|
||||
null,
|
||||
$withdrawRequest
|
||||
);
|
||||
|
||||
$this->walletService->depositToWallet(
|
||||
$this->walletService->system(),
|
||||
$withdrawRequest->amount,
|
||||
WalletTransactionReasonEnum::Withdraw,
|
||||
null,
|
||||
$withdrawRequest
|
||||
);
|
||||
|
||||
$withdrawRequest->update([
|
||||
'status' => WithdrawRequestStatusEnum::Approved->value,
|
||||
'reviewed_at' => now(),
|
||||
'reviewed_by' => $admin->id,
|
||||
]);
|
||||
|
||||
return $withdrawRequest;
|
||||
});
|
||||
}
|
||||
|
||||
public function reject(WithdrawRequest $withdrawRequest, User $admin, ?string $note = null): WithdrawRequest
|
||||
{
|
||||
$this->ensurePending($withdrawRequest);
|
||||
|
||||
$withdrawRequest->update([
|
||||
'status' => WithdrawRequestStatusEnum::Rejected->value,
|
||||
'admin_note' => $note,
|
||||
'reviewed_at' => now(),
|
||||
'reviewed_by' => $admin->id,
|
||||
]);
|
||||
|
||||
return $withdrawRequest;
|
||||
}
|
||||
|
||||
protected function ensurePending(WithdrawRequest $withdrawRequest): void
|
||||
{
|
||||
if ($withdrawRequest->status !== WithdrawRequestStatusEnum::Pending) {
|
||||
throw ValidationException::withMessages(['withdraw_request' => 'این درخواست قبلا بررسی شده است.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"hekmatinasser/verta": "^8.5",
|
||||
"inertiajs/inertia-laravel": "^2.0",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/horizon": "^5.49",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"maatwebsite/excel": "^3.1",
|
||||
"spatie/laravel-permission": "^6.17",
|
||||
|
||||
Generated
+137
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7aaff316edab008067e96caa26142a24",
|
||||
"content-hash": "2f1cb367cfe5d850b1483797ac0810ee",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1741,6 +1741,86 @@
|
||||
},
|
||||
"time": "2025-04-08T19:58:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/horizon",
|
||||
"version": "v5.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/horizon.git",
|
||||
"reference": "6dce8a97b426be5305adad537ad4b6a99de61b31"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/horizon/zipball/6dce8a97b426be5305adad537ad4b6a99de61b31",
|
||||
"reference": "6dce8a97b426be5305adad537ad4b6a99de61b31",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"ext-pcntl": "*",
|
||||
"ext-posix": "*",
|
||||
"illuminate/contracts": "^9.21|^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/queue": "^9.21|^10.0|^11.0|^12.0|^13.0",
|
||||
"illuminate/support": "^9.21|^10.0|^11.0|^12.0|^13.0",
|
||||
"laravel/sentinel": "^1.0",
|
||||
"nesbot/carbon": "^2.17|^3.0",
|
||||
"php": "^8.0",
|
||||
"ramsey/uuid": "^4.0",
|
||||
"symfony/console": "^6.0|^7.0|^8.0",
|
||||
"symfony/error-handler": "^6.0|^7.0|^8.0",
|
||||
"symfony/polyfill-php83": "^1.28",
|
||||
"symfony/process": "^6.0|^7.0|^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.0",
|
||||
"orchestra/testbench": "^7.56|^8.37|^9.16|^10.9|^11.0",
|
||||
"phpstan/phpstan": "^1.10|^2.0",
|
||||
"predis/predis": "^1.1|^2.0|^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-redis": "Required to use the Redis PHP driver.",
|
||||
"predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)."
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"aliases": {
|
||||
"Horizon": "Laravel\\Horizon\\Horizon"
|
||||
},
|
||||
"providers": [
|
||||
"Laravel\\Horizon\\HorizonServiceProvider"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-master": "6.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Horizon\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
}
|
||||
],
|
||||
"description": "Dashboard and code-driven configuration for Laravel queues.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"queue"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/horizon/issues",
|
||||
"source": "https://github.com/laravel/horizon/tree/v5.49.0"
|
||||
},
|
||||
"time": "2026-09-07T12:00:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/prompts",
|
||||
"version": "v0.3.5",
|
||||
@@ -1800,6 +1880,62 @@
|
||||
},
|
||||
"time": "2025-02-11T13:34:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/sentinel",
|
||||
"version": "v1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/sentinel.git",
|
||||
"reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/sentinel/zipball/972d9885d9d14312a118e9565c4e6ecc5e751ea1",
|
||||
"reference": "972d9885d9d14312a118e9565c4e6ecc5e751ea1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"illuminate/container": "^8.37|^9.0|^10.0|^11.0|^12.0|^13.0",
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/pint": "^1.27",
|
||||
"orchestra/testbench": "^6.47.1|^7.56|^8.37|^9.16|^10.9|^11.0",
|
||||
"phpstan/phpstan": "^2.1.33"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Sentinel\\SentinelServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Sentinel\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
},
|
||||
{
|
||||
"name": "Mior Muhammad Zaki",
|
||||
"email": "mior@laravel.com"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/laravel/sentinel/tree/v1.1.0"
|
||||
},
|
||||
"time": "2026-03-24T14:03:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/serializable-closure",
|
||||
"version": "v2.0.4",
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This name appears in notifications and in the Horizon UI. Unique names
|
||||
| can be useful while running multiple instances of Horizon within an
|
||||
| application, allowing you to identify the Horizon you're viewing.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('HORIZON_NAME'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Horizon will be accessible from. If this
|
||||
| setting is null, Horizon will reside under the same domain as the
|
||||
| application. Otherwise, this value will serve as the subdomain.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('HORIZON_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Horizon will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('HORIZON_PATH', 'admin/horizon'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the Redis connection where Horizon will store the
|
||||
| meta information required for it to function. It includes the list
|
||||
| of supervisors, failed jobs, job metrics, and other information.
|
||||
|
|
||||
*/
|
||||
|
||||
'use' => 'default',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This prefix will be used when storing all Horizon data in Redis. You
|
||||
| may modify the prefix when you are running multiple installations
|
||||
| of Horizon on the same server so that they don't have problems.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env(
|
||||
'HORIZON_PREFIX',
|
||||
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will get attached onto each Horizon route, giving you
|
||||
| the chance to add your own middleware to this list or change any of
|
||||
| the existing middleware. Or, you can simply stick with this list.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web', 'admin.access'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Wait Time Thresholds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to configure when the LongWaitDetected event
|
||||
| will be fired. Every connection / queue combination may have its
|
||||
| own, unique threshold (in seconds) before this event is fired.
|
||||
|
|
||||
*/
|
||||
|
||||
'waits' => [
|
||||
'redis:default' => 60,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Trimming Times
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||
| for one hour while all failed jobs are stored for an entire week.
|
||||
|
|
||||
*/
|
||||
|
||||
'trim' => [
|
||||
'recent' => 60,
|
||||
'pending' => 60,
|
||||
'completed' => 60,
|
||||
'recent_failed' => 10080,
|
||||
'failed' => 10080,
|
||||
'monitored' => 10080,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Silenced Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Silencing a job will instruct Horizon to not place the job in the list
|
||||
| of completed jobs within the Horizon dashboard. This setting may be
|
||||
| used to fully remove any noisy jobs from the completed jobs list.
|
||||
|
|
||||
*/
|
||||
|
||||
'silenced' => [
|
||||
// App\Jobs\ExampleJob::class,
|
||||
],
|
||||
|
||||
'silenced_tags' => [
|
||||
// 'notifications',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metrics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure how many snapshots should be kept to display in
|
||||
| the metrics graph. This will get used in combination with Horizon's
|
||||
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||
|
|
||||
*/
|
||||
|
||||
'metrics' => [
|
||||
'trim_snapshots' => [
|
||||
'job' => 24,
|
||||
'queue' => 24,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fast Termination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, Horizon's "terminate" command will not
|
||||
| wait on all of the workers to terminate unless the --wait option
|
||||
| is provided. Fast termination can shorten deployment delay by
|
||||
| allowing a new instance of Horizon to start while the last
|
||||
| instance will continue to terminate each of its workers.
|
||||
|
|
||||
*/
|
||||
|
||||
'fast_termination' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Memory Limit (MB)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value describes the maximum amount of memory the Horizon master
|
||||
| supervisor may consume before it is terminated and restarted. For
|
||||
| configuring these limits on your workers, see the next section.
|
||||
|
|
||||
*/
|
||||
|
||||
'memory_limit' => 64,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Worker Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the queue worker settings used by your application
|
||||
| in all environments. These supervisors and settings handle all your
|
||||
| queued jobs and will be provisioned by Horizon during deployment.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'auto',
|
||||
'autoScalingStrategy' => 'time',
|
||||
'maxProcesses' => 1,
|
||||
'maxTime' => 0,
|
||||
'maxJobs' => 0,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'timeout' => 60,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'maxProcesses' => 10,
|
||||
'balanceMaxShift' => 1,
|
||||
'balanceCooldown' => 3,
|
||||
],
|
||||
],
|
||||
|
||||
'local' => [
|
||||
'supervisor-1' => [
|
||||
'maxProcesses' => 3,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| File Watcher Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following list of directories and files will be watched when using
|
||||
| the `horizon:listen` command. Whenever any directories or files are
|
||||
| changed, Horizon will automatically restart to apply all changes.
|
||||
|
|
||||
*/
|
||||
|
||||
'watch' => [
|
||||
'app',
|
||||
'bootstrap',
|
||||
'config/**/*.php',
|
||||
'database/**/*.php',
|
||||
'public/**/*.php',
|
||||
'resources/**/*.php',
|
||||
'routes',
|
||||
'composer.lock',
|
||||
'composer.json',
|
||||
'.env',
|
||||
],
|
||||
];
|
||||
+12
-4
@@ -35,14 +35,22 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'zibal' => [
|
||||
'merchant' => env('ZIBAL_MERCHANT', 'zibal'),
|
||||
'sandbox' => env('ZIBAL_SANDBOX', true),
|
||||
'callback_url' => env('ZIBAL_CALLBACK_URL'),
|
||||
],
|
||||
|
||||
'iranpayamak' => [
|
||||
'api_url' => env('IRANPAYAMAK_SMS_API_URL', 'https://api.iranpayamak.com/ws/v1/sms/pattern'),
|
||||
'line_number' => env('IRANPAYAMAK_SMS_API_URL', '50002178584000'),
|
||||
'api_key' => env('IRANPAYAMAK_SMS_API_URL', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZMHq0kHtEXubsh'),
|
||||
|
||||
'line_number' => env('IRANPAYAMAK_SMS_LINE_NUMBER', '50002178584000'),
|
||||
'api_key' => env('IRANPAYAMAK_SMS_API_KEY', 'jnCyVq4ein7uGsgIGjHsVRrWz0112IA2v2qbZM5q0kHtEXubsh'),
|
||||
|
||||
'patterns' => [
|
||||
'verify_code' => 'EvYDvfcUSk',
|
||||
'verify_code' => env('IRANPAYAMAK_SMS_PATTERN_VERIFY_CODE', 'EvYDvfcUSk'),
|
||||
'request_registered' => env('IRANPAYAMAK_SMS_PATTERN_REQUEST_REGISTERED' , 'GO2xzefAO8'),
|
||||
'payment_confirmed' => env('IRANPAYAMAK_SMS_PATTERN_PAYMENT_CONFIRMED' , 'bl3k2UZDPe'),
|
||||
'report_ready' => env('IRANPAYAMAK_SMS_PATTERN_REPORT_READY' , 'ImUrYHVWTx'),
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\BankCard>
|
||||
*/
|
||||
class BankCardFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'card_number' => fake()->numerify('################'),
|
||||
'holder_name' => fake()->firstName() . ' ' . fake()->lastName(),
|
||||
'user_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\DocumentModalityEnum;
|
||||
use App\Enums\DocumentStatusEnum;
|
||||
use App\Enums\DocumentPriorityEnum;
|
||||
use App\Models\Pet;
|
||||
@@ -18,7 +19,7 @@ class DocumentFactory extends Factory
|
||||
{
|
||||
return [
|
||||
'code' => fake()->unique()->numberBetween(100000, 999999),
|
||||
'modality' => fake()->randomElement(['CT', 'MRI', 'X-Ray', 'Ultrasound', 'PET']),
|
||||
'modality' => fake()->randomElement(DocumentModalityEnum::cases())->value,
|
||||
'status' => fake()->randomElement(DocumentStatusEnum::cases()),
|
||||
'priority' => fake()->randomElement(DocumentPriorityEnum::cases()),
|
||||
'note' => fake()->optional()->sentence(),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\DocumentModalityEnum;
|
||||
use App\Enums\InvoicePaymentMethodEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Invoice>
|
||||
*/
|
||||
class InvoiceFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'number' => fake()->unique()->numerify('#####'),
|
||||
'amount' => fake()->randomElement(DocumentModalityEnum::cases())->price(),
|
||||
'status' => InvoiceStatusEnum::Pending,
|
||||
'payment_method' => null,
|
||||
'paid_at' => null,
|
||||
'document_id' => null,
|
||||
'user_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function paid(InvoicePaymentMethodEnum $method = InvoicePaymentMethodEnum::Wallet): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => InvoiceStatusEnum::Paid,
|
||||
'payment_method' => $method,
|
||||
'paid_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function canceled(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => InvoiceStatusEnum::Canceled,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OtpCode>
|
||||
*/
|
||||
class OtpCodeFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'phone' => '09' . fake()->numerify('#########'),
|
||||
'code' => (string) fake()->numberBetween(1000, 9999),
|
||||
'expires_at' => now()->addMinutes(3),
|
||||
];
|
||||
}
|
||||
|
||||
public function expired(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'expires_at' => now()->subMinute(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Payment>
|
||||
*/
|
||||
class PaymentFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'track_id' => fake()->unique()->numerify('##########'),
|
||||
'amount' => fake()->numberBetween(100000, 2000000),
|
||||
'status' => PaymentStatusEnum::Pending,
|
||||
'purpose' => PaymentPurposeEnum::Invoice,
|
||||
'ref_number' => null,
|
||||
'verified_at' => null,
|
||||
'invoice_id' => null,
|
||||
'payer_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function paid(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => PaymentStatusEnum::Paid,
|
||||
'ref_number' => fake()->numerify('############'),
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function walletTopUp(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'purpose' => PaymentPurposeEnum::WalletTopUp,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ShiftDayEnum;
|
||||
use App\Enums\UserEnum;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Shift>
|
||||
*/
|
||||
class ShiftFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'day_of_week' => fake()->randomElement(ShiftDayEnum::cases())->value,
|
||||
'date' => null,
|
||||
'start_time' => '08:00',
|
||||
'end_time' => '16:00',
|
||||
'is_active' => true,
|
||||
'user_id' => null,
|
||||
'created_by' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function onDate(string $date): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'day_of_week' => null,
|
||||
'date' => $date,
|
||||
]);
|
||||
}
|
||||
|
||||
public function weekly(ShiftDayEnum $day): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'day_of_week' => $day->value,
|
||||
'date' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function between(string $start, string $end): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'start_time' => $start,
|
||||
'end_time' => $end,
|
||||
]);
|
||||
}
|
||||
|
||||
public function forDoctor(?User $doctor = null): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'user_id' => $doctor?->id ?? User::factory()->state(['level' => UserEnum::Doctor->value]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Wallet>
|
||||
*/
|
||||
class WalletFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'balance' => 0,
|
||||
'user_id' => User::factory(),
|
||||
];
|
||||
}
|
||||
|
||||
public function system(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'user_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\WalletTransactionReasonEnum;
|
||||
use App\Enums\WalletTransactionTypeEnum;
|
||||
use App\Models\Wallet;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WalletTransaction>
|
||||
*/
|
||||
class WalletTransactionFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
$amount = fake()->numberBetween(10000, 1000000);
|
||||
|
||||
return [
|
||||
'type' => WalletTransactionTypeEnum::Deposit,
|
||||
'reason' => WalletTransactionReasonEnum::TopUp,
|
||||
'amount' => $amount,
|
||||
'balance_after' => $amount,
|
||||
'description' => null,
|
||||
'wallet_id' => Wallet::factory(),
|
||||
'invoice_id' => null,
|
||||
'payment_id' => null,
|
||||
'withdraw_request_id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function withdrawal(WalletTransactionReasonEnum $reason = WalletTransactionReasonEnum::InvoicePayment): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'type' => WalletTransactionTypeEnum::Withdrawal,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\WithdrawRequestStatusEnum;
|
||||
use App\Models\BankCard;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WithdrawRequest>
|
||||
*/
|
||||
class WithdrawRequestFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'amount' => fake()->numberBetween(100000, 2000000),
|
||||
'status' => WithdrawRequestStatusEnum::Pending,
|
||||
'admin_note' => null,
|
||||
'reviewed_at' => null,
|
||||
'reviewed_by' => null,
|
||||
'user_id' => User::factory(),
|
||||
'bank_card_id' => fn (array $attributes) => BankCard::factory()->create(['user_id' => $attributes['user_id']])->id,
|
||||
];
|
||||
}
|
||||
|
||||
public function approved(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => WithdrawRequestStatusEnum::Approved,
|
||||
'reviewed_at' => now(),
|
||||
'reviewed_by' => User::factory(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rejected(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => WithdrawRequestStatusEnum::Rejected,
|
||||
'admin_note' => fake()->sentence(),
|
||||
'reviewed_at' => now(),
|
||||
'reviewed_by' => User::factory(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\DocumentModalityEnum;
|
||||
use App\Enums\InvoiceStatusEnum;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('number')->unique();
|
||||
$table->unsignedBigInteger('amount');
|
||||
$table->string('status')->default(InvoiceStatusEnum::Pending->value);
|
||||
$table->string('payment_method')->nullable();
|
||||
$table->timestamp('paid_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('document_id')->nullable()->constrained('documents')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
});
|
||||
|
||||
DB::table('documents')->orderBy('id')->each(function ($document) {
|
||||
$modality = DocumentModalityEnum::tryFrom($document->modality);
|
||||
|
||||
if (!$modality) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = DB::table('invoices')->insertGetId([
|
||||
'number' => 'pending',
|
||||
'amount' => $modality->price(),
|
||||
'status' => InvoiceStatusEnum::Pending->value,
|
||||
'document_id' => $document->id,
|
||||
'user_id' => $document->requester_id,
|
||||
'created_at' => $document->created_at,
|
||||
'updated_at' => $document->created_at,
|
||||
]);
|
||||
|
||||
DB::table('invoices')->where('id', $id)->update([
|
||||
'number' => str_pad((string) $id, 5, '0', STR_PAD_LEFT),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoices');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\PaymentPurposeEnum;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('track_id')->unique();
|
||||
$table->unsignedBigInteger('amount');
|
||||
$table->string('status')->default(PaymentStatusEnum::Pending->value);
|
||||
$table->string('purpose')->default(PaymentPurposeEnum::Invoice->value);
|
||||
$table->string('ref_number')->nullable();
|
||||
$table->timestamp('verified_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('invoice_id')->nullable()->constrained('invoices')->cascadeOnDelete();
|
||||
$table->foreignId('payer_id')->constrained('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shifts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('day_of_week')->nullable();
|
||||
$table->date('date')->nullable();
|
||||
$table->time('start_time');
|
||||
$table->time('end_time');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
$table->index(['day_of_week', 'is_active']);
|
||||
$table->index(['date', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shifts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('balance')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->date('birthdate')->nullable()->after('fullname');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('birthdate');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('wallet_transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('type');
|
||||
$table->string('reason');
|
||||
$table->unsignedBigInteger('amount');
|
||||
$table->unsignedBigInteger('balance_after');
|
||||
$table->string('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('wallet_id')->constrained('wallets')->cascadeOnDelete();
|
||||
$table->foreignId('invoice_id')->nullable()->constrained('invoices')->nullOnDelete();
|
||||
$table->foreignId('payment_id')->nullable()->constrained('payments')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallet_transactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('reports', function (Blueprint $table) {
|
||||
$table->text('technique')->nullable()->after('body');
|
||||
$table->text('finding')->nullable()->after('technique');
|
||||
$table->text('diagnose')->nullable()->after('finding');
|
||||
$table->text('recommendation')->nullable()->after('diagnose');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('reports', function (Blueprint $table) {
|
||||
$table->dropColumn(['technique', 'finding', 'diagnose', 'recommendation']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('wallets', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->foreignId('user_id')->nullable()->change();
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('wallets', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->foreignId('user_id')->nullable(false)->change();
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bank_cards', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('card_number', 16);
|
||||
$table->string('holder_name');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bank_cards');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WithdrawRequestStatusEnum;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('withdraw_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('amount');
|
||||
$table->string('status')->default(WithdrawRequestStatusEnum::Pending->value)->index();
|
||||
$table->text('admin_note')->nullable();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreignId('reviewed_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('bank_card_id')->nullable()->constrained('bank_cards')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('withdraw_requests');
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user