'decimal:2', 'platform_fee' => 'decimal:2', 'tax' => 'decimal:2', 'deductions' => 'decimal:2', 'net_amount' => 'decimal:2', 'calculation_breakdown' => 'array', 'is_paid' => 'boolean', 'confirmed_at' => 'datetime', 'paid_at' => 'datetime', 'earning_date' => 'date', ]; protected static function boot() { parent::boot(); static::creating(function ($earning) { // Set earning date metadata $date = $earning->earning_date ?? now(); $earning->earning_date = $date; $earning->earning_week = $date->weekOfYear; $earning->earning_month = $date->month; $earning->earning_year = $date->year; // Calculate net amount $earning->net_amount = $earning->gross_amount - $earning->platform_fee - $earning->tax - $earning->deductions; }); } /* |-------------------------------------------------------------------------- | Relationships |-------------------------------------------------------------------------- */ public function rider(): BelongsTo { return $this->belongsTo(Rider::class, 'rider_id'); } public function delivery(): BelongsTo { return $this->belongsTo(Delivery::class, 'delivery_id'); } public function payout(): BelongsTo { return $this->belongsTo(RiderPayout::class, 'payout_id'); } /* |-------------------------------------------------------------------------- | Scopes |-------------------------------------------------------------------------- */ public function scopePending($query) { return $query->where('status', 'pending'); } public function scopeConfirmed($query) { return $query->where('status', 'confirmed'); } public function scopePaid($query) { return $query->where('is_paid', true); } public function scopeUnpaid($query) { return $query->where('is_paid', false); } public function scopeDeliveryEarnings($query) { return $query->where('type', 'delivery'); } public function scopeTips($query) { return $query->where('type', 'tip'); } public function scopeBonuses($query) { return $query->where('type', 'bonus'); } public function scopePenalties($query) { return $query->where('type', 'penalty'); } public function scopeForDate($query, $date) { return $query->whereDate('earning_date', $date); } public function scopeForWeek($query, int $week, int $year) { return $query->where('earning_week', $week) ->where('earning_year', $year); } public function scopeForMonth($query, int $month, int $year) { return $query->where('earning_month', $month) ->where('earning_year', $year); } public function scopeForPeriod($query, $startDate, $endDate) { return $query->whereBetween('earning_date', [$startDate, $endDate]); } public function scopePayable($query) { return $query->where('status', 'confirmed') ->where('is_paid', false); } /* |-------------------------------------------------------------------------- | Methods |-------------------------------------------------------------------------- */ public function confirm(): void { $this->update([ 'status' => 'confirmed', 'confirmed_at' => now(), ]); } public function markAsPaid(int $payoutId): void { $this->update([ 'is_paid' => true, 'paid_at' => now(), 'payout_id' => $payoutId, 'status' => 'paid', ]); } public function cancel(): void { $this->update(['status' => 'cancelled']); } public function isPending(): bool { return $this->status === 'pending'; } public function isConfirmed(): bool { return $this->status === 'confirmed'; } public function isPaid(): bool { return $this->is_paid; } public function isDebit(): bool { return in_array($this->type, ['penalty', 'adjustment']) && $this->net_amount < 0; } /* |-------------------------------------------------------------------------- | Static Factory Methods |-------------------------------------------------------------------------- */ public static function createForDelivery(Delivery $delivery): ?self { if (! $delivery->rider) { return null; } $rider = $delivery->rider; $grossAmount = $rider->calculateEarningForDelivery($delivery); $breakdown = [ 'delivery_charge' => $delivery->total_delivery_charge, 'commission_type' => $rider->commission_type->value, 'commission_rate' => $rider->commission_rate, 'distance' => $delivery->distance, 'base_earning' => $grossAmount, ]; // Add any bonuses $bonusAmount = static::calculateDeliveryBonuses($delivery, $rider); $grossAmount += $bonusAmount; $breakdown['bonuses'] = $bonusAmount; return static::create([ 'rider_id' => $rider->id, 'delivery_id' => $delivery->id, 'restaurant_id' => getUserRestaurantId(), 'type' => 'delivery', 'gross_amount' => $grossAmount, 'platform_fee' => 0, 'tax' => 0, 'deductions' => 0, 'net_amount' => $grossAmount, 'currency' => config('restaurant-delivery.pricing.currency', 'BDT'), 'calculation_breakdown' => $breakdown, 'description' => "Delivery #{$delivery->tracking_code}", 'status' => 'pending', 'earning_date' => now(), ]); } public static function createFromTip(DeliveryTip $tip): ?self { return static::create([ 'rider_id' => $tip->rider_id, 'delivery_id' => $tip->delivery_id, 'restaurant_id' => getUserRestaurantId(), 'type' => 'tip', 'gross_amount' => $tip->rider_amount, 'platform_fee' => 0, 'tax' => 0, 'deductions' => 0, 'net_amount' => $tip->rider_amount, 'currency' => $tip->currency, 'calculation_breakdown' => [ 'total_tip' => $tip->amount, 'rider_share' => $tip->rider_share_percentage, 'rider_amount' => $tip->rider_amount, ], 'description' => "Tip for delivery #{$tip->delivery->tracking_code}", 'status' => 'confirmed', 'confirmed_at' => now(), 'earning_date' => now(), ]); } public static function createBonus( Rider $rider, string $subType, float $amount, string $description, ?Delivery $delivery = null ): self { return static::create([ 'rider_id' => $rider->id, 'delivery_id' => $delivery?->id, 'restaurant_id' => getUserRestaurantId(), 'type' => 'bonus', 'sub_type' => $subType, 'gross_amount' => $amount, 'platform_fee' => 0, 'tax' => 0, 'deductions' => 0, 'net_amount' => $amount, 'currency' => config('restaurant-delivery.pricing.currency', 'BDT'), 'description' => $description, 'status' => 'confirmed', 'confirmed_at' => now(), 'earning_date' => now(), ]); } public static function createPenalty( Rider $rider, string $subType, float $amount, string $description, ?Delivery $delivery = null ): self { return static::create([ 'rider_id' => $rider->id, 'delivery_id' => $delivery?->id, 'restaurant_id' => getUserRestaurantId(), 'type' => 'penalty', 'sub_type' => $subType, 'gross_amount' => -$amount, 'platform_fee' => 0, 'tax' => 0, 'deductions' => 0, 'net_amount' => -$amount, 'currency' => config('restaurant-delivery.pricing.currency', 'BDT'), 'description' => $description, 'status' => 'confirmed', 'confirmed_at' => now(), 'earning_date' => now(), ]); } protected static function calculateDeliveryBonuses(Delivery $delivery, Rider $rider): float { $bonus = 0; $config = config('restaurant-delivery.earnings.bonuses'); // Peak hour bonus if ($config['peak_hour_bonus']['enabled'] && static::isPeakHour()) { $bonus += $config['peak_hour_bonus']['amount']; } // Rating bonus if ($config['rating_bonus']['enabled'] && $rider->rating >= $config['rating_bonus']['min_rating']) { $bonus += $config['rating_bonus']['amount']; } return $bonus; } protected static function isPeakHour(): bool { $peakHours = config('restaurant-delivery.pricing.peak_hours.slots', []); $now = now(); $currentDay = $now->dayOfWeekIso; foreach ($peakHours as $slot) { if (! in_array($currentDay, $slot['days'] ?? [])) { continue; } $start = \Carbon\Carbon::createFromTimeString($slot['start']); $end = \Carbon\Carbon::createFromTimeString($slot['end']); if ($now->between($start, $end)) { return true; } } return false; } }