From b132f86ed7ed42062f3bedc275c3a1afcbfdad55 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 18 Dec 2025 19:27:50 +0700 Subject: [PATCH] refactor: Migrate loyalty tier system from points to spending and remove tier rewards. --- app/Livewire/Forms/Auth/RegisterForm.php | 4 +- .../Forms/Studio/Loyalty/Tier/RewardForm.php | 70 ------- .../Forms/Studio/Loyalty/TierForm.php | 28 +-- .../Forms/Studio/Manage/OrderForm.php | 19 +- app/Livewire/Member/Membership.php | 76 +++----- app/Livewire/Member/Overview.php | 13 +- .../Loyalty/{Tier/Index.php => Tier.php} | 59 +++--- app/Livewire/Studio/Loyalty/Tier/Reward.php | 121 ------------ app/Models/Membership.php | 15 +- app/Models/Tier.php | 71 ++++--- database/factories/MembershipFactory.php | 2 +- database/factories/TierFactory.php | 4 +- .../2025_09_24_141358_create_tiers_table.php | 4 +- ..._09_30_042232_create_memberships_table.php | 2 +- ...11_01_190446_create_tier_rewards_table.php | 32 --- database/seeders/CustomerSeeder.php | 2 +- database/seeders/MembershipSeeder.php | 18 +- database/seeders/TierSeeder.php | 20 +- .../studio/loyalty/tier/reward.blade.php | 92 --------- .../{tier/index.blade.php => tiers.blade.php} | 91 ++------- routes/pages/studio.php | 2 +- tests/Feature/Livewire/Auth/RegisterTest.php | 8 +- .../Livewire/Studio/Loyalty/TierTest.php | 184 +++++++++--------- 23 files changed, 270 insertions(+), 667 deletions(-) delete mode 100644 app/Livewire/Forms/Studio/Loyalty/Tier/RewardForm.php rename app/Livewire/Studio/Loyalty/{Tier/Index.php => Tier.php} (74%) delete mode 100644 app/Livewire/Studio/Loyalty/Tier/Reward.php delete mode 100644 database/migrations/2025_11_01_190446_create_tier_rewards_table.php delete mode 100644 resources/views/livewire/studio/loyalty/tier/reward.blade.php rename resources/views/livewire/studio/loyalty/{tier/index.blade.php => tiers.blade.php} (61%) diff --git a/app/Livewire/Forms/Auth/RegisterForm.php b/app/Livewire/Forms/Auth/RegisterForm.php index d7ccba7..483f988 100644 --- a/app/Livewire/Forms/Auth/RegisterForm.php +++ b/app/Livewire/Forms/Auth/RegisterForm.php @@ -95,7 +95,7 @@ public function auth(): string { $this->validateAll(); - $tier = Tier::orderBy('min_points')->first(); + $tier = Tier::orderBy('min_spending')->first(); $maxUser = User::max('id') + 1; $referralCode = ReferralCode::where('code', $this->referral_code)->first(); @@ -117,7 +117,7 @@ public function auth(): string Membership::create([ 'user_id' => $user->id, 'tier_id' => $tier->id, - 'tier_points' => 10, + 'reward_points' => 10, ]); ReferralCode::create([ diff --git a/app/Livewire/Forms/Studio/Loyalty/Tier/RewardForm.php b/app/Livewire/Forms/Studio/Loyalty/Tier/RewardForm.php deleted file mode 100644 index a8c9127..0000000 --- a/app/Livewire/Forms/Studio/Loyalty/Tier/RewardForm.php +++ /dev/null @@ -1,70 +0,0 @@ - ['required', Rule::exists('tiers', 'id')], - 'name' => ['required', 'string', 'max:100'], - 'value' => ['required', new UnsignedInteger], - ]; - } - - public function validationAttributes(): array - { - return [ - 'name' => 'nama', - 'value' => 'nilia', - ]; - } - - public function setRewards(TierReward $tierReward): void - { - $this->tierReward = $tierReward; - - $this->tier_id = $tierReward->tier_id; - $this->name = $tierReward->name; - $this->value = formatCurrencyNumber($tierReward->value); - } - - public function store(): TierReward - { - $this->validate(); - - return TierReward::create($this->prepareSavedData()); - } - - public function update(): TierReward - { - $this->validate(); - - $this->tierReward->update($this->prepareSavedData()); - - return $this->tierReward; - } - - private function prepareSavedData(): array - { - return [ - 'tier_id' => $this->tier_id, - 'name' => $this->name, - 'value' => parseRupiahToInt($this->value), - ]; - } -} diff --git a/app/Livewire/Forms/Studio/Loyalty/TierForm.php b/app/Livewire/Forms/Studio/Loyalty/TierForm.php index 5155cbc..8271a82 100644 --- a/app/Livewire/Forms/Studio/Loyalty/TierForm.php +++ b/app/Livewire/Forms/Studio/Loyalty/TierForm.php @@ -13,30 +13,30 @@ class TierForm extends Form public string $name = ''; - public string $min_points = ''; + public string $min_spending = ''; - public ?string $max_points = null; + public ?string $max_spending = null; public function rules(): array { return [ 'name' => ['required', 'string', 'max:50'], - 'min_points' => ['required', new UnsignedInteger], - 'max_points' => ['nullable', new UnsignedInteger, 'gte:min_points'], + 'min_spending' => ['required', new UnsignedInteger], + 'max_spending' => ['nullable', new UnsignedInteger, 'gte:min_spending'], ]; } public function withValidator($validator): void { $validator->after(function ($validator) { - $minPoints = parseRupiahToInt($this->min_points); - $maxPoints = $this->max_points ? parseRupiahToInt($this->max_points) : null; + $minSpending = parseRupiahToInt($this->min_spending); + $maxSpending = $this->max_spending ? parseRupiahToInt($this->max_spending) : null; $ignoreId = $this->tier?->id; - $tierErrors = Tier::validateTierPoints($minPoints, $maxPoints, $ignoreId); + $tierErrors = Tier::validateTierPoints($minSpending, $maxSpending, $ignoreId); foreach ($tierErrors as $error) { - $validator->errors()->add('min_points', $error); + $validator->errors()->add('min_spending', $error); } }); } @@ -45,8 +45,8 @@ public function validationAttributes(): array { return [ 'name' => 'nama', - 'min_points' => 'min. poin', - 'max_points' => 'maks. poin', + 'min_spending' => 'min. belanja', + 'max_spending' => 'maks. belanja', ]; } @@ -55,8 +55,8 @@ public function setTier(Tier $tier): void $this->tier = $tier; $this->name = $tier->name; - $this->min_points = $tier->min_points; - $this->max_points = $tier->max_points; + $this->min_spending = formatCurrencyNumber($tier->min_spending); + $this->max_spending = $tier->max_spending ? formatCurrencyNumber($tier->max_spending) : null; } public function store(): Tier @@ -83,8 +83,8 @@ private function prepareSavedData(): array { return [ 'name' => $this->name, - 'min_points' => parseRupiahToInt($this->min_points), - 'max_points' => parseRupiahToInt($this->max_points), + 'min_spending' => parseRupiahToInt($this->min_spending), + 'max_spending' => parseRupiahToInt($this->max_spending), ]; } } diff --git a/app/Livewire/Forms/Studio/Manage/OrderForm.php b/app/Livewire/Forms/Studio/Manage/OrderForm.php index 70d13dc..32a8307 100644 --- a/app/Livewire/Forms/Studio/Manage/OrderForm.php +++ b/app/Livewire/Forms/Studio/Manage/OrderForm.php @@ -6,8 +6,10 @@ use App\Enums\OrderStatus; use App\Enums\PaymentMethod; use App\Enums\PaymentStatus; +use App\Enums\PointRecordType; use App\Models\Order; use App\Models\Payment; +use App\Models\PointRecord; use App\Models\User; use App\Models\Voucher; use App\Rules\UnsignedInteger; @@ -193,12 +195,19 @@ public function store(): array } } - // Update member tier points + // Update member total spending and reward points if ($member && $member->membership) { - $member->membership()->update([ - 'tier_points' => DB::raw("tier_points + $pointsEarned"), - 'reward_points' => DB::raw("reward_points + $pointsEarned"), - 'last_transaction_at' => now(), + $member->membership->increment('total_spending', $total); + $member->membership->increment('reward_points', $pointsEarned); + $member->membership->update(['last_transaction_at' => now()]); + $member->membership->recalculateTier(); + + PointRecord::create([ + 'user_id' => $member->id, + 'description' => 'Pembelian #'.$order->invoice_number, + 'change' => $pointsEarned, + 'is_addition' => true, + 'type' => PointRecordType::REWARD, ]); } }); diff --git a/app/Livewire/Member/Membership.php b/app/Livewire/Member/Membership.php index 2e7b52f..50da508 100644 --- a/app/Livewire/Member/Membership.php +++ b/app/Livewire/Member/Membership.php @@ -2,11 +2,8 @@ namespace App\Livewire\Member; -use App\Enums\PointRecordType; use App\Models\Membership as MembershipModel; -use App\Models\PointRecord; use App\Models\Tier; -use App\Models\TierReward; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; use Livewire\Component; @@ -21,48 +18,41 @@ class Membership extends Component public string $nextTierName = ''; - public string $currentPoints = ''; + public string $currentSpending = ''; - public string $nextTierPoints = ''; + public string $nextTierSpending = ''; - public string $progress = ''; - - public array $rewards = []; - - public array $pointRecords = []; + public string $progress = '0'; public function mount() { - $this->membership = MembershipModel::with(['tier', 'tier.rewards'])->where('user_id', auth()->id())->first(); + $this->membership = MembershipModel::with(['tier'])->where('user_id', auth()->id())->first(); if (! $this->membership) { return; } - $monthlyTierAddition = PointRecord::where('user_id', auth()->id()) - ->where('type', PointRecordType::TIER) - ->where('is_addition', true) - ->whereYear('created_at', now()->year) - ->whereMonth('created_at', now()->month) - ->sum('change'); - - $nextTier = Tier::where('min_points', '>', $this->membership->tier_points) - ->orderBy('min_points', 'asc') + $nextTier = Tier::where('min_spending', '>', $this->membership->total_spending) + ->orderBy('min_spending', 'asc') ->first(); $this->nextTierName = $nextTier ? $nextTier->name : 'Maksimal'; - $this->currentPoints = $this->membership->tier_points; + $this->currentSpending = formatCurrencyNumber($this->membership->total_spending, 'Rp'); - $this->nextTierPoints = $nextTier ? $nextTier->min_points : 'Maksimal'; + $this->nextTierSpending = $nextTier ? formatCurrencyNumber($nextTier->min_spending, 'Rp') : 'Maksimal'; - $this->progress = ($this->membership->tier_points / $nextTier->min_points) * 100; + if ($nextTier) { + $this->progress = ($this->membership->total_spending / $nextTier->min_spending) * 100; + } else { + $this->progress = '100'; + } $this->stats = [ [ - 'title' => 'Poin Tier', - 'value' => formatCurrencyNumber($this->membership->tier_points), - 'description' => '+'.formatCurrencyNumber($monthlyTierAddition).' bulan ini', + 'title' => 'Poin Hadiah', + 'value' => formatCurrencyNumber($this->membership->reward_points, ''), + 'description' => 'Gunakan poin untuk hadiah menarik', ], [ 'title' => 'Tier Sekarang', @@ -70,38 +60,16 @@ public function mount() 'description' => 'Terdaftar sejak '.formatDateLocalized($this->membership->created_at), ], [ - 'title' => 'Tier Selanjutnya', - 'value' => $nextTier ? $nextTier->name : 'Maksimal', - 'description' => $nextTier ? ($nextTier->min_points - $this->membership->tier_points).' poin lagi' : 'Anda sudah berada di tier tertinggi', + 'title' => 'Total Belanja', + 'value' => formatCurrencyNumber($this->membership->total_spending, 'Rp'), + 'description' => 'Akumulasi seluruh transaksi', ], [ - 'title' => 'Poin Hadiah', - 'value' => formatCurrencyNumber($this->membership->reward_points), - 'description' => 'Nikmati hadiah sekarang', + 'title' => 'Tier Selanjutnya', + 'value' => $nextTier ? $nextTier->name : 'Maksimal', + 'description' => $nextTier ? formatCurrencyNumber($nextTier->min_spending - $this->membership->total_spending, 'Rp').' lagi' : 'Anda sudah berada di tier tertinggi', ], ]; - - $this->rewards = TierReward::where('tier_id', $this->membership->tier_id) - ->latest() - ->get() - ->map(fn ($reward) => [ - 'name' => $reward->name, - 'value' => formatCurrencyNumber($reward->value, 'Rp'), - ]) - ->toArray(); - - $this->pointRecords = PointRecord::where('user_id', auth()->id()) - ->latest() - ->limit(10) - ->get() - ->map(fn ($record) => [ - 'date' => formatDateLocalized($record->created_at), - 'description' => $record->description, - 'change' => formatCurrencyNumber($record->change), - 'type' => $record->type->label(), - 'is_addition' => $record->is_addition, - ]) - ->toArray(); } public function render() diff --git a/app/Livewire/Member/Overview.php b/app/Livewire/Member/Overview.php index 9e2c210..383e67d 100644 --- a/app/Livewire/Member/Overview.php +++ b/app/Livewire/Member/Overview.php @@ -2,10 +2,8 @@ namespace App\Livewire\Member; -use App\Enums\PointRecordType; use App\Livewire\Forms\Studio\Loyalty\CustomerForm; use App\Models\Membership; -use App\Models\PointRecord; use App\Models\Tier; use App\Traits\Components\WithToast; use Illuminate\Support\Facades\DB; @@ -51,16 +49,7 @@ public function save() if (! auth()->user()->membership()->exists()) { Membership::create([ 'user_id' => auth()->id(), - 'tier_id' => Tier::orderBy('min_points')->first()->id, - 'tier_points' => 10, - ]); - - PointRecord::create([ - 'user_id' => auth()->id(), - 'description' => 'Pendaftaran', - 'change' => 10, - 'is_addition' => true, - 'type' => PointRecordType::TIER, + 'tier_id' => Tier::orderBy('min_spending')->first()->id, ]); } }); diff --git a/app/Livewire/Studio/Loyalty/Tier/Index.php b/app/Livewire/Studio/Loyalty/Tier.php similarity index 74% rename from app/Livewire/Studio/Loyalty/Tier/Index.php rename to app/Livewire/Studio/Loyalty/Tier.php index e832708..0244400 100644 --- a/app/Livewire/Studio/Loyalty/Tier/Index.php +++ b/app/Livewire/Studio/Loyalty/Tier.php @@ -1,9 +1,10 @@ tiers = TierModel::withCount('memberships') - ->withCount('rewards') - ->orderBy('min_points', 'asc') + ->orderBy('min_spending', 'asc') ->get() - ->map(function ($tier) { - return [ - 'id' => $tier->id, - 'hash' => $tier->hash, - 'name' => $tier->name, - 'min_points' => formatCurrencyNumber($tier->min_points), - 'max_points' => formatCurrencyNumber($tier->max_points), - 'total_members' => formatCurrencyNumber($tier->memberships_count), - 'total_rewards' => $tier->rewards_count, - ]; - }) + ->map(fn (TierModel $tier) => [ + 'id' => $tier->id, + 'hash' => $tier->hash, + 'name' => $tier->name, + 'min_spending' => formatCurrencyNumber($tier->min_spending, 'Rp'), + 'max_spending' => $tier->max_spending ? formatCurrencyNumber($tier->max_spending, 'Rp') : 'Tak terbatas', + 'total_members' => formatCurrencyNumber($tier->memberships_count), + 'top_member' => $this->getTopMemberStats($tier->id), + ]) ->toArray(); } public function getTopMemberStats($tierId): ?array { - $tier = TierModel::with(['memberships.user.customer.orders.items'])->find($tierId); - - if (! $tier) { - return null; - } - - $topMembership = $tier->memberships() + $topMembership = Membership::where('tier_id', $tierId) ->with(['user.customer.orders.items']) - ->orderByDesc('tier_points') + ->orderByDesc('total_spending') ->first(); if (! $topMembership || ! $topMembership->user || ! $topMembership->user->customer) { return null; } - $orders = $topMembership->user->customer->orders; + $customer = $topMembership->user->customer; + $orders = $customer->orders; $totalOrders = $orders->count(); // Sum items @@ -89,8 +82,8 @@ public function getTopMemberStats($tierId): ?array $grandTotal = $orders->sum('total'); return [ - 'name' => $topMembership->user->customer->name, - 'points' => formatCurrencyNumber($topMembership->tier_points), + 'name' => $customer->name, + 'spending' => formatCurrencyNumber($topMembership->total_spending, 'Rp'), 'total_orders' => formatCurrencyNumber($totalOrders), 'items' => [ 'total_perfume' => formatCurrencyNumber($totalPerfume), @@ -111,7 +104,7 @@ public function create(): void $this->form->store(); - // Reorder the entire tier chain to ensure proper max_points + // Reorder the entire tier chain to ensure proper max_spending TierModel::reorderTierChain(); // Reload tiers to reflect the changes @@ -126,13 +119,13 @@ public function update(): void { $this->canOrAbort('update tier'); - $oldMinPoints = $this->form->tier->min_points; - $newMinPoints = parseRupiahToInt($this->form->min_points); + $oldMinSpending = $this->form->tier->min_spending; + $newMinSpending = parseRupiahToInt($this->form->min_spending); $this->form->update(); - // If min_points changed, reorder the entire tier chain - if ($oldMinPoints !== $newMinPoints) { + // If min_spending changed, reorder the entire tier chain + if ($oldMinSpending !== $newMinSpending) { TierModel::reorderTierChain(); } @@ -158,7 +151,7 @@ public function delete(TierModel $tier): void $tier->delete(); - // Reorder tier chain after deletion to ensure proper max_points + // Reorder tier chain after deletion to ensure proper max_spending TierModel::reorderTierChain(); // Reload tiers to reflect the changes @@ -183,7 +176,7 @@ public function openModal(string $method, string $modalTitle, ?string $id = null public function render(): View { - return view('livewire.studio.loyalty.tier.index', [ + return view('livewire.studio.loyalty.tiers', [ 'pageTitle' => 'Tier', ]); } diff --git a/app/Livewire/Studio/Loyalty/Tier/Reward.php b/app/Livewire/Studio/Loyalty/Tier/Reward.php deleted file mode 100644 index be192ec..0000000 --- a/app/Livewire/Studio/Loyalty/Tier/Reward.php +++ /dev/null @@ -1,121 +0,0 @@ -canOrAbort('create tier reward'); - - $reward = $this->form->store(); - - $this->rewards = collect($this->rewards) - ->push( - $reward->only(['tier_id', 'name', 'value']) + ['hash' => $reward->hash] - ) - ->toArray(); - - $this->dispatch('data:tiersUpdated')->to(Tier::class); - - $this->toast('Hadiah berhasil ditambahkan.'); - - Flux::modal('reward-form-modal')->close(); - } - - public function update(): void - { - $this->canOrAbort('update tier reward'); - - $reward = $this->form->update(); - - $updatedReward = $reward->only(['tier_id', 'name', 'value']) + [ - 'hash' => $reward->hash, - ]; - - $this->rewards = collect($this->rewards) - ->map( - fn ($item) => $item['hash'] === $updatedReward['hash'] - ? $updatedReward - : $item - ) - ->toArray(); - - $this->toast('Hadiah berhasil diperbarui.'); - - Flux::modal('reward-form-modal')->close(); - } - - public function delete(TierReward $reward): void - { - $this->canOrAbort('delete tier reward'); - - $reward->delete(); - - $this->rewards = collect($this->rewards) - ->reject(fn ($item) => $item['hash'] === $reward->hash) - ->values() - ->toArray(); - - $this->toast('Hadiah berhasil dihapus.'); - - $this->dispatch('data:tiersUpdated')->to(Tier::class); - - Flux::modal('delete-reward-confirmation')->close(); - } - - #[On('modal:open:reward')] - public function openModal(string $method, string $modalTitle, ?string $tier_id = null, ?string $id = null) - { - $this->resetValidation(); - $this->resetErrorBag(); - - $this->method = $method; - $this->modalTitle = $modalTitle; - - $this->form->tier_id = $tier_id; - - $this->rewards = TierReward::where('tier_id', $tier_id) - ->latest() - ->get() - ->append('hash') - ->toArray(); - - $id && $this->form->setRewards( - TierReward::byHashOrFail($id) - ); - } - - public function render(): View - { - return view('livewire.studio.loyalty.tier.reward', [ - 'pageTitle' => 'Hadiah', - ]); - } -} diff --git a/app/Models/Membership.php b/app/Models/Membership.php index bf09423..758023c 100644 --- a/app/Models/Membership.php +++ b/app/Models/Membership.php @@ -2,6 +2,8 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; @@ -16,7 +18,7 @@ class Membership extends Model protected function casts(): array { return [ - 'tier_points' => 'int', + 'total_spending' => 'int', 'reward_points' => 'int', ]; } @@ -30,4 +32,15 @@ public function tier(): BelongsTo { return $this->belongsTo(Tier::class); } + + #[Scope] + protected function scopeForSpending(Builder $query, $totalSpending): void + { + $query + ->where('min_spending', '<=', $totalSpending) + ->where(function ($q) use ($totalSpending) { + $q->whereNull('max_spending') + ->orWhere('max_spending', '>=', $totalSpending); + }); + } } diff --git a/app/Models/Tier.php b/app/Models/Tier.php index a1ab30c..9635490 100644 --- a/app/Models/Tier.php +++ b/app/Models/Tier.php @@ -21,21 +21,21 @@ class Tier extends Model protected function casts(): array { return [ - 'min_points' => 'int', - 'max_points' => 'int', + 'min_spending' => 'int', + 'max_spending' => 'int', ]; } - public static function isOverlap(int $minPoints, ?int $maxPoints, ?int $ignoreId = null): bool + public static function isOverlap(int $minSpending, ?int $maxSpending, ?int $ignoreId = null): bool { - $maxPointsCheck = $maxPoints ?? PHP_INT_MAX; + $maxSpendingCheck = $maxSpending ?? PHP_INT_MAX; return self::when($ignoreId, fn ($query) => $query->where('id', '!=', $ignoreId)) ->get() - ->contains(function ($tier) use ($minPoints, $maxPointsCheck) { - $tierMax = $tier->max_points ?? PHP_INT_MAX; + ->contains(function ($tier) use ($minSpending, $maxSpendingCheck) { + $tierMax = $tier->max_spending ?? PHP_INT_MAX; - return ($minPoints <= $tierMax) && ($maxPointsCheck >= $tier->min_points); + return ($minSpending <= $tierMax) && ($maxSpendingCheck >= $tier->min_spending); }); } @@ -44,11 +44,6 @@ public function memberships(): HasMany return $this->hasMany(Membership::class); } - public function rewards(): HasMany - { - return $this->hasMany(TierReward::class); - } - public function vouchers(): BelongsToMany { return $this->belongsToMany(Voucher::class); @@ -56,45 +51,45 @@ public function vouchers(): BelongsToMany public static function getHighestTier() { - return self::orderByDesc('min_points')->first(); + return self::orderByDesc('min_spending')->first(); } - public static function updatePreviousTierMaxPoints(int $newMinPoints): void + public static function updatePreviousTierMaxPoints(int $newMinSpending): void { - $previousTier = self::whereNull('max_points') - ->orWhere('max_points', '>=', $newMinPoints) - ->orderByDesc('min_points') + $previousTier = self::whereNull('max_spending') + ->orWhere('max_spending', '>=', $newMinSpending) + ->orderByDesc('min_spending') ->first(); - if ($previousTier && is_null($previousTier->max_points)) { - $previousTier->update(['max_points' => $newMinPoints - 1]); + if ($previousTier && is_null($previousTier->max_spending)) { + $previousTier->update(['max_spending' => $newMinSpending - 1]); } } - public static function validateTierPoints(int $minPoints, ?int $maxPoints = null, ?int $ignoreId = null): array + public static function validateTierPoints(int $minSpending, ?int $maxSpending = null, ?int $ignoreId = null): array { $errors = []; // Get the highest tier (excluding current if updating) $highestTier = self::when($ignoreId, fn ($query) => $query->where('id', '!=', $ignoreId)) - ->orderByDesc('min_points') + ->orderByDesc('min_spending') ->first(); - // If there's a highest tier with null max_points, it should be updated first - if ($highestTier && is_null($highestTier->max_points)) { - if ($minPoints <= $highestTier->min_points) { - $errors[] = "Min. poin harus lebih besar dari {$highestTier->min_points} (tier {$highestTier->name})"; + // If there's a highest tier with null max_spending, it should be updated first + if ($highestTier && is_null($highestTier->max_spending)) { + if ($minSpending <= $highestTier->min_spending) { + $errors[] = "Min. belanja harus lebih besar dari {$highestTier->min_spending} (tier {$highestTier->name})"; } } elseif ($highestTier) { - $requiredMinPoints = ($highestTier->max_points ?? $highestTier->min_points) + 1; - if ($minPoints <= $highestTier->max_points) { - $errors[] = "Min. poin harus lebih besar dari {$highestTier->max_points} (tier {$highestTier->name})"; + $requiredMinSpending = ($highestTier->max_spending ?? $highestTier->min_spending) + 1; + if ($minSpending <= $highestTier->max_spending) { + $errors[] = "Min. belanja harus lebih besar dari {$highestTier->max_spending} (tier {$highestTier->name})"; } } // Check for overlap with existing tiers - if (self::isOverlap($minPoints, $maxPoints, $ignoreId)) { - $errors[] = 'Rentang poin tier tumpang tindih dengan tier lain'; + if (self::isOverlap($minSpending, $maxSpending, $ignoreId)) { + $errors[] = 'Rentang belanja tier tumpang tindih dengan tier lain'; } return $errors; @@ -102,16 +97,16 @@ public static function validateTierPoints(int $minPoints, ?int $maxPoints = null public static function reorderTierChain(): void { - $tiers = self::orderBy('min_points')->get(); + $tiers = self::orderBy('min_spending')->get(); foreach ($tiers as $index => $tier) { if ($index < $tiers->count() - 1) { - // Set max_points to min_points of next tier - 1 + // Set max_spending to min_spending of next tier - 1 $nextTier = $tiers[$index + 1]; - $tier->update(['max_points' => $nextTier->min_points - 1]); + $tier->update(['max_spending' => $nextTier->min_spending - 1]); } else { - // Last tier should have null max_points - $tier->update(['max_points' => null]); + // Last tier should have null max_spending + $tier->update(['max_spending' => null]); } } } @@ -123,7 +118,7 @@ public static function canDeleteTier(int $tierId): array return ['can_delete' => false, 'message' => 'Tier tidak ditemukan']; } - $tiers = self::orderBy('min_points')->get(); + $tiers = self::orderBy('min_spending')->get(); $tierIndex = $tiers->search(fn ($t) => $t->id === $tierId); // Can always delete the lowest tier (first tier) @@ -131,7 +126,7 @@ public static function canDeleteTier(int $tierId): array return ['can_delete' => true, 'message' => '']; } - // Can always delete the highest tier (last tier with null max_points) + // Can always delete the highest tier (last tier with null max_spending) if ($tierIndex === $tiers->count() - 1) { return ['can_delete' => true, 'message' => '']; } @@ -139,7 +134,7 @@ public static function canDeleteTier(int $tierId): array // Cannot delete middle tiers as it would create gaps in the tier chain return [ 'can_delete' => false, - 'message' => 'Tidak dapat menghapus tier tengah karena akan mengacaukan sistem poin. Tier harus membentuk rantai yang kontinyu.', + 'message' => 'Tidak dapat menghapus tier tengah karena akan mengacaukan sistem belanja. Tier harus membentuk rantai yang kontinyu.', ]; } } diff --git a/database/factories/MembershipFactory.php b/database/factories/MembershipFactory.php index b3f2aef..7ecba95 100644 --- a/database/factories/MembershipFactory.php +++ b/database/factories/MembershipFactory.php @@ -16,7 +16,7 @@ public function definition(): array return [ 'user_id' => User::factory(), 'tier_id' => Tier::factory(), - 'tier_points' => $this->faker->numberBetween(0, 1000), + 'total_spending' => $this->faker->numberBetween(0, 5000000), 'reward_points' => $this->faker->numberBetween(0, 1000), 'last_transaction_at' => $this->faker->optional()->dateTimeBetween('-30 days', 'now'), ]; diff --git a/database/factories/TierFactory.php b/database/factories/TierFactory.php index f7e8eb0..e2a4a6c 100644 --- a/database/factories/TierFactory.php +++ b/database/factories/TierFactory.php @@ -13,8 +13,8 @@ public function definition(): array { return [ 'name' => $this->faker->name(), - 'min_points' => $this->faker->randomNumber(), - 'max_points' => $this->faker->optional()->randomNumber(), + 'min_spending' => $this->faker->randomNumber(), + 'max_spending' => $this->faker->optional()->randomNumber(), ]; } } diff --git a/database/migrations/2025_09_24_141358_create_tiers_table.php b/database/migrations/2025_09_24_141358_create_tiers_table.php index dc43561..9539864 100644 --- a/database/migrations/2025_09_24_141358_create_tiers_table.php +++ b/database/migrations/2025_09_24_141358_create_tiers_table.php @@ -14,8 +14,8 @@ public function up(): void Schema::create('tiers', function (Blueprint $table) { $table->id(); $table->string('name', 50); - $table->unsignedInteger('min_points'); - $table->unsignedInteger('max_points')->nullable(); + $table->unsignedBigInteger('min_spending'); + $table->unsignedBigInteger('max_spending')->nullable(); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); $table->softDeletes(); diff --git a/database/migrations/2025_09_30_042232_create_memberships_table.php b/database/migrations/2025_09_30_042232_create_memberships_table.php index c4eb12b..8e08c11 100644 --- a/database/migrations/2025_09_30_042232_create_memberships_table.php +++ b/database/migrations/2025_09_30_042232_create_memberships_table.php @@ -15,7 +15,7 @@ public function up(): void $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->foreignId('tier_id')->constrained()->cascadeOnDelete(); - $table->unsignedInteger('tier_points')->default(0); + $table->unsignedBigInteger('total_spending')->default(0); $table->unsignedInteger('reward_points')->default(0); $table->timestamp('last_transaction_at')->nullable(); $table->timestamp('created_at')->useCurrent(); diff --git a/database/migrations/2025_11_01_190446_create_tier_rewards_table.php b/database/migrations/2025_11_01_190446_create_tier_rewards_table.php deleted file mode 100644 index 2b7808c..0000000 --- a/database/migrations/2025_11_01_190446_create_tier_rewards_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->foreignId('tier_id')->constrained()->cascadeOnDelete(); - $table->string('name', 100); - $table->unsignedInteger('value'); - $table->timestamp('created_at')->useCurrent(); - $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); - $table->softDeletes(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('tier_rewards'); - } -}; diff --git a/database/seeders/CustomerSeeder.php b/database/seeders/CustomerSeeder.php index a1d4d32..bec7ba8 100644 --- a/database/seeders/CustomerSeeder.php +++ b/database/seeders/CustomerSeeder.php @@ -10,7 +10,7 @@ class CustomerSeeder extends Seeder { public function run(): void { - for ($i = 0; $i < 500; $i++) { + for ($i = 0; $i < 10; $i++) { $hasUser = fake()->boolean(70); $userId = null; diff --git a/database/seeders/MembershipSeeder.php b/database/seeders/MembershipSeeder.php index 904b1d3..3fa73b5 100644 --- a/database/seeders/MembershipSeeder.php +++ b/database/seeders/MembershipSeeder.php @@ -14,24 +14,28 @@ public function run(): void $tiers = Tier::all(); $userIds = Customer::whereNotNull('user_id') - ->pluck('id') + ->pluck('user_id') ->unique() ->toArray(); foreach ($userIds as $userId) { - $totalPoints = fake()->numberBetween(0, 10000); + $totalSpending = fake()->numberBetween(0, 15000000); - $tier = $tiers->first(function ($tier) use ($totalPoints) { - $min = $tier->min_points; - $max = $tier->max_points ?? PHP_INT_MAX; + $tier = $tiers->first(function ($tier) use ($totalSpending) { + $min = $tier->min_spending; + $max = $tier->max_spending ?? PHP_INT_MAX; - return $totalPoints >= $min && $totalPoints <= $max; + return $totalSpending >= $min && $totalSpending <= $max; }); + if (! $tier) { + $tier = $tiers->first(); // Default to Bronze + } + Membership::create([ 'user_id' => $userId, 'tier_id' => $tier->id, - 'tier_points' => $totalPoints, + 'total_spending' => $totalSpending, ]); } } diff --git a/database/seeders/TierSeeder.php b/database/seeders/TierSeeder.php index 5d2595d..b1a8cf3 100644 --- a/database/seeders/TierSeeder.php +++ b/database/seeders/TierSeeder.php @@ -12,28 +12,28 @@ public function run(): void $tiers = [ [ 'name' => 'Bronze', - 'min_points' => 0, - 'max_points' => 49, + 'min_spending' => 0, + 'max_spending' => 499999, ], [ 'name' => 'Silver', - 'min_points' => 50, - 'max_points' => 99, + 'min_spending' => 500000, + 'max_spending' => 1499999, ], [ 'name' => 'Gold', - 'min_points' => 100, - 'max_points' => 499, + 'min_spending' => 1500000, + 'max_spending' => 4999999, ], [ 'name' => 'Platinum', - 'min_points' => 500, - 'max_points' => 1999, + 'min_spending' => 5000000, + 'max_spending' => 9999999, ], [ 'name' => 'Diamond', - 'min_points' => 2000, - 'max_points' => null, + 'min_spending' => 10000000, + 'max_spending' => null, ], ]; diff --git a/resources/views/livewire/studio/loyalty/tier/reward.blade.php b/resources/views/livewire/studio/loyalty/tier/reward.blade.php deleted file mode 100644 index 01e76fc..0000000 --- a/resources/views/livewire/studio/loyalty/tier/reward.blade.php +++ /dev/null @@ -1,92 +0,0 @@ -
- -
- {{ $modalTitle }} - - - - Nama - Nilai - Aksi - - - - @foreach ($rewards as $reward) - - {{ $reward['name'] }} - {{ formatCurrencyNumber($reward['value'], 'Rp') }} - -
- @can('update tier reward') - - - - - - - @endcan - - @can('update tier reward') - - - - - - - @endcan -
-
-
- @endforeach -
-
-
-
- - -
- {{ $modalTitle }} - - - - - Nama * - - - - - - Nilai * - - Nilai hanya berupa rupiah, bukan persentase. - - Rp - - - - - -
- - - Simpan - -
-
-
- - @include('components.modals.confirmation', [ - 'modalName' => 'delete-reward-confirmation', - 'modalTitle' => 'Apakah Anda yakin?', - 'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.', - 'buttonVariant' => 'primary', - 'buttonColor' => 'danger', - 'buttonText' => 'Ya, Hapus', - ]) -
diff --git a/resources/views/livewire/studio/loyalty/tier/index.blade.php b/resources/views/livewire/studio/loyalty/tiers.blade.php similarity index 61% rename from resources/views/livewire/studio/loyalty/tier/index.blade.php rename to resources/views/livewire/studio/loyalty/tiers.blade.php index 95a639e..cdc6564 100644 --- a/resources/views/livewire/studio/loyalty/tier/index.blade.php +++ b/resources/views/livewire/studio/loyalty/tiers.blade.php @@ -21,23 +21,6 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
- @can('create tier reward') - @if ($tier['total_rewards'] == 0) - - Belum ada hadiah untuk tier ini, silakan tambah terlebih dahulu. - - - - - - Tambah - - - - @endif - @endcan -
{{ $tier['name'] }} @@ -45,12 +28,12 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900
- Min. Poin: - {{ $tier['min_points'] }} + Min. Belanja: + {{ $tier['min_spending'] }}
- Maks. Poin: - {{ $tier['max_points'] }} + Maks. Belanja: + {{ $tier['max_spending'] }}
@@ -60,23 +43,15 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900 {{ $tier['total_members'] }}
-
- Total Hadiah: - {{ $tier['total_rewards'] }} -
- - @php - $topMember = $this->getTopMemberStats($tier['id']); - @endphp - @if ($topMember) + @if ($tier['top_member'])
- Member Dengan Poin + Member Dengan Belanja Tertinggi:
- {{ $topMember['name'] }} - {{ $topMember['points'] }} Poin + {{ $tier['top_member']['name'] }} + {{ $tier['top_member']['spending'] }}
Statistik Order
Order: - {{ $topMember['total_orders'] }}x + {{ $tier['top_member']['total_orders'] }}x
Parfum Yang Dibeli: - {{ $topMember['items']['total_perfume'] }} ml + {{ $tier['top_member']['items']['total_perfume'] }} ml
Produk Yang Dibeli: - {{ $topMember['items']['total_product'] }} pcs + {{ $tier['top_member']['items']['total_product'] }} pcs
Botol Yang Dibeli: - {{ $topMember['items']['total_bottle'] }} pcs + {{ $tier['top_member']['items']['total_bottle'] }} pcs
Subtotal Belanja: - {{ $topMember['total']['subtotal'] }} + {{ $tier['top_member']['total']['subtotal'] }}
Diskon: - {{ $topMember['total']['discount'] }} + {{ $tier['top_member']['total']['discount'] }}
Total Belanja: - {{ $topMember['total']['grand_total'] }} + {{ $tier['top_member']['total']['grand_total'] }}
@@ -116,26 +91,6 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
- @can('view tier reward') - - - - - - - @endcan - - @can('create tier reward') - - - - - - - @endcan - @can('update tier') @@ -182,17 +137,17 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t - Min. Poin * - Min. Belanja * + - + - Maks. Poin - Maks. Belanja + - +
@@ -203,8 +158,4 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
- - @can('create tier reward') - @livewire('studio.loyalty.tier.reward') - @endcan diff --git a/routes/pages/studio.php b/routes/pages/studio.php index cdf1d9d..b91cb64 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -25,7 +25,7 @@ use App\Livewire\Studio\Information\Faq as FaqComponent; use App\Livewire\Studio\Information\PriceRequest as PriceRequestComponent; use App\Livewire\Studio\Loyalty\Customer as CustomerComponent; -use App\Livewire\Studio\Loyalty\Tier\Index as TierComponent; +use App\Livewire\Studio\Loyalty\Tier as TierComponent; use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate; use App\Livewire\Studio\Loyalty\Voucher\Edit as VoucherEdit; use App\Livewire\Studio\Loyalty\Voucher\Index as VoucherIndex; diff --git a/tests/Feature/Livewire/Auth/RegisterTest.php b/tests/Feature/Livewire/Auth/RegisterTest.php index e9e969e..c301b67 100644 --- a/tests/Feature/Livewire/Auth/RegisterTest.php +++ b/tests/Feature/Livewire/Auth/RegisterTest.php @@ -344,7 +344,6 @@ function makeReferralCode(): ReferralCode expect($user->status)->toBe(UserStatus::ACTIVE); expect($user->hasRole('Customer'))->toBeTrue(); expect(Membership::where('user_id', $user->id)->exists())->toBeTrue(); - expect(PointRecord::where('user_id', $user->id)->exists())->toBeTrue(); expect(ReferralCode::where('user_id', $user->id)->exists())->toBeTrue(); expect(auth()->check())->toBeTrue(); @@ -370,16 +369,17 @@ function makeReferralCode(): ReferralCode |-------------------------------------------------------------------------- */ -it('creates membership with correct tier and points', function () { +it('creates membership with correct tier and initial data', function () { attemptRegistration() ->assertHasNoErrors(); $user = User::where('email', 'info.pangestuyoga@gmail.com')->first(); $membership = Membership::where('user_id', $user->id)->first(); - $tier = Tier::orderBy('min_points')->first(); + $tier = Tier::orderBy('min_spending')->first(); expect($membership->tier_id)->toBe($tier->id); - expect($membership->tier_points)->toBe(10); + expect($membership->total_spending)->toBe(0); + expect($membership->reward_points)->toBe(10); }); it('creates point record for registration', function () { diff --git a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php index 5fd054f..845df42 100644 --- a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php +++ b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php @@ -49,13 +49,13 @@ function mountTierComponent(User $user) it('renders page successfully', function () { mountTierComponent($this->user) - ->assertViewIs('livewire.studio.loyalty.tier.index') + ->assertViewIs('livewire.studio.loyalty.tiers') ->assertViewHas('pageTitle', 'Tier'); }); it('loads tiers on mount', function () { - $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); - $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_points' => 101, 'max_points' => 500]); + $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]); + $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 100001, 'max_spending' => 500000]); $component = mountTierComponent($this->user); @@ -66,18 +66,17 @@ function mountTierComponent(User $user) }); it('displays all tiers in the view', function () { - $tier = TierModel::factory()->create(['name' => 'Gold', 'min_points' => 100, 'max_points' => 500]); + $tier = TierModel::factory()->create(['name' => 'Gold', 'min_spending' => 1000000, 'max_spending' => 5000000]); mountTierComponent($this->user) ->assertSee('Gold') - ->assertSee('100') - ->assertSee('500'); + ->assertSee('1.000.000'); }); -it('sorts tiers by min_points ascending', function () { - $tier3 = TierModel::factory()->create(['name' => 'Platinum', 'min_points' => 1000, 'max_points' => null]); - $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); - $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_points' => 101, 'max_points' => 500]); +it('sorts tiers by min_spending ascending', function () { + $tier3 = TierModel::factory()->create(['name' => 'Platinum', 'min_spending' => 10000000, 'max_spending' => null]); + $tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 1000000]); + $tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 1000001, 'max_spending' => 5000000]); $component = mountTierComponent($this->user); @@ -100,8 +99,8 @@ function mountTierComponent(User $user) mountTierComponent($this->user) ->set('form.name', 'Gold') - ->set('form.min_points', '1000') - ->set('form.max_points', '5000') + ->set('form.min_spending', '1000000') + ->set('form.max_spending', '5000000') ->call('create') ->assertForbidden(); }); @@ -110,8 +109,8 @@ function mountTierComponent(User $user) mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') - ->set('form.min_points', '1.000') - ->set('form.max_points', '5.000') + ->set('form.min_spending', '1.000.000') + ->set('form.max_spending', '5.000.000') ->call('create') ->assertHasNoErrors() ->assertSet('tiers', function ($tiers) { @@ -122,36 +121,36 @@ function mountTierComponent(User $user) $this->assertDatabaseHas('tiers', [ 'name' => 'Gold', - 'min_points' => 1000, - 'max_points' => 5000, + 'min_spending' => 1000000, + 'max_spending' => null, // reorderTierChain makes last tier null ]); }); -it('can create a tier without max_points', function () { +it('can create a tier without max_spending', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Platinum') - ->set('form.min_points', '5.001') - ->set('form.max_points', null) + ->set('form.min_spending', '5.000.001') + ->set('form.max_spending', null) ->call('create') ->assertHasNoErrors(); $this->assertDatabaseHas('tiers', [ 'name' => 'Platinum', - 'min_points' => 5001, - 'max_points' => null, + 'min_spending' => 5000001, + 'max_spending' => null, ]); }); it('sorts tiers after creating new tier', function () { - TierModel::factory()->create(['name' => 'Silver', 'min_points' => 101, 'max_points' => 500]); - TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 500001, 'max_spending' => 1000000]); + TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 500000]); $component = mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') - ->set('form.min_points', '501') - ->set('form.max_points', '1000') + ->set('form.min_spending', '1.000.001') + ->set('form.max_spending', '2.000.000') ->call('create'); expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class); @@ -166,7 +165,7 @@ function mountTierComponent(User $user) ->call('create') ->assertHasErrors([ 'form.name', - 'form.min_points', + 'form.min_spending', ]); }); @@ -174,28 +173,28 @@ function mountTierComponent(User $user) mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', str_repeat('a', 51)) - ->set('form.min_points', '100') + ->set('form.min_spending', '100') ->call('create') ->assertHasErrors(['form.name']); }); -it('validates min_points is unsigned integer when creating tier', function () { +it('validates min_spending is unsigned integer when creating tier', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') - ->set('form.min_points', '-100') + ->set('form.min_spending', '-100') ->call('create') - ->assertHasErrors(['form.min_points']); + ->assertHasErrors(['form.min_spending']); }); -it('validates max_points is unsigned integer when creating tier', function () { +it('validates max_spending is unsigned integer when creating tier', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') - ->set('form.min_points', '100') - ->set('form.max_points', '-50') + ->set('form.min_spending', '100') + ->set('form.max_spending', '-50') ->call('create') - ->assertHasErrors(['form.max_points']); + ->assertHasErrors(['form.max_spending']); }); /* @@ -208,7 +207,7 @@ function mountTierComponent(User $user) $this->ownerRole->revokePermissionTo('update tier'); Gate::define('update tier', fn () => false); - $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]); mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) @@ -218,16 +217,16 @@ function mountTierComponent(User $user) }); it('can update a tier when authorized', function () { - $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); + $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]); mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->assertSet('form.name', 'Bronze') - ->assertSet('form.min_points', '0') - ->assertSet('form.max_points', '100') + ->assertSet('form.min_spending', formatCurrencyNumber($tier->min_spending)) + ->assertSet('form.max_spending', formatCurrencyNumber($tier->max_spending)) ->set('form.name', 'Gold') - ->set('form.min_points', '1.000') - ->set('form.max_points', '5.000') + ->set('form.min_spending', '1.000.000') + ->set('form.max_spending', '5.000.000') ->call('update') ->assertHasNoErrors() ->assertSet('tiers', function ($tiers) use ($tier) { @@ -238,21 +237,21 @@ function mountTierComponent(User $user) $tier->refresh(); expect($tier->name)->toBe('Gold'); - expect($tier->min_points)->toBe(1000); - expect($tier->max_points)->toBe(5000); + expect($tier->min_spending)->toBe(1000000); + expect($tier->max_spending)->toBeNull(); // reorderTierChain makes last tier null }); -it('can update tier without max_points', function () { - $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_points' => 0, 'max_points' => 100]); +it('can update tier without max_spending', function () { + $tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]); mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) - ->set('form.max_points', null) + ->set('form.max_spending', null) ->call('update') ->assertHasNoErrors(); $tier->refresh(); - expect($tier->max_points)->toBeNull(); + expect($tier->max_spending)->toBeNull(); }); it('validates required fields when updating tier', function () { @@ -261,11 +260,11 @@ function mountTierComponent(User $user) mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->set('form.name', '') - ->set('form.min_points', '') + ->set('form.min_spending', '') ->call('update') ->assertHasErrors([ 'form.name', - 'form.min_points', + 'form.min_spending', ]); }); @@ -321,7 +320,7 @@ function mountTierComponent(User $user) $membership = Membership::create([ 'user_id' => $user->id, 'tier_id' => $tier->id, - 'tier_points' => 100, + 'total_spending' => 100000, ]); mountTierComponent($this->user) @@ -345,26 +344,26 @@ function mountTierComponent(User $user) $customer1 = Customer::factory()->create(['user_id' => $user1->id]); $customer2 = Customer::factory()->create(['user_id' => $user2->id]); - Membership::create(['user_id' => $user1->id, 'tier_id' => $tier->id, 'tier_points' => 100]); - Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'tier_points' => 200]); + Membership::create(['user_id' => $user1->id, 'tier_id' => $tier->id, 'total_spending' => 100000]); + Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'total_spending' => 200000]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); - expect($loadedTier->total_members)->toBe(2); + expect($loadedTier->total_members)->toBe('2'); }); it('loads tiers with rewards count', function () { $tier = TierModel::factory()->create(); - TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 1', 'value' => 100]); - TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 2', 'value' => 200]); - TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 3', 'value' => 300]); + TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 1', 'points' => 100]); + TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 2', 'points' => 200]); + TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 3', 'points' => 300]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); - expect((int) $loadedTier->total_rewards)->toBe(3); + expect($loadedTier['total_rewards'])->toBe('3'); }); it('loads top member stats when tier has memberships', function () { @@ -375,17 +374,17 @@ function mountTierComponent(User $user) $customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'John Doe']); $customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Jane Doe']); - // Create memberships with different tier_points + // Create memberships with different total_spending $membership1 = Membership::create([ 'user_id' => $user1->id, 'tier_id' => $tier->id, - 'tier_points' => 500, // Higher points + 'total_spending' => 5000000, // Higher spending ]); $membership2 = Membership::create([ 'user_id' => $user2->id, 'tier_id' => $tier->id, - 'tier_points' => 200, // Lower points + 'total_spending' => 2000000, // Lower spending ]); // Create orders for user1 (top member) @@ -422,7 +421,6 @@ function mountTierComponent(User $user) 'sku' => 'TP1-0001', 'cost_price' => 10000, 'sale_price' => 50000, - 'point_per_ml' => 500, ]); $product1 = Product::create([ 'name' => 'Test Product 1', @@ -430,7 +428,6 @@ function mountTierComponent(User $user) 'sku' => 'PR1-0001', 'cost_price' => 5000, 'sale_price' => 25000, - 'point_per_pcs' => 250, ]); $bottle1 = Bottle::factory()->create(); @@ -472,7 +469,6 @@ function mountTierComponent(User $user) 'sku' => 'TP2-0001', 'cost_price' => 10000, 'sale_price' => 50000, - 'point_per_ml' => 500, ]); OrderItem::create([ 'order_id' => $order2->id, @@ -489,9 +485,9 @@ function mountTierComponent(User $user) $loadedTier = $component->tiers->firstWhere('id', $tier->id); expect($loadedTier->top_member)->not->toBeNull(); expect($loadedTier->top_member['name'])->toBe('John Doe'); - expect($loadedTier->top_member['points'])->toContain('500'); - expect($loadedTier->top_member['total_orders'])->toContain('2'); - expect($loadedTier->top_member['items']['total_perfume'])->toContain('3'); + expect($loadedTier->top_member['spending'])->toContain('5.000.000'); + expect($loadedTier->top_member['total_orders'])->toBe('2'); + expect($loadedTier->top_member['items']['total_perfume'])->toBe('3'); expect($loadedTier->top_member['items']['total_product'])->toContain('3'); expect($loadedTier->top_member['items']['total_bottle'])->toContain('1'); }); @@ -542,23 +538,23 @@ function mountTierComponent(User $user) expect($component->method)->toBe('update'); expect($component->modalTitle)->toBe('Ubah Tier'); expect($component->form->name)->toBe($tier->name); - expect($component->form->min_points)->toBe((string) $tier->min_points); - expect($component->form->max_points)->toBe($tier->max_points ? (string) $tier->max_points : null); + expect($component->form->min_spending)->toBe(formatCurrencyNumber($tier->min_spending)); + expect($component->form->max_spending)->toBe($tier->max_spending ? formatCurrencyNumber($tier->max_spending) : null); }); it('sets form data when opening update modal', function () { $tier = TierModel::factory()->create([ 'name' => 'Gold', - 'min_points' => 1000, - 'max_points' => 5000, + 'min_spending' => 1000000, + 'max_spending' => 5000000, ]); $component = mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash); expect($component->form->name)->toBe('Gold'); - expect($component->form->min_points)->toBe('1000'); - expect($component->form->max_points)->toBe('5000'); + expect($component->form->min_spending)->toBe('1.000.000'); + expect($component->form->max_spending)->toBe('5.000.000'); }); /* @@ -567,31 +563,31 @@ function mountTierComponent(User $user) |-------------------------------------------------------------------------- */ -it('formats points with currency when displaying', function () { +it('formats spending with currency when displaying', function () { $tier = TierModel::factory()->create([ - 'min_points' => 1000, - 'max_points' => 5000, + 'min_spending' => 1000000, + 'max_spending' => 5000000, ]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); - expect(formatCurrencyNumber($loadedTier->min_points))->toContain('1.000'); - expect(formatCurrencyNumber($loadedTier->max_points))->toContain('5.000'); + expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('1.000.000'); + expect(formatCurrencyNumber($loadedTier->max_spending))->toContain('5.000.000'); }); it('handles currency formatting in form input', function () { mountTierComponent($this->user) ->call('openModal', 'create', 'Tambah Tier') ->set('form.name', 'Gold') - ->set('form.min_points', '1.000') - ->set('form.max_points', '5.000') + ->set('form.min_spending', '1.000.000') + ->set('form.max_spending', '5.000.000') ->call('create') ->assertHasNoErrors(); $tier = TierModel::first(); - expect($tier->min_points)->toBe(1000); - expect($tier->max_points)->toBe(5000); + expect($tier->min_spending)->toBe(1000000); + expect($tier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null }); /* @@ -600,30 +596,30 @@ function mountTierComponent(User $user) |-------------------------------------------------------------------------- */ -it('handles tier with null max_points correctly', function () { +it('handles tier with null max_spending correctly', function () { $tier = TierModel::factory()->create([ 'name' => 'Platinum', - 'min_points' => 10000, - 'max_points' => null, + 'min_spending' => 10000000, + 'max_spending' => null, ]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); - expect(formatCurrencyNumber($loadedTier->max_points))->not->toBeNull(); + expect(formatCurrencyNumber($loadedTier->max_spending))->not->toBeNull(); }); -it('handles tier with zero min_points', function () { +it('handles tier with zero min_spending', function () { $tier = TierModel::factory()->create([ 'name' => 'Starter', - 'min_points' => 0, - 'max_points' => 100, + 'min_spending' => 0, + 'max_spending' => 100000, ]); $component = mountTierComponent($this->user); $loadedTier = $component->tiers->firstWhere('id', $tier->id); - expect(formatCurrencyNumber($loadedTier->min_points))->toContain('0'); + expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('0'); }); it('handles empty tiers list', function () { @@ -635,19 +631,19 @@ function mountTierComponent(User $user) it('updates tier array correctly after update', function () { $tier = TierModel::factory()->create([ 'name' => 'Bronze', - 'min_points' => 0, - 'max_points' => 100, + 'min_spending' => 0, + 'max_spending' => 100000, ]); $component = mountTierComponent($this->user) ->call('openModal', 'update', 'Ubah Tier', $tier->hash) ->set('form.name', 'Gold') - ->set('form.min_points', '1.000') - ->set('form.max_points', '5.000') + ->set('form.min_spending', '1.000.000') + ->set('form.max_spending', '5.000.000') ->call('update'); $updatedTier = $component->tiers->firstWhere('hash', $tier->hash); expect($updatedTier->name)->toBe('Gold'); - expect(formatCurrencyNumber($updatedTier->min_points))->toContain('1.000'); - expect(formatCurrencyNumber($updatedTier->max_points))->toContain('5.000'); + expect($updatedTier->formatted_min_spending)->toContain('1.000.000'); + expect($updatedTier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null });