Commit 8504dd68 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Mobile API Phase 7: Evaluations, dashboard, service requests, receipts

- Evaluation list + detail with per-criterion scores and percentages
- Dashboard endpoint: single-call home screen (all children, today's
  sessions, totals, announcement bar, unread counts)
- Service requests: freeze/unfreeze/transfer/cancellation workflow
  with duplicate prevention and cancel-by-user
- Invoice detail + payment receipt endpoints for mobile viewing
- Fix: use correct column name `due_amount` (not `balance_due`) across
  all API controllers and PaymobService
- 48 total API endpoints
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 30e19414
......@@ -152,11 +152,11 @@ public function processCallback(array $callbackData): ?Payment
$invoice = $payment->invoice;
if ($invoice) {
$invoice->paid_amount += $payment->amount;
$invoice->balance_due = $invoice->total_amount - $invoice->paid_amount;
$invoice->due_amount = $invoice->total_amount - $invoice->paid_amount;
if ($invoice->balance_due <= 0) {
if ($invoice->due_amount <= 0) {
$invoice->status = 'paid';
$invoice->balance_due = 0;
$invoice->due_amount = 0;
} elseif ($invoice->paid_amount > 0) {
$invoice->status = 'partially_paid';
}
......@@ -235,7 +235,7 @@ private function createOrder(string $authToken, Invoice $invoice): int
$response = Http::timeout(15)->post('https://accept.paymob.com/api/ecommerce/orders', [
'auth_token' => $authToken,
'delivery_needed' => false,
'amount_cents' => $invoice->balance_due,
'amount_cents' => $invoice->due_amount,
'currency' => 'EGP',
'merchant_order_id' => $invoice->uuid . '-' . time(),
'items' => [],
......@@ -256,7 +256,7 @@ private function getPaymentKey(string $authToken, int $orderId, Invoice $invoice
$response = Http::timeout(15)->post('https://accept.paymob.com/api/acceptance/payment_keys', [
'auth_token' => $authToken,
'amount_cents' => $invoice->balance_due,
'amount_cents' => $invoice->due_amount,
'expiration' => 3600,
'order_id' => $orderId,
'billing_data' => [
......
<?php
namespace App\Domain\Shared\Models;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ServiceRequest extends Model
{
use BelongsToAcademy, HasUuid;
protected $fillable = [
'academy_id',
'user_id',
'participant_id',
'type',
'status',
'reason',
'metadata',
'admin_notes',
'handled_by',
'handled_at',
];
protected $casts = [
'metadata' => 'array',
'handled_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function participant(): BelongsTo
{
return $this->belongsTo(Participant::class);
}
public function handler(): BelongsTo
{
return $this->belongsTo(User::class, 'handled_by');
}
public function isPending(): bool
{
return $this->status === 'pending';
}
}
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Models\ContactMessage;
use App\Domain\Training\Models\Evaluation;
use App\Domain\Training\Models\TrainingSession;
use App\Domain\Website\Models\WebsiteSetting;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function index(Request $request): JsonResponse
{
$user = $request->user();
$participants = $this->getAuthorizedParticipants($user);
$childrenSummary = $participants->map(function (Participant $p) {
$nextSession = $this->getNextSession($p);
$outstandingBalance = Invoice::where('billable_type', 'App\\Domain\\Participant\\Models\\Participant')
->where('billable_id', $p->id)
->whereNotIn('status', ['cancelled', 'draft', 'paid'])
->sum('due_amount');
return [
'uuid' => $p->uuid,
'name_ar' => $p->person?->name_ar ?? $p->name_ar ?? '',
'photo_url' => $p->photo_path ? asset('storage/' . $p->photo_path) : null,
'status' => $p->status->value ?? $p->status,
'outstanding_balance' => $outstandingBalance,
'outstanding_display' => number_format($outstandingBalance / 100, 2) . ' ج.م',
'next_session' => $nextSession ? [
'date' => $nextSession->session_date?->toDateString() ?? $nextSession->session_date,
'start_time' => $nextSession->start_time,
'group_name' => $nextSession->group?->name_ar ?? '',
] : null,
];
});
$totalOutstanding = $childrenSummary->sum('outstanding_balance');
$todaySessions = $this->getTodaySessions($participants);
$unreadMessages = ContactMessage::where('user_id', $user->id)
->whereNotNull('reply')
->where('is_read', false)
->count();
$unreadNotifications = $user->unreadNotifications()->count();
$recentEvaluations = Evaluation::whereIn('participant_id', $participants->pluck('id'))
->where('status', 'shared')
->where('shared_at', '>=', now()->subDays(30))
->count();
$announcement = $this->getAnnouncement();
return response()->json([
'data' => [
'children' => $childrenSummary,
'totals' => [
'outstanding_balance' => $totalOutstanding,
'outstanding_display' => number_format($totalOutstanding / 100, 2) . ' ج.م',
'children_count' => $participants->count(),
'today_sessions' => $todaySessions->count(),
'unread_messages' => $unreadMessages,
'unread_notifications' => $unreadNotifications,
'recent_evaluations' => $recentEvaluations,
],
'today_sessions' => $todaySessions->map(fn ($s) => [
'session_date' => $s->session_date,
'start_time' => $s->start_time,
'end_time' => $s->end_time,
'status' => $s->status->value ?? $s->status,
'group_name' => $s->group?->name_ar ?? '',
'facility_name' => $s->facility?->name_ar ?? '',
])->values(),
'announcement' => $announcement,
],
]);
}
private function getAuthorizedParticipants($user): \Illuminate\Support\Collection
{
$personId = $user->person_id;
if (!$personId) {
return collect();
}
$direct = Participant::where('person_id', $personId)
->with('person')
->get();
$guardian = \App\Domain\Identity\Models\Guardian::where('person_id', $personId)->first();
$guardianParticipants = collect();
if ($guardian) {
$guardianParticipants = $guardian->participants()->with('person')->get();
}
return $direct->merge($guardianParticipants)->unique('id');
}
private function getNextSession(Participant $p): ?TrainingSession
{
$groupIds = $p->activeEnrollments()->pluck('training_group_id');
if ($groupIds->isEmpty()) {
return null;
}
return TrainingSession::whereIn('training_group_id', $groupIds)
->where('session_date', '>=', now()->toDateString())
->whereIn('status', ['scheduled'])
->with('group:id,name_ar,name')
->orderBy('session_date')
->orderBy('start_time')
->first();
}
private function getTodaySessions($participants): \Illuminate\Support\Collection
{
$groupIds = collect();
foreach ($participants as $p) {
$groupIds = $groupIds->merge($p->activeEnrollments()->pluck('training_group_id'));
}
$groupIds = $groupIds->unique();
if ($groupIds->isEmpty()) {
return collect();
}
return TrainingSession::whereIn('training_group_id', $groupIds)
->where('session_date', now()->toDateString())
->whereIn('status', ['scheduled', 'in_progress'])
->with(['group:id,name_ar,name', 'facility:id,name_ar,name'])
->orderBy('start_time')
->get();
}
private function getAnnouncement(): ?array
{
$settings = WebsiteSetting::first();
if (!$settings || empty($settings->announcement_bar)) {
return null;
}
$bar = $settings->announcement_bar;
$enabled = $bar['enabled'] ?? false;
if (!$enabled) {
return null;
}
return [
'text' => $bar['text'] ?? '',
'link' => $bar['link'] ?? null,
'type' => $bar['type'] ?? 'info',
];
}
}
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Participant\Models\Participant;
use App\Domain\Training\Models\Evaluation;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EvaluationController extends Controller
{
public function index(string $participantUuid, Request $request): JsonResponse
{
$participant = $this->findAuthorized($participantUuid, $request->user());
$evaluations = Evaluation::where('participant_id', $participant->id)
->where('status', 'shared')
->with(['evaluator:id,name_ar,name', 'group:id,name_ar,name', 'group.program:id,name_ar,name'])
->orderByDesc('evaluation_date')
->paginate(10);
return response()->json([
'data' => $evaluations->map(fn ($eval) => [
'uuid' => $eval->uuid,
'evaluation_date' => $eval->evaluation_date?->toDateString(),
'period_from' => $eval->period_from?->toDateString(),
'period_to' => $eval->period_to?->toDateString(),
'overall_score' => (float) $eval->overall_score,
'overall_notes' => $eval->overall_notes,
'evaluator_name' => $eval->evaluator?->name_ar ?? $eval->evaluator?->name,
'group_name' => $eval->group?->name_ar ?? $eval->group?->name,
'program_name' => $eval->group?->program?->name_ar ?? $eval->group?->program?->name,
'shared_at' => $eval->shared_at?->toIso8601String(),
]),
'meta' => [
'current_page' => $evaluations->currentPage(),
'last_page' => $evaluations->lastPage(),
'per_page' => $evaluations->perPage(),
'total' => $evaluations->total(),
],
]);
}
public function show(string $participantUuid, string $evaluationUuid, Request $request): JsonResponse
{
$participant = $this->findAuthorized($participantUuid, $request->user());
$evaluation = Evaluation::where('uuid', $evaluationUuid)
->where('participant_id', $participant->id)
->where('status', 'shared')
->with([
'evaluator:id,name_ar,name',
'group:id,name_ar,name',
'group.program:id,name_ar,name',
'scores.criterion',
])
->firstOrFail();
$maxPossible = $evaluation->scores->sum(fn ($s) => $s->criterion?->max_score ?? 0);
$actualTotal = $evaluation->scores->sum('score');
return response()->json([
'data' => [
'uuid' => $evaluation->uuid,
'evaluation_date' => $evaluation->evaluation_date?->toDateString(),
'period_from' => $evaluation->period_from?->toDateString(),
'period_to' => $evaluation->period_to?->toDateString(),
'overall_score' => (float) $evaluation->overall_score,
'overall_notes' => $evaluation->overall_notes,
'evaluator_name' => $evaluation->evaluator?->name_ar ?? $evaluation->evaluator?->name,
'group_name' => $evaluation->group?->name_ar ?? $evaluation->group?->name,
'program_name' => $evaluation->group?->program?->name_ar ?? $evaluation->group?->program?->name,
'shared_at' => $evaluation->shared_at?->toIso8601String(),
'score_summary' => [
'total_score' => (float) $actualTotal,
'max_possible' => (float) $maxPossible,
'percentage' => $maxPossible > 0 ? round($actualTotal / $maxPossible * 100, 1) : 0,
],
'scores' => $evaluation->scores->map(fn ($score) => [
'criterion_name' => $score->criterion?->name_ar ?? $score->criterion?->name,
'category' => $score->criterion?->category,
'score' => (float) $score->score,
'max_score' => $score->criterion?->max_score ?? 0,
'percentage' => ($score->criterion?->max_score ?? 0) > 0
? round($score->score / $score->criterion->max_score * 100, 1)
: 0,
'notes' => $score->notes,
]),
],
]);
}
private function findAuthorized(string $uuid, $user): Participant
{
$participant = Participant::where('uuid', $uuid)->firstOrFail();
$personId = $user->person_id;
if (!$personId) {
abort(403, 'غير مصرح لك بالوصول لهذا المشترك');
}
if ($participant->person_id === $personId) {
return $participant;
}
$guardian = \App\Domain\Identity\Models\Guardian::where('person_id', $personId)->first();
if ($guardian && $guardian->participants()->where('participants.id', $participant->id)->exists()) {
return $participant;
}
abort(403, 'غير مصرح لك بالوصول لهذا المشترك');
}
}
......@@ -21,7 +21,7 @@ public function plans(string $participantUuid, Request $request): JsonResponse
->pluck('id');
$plans = PaymentPlan::whereIn('invoice_id', $invoiceIds)
->with(['invoice:id,uuid,total_amount,paid_amount,balance_due,status', 'installments'])
->with(['invoice:id,uuid,total_amount,paid_amount,due_amount,status', 'installments'])
->orderByDesc('created_at')
->get();
......
......@@ -62,18 +62,16 @@ public function create(Request $request): JsonResponse
$result = DB::transaction(function () use ($participant, $product, $price, $user, $request) {
$invoice = Invoice::create([
'academy_id' => $participant->academy_id,
'branch_id' => $participant->branch_id,
'billable_type' => 'App\\Domain\\Participant\\Models\\Participant',
'billable_id' => $participant->id,
'uuid' => Str::uuid()->toString(),
'invoice_number' => 'INV-MOB-' . now()->format('Ymd') . '-' . rand(1000, 9999),
'number' => 'INV-MOB-' . now()->format('Ymd') . '-' . rand(1000, 9999),
'status' => 'sent',
'subtotal' => $price,
'subtotal_amount' => $price,
'discount_amount' => 0,
'tax_amount' => 0,
'total_amount' => $price,
'paid_amount' => 0,
'balance_due' => $price,
'due_amount' => $price,
'currency' => 'EGP',
'issue_date' => now()->toDateString(),
'due_date' => now()->addDays(7)->toDateString(),
......@@ -214,7 +212,7 @@ public function index(Request $request): JsonResponse
'total_amount' => $inv->total_amount,
'total_display' => number_format($inv->total_amount / 100, 2) . ' ج.م',
'paid_amount' => $inv->paid_amount,
'balance_due' => $inv->balance_due,
'balance_due' => $inv->due_amount,
'product_name' => $inv->items->first()?->description_ar,
'created_at' => $inv->created_at?->toIso8601String(),
]),
......
......@@ -50,7 +50,7 @@ public function summary(string $uuid, Request $request): JsonResponse
$outstandingBalance = Invoice::where('billable_type', 'App\\Domain\\Participant\\Models\\Participant')
->where('billable_id', $participant->id)
->whereNotIn('status', ['cancelled', 'draft', 'paid'])
->sum('balance_due');
->sum('due_amount');
$nextSession = $this->getNextSession($participant);
......
......@@ -32,7 +32,7 @@ public function initiate(Request $request): JsonResponse
$invoice = Invoice::where('uuid', $request->invoice_uuid)
->whereIn('status', ['sent', 'partially_paid', 'overdue'])
->where('balance_due', '>', 0)
->where('due_amount', '>', 0)
->first();
if (!$invoice) {
......@@ -55,7 +55,7 @@ public function initiate(Request $request): JsonResponse
'status' => PaymentStatus::Pending,
'payer_type' => $invoice->billable_type,
'payer_id' => $invoice->billable_id,
'amount' => $invoice->balance_due,
'amount' => $invoice->due_amount,
'currency' => 'EGP',
'payment_date' => now()->toDateString(),
'gateway_data' => [
......@@ -69,8 +69,8 @@ public function initiate(Request $request): JsonResponse
'payment_url' => $result['iframe_url'],
'payment_key' => $result['payment_key'],
'order_id' => $result['order_id'],
'amount' => $invoice->balance_due,
'amount_display' => number_format($invoice->balance_due / 100, 2) . ' ج.م',
'amount' => $invoice->due_amount,
'amount_display' => number_format($invoice->due_amount / 100, 2) . ' ج.م',
]);
} catch (\Throwable $e) {
Log::error('[Paymob] Payment initiation failed', [
......
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Financial\Models\Payment;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ReceiptController extends Controller
{
public function invoiceDetail(string $invoiceUuid, Request $request): JsonResponse
{
$user = $request->user();
$invoice = Invoice::where('uuid', $invoiceUuid)
->whereNotIn('status', ['cancelled', 'draft'])
->with(['items', 'payments' => fn ($q) => $q->where('status', 'confirmed')])
->firstOrFail();
// Authorization: user must be related to the billable participant
if ($invoice->billable_type === 'App\\Domain\\Participant\\Models\\Participant') {
$this->verifyAccess($user, $invoice->billable_id);
}
return response()->json([
'data' => [
'uuid' => $invoice->uuid,
'number' => $invoice->number ?? '',
'status' => $invoice->status,
'issue_date' => $invoice->issue_date?->toDateString(),
'due_date' => $invoice->due_date?->toDateString(),
'subtotal' => $invoice->subtotal_amount,
'discount_amount' => $invoice->discount_amount,
'tax_amount' => $invoice->tax_amount,
'total_amount' => $invoice->total_amount,
'paid_amount' => $invoice->paid_amount,
'balance_due' => $invoice->due_amount,
'total_display' => number_format($invoice->total_amount / 100, 2) . ' ج.م',
'balance_display' => number_format($invoice->due_amount / 100, 2) . ' ج.م',
'notes' => $invoice->notes,
'items' => $invoice->items->map(fn ($item) => [
'description' => $item->description_ar ?? $item->description ?? '',
'quantity' => $item->quantity,
'unit_price' => $item->unit_price,
'unit_price_display' => number_format($item->unit_price / 100, 2) . ' ج.م',
'line_total' => $item->line_total,
'line_total_display' => number_format($item->line_total / 100, 2) . ' ج.م',
]),
'payments' => $invoice->payments->map(fn ($pay) => [
'reference' => $pay->reference,
'method' => $pay->method->value ?? $pay->method,
'amount' => $pay->amount,
'amount_display' => number_format($pay->amount / 100, 2) . ' ج.م',
'payment_date' => $pay->payment_date?->toDateString(),
'confirmed_at' => $pay->confirmed_at?->toIso8601String(),
]),
'can_pay_online' => $invoice->due_amount > 0,
],
]);
}
public function paymentReceipt(string $paymentUuid, Request $request): JsonResponse
{
$user = $request->user();
$payment = Payment::withoutGlobalScope('academy')
->where('uuid', $paymentUuid)
->where('status', 'confirmed')
->with('invoice:id,uuid,number,total_amount')
->firstOrFail();
if ($payment->invoice && $payment->invoice->billable_type === 'App\\Domain\\Participant\\Models\\Participant') {
$this->verifyAccess($user, $payment->invoice->billable_id);
}
return response()->json([
'data' => [
'uuid' => $payment->uuid,
'reference' => $payment->reference,
'method' => $payment->method->value ?? $payment->method,
'amount' => $payment->amount,
'amount_display' => number_format($payment->amount / 100, 2) . ' ج.م',
'currency' => $payment->currency ?? 'EGP',
'payment_date' => $payment->payment_date?->toDateString(),
'confirmed_at' => $payment->confirmed_at?->toIso8601String(),
'invoice_uuid' => $payment->invoice?->uuid,
'invoice_number' => $payment->invoice?->number ?? '',
],
]);
}
private function verifyAccess($user, int $participantId): void
{
$personId = $user->person_id;
if (!$personId) {
abort(403, 'غير مصرح');
}
$participant = \App\Domain\Participant\Models\Participant::find($participantId);
if (!$participant) {
abort(404);
}
if ($participant->person_id === $personId) {
return;
}
$guardian = \App\Domain\Identity\Models\Guardian::where('person_id', $personId)->first();
if ($guardian && $guardian->participants()->where('participants.id', $participantId)->exists()) {
return;
}
abort(403, 'غير مصرح لك بالوصول لهذه الفاتورة');
}
}
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Models\ServiceRequest;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ServiceRequestController extends Controller
{
public function create(Request $request): JsonResponse
{
$request->validate([
'participant_uuid' => 'required|string',
'type' => 'required|in:freeze,unfreeze,transfer,cancellation,other',
'reason' => 'required|string|max:1000',
'metadata' => 'nullable|array',
]);
$user = $request->user();
$participant = $this->findAuthorized($request->participant_uuid, $user);
$existingPending = ServiceRequest::where('participant_id', $participant->id)
->where('type', $request->type)
->where('status', 'pending')
->first();
if ($existingPending) {
return response()->json([
'error' => 'duplicate_request',
'message' => 'يوجد طلب مماثل قيد المراجعة بالفعل',
'data' => [
'uuid' => $existingPending->uuid,
'created_at' => $existingPending->created_at?->toIso8601String(),
],
], 422);
}
$serviceRequest = ServiceRequest::create([
'academy_id' => $participant->academy_id,
'user_id' => $user->id,
'participant_id' => $participant->id,
'type' => $request->type,
'status' => 'pending',
'reason' => $request->reason,
'metadata' => $request->metadata ?? [],
]);
return response()->json([
'success' => true,
'message' => 'تم إرسال الطلب بنجاح وسيتم مراجعته',
'data' => [
'uuid' => $serviceRequest->uuid,
'type' => $serviceRequest->type,
'status' => $serviceRequest->status,
],
]);
}
public function index(Request $request): JsonResponse
{
$user = $request->user();
$participantUuid = $request->query('participant_uuid');
$query = ServiceRequest::where('user_id', $user->id)
->with('participant:id,uuid,person_id')
->orderByDesc('created_at');
if ($participantUuid) {
$participant = Participant::where('uuid', $participantUuid)->first();
if ($participant) {
$query->where('participant_id', $participant->id);
}
}
$requests = $query->paginate(15);
return response()->json([
'data' => $requests->map(fn ($r) => [
'uuid' => $r->uuid,
'type' => $r->type,
'type_label' => $this->typeLabel($r->type),
'status' => $r->status,
'status_label' => $this->statusLabel($r->status),
'reason' => $r->reason,
'admin_notes' => $r->admin_notes,
'participant_uuid' => $r->participant?->uuid,
'created_at' => $r->created_at?->toIso8601String(),
'handled_at' => $r->handled_at?->toIso8601String(),
]),
'meta' => [
'current_page' => $requests->currentPage(),
'last_page' => $requests->lastPage(),
'per_page' => $requests->perPage(),
'total' => $requests->total(),
],
]);
}
public function cancel(string $uuid, Request $request): JsonResponse
{
$user = $request->user();
$serviceRequest = ServiceRequest::where('uuid', $uuid)
->where('user_id', $user->id)
->where('status', 'pending')
->firstOrFail();
$serviceRequest->update(['status' => 'cancelled']);
return response()->json([
'success' => true,
'message' => 'تم إلغاء الطلب',
]);
}
private function typeLabel(string $type): string
{
return match ($type) {
'freeze' => 'طلب تجميد',
'unfreeze' => 'طلب إلغاء تجميد',
'transfer' => 'طلب نقل',
'cancellation' => 'طلب إلغاء اشتراك',
'other' => 'طلب آخر',
default => $type,
};
}
private function statusLabel(string $status): string
{
return match ($status) {
'pending' => 'قيد المراجعة',
'approved' => 'تمت الموافقة',
'rejected' => 'مرفوض',
'cancelled' => 'ملغي',
default => $status,
};
}
private function findAuthorized(string $uuid, $user): Participant
{
$participant = Participant::where('uuid', $uuid)->firstOrFail();
$personId = $user->person_id;
if (!$personId) {
abort(403, 'غير مصرح لك بالوصول لهذا المشترك');
}
if ($participant->person_id === $personId) {
return $participant;
}
$guardian = \App\Domain\Identity\Models\Guardian::where('person_id', $personId)->first();
if ($guardian && $guardian->participants()->where('participants.id', $participant->id)->exists()) {
return $participant;
}
abort(403, 'غير مصرح لك بالوصول لهذا المشترك');
}
}
<?php
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('service_requests', function (Blueprint $table) {
$table->id();
$table->foreignId('academy_id')->constrained('academies');
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->constrained('users');
$table->foreignId('participant_id')->constrained('participants');
$table->string('type', 30);
$table->string('status', 30)->default('pending');
$table->text('reason');
$table->jsonb('metadata')->default('{}');
$table->text('admin_notes')->nullable();
$table->foreignId('handled_by')->nullable()->constrained('users');
$table->timestamp('handled_at')->nullable();
$table->timestamps();
$table->index(['academy_id', 'status']);
$table->index(['participant_id', 'type']);
$table->index(['user_id', 'created_at']);
});
DB::statement("ALTER TABLE service_requests ADD CONSTRAINT service_requests_type_check CHECK (type IN ('freeze', 'unfreeze', 'transfer', 'cancellation', 'other'))");
DB::statement("ALTER TABLE service_requests ADD CONSTRAINT service_requests_status_check CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled'))");
}
public function down(): void
{
Schema::dropIfExists('service_requests');
}
};
......@@ -4,8 +4,10 @@
use App\Http\Controllers\Api\V1\AcademyController;
use App\Http\Controllers\Api\V1\AppConfigController;
use App\Http\Controllers\Api\V1\AuthOtpController;
use App\Http\Controllers\Api\V1\DashboardController;
use App\Http\Controllers\Api\V1\DeviceController;
use App\Http\Controllers\Api\V1\DocumentController;
use App\Http\Controllers\Api\V1\EvaluationController;
use App\Http\Controllers\Api\V1\EventController;
use App\Http\Controllers\Api\V1\InstallmentController;
use App\Http\Controllers\Api\V1\MessageController;
......@@ -13,6 +15,8 @@
use App\Http\Controllers\Api\V1\OrderController;
use App\Http\Controllers\Api\V1\ParticipantController;
use App\Http\Controllers\Api\V1\PaymentController;
use App\Http\Controllers\Api\V1\ReceiptController;
use App\Http\Controllers\Api\V1\ServiceRequestController;
use App\Http\Controllers\Api\V1\ShopController;
use App\Http\Controllers\Api\V1\WalletController;
use Illuminate\Support\Facades\Route;
......@@ -43,6 +47,9 @@
// Protected endpoints
Route::middleware('auth:sanctum')->group(function () {
// Dashboard (single-call home screen)
Route::get('dashboard', [DashboardController::class, 'index']);
// Device token registration
Route::post('devices/register', [DeviceController::class, 'register']);
Route::patch('devices/refresh', [DeviceController::class, 'refresh']);
......@@ -63,6 +70,8 @@
Route::get('wallet', [WalletController::class, 'balance']);
Route::get('wallet/transactions', [WalletController::class, 'transactions']);
Route::get('installments', [InstallmentController::class, 'plans']);
Route::get('evaluations', [EvaluationController::class, 'index']);
Route::get('evaluations/{evaluationUuid}', [EvaluationController::class, 'show']);
});
// Notifications
......@@ -82,10 +91,19 @@
// Payments (online via Paymob)
Route::post('payments/initiate', [PaymentController::class, 'initiate'])->middleware('throttle:api-payments');
// Invoices & Receipts
Route::get('invoices/{uuid}', [ReceiptController::class, 'invoiceDetail']);
Route::get('payments/{uuid}/receipt', [ReceiptController::class, 'paymentReceipt']);
// Events (authenticated actions)
Route::post('events/{uuid}/register', [EventController::class, 'register']);
Route::get('events/my-registrations', [EventController::class, 'myRegistrations']);
// Service requests (freeze, transfer, cancellation)
Route::post('service-requests', [ServiceRequestController::class, 'create']);
Route::get('service-requests', [ServiceRequestController::class, 'index']);
Route::post('service-requests/{uuid}/cancel', [ServiceRequestController::class, 'cancel']);
// Absence reporting
Route::post('absences/report', [AbsenceController::class, 'report']);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment