Remove booking pricing and fix venue image delivery

هذا الالتزام موجود في:
Abdul Kareem
2026-02-22 21:13:34 +03:00
الأصل 090427cf2c
التزام 23fb3a5d92
4 ملفات معدلة مع 145 إضافات و70 حذوفات

عرض الملف

@@ -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;
}
}

عرض الملف

@@ -10,7 +10,8 @@ return new class extends Migration
{
Schema::create('pricing_rules', function (Blueprint $table) {
$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->foreignId('venue_id')->nullable()->constrained('venues')->nullOnDelete();
$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]);
});
Route::get('/public-files/{path}', [VenueController::class, 'publicFile'])
->where('path', '.*');
Route::get('/venues', [VenueController::class, 'index']);
Route::get('/venues/{id}', [VenueController::class, 'show']);
Route::middleware(['auth:sanctum', 'role:vendor'])->post('/venues', [VenueController::class, 'store']);