backend: align API/models with current PostgreSQL schema
فشلت بعض الفحوصات
Deploy Backend / deploy (push) Has been cancelled

هذا الالتزام موجود في:
Abdul Kareem
2026-02-19 19:40:35 +03:00
الأصل 4ee662a0ae
التزام 5764dd03e4
14 ملفات معدلة مع 673 إضافات و110 حذوفات

عرض الملف

@@ -1,36 +1,39 @@
FROM php:8.3-fpm FROM php:8.2-cli
RUN apt-get update && apt-get install -y --no-install-recommends \ # Install system dependencies
libpq-dev \ RUN apt-get update && apt-get install -y \
libicu-dev \
libpng-dev \
libzip-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
unzip \
git \ git \
curl \ curl \
libpng-dev \
libonig-dev \
libxml2-dev \
zip \ zip \
unzip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ # Install PHP extensions
&& docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql intl gd zip RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer # Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www WORKDIR /var/www
COPY composer.json composer.lock ./ # Copy existing application directory contents
RUN composer install --no-dev --prefer-dist --no-interaction --no-scripts --optimize-autoloader COPY . /var/www
COPY . . # Install dependencies
RUN composer install --no-interaction --no-dev --optimize-autoloader
RUN chown -R www-data:www-data storage bootstrap/cache \ # Set permissions
&& chmod -R ug+rw storage bootstrap/cache \ RUN chown -R www-data:www-data /var/www \
&& chmod +x docker/entrypoint.sh && chmod -R 755 /var/www/storage \
&& chmod -R 755 /var/www/bootstrap/cache
ENTRYPOINT ["docker/entrypoint.sh"] # Expose port 8000
EXPOSE 8000
EXPOSE 9000 # Start Laravel development server
CMD php artisan serve --host=0.0.0.0 --port=8000
CMD ["php-fpm"]

عرض الملف

@@ -12,10 +12,13 @@ use App\Models\ReservationTableAssignment;
use App\Models\VenueTable; use App\Models\VenueTable;
use App\Models\ReservationReminder; use App\Models\ReservationReminder;
use App\Models\Venue; use App\Models\Venue;
use App\Models\UserBlock;
use App\Models\UserStrike;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ReservationController extends Controller class ReservationController extends Controller
{ {
@@ -393,7 +396,7 @@ class ReservationController extends Controller
private function getPricingRule(?Venue $venue): ?PricingRule private function getPricingRule(?Venue $venue): ?PricingRule
{ {
if (! $venue) { if (! $venue) {
return PricingRule::where('scope', 'global')->where('is_active', true)->latest('id')->first(); return PricingRule::where('scope', 'global_type')->where('is_active', true)->latest('id')->first();
} }
$venueRule = PricingRule::where('scope', 'venue') $venueRule = PricingRule::where('scope', 'venue')
@@ -406,7 +409,7 @@ class ReservationController extends Controller
return $venueRule; return $venueRule;
} }
$typeRule = PricingRule::where('scope', 'type') $typeRule = PricingRule::where('scope', 'global_type')
->where('venue_type', $venue->type) ->where('venue_type', $venue->type)
->where('is_active', true) ->where('is_active', true)
->latest('id') ->latest('id')
@@ -416,7 +419,7 @@ class ReservationController extends Controller
return $typeRule; return $typeRule;
} }
return PricingRule::where('scope', 'global')->where('is_active', true)->latest('id')->first(); return PricingRule::where('scope', 'global_type')->where('is_active', true)->latest('id')->first();
} }
private function findAvailableTable(Reservation $reservation, bool $includePending, bool $lockRows = false): ?VenueTable private function findAvailableTable(Reservation $reservation, bool $includePending, bool $lockRows = false): ?VenueTable
@@ -501,6 +504,7 @@ class ReservationController extends Controller
return ReservationReminder::create([ return ReservationReminder::create([
'reservation_id' => $reservation->id, 'reservation_id' => $reservation->id,
'user_id' => $reservation->customer_id,
'send_at' => $sendAt, 'send_at' => $sendAt,
'sent_at' => null, 'sent_at' => null,
]); ]);
@@ -563,10 +567,11 @@ class ReservationController extends Controller
{ {
Notification::create([ Notification::create([
'user_id' => $reservation->customer_id, 'user_id' => $reservation->customer_id,
'type' => $type, 'type' => 'reservation_status',
'title' => $title, 'title' => $title,
'body' => $body, 'body' => $body,
'data_json' => [ 'data_json' => [
'event' => $type,
'reservation_id' => $reservation->id, 'reservation_id' => $reservation->id,
'venue_id' => $reservation->venue_id, 'venue_id' => $reservation->venue_id,
'status' => $reservation->status, 'status' => $reservation->status,
@@ -596,6 +601,9 @@ class ReservationController extends Controller
return; return;
} }
if (Schema::hasColumn('users', 'strike_count')
&& Schema::hasColumn('users', 'blocked_until')
&& Schema::hasColumn('users', 'blocked_permanent')) {
$customer->strike_count = $customer->strike_count + 1; $customer->strike_count = $customer->strike_count + 1;
if ($customer->strike_count >= 9) { if ($customer->strike_count >= 9) {
@@ -607,6 +615,66 @@ class ReservationController extends Controller
} }
$customer->save(); $customer->save();
return;
}
if (! Schema::hasTable('user_strikes') || ! Schema::hasTable('user_blocks')) {
return;
}
UserStrike::create([
'user_id' => $customer->id,
'type' => 'no_show',
'created_at' => now(),
]);
$strikeCount = UserStrike::query()
->where('user_id', $customer->id)
->where('type', 'no_show')
->count();
UserBlock::query()
->where('user_id', $customer->id)
->where('is_active', true)
->update(['is_active' => false]);
if ($strikeCount >= 9) {
UserBlock::create([
'user_id' => $customer->id,
'level' => 'permanent',
'reason' => 'Exceeded no-show threshold',
'blocked_until' => null,
'created_by' => 'system',
'is_active' => true,
'created_at' => now(),
]);
return;
}
if ($strikeCount >= 6) {
UserBlock::create([
'user_id' => $customer->id,
'level' => 'month',
'reason' => 'Exceeded no-show threshold',
'blocked_until' => now()->addDays(30),
'created_by' => 'system',
'is_active' => true,
'created_at' => now(),
]);
return;
}
if ($strikeCount >= 3) {
UserBlock::create([
'user_id' => $customer->id,
'level' => 'week',
'reason' => 'Exceeded no-show threshold',
'blocked_until' => now()->addDays(7),
'created_by' => 'system',
'is_active' => true,
'created_at' => now(),
]);
}
} }
private function canCancel(Reservation $reservation): bool private function canCancel(Reservation $reservation): bool

عرض الملف

@@ -3,13 +3,17 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Amenity;
use App\Models\Offer;
use App\Models\Role; use App\Models\Role;
use App\Models\User; use App\Models\User;
use App\Models\Venue; use App\Models\Venue;
use App\Models\VenueImage;
use App\Models\VenueTable; use App\Models\VenueTable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
class VenueController extends Controller class VenueController extends Controller
@@ -23,19 +27,8 @@ class VenueController extends Controller
'per_page' => 'nullable|integer|min:1|max:50', 'per_page' => 'nullable|integer|min:1|max:50',
]); ]);
$query = Venue::query() $query = Venue::query()->select($this->venueSelectColumns());
->select([ $this->applyVenueRelations($query);
'id',
'name',
'type',
'description',
'address_text',
'lat',
'lng',
'amenities',
'image_urls',
'offers',
]);
$query->where('is_active', true); $query->where('is_active', true);
@@ -50,9 +43,12 @@ class VenueController extends Controller
$perPage = $validated['per_page'] ?? 10; $perPage = $validated['per_page'] ?? 10;
$paginator = $query->orderByDesc('id')->paginate($perPage); $paginator = $query->orderByDesc('id')->paginate($perPage);
$data = collect($paginator->items())
->map(fn (Venue $venue) => $this->serializeVenue($venue))
->values();
return response()->json([ return response()->json([
'data' => $paginator->items(), 'data' => $data,
'meta' => [ 'meta' => [
'current_page' => $paginator->currentPage(), 'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(), 'per_page' => $paginator->perPage(),
@@ -66,23 +62,14 @@ class VenueController extends Controller
public function show(int $id) public function show(int $id)
{ {
$venue = Venue::query() $venue = Venue::query()
->select([ ->select($this->venueSelectColumns())
'id',
'name',
'type',
'description',
'address_text',
'lat',
'lng',
'amenities',
'image_urls',
'offers',
])
->where('is_active', true) ->where('is_active', true)
->findOrFail($id); ->findOrFail($id);
$venue->loadMissing($this->venueRelations());
return response()->json([ return response()->json([
'data' => $venue, 'data' => $this->serializeVenue($venue),
]); ]);
} }
@@ -111,24 +98,31 @@ class VenueController extends Controller
'offers.*.is_active' => 'nullable|boolean', 'offers.*.is_active' => 'nullable|boolean',
]); ]);
$offers = $this->sanitizeOffers($validated['offers'] ?? []);
$imageUrls = $this->appendUploadedImages($request, $validated['image_urls'] ?? []);
$venue = Venue::create([ $venue = Venue::create([
$ownerColumn => $user->id, $ownerColumn => $user->id,
'name' => $validated['name'], 'name' => $validated['name'],
'type' => $validated['type'], 'type' => $validated['type'],
'description' => $validated['description'] ?? null, 'description' => $validated['description'] ?? null,
'address_text' => $validated['address_text'] ?? null, 'address_text' => $validated['address_text'] ?? null,
'lat' => $validated['lat'] ?? null, 'lat' => $validated['lat'] ?? 0,
'lng' => $validated['lng'] ?? null, 'lng' => $validated['lng'] ?? 0,
'is_active' => true, 'is_active' => true,
'phone' => $validated['phone'] ?? null, 'phone' => $validated['phone'] ?? null,
'amenities' => $validated['amenities'] ?? [], ...$this->inlineVenuePayload($validated['amenities'] ?? [], $imageUrls, $offers),
'image_urls' => $validated['image_urls'] ?? [],
'offers' => $this->sanitizeOffers($validated['offers'] ?? []),
]); ]);
$this->syncStructuredVenueData(
$venue,
$validated['amenities'] ?? [],
$imageUrls,
$offers
);
$venue = $this->reloadVenueForResponse($venue->id);
return response()->json([ return response()->json([
'message' => 'Venue created successfully', 'message' => 'Venue created successfully',
'venue' => $venue, 'venue' => $this->serializeVenue($venue),
], 201); ], 201);
} }
@@ -143,25 +137,15 @@ class VenueController extends Controller
$query = Venue::query() $query = Venue::query()
->select([ ->select([
'id',
DB::raw($ownerColumn . ' as vendor_id'), DB::raw($ownerColumn . ' as vendor_id'),
'name', ...$this->venueSelectColumns(),
'type',
'description',
'address_text',
'lat',
'lng',
'is_active',
'phone',
'amenities',
'image_urls',
'offers',
]) ])
->withCount([ ->withCount([
'tables as table_count' => function ($q) { 'tables as table_count' => function ($q) {
$q->where('is_active', true); $q->where('is_active', true);
}, },
]); ]);
$this->applyVenueRelations($query);
if (! empty($validated['search'])) { if (! empty($validated['search'])) {
$query->where('name', 'like', '%' . $validated['search'] . '%'); $query->where('name', 'like', '%' . $validated['search'] . '%');
@@ -169,9 +153,12 @@ class VenueController extends Controller
$perPage = $validated['per_page'] ?? 20; $perPage = $validated['per_page'] ?? 20;
$paginator = $query->orderByDesc('id')->paginate($perPage); $paginator = $query->orderByDesc('id')->paginate($perPage);
$data = collect($paginator->items())
->map(fn (Venue $venue) => $this->serializeVenue($venue, true))
->values();
return response()->json([ return response()->json([
'data' => $paginator->items(), 'data' => $data,
'meta' => [ 'meta' => [
'current_page' => $paginator->currentPage(), 'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(), 'per_page' => $paginator->perPage(),
@@ -225,7 +212,7 @@ class VenueController extends Controller
'last_name' => 'User', 'last_name' => 'User',
'email' => $validated['vendor_email'], 'email' => $validated['vendor_email'],
'phone' => $validated['vendor_phone'], 'phone' => $validated['vendor_phone'],
'password' => Hash::make($validated['vendor_password']), 'password_hash' => Hash::make($validated['vendor_password']),
]); ]);
$vendor->roles()->syncWithoutDetaching([$vendorRole->id]); $vendor->roles()->syncWithoutDetaching([$vendorRole->id]);
$vendorId = $vendor->id; $vendorId = $vendor->id;
@@ -245,6 +232,7 @@ class VenueController extends Controller
$ownerColumn = Venue::ownerColumn(); $ownerColumn = Venue::ownerColumn();
$imageUrls = $validated['image_urls'] ?? []; $imageUrls = $validated['image_urls'] ?? [];
$imageUrls = $this->appendUploadedImages($request, $imageUrls); $imageUrls = $this->appendUploadedImages($request, $imageUrls);
$offers = $this->sanitizeOffers($validated['offers'] ?? []);
$venue = Venue::create([ $venue = Venue::create([
$ownerColumn => $vendorId, $ownerColumn => $vendorId,
@@ -252,25 +240,28 @@ class VenueController extends Controller
'type' => $validated['type'], 'type' => $validated['type'],
'description' => $validated['description'] ?? null, 'description' => $validated['description'] ?? null,
'address_text' => $validated['address_text'] ?? null, 'address_text' => $validated['address_text'] ?? null,
'lat' => $validated['lat'] ?? null, 'lat' => $validated['lat'] ?? 0,
'lng' => $validated['lng'] ?? null, 'lng' => $validated['lng'] ?? 0,
'is_active' => $validated['is_active'] ?? true, 'is_active' => $validated['is_active'] ?? true,
'phone' => $validated['phone'] ?? null, 'phone' => $validated['phone'] ?? null,
'amenities' => $validated['amenities'] ?? [], ...$this->inlineVenuePayload($validated['amenities'] ?? [], $imageUrls, $offers),
'image_urls' => $imageUrls,
'offers' => $this->sanitizeOffers($validated['offers'] ?? []),
]); ]);
$this->syncStructuredVenueData(
$venue,
$validated['amenities'] ?? [],
$imageUrls,
$offers
);
$this->syncVenueTables($venue, (int) ($validated['table_count'] ?? 4)); $this->syncVenueTables($venue, (int) ($validated['table_count'] ?? 4));
}); });
$venue = $this->reloadVenueForResponse($venue->id);
return response()->json([ return response()->json([
'message' => 'Venue created successfully', 'message' => 'Venue created successfully',
'venue' => $venue->loadCount([ 'venue' => $this->serializeVenue($venue->loadCount([
'tables as table_count' => function ($q) { 'tables as table_count' => fn ($q) => $q->where('is_active', true),
$q->where('is_active', true); ]), true),
},
]),
], 201); ], 201);
} }
@@ -307,26 +298,62 @@ class VenueController extends Controller
$validated['offers'] = $this->sanitizeOffers($validated['offers'] ?? []); $validated['offers'] = $this->sanitizeOffers($validated['offers'] ?? []);
} }
$baseImageUrls = array_key_exists('image_urls', $validated) $hasAmenityInput = array_key_exists('amenities', $validated);
$hasOfferInput = array_key_exists('offers', $validated);
$hasImageInput = array_key_exists('image_urls', $validated) || $request->hasFile('images');
$resolvedImageUrls = $hasImageInput
? $this->appendUploadedImages(
$request,
array_key_exists('image_urls', $validated)
? ($validated['image_urls'] ?? []) ? ($validated['image_urls'] ?? [])
: ($venue->image_urls ?? []); : $this->resolveImageUrls($venue)
$validated['image_urls'] = $this->appendUploadedImages($request, $baseImageUrls); )
: null;
$venue->fill($validated); $payload = collect($validated)->only([
'vendor_id',
'name',
'type',
'description',
'address_text',
'lat',
'lng',
'is_active',
'phone',
])->all();
if ($this->hasInlineVenueArrays()) {
if ($hasAmenityInput) {
$payload['amenities'] = $validated['amenities'] ?? [];
}
if ($hasOfferInput) {
$payload['offers'] = $validated['offers'] ?? [];
}
if ($hasImageInput) {
$payload['image_urls'] = $resolvedImageUrls ?? [];
}
}
$venue->fill($payload);
$venue->save(); $venue->save();
$this->syncStructuredVenueData(
$venue,
$hasAmenityInput ? ($validated['amenities'] ?? []) : null,
$hasImageInput ? ($resolvedImageUrls ?? []) : null,
$hasOfferInput ? ($validated['offers'] ?? []) : null
);
if (array_key_exists('table_count', $validated)) { if (array_key_exists('table_count', $validated)) {
$this->syncVenueTables($venue, (int) $validated['table_count']); $this->syncVenueTables($venue, (int) $validated['table_count']);
} }
}); });
$venue = $this->reloadVenueForResponse($venue->id)->loadCount([
'tables as table_count' => fn ($q) => $q->where('is_active', true),
]);
return response()->json([ return response()->json([
'message' => 'Venue updated successfully', 'message' => 'Venue updated successfully',
'venue' => $venue->loadCount([ 'venue' => $this->serializeVenue($venue, true),
'tables as table_count' => function ($q) {
$q->where('is_active', true);
},
]),
]); ]);
} }
@@ -369,7 +396,7 @@ class VenueController extends Controller
VenueTable::create([ VenueTable::create([
'venue_id' => $venue->id, 'venue_id' => $venue->id,
'seating_area_id' => null, 'seating_area_id' => null,
'name' => 'Table ' . $i, VenueTable::labelColumn() => 'Table ' . $i,
'capacity' => 4, 'capacity' => 4,
'is_active' => true, 'is_active' => true,
]); ]);
@@ -429,4 +456,207 @@ class VenueController extends Controller
return $urls; return $urls;
} }
private function venueSelectColumns(): array
{
$columns = [
'id',
'name',
'type',
'description',
'address_text',
'lat',
'lng',
'is_active',
'phone',
];
if ($this->hasInlineVenueArrays()) {
$columns[] = 'amenities';
$columns[] = 'image_urls';
$columns[] = 'offers';
}
return $columns;
}
private function venueRelations(): array
{
if (! $this->hasStructuredVenueData()) {
return [];
}
return [
'images:id,venue_id,url,sort_order',
'offersRelation:id,venue_id,title,description,image_url,is_active,start_at,end_at',
'amenitiesRelation:id,name',
];
}
private function applyVenueRelations($query): void
{
$relations = $this->venueRelations();
if (! empty($relations)) {
$query->with($relations);
}
}
private function reloadVenueForResponse(int $venueId): Venue
{
$query = Venue::query()->select($this->venueSelectColumns());
$this->applyVenueRelations($query);
return $query->findOrFail($venueId);
}
private function serializeVenue(Venue $venue, bool $includeAdminFields = false): array
{
$data = [
'id' => $venue->id,
'name' => $venue->name,
'type' => $venue->type,
'description' => $venue->description,
'address_text' => $venue->address_text,
'lat' => $venue->lat,
'lng' => $venue->lng,
'phone' => $venue->phone,
'is_active' => (bool) $venue->is_active,
'amenities' => $this->resolveAmenities($venue),
'image_urls' => $this->resolveImageUrls($venue),
'offers' => $this->resolveOffers($venue),
];
if ($includeAdminFields) {
$ownerColumn = Venue::ownerColumn();
$data['vendor_id'] = $venue->vendor_id ?? $venue->{$ownerColumn} ?? null;
$data['table_count'] = $venue->table_count ?? null;
}
return $data;
}
private function resolveAmenities(Venue $venue): array
{
if ($this->hasInlineVenueArrays()) {
return is_array($venue->amenities) ? $venue->amenities : [];
}
if ($this->hasStructuredVenueData()) {
return ($venue->amenitiesRelation ?? collect())
->pluck('name')
->values()
->all();
}
return [];
}
private function resolveImageUrls(Venue $venue): array
{
if ($this->hasInlineVenueArrays()) {
return is_array($venue->image_urls) ? $venue->image_urls : [];
}
if ($this->hasStructuredVenueData()) {
return ($venue->images ?? collect())
->sortBy('sort_order')
->pluck('url')
->values()
->all();
}
return [];
}
private function resolveOffers(Venue $venue): array
{
if ($this->hasInlineVenueArrays()) {
return is_array($venue->offers) ? $venue->offers : [];
}
if ($this->hasStructuredVenueData()) {
return ($venue->offersRelation ?? collect())
->map(fn (Offer $offer) => [
'title' => $offer->title,
'description' => $offer->description,
'image_url' => $offer->image_url,
'is_active' => (bool) $offer->is_active,
])
->values()
->all();
}
return [];
}
private function hasInlineVenueArrays(): bool
{
return Schema::hasColumn('venues', 'amenities')
&& Schema::hasColumn('venues', 'image_urls')
&& Schema::hasColumn('venues', 'offers');
}
private function hasStructuredVenueData(): bool
{
return Schema::hasTable('venue_images')
&& Schema::hasTable('offers')
&& Schema::hasTable('venue_amenities')
&& Schema::hasTable('amenities');
}
private function inlineVenuePayload(array $amenities, array $imageUrls, array $offers): array
{
if (! $this->hasInlineVenueArrays()) {
return [];
}
return [
'amenities' => $amenities,
'image_urls' => $imageUrls,
'offers' => $offers,
];
}
private function syncStructuredVenueData(Venue $venue, ?array $amenities, ?array $imageUrls, ?array $offers): void
{
if (! $this->hasStructuredVenueData()) {
return;
}
if ($amenities !== null) {
$amenityIds = collect($amenities)
->map(fn ($name) => trim((string) $name))
->filter(fn ($name) => $name !== '')
->map(fn ($name) => Amenity::firstOrCreate(['name' => $name])->id)
->values()
->all();
$venue->amenitiesRelation()->sync($amenityIds);
}
if ($imageUrls !== null) {
VenueImage::query()->where('venue_id', $venue->id)->delete();
foreach (array_values($imageUrls) as $index => $url) {
VenueImage::create([
'venue_id' => $venue->id,
'url' => $url,
'sort_order' => $index,
]);
}
}
if ($offers !== null) {
Offer::query()->where('venue_id', $venue->id)->delete();
foreach ($offers as $offer) {
Offer::create([
'venue_id' => $venue->id,
'title' => $offer['title'],
'description' => $offer['description'] ?: null,
'image_url' => $offer['image_url'] ?: null,
'is_active' => $offer['is_active'] ?? true,
'start_at' => now(),
'end_at' => now()->addDays(30),
]);
}
}
}
} }

