'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); $this->toast('Ups, kamu harus login dulu nih buat klaim vouchernya! πŸ”βœ¨ Silakan masuk ke akunmu ya~', 'Waduh Belum Masuk', 'danger'); 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('Yahh, sayang sekali vouchernya udah gak tersedia atau kadaluarsa nih πŸ₯ΊπŸ’” Coba cek voucher lainnya yuk!', 'Voucher Tidak Tersedia', '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('Maaf ya, voucher ini spesial buat tier tertentu aja πŸŒŸπŸ‘‘ Terus tingkatkan tiermu buat dapetin aksesnya!', 'Upgrade Tier Yuk', '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('Waduh, vouchernya gak ketemu nih πŸ•΅οΈβ€β™‚οΈβŒ Mungkin udah dihapus atau ada kesalahan teknis.', 'Voucher Hilang', 'danger'); return; } if ($result === 'stock_empty') { $this->toast('Yahh, kamu telat! Kuota vouchernya udah ludes πŸ˜­πŸ’¨ Jangan sedih, coba buruan klaim voucher yang lain ya!', 'Kehabisan Stok', 'danger'); return; } if ($result === 'limit_reached') { $this->toast('Wow, semangat banget! Tapi kamu udah maksimal klaim voucher ini nih πŸ›‘βœ‹ Bagi-bagi kesempatan ke yang lain ya~', 'Batas Tercapai', 'warning'); return; } if ($result === 'success') { $this->toast( 'Hore! πŸŽ‰ Voucher berhasil diklaim buat kamu! 🎁🎈 Jangan lupa cek menu Voucher di dashboard ya, selamat belanja!', 'Klaim 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) ->through(function (VoucherModel $voucher) { $voucher->formatted_min_purchase = formatCurrencyNumber($voucher->min_purchase, 'Rp'); $voucher->formatted_end_date = formatDateLocalized($voucher->end_date); return $voucher; }); if ($this->selectedVoucher) { $this->selectedVoucher->formatted_min_purchase = formatCurrencyNumber($this->selectedVoucher->min_purchase, 'Rp'); $this->selectedVoucher->formatted_end_date = formatDateLocalized($this->selectedVoucher->end_date); } return view('livewire.home.vouchers', [ 'pageTitle' => 'Voucher', 'pageDesc' => 'Nikmati berbagai voucher menarik biar belanja kamu makin hemat dan happy! πŸ›οΈπŸ’Έβœ¨', 'vouchers' => $vouchers, ]); } }