Commit 40734561 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Mobile API Phase 3: Academy explore + shop endpoints

- Create AcademyController (public, no auth required):
  - GET academy/news — paginated news articles with images
  - GET academy/news/{uuid} — full article body
  - GET academy/programs — active training programs with activity info
  - GET academy/events — upcoming published events with registration status
  - GET academy/gallery — paginated media gallery
- Create ShopController (auth required):
  - GET products — essential products with purchase history check
    (includes already_purchased_at per participant per year)
- Total: 26 API endpoints
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0b2c6122
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Event\Models\Event;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Website\Models\Media;
use App\Domain\Website\Models\WebsiteNews;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class AcademyController extends Controller
{
public function news(Request $request): JsonResponse
{
$news = WebsiteNews::whereNotNull('published_at')
->where('published_at', '<=', now())
->orderByDesc('published_at')
->with('image')
->paginate(15);
return response()->json([
'data' => $news->map(fn ($item) => [
'id' => $item->id,
'uuid' => $item->uuid,
'title' => $item->title,
'excerpt' => $item->excerpt,
'category' => $item->category,
'image_url' => $item->image?->url ? asset('storage/' . $item->image->url) : null,
'published_at' => $item->published_at?->toIso8601String(),
'is_featured' => $item->is_featured,
]),
'meta' => [
'current_page' => $news->currentPage(),
'last_page' => $news->lastPage(),
'per_page' => $news->perPage(),
'total' => $news->total(),
],
]);
}
public function newsShow(string $uuid): JsonResponse
{
$article = WebsiteNews::where('uuid', $uuid)
->whereNotNull('published_at')
->with('image')
->firstOrFail();
return response()->json([
'data' => [
'id' => $article->id,
'uuid' => $article->uuid,
'title' => $article->title,
'body' => $article->body,
'category' => $article->category,
'image_url' => $article->image?->url ? asset('storage/' . $article->image->url) : null,
'published_at' => $article->published_at?->toIso8601String(),
],
]);
}
public function programs(): JsonResponse
{
$programs = TrainingProgram::active()
->with(['activity'])
->orderBy('name_ar')
->get();
return response()->json([
'data' => $programs->map(fn ($p) => [
'id' => $p->id,
'uuid' => $p->uuid,
'name_ar' => $p->name_ar,
'name' => $p->name,
'description_ar' => $p->description_ar,
'activity' => [
'id' => $p->activity?->id,
'name_ar' => $p->activity?->name_ar,
'category' => $p->activity?->category,
],
'age_min' => $p->age_min,
'age_max' => $p->age_max,
'gender' => $p->gender,
]),
]);
}
public function events(): JsonResponse
{
$events = Event::where('status', 'published')
->where('starts_at', '>=', now())
->orderBy('starts_at')
->with('cover')
->paginate(15);
return response()->json([
'data' => $events->map(fn ($e) => [
'id' => $e->id,
'uuid' => $e->uuid,
'title' => $e->title,
'description' => $e->description,
'type' => $e->type?->value,
'starts_at' => $e->starts_at?->toIso8601String(),
'ends_at' => $e->ends_at?->toIso8601String(),
'location_name' => $e->location_name,
'cover_url' => $e->cover?->url ? asset('storage/' . $e->cover->url) : null,
'max_capacity' => $e->max_capacity,
'registrations_count' => $e->registrations_count,
'is_registration_open' => $e->isRegistrationOpen(),
'spots_remaining' => $e->spotsRemaining(),
]),
'meta' => [
'current_page' => $events->currentPage(),
'last_page' => $events->lastPage(),
'per_page' => $events->perPage(),
'total' => $events->total(),
],
]);
}
public function gallery(): JsonResponse
{
$media = Media::where('collection', 'gallery')
->orderByDesc('created_at')
->paginate(20);
return response()->json([
'data' => $media->map(fn ($m) => [
'id' => $m->id,
'url' => $m->url ? asset('storage/' . $m->url) : null,
'caption' => $m->caption,
'type' => $m->type,
'created_at' => $m->created_at?->toIso8601String(),
]),
'meta' => [
'current_page' => $media->currentPage(),
'last_page' => $media->lastPage(),
'per_page' => $media->perPage(),
'total' => $media->total(),
],
]);
}
}
<?php
namespace App\Http\Controllers\Api\V1;
use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Inventory\Models\Product;
use App\Domain\Participant\Models\Participant;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ShopController extends Controller
{
public function products(Request $request): JsonResponse
{
$user = $request->user();
$products = Product::where('is_active', true)
->where('is_essential', true)
->orderBy('name_ar')
->with('installmentPlans')
->get();
$participantUuid = $request->query('participant_uuid');
$participant = null;
if ($participantUuid) {
$participant = Participant::where('uuid', $participantUuid)->first();
}
$productType = 'App\\Domain\\Inventory\\Models\\Product';
$yearStart = now()->startOfYear();
return response()->json([
'data' => $products->map(function ($product) use ($participant, $productType, $yearStart) {
$alreadyPurchasedAt = null;
if ($participant) {
$previousPurchase = InvoiceItem::where('itemable_type', $productType)
->where('itemable_id', $product->id)
->where('created_at', '>=', $yearStart)
->whereHas('invoice', fn ($q) => $q
->where('billable_type', 'App\\Domain\\Participant\\Models\\Participant')
->where('billable_id', $participant->id)
->whereNotIn('status', ['cancelled', 'draft'])
)
->orderByDesc('created_at')
->first();
$alreadyPurchasedAt = $previousPurchase?->created_at?->format('Y-m-d');
}
return [
'id' => $product->id,
'name_ar' => $product->name_ar,
'name' => $product->name,
'sku' => $product->sku,
'selling_price' => $product->selling_price,
'photo_url' => $product->photo_path ? asset('storage/' . $product->photo_path) : null,
'already_purchased_at' => $alreadyPurchasedAt,
'installment_plans' => $product->installmentPlans->map(fn ($plan) => [
'id' => $plan->id,
'name_ar' => $plan->name_ar ?? $plan->name,
'installments_count' => $plan->installments_count,
'down_payment' => $plan->down_payment,
]),
];
}),
]);
}
}
<?php
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\DeviceController;
use App\Http\Controllers\Api\V1\NotificationController;
use App\Http\Controllers\Api\V1\ParticipantController;
use App\Http\Controllers\Api\V1\ShopController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(function () {
......@@ -21,6 +23,15 @@
});
});
// Public academy content (accessible without login for explore)
Route::prefix('academy')->group(function () {
Route::get('news', [AcademyController::class, 'news']);
Route::get('news/{uuid}', [AcademyController::class, 'newsShow']);
Route::get('programs', [AcademyController::class, 'programs']);
Route::get('events', [AcademyController::class, 'events']);
Route::get('gallery', [AcademyController::class, 'gallery']);
});
// Protected endpoints
Route::middleware('auth:sanctum')->group(function () {
// Device token registration
......@@ -47,5 +58,8 @@
Route::post('notifications/read-all', [NotificationController::class, 'markAllAsRead']);
Route::get('notifications/preferences', [NotificationController::class, 'getPreferences']);
Route::post('notifications/preferences', [NotificationController::class, 'updatePreferences']);
// Shop (essential products)
Route::get('products', [ShopController::class, 'products']);
});
});
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