refactor: Migrate loyalty tier system from points to spending and remove tier rewards.
This commit is contained in:
parent
ce7ac20793
commit
b132f86ed7
@ -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([
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms\Studio\Loyalty\Tier;
|
||||
|
||||
use App\Models\TierReward;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class RewardForm extends Form
|
||||
{
|
||||
public ?TierReward $tierReward = null;
|
||||
|
||||
public string $tier_id = '';
|
||||
|
||||
public string $name = '';
|
||||
|
||||
public string $value = '';
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tier_id' => ['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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Loyalty\Tier;
|
||||
namespace App\Livewire\Studio\Loyalty;
|
||||
|
||||
use App\Livewire\Forms\Studio\Loyalty\TierForm;
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\Tier as TierModel;
|
||||
@ -20,7 +21,7 @@
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Tier')]
|
||||
class Index extends Component
|
||||
class Tier extends Component
|
||||
{
|
||||
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
|
||||
|
||||
@ -41,41 +42,33 @@ public function mount(): void
|
||||
public function loadTiers(): void
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Loyalty\Tier;
|
||||
|
||||
use App\Livewire\Forms\Studio\Loyalty\Tier\RewardForm;
|
||||
use App\Livewire\Studio\Loyalty\Tier\Index as Tier;
|
||||
use App\Models\TierReward;
|
||||
use App\Traits\Authorization\WithAuthorization;
|
||||
use App\Traits\Components\WithCloseModal;
|
||||
use App\Traits\Components\WithConfirmation;
|
||||
use App\Traits\Components\WithToast;
|
||||
use App\Traits\Notification\WithSubscribeNotification;
|
||||
use App\Traits\Utilities\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Hadiah')]
|
||||
class Reward extends Component
|
||||
{
|
||||
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
|
||||
|
||||
public RewardForm $form;
|
||||
|
||||
public string $modalTitle = '';
|
||||
|
||||
public string $method = '';
|
||||
|
||||
public array $rewards = [];
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
];
|
||||
|
||||
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tier_rewards', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@ -1,92 +0,0 @@
|
||||
<div>
|
||||
<flux:modal name="reward-view-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto"
|
||||
@close="closeModal('reward-view-modal')">
|
||||
<div class="p-4 space-y-6">
|
||||
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
|
||||
|
||||
<flux:table class="bo">
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Nama</flux:table.column>
|
||||
<flux:table.column>Nilai</flux:table.column>
|
||||
<flux:table.column>Aksi</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@foreach ($rewards as $reward)
|
||||
<flux:table.row>
|
||||
<flux:table.cell>{{ $reward['name'] }}</flux:table.cell>
|
||||
<flux:table.cell>{{ formatCurrencyNumber($reward['value'], 'Rp') }}</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
|
||||
@can('update tier reward')
|
||||
<flux:modal.trigger name="reward-form-modal">
|
||||
<flux:tooltip content="Ubah">
|
||||
<flux:button variant="primary" color="yellow" icon="pencil-square"
|
||||
size="sm"
|
||||
wire:click="$dispatch('modal:open:reward', {method: 'update', 'modalTitle': 'Ubah Hadiah', 'tier_id': '{{ $reward['tier_id'] }}', 'id' : '{{ $reward['hash'] }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@endcan
|
||||
|
||||
@can('update tier reward')
|
||||
<flux:modal.trigger name="delete-reward-confirmation">
|
||||
<flux:tooltip content="Hapus">
|
||||
<flux:button variant="danger" icon="trash" size="sm"
|
||||
wire:click="$dispatch('fn:confirmAction', {id: '{{ $reward['hash'] }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@endcan
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforeach
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
<flux:modal name="reward-form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto"
|
||||
@close="closeModal('reward-form-modal')">
|
||||
<div class="p-4 space-y-6">
|
||||
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
|
||||
|
||||
<flux:input name="form.tier_id" type="hidden"></flux:input>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="Voucher Belanja 20k" wire:model="form.name" autofocus autocomplete="off" />
|
||||
<flux:error name="form.name" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Nilai <span class="text-red-500 ms-1">*</span>
|
||||
</flux:label>
|
||||
<flux:description>Nilai hanya berupa rupiah, bukan persentase.</flux:description>
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix>Rp</flux:input.group.prefix>
|
||||
<flux:input placeholder="xxx" x-mask:dynamic="$money($input, ',')" wire:model="form.value"
|
||||
autocomplete="off" />
|
||||
</flux:input.group>
|
||||
<flux:error name="form.value" />
|
||||
</flux:field>
|
||||
|
||||
<div class="flex">
|
||||
<flux:spacer />
|
||||
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="{{ $method }}">
|
||||
Simpan
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
@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',
|
||||
])
|
||||
</div>
|
||||
@ -21,23 +21,6 @@
|
||||
class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<div class="space-y-4">
|
||||
|
||||
@can('create tier reward')
|
||||
@if ($tier['total_rewards'] == 0)
|
||||
<flux:callout icon="exclamation-circle" variant="warning" inline>
|
||||
<flux:callout.heading>Belum ada hadiah untuk tier ini, silakan tambah terlebih dahulu.
|
||||
</flux:callout.heading>
|
||||
|
||||
<x-slot name="actions">
|
||||
<flux:modal.trigger name="reward-form-modal">
|
||||
<flux:button variant="primary" color="primary" size="sm"
|
||||
wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': 'Tambah Hadiah', 'tier_id': '{{ $tier['id'] }}'})">
|
||||
Tambah</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</x-slot>
|
||||
</flux:callout>
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
<div class="flex items-start justify-between">
|
||||
<flux:heading size="lg">{{ $tier['name'] }}
|
||||
</flux:heading>
|
||||
@ -45,12 +28,12 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">Min. Poin:</span>
|
||||
<span class="font-medium">{{ $tier['min_points'] }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">Min. Belanja:</span>
|
||||
<span class="font-medium">{{ $tier['min_spending'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">Maks. Poin:</span>
|
||||
<span class="font-medium">{{ $tier['max_points'] }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">Maks. Belanja:</span>
|
||||
<span class="font-medium">{{ $tier['max_spending'] }}</span>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
@ -60,23 +43,15 @@ class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900
|
||||
<span class="font-medium">{{ $tier['total_members'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">Total Hadiah:</span>
|
||||
<span class="font-medium">{{ $tier['total_rewards'] }}</span>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$topMember = $this->getTopMemberStats($tier['id']);
|
||||
@endphp
|
||||
@if ($topMember)
|
||||
@if ($tier['top_member'])
|
||||
<div class="text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400 font-semibold">Member Dengan Poin
|
||||
<span class="text-gray-500 dark:text-gray-400 font-semibold">Member Dengan Belanja
|
||||
Tertinggi:</span>
|
||||
|
||||
<div
|
||||
class="mt-1 flex items-center justify-between bg-gradient-to-r from-emerald-400 to-emerald-600 text-white rounded px-3 py-3 text-sm font-medium shadow-lg">
|
||||
<span>{{ $topMember['name'] }}</span>
|
||||
<span>{{ $topMember['points'] }} Poin</span>
|
||||
<span>{{ $tier['top_member']['name'] }}</span>
|
||||
<span>{{ $tier['top_member']['spending'] }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -84,31 +59,31 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
|
||||
<h4 class="font-semibold mb-2">Statistik Order</h4>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>Order:</span>
|
||||
<span>{{ $topMember['total_orders'] }}x</span>
|
||||
<span>{{ $tier['top_member']['total_orders'] }}x</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>Parfum Yang Dibeli:</span>
|
||||
<span>{{ $topMember['items']['total_perfume'] }} ml</span>
|
||||
<span>{{ $tier['top_member']['items']['total_perfume'] }} ml</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>Produk Yang Dibeli:</span>
|
||||
<span>{{ $topMember['items']['total_product'] }} pcs</span>
|
||||
<span>{{ $tier['top_member']['items']['total_product'] }} pcs</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>Botol Yang Dibeli:</span>
|
||||
<span>{{ $topMember['items']['total_bottle'] }} pcs</span>
|
||||
<span>{{ $tier['top_member']['items']['total_bottle'] }} pcs</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>Subtotal Belanja:</span>
|
||||
<span>{{ $topMember['total']['subtotal'] }}</span>
|
||||
<span>{{ $tier['top_member']['total']['subtotal'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>Diskon:</span>
|
||||
<span>{{ $topMember['total']['discount'] }}</span>
|
||||
<span>{{ $tier['top_member']['total']['discount'] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm mt-1">
|
||||
<span>Total Belanja:</span>
|
||||
<span>{{ $topMember['total']['grand_total'] }}</span>
|
||||
<span>{{ $tier['top_member']['total']['grand_total'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -116,26 +91,6 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
|
||||
@can('view tier reward')
|
||||
<flux:modal.trigger name="reward-view-modal">
|
||||
<flux:tooltip content="Lihat Hadiah">
|
||||
<flux:button variant="primary" color="blue" icon="eye" size="sm"
|
||||
wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': '{{ $tier['name'] }}', 'tier_id': '{{ $tier['id'] }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@endcan
|
||||
|
||||
@can('create tier reward')
|
||||
<flux:modal.trigger name="reward-form-modal">
|
||||
<flux:tooltip content="Tambah Hadiah">
|
||||
<flux:button variant="primary" color="primary" icon="plus" size="sm"
|
||||
wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': 'Tambah Hadiah', 'tier_id': '{{ $tier['id'] }}'})">
|
||||
</flux:button>
|
||||
</flux:tooltip>
|
||||
</flux:modal.trigger>
|
||||
@endcan
|
||||
|
||||
@can('update tier')
|
||||
<flux:modal.trigger name="form-modal">
|
||||
<flux:tooltip content="Ubah">
|
||||
@ -182,17 +137,17 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Min. Poin <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="100" wire:model="form.min_points" autocomplete="off"
|
||||
<flux:label>Min. Belanja <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="100.000" wire:model="form.min_spending" autocomplete="off"
|
||||
x-mask:dynamic="$money($input, ',')" />
|
||||
<flux:error name="form.min_points" />
|
||||
<flux:error name="form.min_spending" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Maks. Poin </flux:label>
|
||||
<flux:input placeholder="499" wire:model="form.max_points" autocomplete="off"
|
||||
<flux:label>Maks. Belanja </flux:label>
|
||||
<flux:input placeholder="499.000" wire:model="form.max_spending" autocomplete="off"
|
||||
x-mask:dynamic="$money($input, ',')" />
|
||||
<flux:error name="form.max_points" />
|
||||
<flux:error name="form.max_spending" />
|
||||
</flux:field>
|
||||
|
||||
<div class="flex">
|
||||
@ -203,8 +158,4 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
@can('create tier reward')
|
||||
@livewire('studio.loyalty.tier.reward')
|
||||
@endcan
|
||||
</flux:main>
|
||||
@ -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;
|
||||
|
||||
@ -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 () {
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user