17
app/Models/Amenity.php Normal file
عرض الملف

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Amenity extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = [
'name',
];
}

30
app/Models/Offer.php Normal file
عرض الملف

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Offer extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = [
'venue_id',
'title',
'description',
'image_url',
'start_at',
'end_at',
'is_active',
'created_at',
];
protected $casts = [
'is_active' => 'boolean',
'start_at' => 'datetime',
'end_at' => 'datetime',
];
}

عرض الملف

@@ -4,21 +4,43 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Schema;
class ReservationStatusHistory extends Model class ReservationStatusHistory extends Model
{ {
use HasFactory; use HasFactory;
public $timestamps = false; public $timestamps = false;
protected static ?string $resolvedTable = null;
protected $fillable = [ protected $fillable = [
'reservation_id', 'reservation_id',
'old_status', 'old_status',
'new_status', 'new_status',
'changed_by_user_id', 'changed_by_user_id',
'note',
'created_at', 'created_at',
]; ];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = self::tableName();
}
public static function tableName(): string
{
if (self::$resolvedTable !== null) {
return self::$resolvedTable;
}
self::$resolvedTable = Schema::hasTable('reservation_status_histories')
? 'reservation_status_histories'
: 'reservation_status_history';
return self::$resolvedTable;
}
public function reservation() public function reservation()
{ {
return $this->belongsTo(Reservation::class); return $this->belongsTo(Reservation::class);

عرض الملف

@@ -4,15 +4,19 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Schema;
class ReservationTableAssignment extends Model class ReservationTableAssignment extends Model
{ {
use HasFactory; use HasFactory;
public $timestamps = false; public $timestamps = false;
protected static ?string $resolvedTable = null;
protected static ?string $resolvedTableForeignKey = null;
protected $fillable = [ protected $fillable = [
'reservation_id', 'reservation_id',
'table_id',
'venue_table_id', 'venue_table_id',
'assigned_at', 'assigned_at',
]; ];
@@ -21,6 +25,49 @@ class ReservationTableAssignment extends Model
'assigned_at' => 'datetime', 'assigned_at' => 'datetime',
]; ];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = self::tableName();
}
public static function tableName(): string
{
if (self::$resolvedTable !== null) {
return self::$resolvedTable;
}
self::$resolvedTable = Schema::hasTable('reservation_table_assignments')
? 'reservation_table_assignments'
: 'reservation_tables';
return self::$resolvedTable;
}
public static function tableForeignKey(): string
{
if (self::$resolvedTableForeignKey !== null) {
return self::$resolvedTableForeignKey;
}
self::$resolvedTableForeignKey = Schema::hasColumn(self::tableName(), 'venue_table_id')
? 'venue_table_id'
: 'table_id';
return self::$resolvedTableForeignKey;
}
public function setVenueTableIdAttribute($value): void
{
$this->attributes[self::tableForeignKey()] = $value;
}
public function getVenueTableIdAttribute()
{
$column = self::tableForeignKey();
return $this->attributes[$column] ?? null;
}
public function reservation() public function reservation()
{ {
return $this->belongsTo(Reservation::class); return $this->belongsTo(Reservation::class);
@@ -28,6 +75,6 @@ class ReservationTableAssignment extends Model
public function table() public function table()
{ {
return $this->belongsTo(VenueTable::class, 'venue_table_id'); return $this->belongsTo(VenueTable::class, self::tableForeignKey());
} }
} }

عرض الملف

@@ -12,11 +12,15 @@ use App\Models\Reservation;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use App\Models\Notification; use App\Models\Notification;
use App\Models\UserFeedback; use App\Models\UserFeedback;
use App\Models\UserBlock;
use Illuminate\Support\Facades\Schema;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasApiTokens, HasFactory, Notifiable; use HasApiTokens, HasFactory, Notifiable;
protected static ?bool $hasLegacyBlockColumns = null;
protected static ?bool $hasUserBlocksTable = null;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@@ -86,6 +90,7 @@ class User extends Authenticatable
public function isBlocked(): bool public function isBlocked(): bool
{ {
if ($this->hasLegacyBlockColumns()) {
if ($this->blocked_permanent) { if ($this->blocked_permanent) {
return true; return true;
} }
@@ -96,4 +101,42 @@ class User extends Authenticatable
return false; return false;
} }
if (! $this->hasUserBlocksTable()) {
return false;
}
return UserBlock::query()
->where('user_id', $this->id)
->where('is_active', true)
->where(function ($q) {
$q->whereNull('blocked_until')
->orWhere('blocked_until', '>', now());
})
->exists();
}
private function hasLegacyBlockColumns(): bool
{
if (self::$hasLegacyBlockColumns !== null) {
return self::$hasLegacyBlockColumns;
}
self::$hasLegacyBlockColumns = Schema::hasColumn('users', 'blocked_until')
&& Schema::hasColumn('users', 'blocked_permanent')
&& Schema::hasColumn('users', 'strike_count');
return self::$hasLegacyBlockColumns;
}
private function hasUserBlocksTable(): bool
{
if (self::$hasUserBlocksTable !== null) {
return self::$hasUserBlocksTable;
}
self::$hasUserBlocksTable = Schema::hasTable('user_blocks');
return self::$hasUserBlocksTable;
}
} }

32
app/Models/UserBlock.php Normal file
عرض الملف

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class UserBlock extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'user_blocks';
protected $fillable = [
'user_id',
'level',
'reason',
'blocked_until',
'created_by',
'created_by_user_id',
'trigger_venue_id',
'is_active',
'created_at',
];
protected $casts = [
'blocked_until' => 'datetime',
'is_active' => 'boolean',
];
}

23
app/Models/UserStrike.php Normal file
عرض الملف

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class UserStrike extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'user_strikes';
protected $fillable = [
'user_id',
'reservation_id',
'venue_id',
'type',
'created_at',
];
}

عرض الملف

@@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Schema;
use App\Models\Reservation; use App\Models\Reservation;
use App\Models\SeatingArea; use App\Models\SeatingArea;
use App\Models\VenueTable; use App\Models\VenueTable;
use App\Models\Offer;
use App\Models\VenueImage;
use App\Models\Amenity;
class Venue extends Model class Venue extends Model
{ {
@@ -29,6 +32,7 @@ class Venue extends Model
'amenities', 'amenities',
'image_urls', 'image_urls',
'offers', 'offers',
'created_by_admin_id',
]; ];
protected $casts = [ protected $casts = [
@@ -80,4 +84,19 @@ class Venue extends Model
{ {
return $this->hasMany(VenueTable::class); return $this->hasMany(VenueTable::class);
} }
public function images()
{
return $this->hasMany(VenueImage::class);
}
public function offersRelation()
{
return $this->hasMany(Offer::class);
}
public function amenitiesRelation()
{
return $this->belongsToMany(Amenity::class, 'venue_amenities', 'venue_id', 'amenity_id');
}
} }

21
app/Models/VenueImage.php Normal file
عرض الملف

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class VenueImage extends Model
{
use HasFactory;
public $timestamps = false;
protected $table = 'venue_images';
protected $fillable = [
'venue_id',
'url',
'sort_order',
];
}

عرض الملف

@@ -7,6 +7,10 @@
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
"ext-gd": "*",
"ext-intl": "*",
"ext-pdo_pgsql": "*",
"ext-zip": "*",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.3", "laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1" "laravel/tinker": "^2.10.1"

8
composer.lock مولّد
عرض الملف

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ad91ab9bde70e3576f2b64dd11542bc4", "content-hash": "bf1df9d7ebfcbdc683785439b472c678",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -8452,7 +8452,11 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.2" "php": "^8.2",
"ext-gd": "*",
"ext-intl": "*",
"ext-pdo_pgsql": "*",
"ext-zip": "*"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.9.0"