'Voucher', ])] class Voucher extends Component { use WithoutUrlPagination, WithPagination, WithToast; public array $outlets = []; public ?string $search = null; public ?string $outlet_id = null; public ?string $sort = null; public ?VoucherModel $selectedVoucher = null; public bool $showDetailModal = false; public function mount(): void { $this->outlets = Outlet::query()->operational()->pluck('name', 'id')->toArray(); } public function updatedSearch(): void { $this->resetPage(); } public function updatedOutletId(): void { $this->resetPage(); } public function updatedSort(): void { $this->resetPage(); } public function showDetail(VoucherModel $voucher): void { $this->selectedVoucher = $voucher; $this->selectedVoucher->load(['outlets', 'tiers']); $this->showDetailModal = true; } public function closeDetail(): void { $this->showDetailModal = false; $this->selectedVoucher = null; } public function claim(VoucherModel $voucher): void { $user = auth()->user(); if (! auth()->check()) { $this->redirect(route('login'), navigate: true); return; } // 1. Check if voucher exists and is active if ( Carbon::parse($voucher->start_date)->gt(now()) || ( $voucher->end_date && Carbon::parse($voucher->end_date)->lt(now()) ) ) { $this->toast('Voucher tidak tersedia atau sudah kadaluarsa.', 'Gagal', 'danger'); return; } // 2. Check Tier Requirement (before transaction) $userTierId = $user->membership?->tier_id; $voucher->load('tiers'); // Ensure tiers are loaded $voucherTierIds = $voucher->tiers->pluck('id'); if ( $voucherTierIds->isNotEmpty() && (! $userTierId || ! $voucherTierIds->contains($userTierId)) ) { $this->toast('Voucher ini hanya tersedia untuk tier tertentu.', 'Gagal', 'danger'); return; } // 3. Execute claim within transaction for data consistency $result = DB::transaction(function () use ($user, $voucher) { // Lock voucher row to prevent race condition $v = VoucherModel::whereKey($voucher->id) ->lockForUpdate() ->first(); if (! $v) { return 'not_found'; } // Check if available_count (stock) is sufficient // available_count harus > 0 untuk bisa di-claim if ($v->quota !== null && $v->available_count <= 0) { return 'stock_empty'; } // Check how many times this user has claimed this voucher // Using SUM quantity to calculate total voucher that has been claimed $userClaimedQuantity = DB::table('user_voucher') ->where('user_id', $user->id) ->where('voucher_id', $v->id) ->sum('quantity'); // Check if user has reached the limit if ($v->limit_per_user !== null && $userClaimedQuantity >= $v->limit_per_user) { return 'limit_reached'; } // Check if user already has a record for this voucher $existingPivot = DB::table('user_voucher') ->where('user_id', $user->id) ->where('voucher_id', $v->id) ->first(); if ($existingPivot) { // User already has this voucher, increment the quantity DB::table('user_voucher') ->where('user_id', $user->id) ->where('voucher_id', $v->id) ->update([ 'quantity' => $existingPivot->quantity + 1, 'updated_at' => now(), ]); } else { // User doesn't have this voucher yet, create new record $user->vouchers()->attach($v->id, [ 'quantity' => 1, 'created_at' => now(), 'updated_at' => now(), ]); } // Decrease available stock $v->decrement('available_count'); return 'success'; }); // Handle transaction results if ($result === 'not_found') { $this->toast('Voucher tidak ditemukan.', 'Gagal', 'danger'); return; } if ($result === 'stock_empty') { $this->toast('Klaim gagal! Kuota voucher sudah habis.', 'Gagal', 'danger'); return; } if ($result === 'limit_reached') { $this->toast('Anda sudah mencapai batas maksimal klaim voucher ini.', 'Peringatan', 'warning'); return; } if ($result === 'success') { $this->toast( 'Voucher berhasil diklaim! Silakan cek di menu Voucher di halaman dasbor.', 'Berhasil', 'success' ); } } public function render(): View { $vouchers = VoucherModel::query() ->with(['tiers', 'outlets']) ->active() ->show() ->where(function ($query) { $query->where('name', 'like', '%'.$this->search.'%') ->orWhere('code', 'like', '%'.$this->search.'%'); }) ->when($this->outlet_id, function ($query) { $query->whereHas('outlets', function ($q) { $q->where('outlets.id', $this->outlet_id); }); }) ->when($this->sort, function ($query) { match ($this->sort) { 'latest' => $query->orderBy('created_at', 'desc'), 'biggest' => $query->orderBy('discount_amount', 'desc'), 'ending-soon' => $query->orderByRaw('end_date IS NULL')->orderBy('end_date', 'asc'), default => $query->latest(), }; }, fn ($query) => $query->latest()) ->paginate(6); $user = auth()->user(); $userTierId = $user?->membership?->tier_id; $voucherIds = $vouchers->pluck('id'); if ($this->selectedVoucher) { $voucherIds->push($this->selectedVoucher->id); } $userClaimedCounts = $user ? DB::table('user_voucher') ->where('user_id', $user->id) ->whereIn('voucher_id', $voucherIds->unique()) ->select('voucher_id', DB::raw('SUM(quantity) as total_quantity')) ->groupBy('voucher_id') ->pluck('total_quantity', 'voucher_id') ->toArray() : []; return view('livewire.home.vouchers', [ 'pageTitle' => 'Voucher', 'pageDesc' => 'Nikmati berbagai voucher menarik untuk belanja lebih hemat dan menyenangkan.', 'vouchers' => $vouchers, ]); } }