أُنشئ من Tokal/Test
Remove booking pricing and fix venue image delivery
هذا الالتزام موجود في:
@@ -6,8 +6,6 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\Reservation;
|
use App\Models\Reservation;
|
||||||
use App\Models\Notification;
|
use App\Models\Notification;
|
||||||
use App\Models\ReservationStatusHistory;
|
use App\Models\ReservationStatusHistory;
|
||||||
use App\Models\ReservationFee;
|
|
||||||
use App\Models\PricingRule;
|
|
||||||
use App\Models\ReservationTableAssignment;
|
use App\Models\ReservationTableAssignment;
|
||||||
use App\Models\VenueTable;
|
use App\Models\VenueTable;
|
||||||
use App\Models\ReservationReminder;
|
use App\Models\ReservationReminder;
|
||||||
@@ -304,12 +302,11 @@ class ReservationController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$reservation = null;
|
$reservation = null;
|
||||||
$fee = null;
|
|
||||||
$table = null;
|
$table = null;
|
||||||
$reminder = null;
|
$reminder = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::transaction(function () use ($validated, $user, &$reservation, &$fee, &$table, &$reminder) {
|
DB::transaction(function () use ($validated, $user, &$reservation, &$table, &$reminder) {
|
||||||
$reservation = Reservation::create([
|
$reservation = Reservation::create([
|
||||||
'code' => $this->generateCode(),
|
'code' => $this->generateCode(),
|
||||||
'customer_id' => $user->id,
|
'customer_id' => $user->id,
|
||||||
@@ -332,7 +329,6 @@ class ReservationController extends Controller
|
|||||||
'venue_table_id' => $table->id,
|
'venue_table_id' => $table->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$fee = $this->createReservationFee($reservation);
|
|
||||||
$reminder = $this->createReservationReminder($reservation);
|
$reminder = $this->createReservationReminder($reservation);
|
||||||
$this->logStatusChange($reservation, null, 'pending', $user->id);
|
$this->logStatusChange($reservation, null, 'pending', $user->id);
|
||||||
});
|
});
|
||||||
@@ -350,12 +346,6 @@ class ReservationController extends Controller
|
|||||||
'reservation_time' => $reservation->reservation_time,
|
'reservation_time' => $reservation->reservation_time,
|
||||||
'party_size' => $reservation->party_size,
|
'party_size' => $reservation->party_size,
|
||||||
],
|
],
|
||||||
'fee' => [
|
|
||||||
'price_per_person' => $fee->price_per_person,
|
|
||||||
'party_size' => $fee->party_size,
|
|
||||||
'total_amount' => $fee->total_amount,
|
|
||||||
'currency' => $fee->currency,
|
|
||||||
],
|
|
||||||
'table' => [
|
'table' => [
|
||||||
'id' => $table->id,
|
'id' => $table->id,
|
||||||
'name' => $table->name,
|
'name' => $table->name,
|
||||||
@@ -377,60 +367,6 @@ class ReservationController extends Controller
|
|||||||
return $code;
|
return $code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createReservationFee(Reservation $reservation): ReservationFee
|
|
||||||
{
|
|
||||||
$venue = $reservation->venue ?: Venue::find($reservation->venue_id);
|
|
||||||
|
|
||||||
$rule = $this->getPricingRule($venue);
|
|
||||||
$pricePerPerson = $rule ? (float) $rule->price_per_person : 0.0;
|
|
||||||
$total = $pricePerPerson * (int) $reservation->party_size;
|
|
||||||
$currency = env('APP_CURRENCY', 'USD');
|
|
||||||
|
|
||||||
return ReservationFee::create([
|
|
||||||
'reservation_id' => $reservation->id,
|
|
||||||
'pricing_rule_id' => $rule?->id,
|
|
||||||
'price_per_person' => $pricePerPerson,
|
|
||||||
'party_size' => $reservation->party_size,
|
|
||||||
'total_amount' => $total,
|
|
||||||
'currency' => $currency,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getPricingRule(?Venue $venue): ?PricingRule
|
|
||||||
{
|
|
||||||
if (! $venue) {
|
|
||||||
return PricingRule::where('scope', 'global_type')
|
|
||||||
->where('is_active', true)
|
|
||||||
->latest('id')
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
$venueRule = PricingRule::where('scope', 'venue')
|
|
||||||
->where('venue_id', $venue->id)
|
|
||||||
->where('is_active', true)
|
|
||||||
->latest('id')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($venueRule) {
|
|
||||||
return $venueRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
$typeRule = PricingRule::where('scope', 'global_type')
|
|
||||||
->where('venue_type', (string) $venue->type)
|
|
||||||
->where('is_active', true)
|
|
||||||
->latest('id')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($typeRule) {
|
|
||||||
return $typeRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
$durationMinutes = (int) env('RESERVATION_DURATION_MINUTES', 90);
|
$durationMinutes = (int) env('RESERVATION_DURATION_MINUTES', 90);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use Illuminate\Support\Facades\Cache;
|
|||||||
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\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class VenueController extends Controller
|
class VenueController extends Controller
|
||||||
@@ -77,6 +78,21 @@ class VenueController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUBLIC FILE PROXY (serves files from storage/app/public without relying on /public/storage symlink)
|
||||||
|
public function publicFile(string $path)
|
||||||
|
{
|
||||||
|
$normalized = ltrim(trim($path), '/');
|
||||||
|
if ($normalized === '' || str_contains($normalized, '..')) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Storage::disk('public')->exists($normalized)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Storage::disk('public')->response($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
// CREATE VENUE (vendor only)
|
// CREATE VENUE (vendor only)
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
@@ -453,7 +469,8 @@ class VenueController extends Controller
|
|||||||
if ($request->hasFile('images')) {
|
if ($request->hasFile('images')) {
|
||||||
foreach ($request->file('images') as $image) {
|
foreach ($request->file('images') as $image) {
|
||||||
$path = $image->store('venues', 'public');
|
$path = $image->store('venues', 'public');
|
||||||
$urls[] = rtrim($request->getSchemeAndHttpHost(), '/') . '/storage/' . ltrim($path, '/');
|
// Store a relative public-storage path to avoid environment-specific hosts (localhost/127.0.0.1).
|
||||||
|
$urls[] = '/storage/' . ltrim($path, '/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +585,11 @@ class VenueController extends Controller
|
|||||||
private function resolveImageUrls(Venue $venue): array
|
private function resolveImageUrls(Venue $venue): array
|
||||||
{
|
{
|
||||||
if ($this->hasInlineVenueArrays()) {
|
if ($this->hasInlineVenueArrays()) {
|
||||||
return is_array($venue->image_urls) ? $venue->image_urls : [];
|
return collect(is_array($venue->image_urls) ? $venue->image_urls : [])
|
||||||
|
->map(fn ($url) => $this->normalizePublicAssetUrl($url))
|
||||||
|
->filter(fn ($url) => $url !== '')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasStructuredVenueData()) {
|
if ($this->hasStructuredVenueData()) {
|
||||||
@@ -579,6 +600,8 @@ class VenueController extends Controller
|
|||||||
return ($venue->images ?? collect())
|
return ($venue->images ?? collect())
|
||||||
->sortBy('sort_order')
|
->sortBy('sort_order')
|
||||||
->pluck('url')
|
->pluck('url')
|
||||||
|
->map(fn ($url) => $this->normalizePublicAssetUrl($url))
|
||||||
|
->filter(fn ($url) => $url !== '')
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
@@ -589,7 +612,18 @@ class VenueController extends Controller
|
|||||||
private function resolveOffers(Venue $venue): array
|
private function resolveOffers(Venue $venue): array
|
||||||
{
|
{
|
||||||
if ($this->hasInlineVenueArrays()) {
|
if ($this->hasInlineVenueArrays()) {
|
||||||
return is_array($venue->offers) ? $venue->offers : [];
|
return collect(is_array($venue->offers) ? $venue->offers : [])
|
||||||
|
->map(function ($offer) {
|
||||||
|
if (! is_array($offer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$offer['image_url'] = $this->normalizePublicAssetUrl($offer['image_url'] ?? null) ?: null;
|
||||||
|
return $offer;
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->hasStructuredVenueData()) {
|
if ($this->hasStructuredVenueData()) {
|
||||||
@@ -601,7 +635,7 @@ class VenueController extends Controller
|
|||||||
->map(fn (Offer $offer) => [
|
->map(fn (Offer $offer) => [
|
||||||
'title' => $offer->title,
|
'title' => $offer->title,
|
||||||
'description' => $offer->description,
|
'description' => $offer->description,
|
||||||
'image_url' => $offer->image_url,
|
'image_url' => $this->normalizePublicAssetUrl($offer->image_url) ?: null,
|
||||||
'is_active' => (bool) $offer->is_active,
|
'is_active' => (bool) $offer->is_active,
|
||||||
])
|
])
|
||||||
->values()
|
->values()
|
||||||
@@ -712,4 +746,105 @@ class VenueController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizePublicAssetUrl(mixed $value): string
|
||||||
|
{
|
||||||
|
$raw = trim((string) ($value ?? ''));
|
||||||
|
if ($raw === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$appUrl = (string) config('app.url', '');
|
||||||
|
$appHost = parse_url($appUrl, PHP_URL_HOST);
|
||||||
|
$appScheme = parse_url($appUrl, PHP_URL_SCHEME) ?: 'https';
|
||||||
|
|
||||||
|
// Relative public storage paths -> proxy route
|
||||||
|
if (str_starts_with($raw, '/storage/')) {
|
||||||
|
return $this->publicStorageProxyUrl(substr($raw, strlen('/storage/')));
|
||||||
|
}
|
||||||
|
if (str_starts_with($raw, 'storage/')) {
|
||||||
|
return $this->publicStorageProxyUrl(substr($raw, strlen('storage/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! preg_match('#^https?://#i', $raw)) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = parse_url($raw);
|
||||||
|
if (! is_array($parts) || empty($parts['host'])) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = strtolower((string) $parts['host']);
|
||||||
|
$path = (string) ($parts['path'] ?? '');
|
||||||
|
|
||||||
|
// Any URL pointing to /storage/* should be served through the API proxy to avoid /storage symlink issues.
|
||||||
|
if (str_starts_with($path, '/storage/')) {
|
||||||
|
return $this->publicStorageProxyUrl(substr($path, strlen('/storage/')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite local/private hosts to the configured app host.
|
||||||
|
if ($this->isLocalOrPrivateHost($host) && ! empty($appHost)) {
|
||||||
|
$port = parse_url($appUrl, PHP_URL_PORT);
|
||||||
|
$rewritten = $parts;
|
||||||
|
$rewritten['scheme'] = $appScheme;
|
||||||
|
$rewritten['host'] = (string) $appHost;
|
||||||
|
if ($port !== null) {
|
||||||
|
$rewritten['port'] = (int) $port;
|
||||||
|
} else {
|
||||||
|
unset($rewritten['port']);
|
||||||
|
}
|
||||||
|
return $this->buildUrlFromParts($rewritten);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer HTTPS for hosted domains and the current app host.
|
||||||
|
$isHostedGhaymah = str_ends_with($host, '.hosted.ghaymah.systems');
|
||||||
|
if (($isHostedGhaymah || (! empty($appHost) && $host === strtolower((string) $appHost)))
|
||||||
|
&& (($parts['scheme'] ?? 'http') === 'http')) {
|
||||||
|
$parts['scheme'] = 'https';
|
||||||
|
return $this->buildUrlFromParts($parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function publicStorageProxyUrl(string $relativePath): string
|
||||||
|
{
|
||||||
|
$clean = ltrim($relativePath, '/');
|
||||||
|
if ($clean === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = rtrim((string) config('app.url', ''), '/');
|
||||||
|
if ($base === '') {
|
||||||
|
$base = request()->getSchemeAndHttpHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base . '/api/public-files/' . str_replace('%2F', '/', rawurlencode($clean));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isLocalOrPrivateHost(string $host): bool
|
||||||
|
{
|
||||||
|
if (in_array($host, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/^(10\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[0-1])\\.)/', $host) === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildUrlFromParts(array $parts): string
|
||||||
|
{
|
||||||
|
$scheme = (string) ($parts['scheme'] ?? 'https');
|
||||||
|
$host = (string) ($parts['host'] ?? '');
|
||||||
|
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||||
|
$path = (string) ($parts['path'] ?? '');
|
||||||
|
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||||
|
$fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
|
||||||
|
|
||||||
|
return $scheme . '://' . $host . $port . $path . $query . $fragment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('pricing_rules', function (Blueprint $table) {
|
Schema::create('pricing_rules', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->enum('scope', ['global', 'type', 'venue']);
|
// Keep enum values aligned with the PostgreSQL schema (`pricing_scope_enum`).
|
||||||
|
$table->enum('scope', ['global_type', 'venue']);
|
||||||
$table->string('venue_type')->nullable();
|
$table->string('venue_type')->nullable();
|
||||||
$table->foreignId('venue_id')->nullable()->constrained('venues')->nullOnDelete();
|
$table->foreignId('venue_id')->nullable()->constrained('venues')->nullOnDelete();
|
||||||
$table->decimal('price_per_person', 10, 2);
|
$table->decimal('price_per_person', 10, 2);
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ Route::middleware(['auth:sanctum', 'role:customer'])->get('/customer-only', func
|
|||||||
return response()->json(['ok' => true]);
|
return response()->json(['ok' => true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('/public-files/{path}', [VenueController::class, 'publicFile'])
|
||||||
|
->where('path', '.*');
|
||||||
|
|
||||||
Route::get('/venues', [VenueController::class, 'index']);
|
Route::get('/venues', [VenueController::class, 'index']);
|
||||||
Route::get('/venues/{id}', [VenueController::class, 'show']);
|
Route::get('/venues/{id}', [VenueController::class, 'show']);
|
||||||
Route::middleware(['auth:sanctum', 'role:vendor'])->post('/venues', [VenueController::class, 'store']);
|
Route::middleware(['auth:sanctum', 'role:vendor'])->post('/venues', [VenueController::class, 'store']);
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم