أُنشئ من 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\Notification;
|
||||
use App\Models\ReservationStatusHistory;
|
||||
use App\Models\ReservationFee;
|
||||
use App\Models\PricingRule;
|
||||
use App\Models\ReservationTableAssignment;
|
||||
use App\Models\VenueTable;
|
||||
use App\Models\ReservationReminder;
|
||||
@@ -304,12 +302,11 @@ class ReservationController extends Controller
|
||||
}
|
||||
|
||||
$reservation = null;
|
||||
$fee = null;
|
||||
$table = null;
|
||||
$reminder = null;
|
||||
|
||||
try {
|
||||
DB::transaction(function () use ($validated, $user, &$reservation, &$fee, &$table, &$reminder) {
|
||||
DB::transaction(function () use ($validated, $user, &$reservation, &$table, &$reminder) {
|
||||
$reservation = Reservation::create([
|
||||
'code' => $this->generateCode(),
|
||||
'customer_id' => $user->id,
|
||||
@@ -332,7 +329,6 @@ class ReservationController extends Controller
|
||||
'venue_table_id' => $table->id,
|
||||
]);
|
||||
|
||||
$fee = $this->createReservationFee($reservation);
|
||||
$reminder = $this->createReservationReminder($reservation);
|
||||
$this->logStatusChange($reservation, null, 'pending', $user->id);
|
||||
});
|
||||
@@ -350,12 +346,6 @@ class ReservationController extends Controller
|
||||
'reservation_time' => $reservation->reservation_time,
|
||||
'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' => [
|
||||
'id' => $table->id,
|
||||
'name' => $table->name,
|
||||
@@ -377,60 +367,6 @@ class ReservationController extends Controller
|
||||
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
|
||||
{
|
||||
$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\Hash;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
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)
|
||||
public function store(Request $request)
|
||||
{
|
||||
@@ -453,7 +469,8 @@ class VenueController extends Controller
|
||||
if ($request->hasFile('images')) {
|
||||
foreach ($request->file('images') as $image) {
|
||||
$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
|
||||
{
|
||||
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()) {
|
||||
@@ -579,6 +600,8 @@ class VenueController extends Controller
|
||||
return ($venue->images ?? collect())
|
||||
->sortBy('sort_order')
|
||||
->pluck('url')
|
||||
->map(fn ($url) => $this->normalizePublicAssetUrl($url))
|
||||
->filter(fn ($url) => $url !== '')
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
@@ -589,7 +612,18 @@ class VenueController extends Controller
|
||||
private function resolveOffers(Venue $venue): array
|
||||
{
|
||||
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()) {
|
||||
@@ -601,7 +635,7 @@ class VenueController extends Controller
|
||||
->map(fn (Offer $offer) => [
|
||||
'title' => $offer->title,
|
||||
'description' => $offer->description,
|
||||
'image_url' => $offer->image_url,
|
||||
'image_url' => $this->normalizePublicAssetUrl($offer->image_url) ?: null,
|
||||
'is_active' => (bool) $offer->is_active,
|
||||
])
|
||||
->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;
|
||||
}
|
||||
}
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم