config = config('restaurant-delivery.earnings'); } /* |-------------------------------------------------------------------------- | Delivery Earnings |-------------------------------------------------------------------------- */ /** * Create earning record for a completed delivery */ public function createDeliveryEarning(Delivery $delivery): ?RiderEarning { if (! $delivery->rider) { return null; } try { $earning = DB::transaction(function () use ($delivery) { // Create base earning $earning = RiderEarning::createForDelivery($delivery); if (! $earning) { return null; } // Check and apply bonuses $this->applyDeliveryBonuses($delivery, $earning); // Auto-confirm if configured $earning->confirm(); return $earning; }); Log::info('Delivery earning created', [ 'earning_id' => $earning?->id, 'delivery_id' => $delivery->id, 'amount' => $earning?->net_amount, ]); return $earning; } catch (\Exception $e) { Log::error('Failed to create delivery earning', [ 'delivery_id' => $delivery->id, 'error' => $e->getMessage(), ]); return null; } } /** * Apply bonuses to a delivery earning */ protected function applyDeliveryBonuses(Delivery $delivery, RiderEarning $earning): void { $rider = $delivery->rider; $bonuses = $this->config['bonuses']; // Peak hour bonus if ($bonuses['peak_hour_bonus']['enabled'] && $this->isPeakHour()) { $this->createBonus( $rider, 'peak_hour', $bonuses['peak_hour_bonus']['amount'], 'Peak hour bonus', $delivery ); } // Rating bonus if ($bonuses['rating_bonus']['enabled'] && $rider->rating >= $bonuses['rating_bonus']['min_rating']) { $this->createBonus( $rider, 'rating', $bonuses['rating_bonus']['amount'], 'High rating bonus', $delivery ); } // Consecutive delivery bonus if ($bonuses['consecutive_delivery_bonus']['enabled']) { $consecutiveCount = $this->getConsecutiveDeliveryCount($rider); if ($consecutiveCount >= $bonuses['consecutive_delivery_bonus']['threshold']) { $this->createBonus( $rider, 'consecutive', $bonuses['consecutive_delivery_bonus']['amount'], "Consecutive delivery bonus ({$consecutiveCount} deliveries)", $delivery ); } } } /* |-------------------------------------------------------------------------- | Bonus Management |-------------------------------------------------------------------------- */ /** * Create a bonus earning */ public function createBonus( Rider $rider, string $bonusType, float $amount, string $description, ?Delivery $delivery = null ): RiderEarning { return RiderEarning::createBonus($rider, $bonusType, $amount, $description, $delivery); } /** * Check and apply weekly target bonus */ public function checkWeeklyTargetBonus(Rider $rider): ?RiderEarning { $bonusConfig = $this->config['bonuses']['weekly_target_bonus']; if (! $bonusConfig['enabled']) { return null; } $weekStart = now()->startOfWeek(); $weekEnd = now()->endOfWeek(); // Get weekly delivery count $weeklyDeliveries = $rider->deliveries() ->completed() ->whereBetween('delivered_at', [$weekStart, $weekEnd]) ->count(); // Check if already received bonus this week $existingBonus = RiderEarning::where('rider_id', $rider->id) ->where('type', 'bonus') ->where('sub_type', 'weekly_target') ->whereBetween('earning_date', [$weekStart, $weekEnd]) ->exists(); if ($existingBonus) { return null; } // Find applicable bonus tier $targets = collect($bonusConfig['targets'])->sortByDesc('deliveries'); foreach ($targets as $target) { if ($weeklyDeliveries >= $target['deliveries']) { return $this->createBonus( $rider, 'weekly_target', $target['bonus'], "Weekly target bonus ({$weeklyDeliveries} deliveries)" ); } } return null; } /** * Get available bonuses for a rider */ public function getAvailableBonuses(Rider $rider): array { return RiderBonus::where('is_active', true) ->where(function ($q) { $q->whereNull('valid_from') ->orWhere('valid_from', '<=', now()); }) ->where(function ($q) { $q->whereNull('valid_until') ->orWhere('valid_until', '>=', now()); }) ->where(function ($q) use ($rider) { $q->whereNull('min_rider_rating') ->orWhere('min_rider_rating', '<=', $rider->rating); }) ->get() ->filter(fn ($bonus) => $this->riderEligibleForBonus($rider, $bonus)) ->map(fn ($bonus) => [ 'id' => $bonus->id, 'name' => $bonus->name, 'type' => $bonus->type, 'amount' => $bonus->amount, 'description' => $bonus->description, ]) ->values() ->toArray(); } protected function riderEligibleForBonus(Rider $rider, RiderBonus $bonus): bool { // Check rider type if ($bonus->applicable_rider_types) { if (! in_array($rider->type->value, $bonus->applicable_rider_types)) { return false; } } // Check zones if ($bonus->applicable_zones && $rider->assigned_zones) { $intersection = array_intersect($bonus->applicable_zones, $rider->assigned_zones); if (empty($intersection)) { return false; } } // Check usage limits if ($bonus->max_uses_per_rider) { $riderUses = RiderEarning::where('rider_id', $rider->id) ->where('sub_type', $bonus->code) ->count(); if ($riderUses >= $bonus->max_uses_per_rider) { return false; } } return true; } /* |-------------------------------------------------------------------------- | Penalty Management |-------------------------------------------------------------------------- */ /** * Create a penalty */ public function createPenalty( Rider $rider, string $penaltyType, float $amount, string $description, ?Delivery $delivery = null ): RiderEarning { return RiderEarning::createPenalty($rider, $penaltyType, $amount, $description, $delivery); } /** * Apply cancellation penalty */ public function applyCancellationPenalty(Rider $rider, Delivery $delivery): ?RiderEarning { $penaltyConfig = $this->config['penalties']['cancellation']; if (! $penaltyConfig['enabled']) { return null; } // Check free cancellations $todayCancellations = RiderEarning::where('rider_id', $rider->id) ->where('type', 'penalty') ->where('sub_type', 'cancellation') ->whereDate('earning_date', now()->toDateString()) ->count(); if ($todayCancellations < $penaltyConfig['free_cancellations']) { return null; } return $this->createPenalty( $rider, 'cancellation', $penaltyConfig['amount'], "Cancellation penalty for delivery #{$delivery->tracking_code}", $delivery ); } /** * Apply late delivery penalty */ public function applyLateDeliveryPenalty(Rider $rider, Delivery $delivery): ?RiderEarning { $penaltyConfig = $this->config['penalties']['late_delivery']; if (! $penaltyConfig['enabled']) { return null; } $delayMinutes = $delivery->getDelayMinutes(); if (! $delayMinutes || $delayMinutes < $penaltyConfig['threshold']) { return null; } return $this->createPenalty( $rider, 'late_delivery', $penaltyConfig['amount'], "Late delivery penalty ({$delayMinutes} min late)", $delivery ); } /* |-------------------------------------------------------------------------- | Payout Management |-------------------------------------------------------------------------- */ /** * Generate payouts for all eligible riders */ public function generatePayouts(string $periodType = 'weekly'): array { $results = [ 'generated' => 0, 'skipped' => 0, 'errors' => 0, ]; // Get period dates [$periodStart, $periodEnd] = $this->getPayoutPeriodDates($periodType); // Get all active riders $riders = Rider::active()->get(); foreach ($riders as $rider) { try { $payout = RiderPayout::createForRider($rider, $periodStart, $periodEnd, $periodType); if ($payout) { $results['generated']++; } else { $results['skipped']++; } } catch (\Exception $e) { $results['errors']++; Log::error('Failed to generate payout', [ 'rider_id' => $rider->id, 'error' => $e->getMessage(), ]); } } return $results; } /** * Process a payout */ public function processPayout(RiderPayout $payout, array $paymentDetails): bool { try { DB::transaction(function () use ($payout, $paymentDetails) { $payout->markAsCompleted( $paymentDetails['reference'], $paymentDetails ); }); Log::info('Payout processed', [ 'payout_id' => $payout->id, 'amount' => $payout->net_amount, 'reference' => $paymentDetails['reference'], ]); return true; } catch (\Exception $e) { Log::error('Failed to process payout', [ 'payout_id' => $payout->id, 'error' => $e->getMessage(), ]); return false; } } protected function getPayoutPeriodDates(string $periodType): array { return match ($periodType) { 'daily' => [ now()->subDay()->startOfDay(), now()->subDay()->endOfDay(), ], 'weekly' => [ now()->subWeek()->startOfWeek(), now()->subWeek()->endOfWeek(), ], 'monthly' => [ now()->subMonth()->startOfMonth(), now()->subMonth()->endOfMonth(), ], default => [ now()->subWeek()->startOfWeek(), now()->subWeek()->endOfWeek(), ], }; } /* |-------------------------------------------------------------------------- | Statistics |-------------------------------------------------------------------------- */ /** * Get rider earnings summary */ public function getRiderEarningsSummary(Rider $rider, ?string $period = null): array { $query = $rider->earnings()->confirmed(); $startDate = match ($period) { 'today' => now()->startOfDay(), 'week' => now()->startOfWeek(), 'month' => now()->startOfMonth(), 'year' => now()->startOfYear(), default => null, }; if ($startDate) { $query->where('earning_date', '>=', $startDate); } $earnings = $query->get(); $deliveryEarnings = $earnings->where('type', 'delivery'); $tips = $earnings->where('type', 'tip'); $bonuses = $earnings->where('type', 'bonus'); $penalties = $earnings->where('type', 'penalty'); return [ 'total_earnings' => $earnings->sum('net_amount'), 'delivery_earnings' => [ 'amount' => $deliveryEarnings->sum('net_amount'), 'count' => $deliveryEarnings->count(), ], 'tips' => [ 'amount' => $tips->sum('net_amount'), 'count' => $tips->count(), ], 'bonuses' => [ 'amount' => $bonuses->sum('net_amount'), 'count' => $bonuses->count(), 'breakdown' => $bonuses->groupBy('sub_type')->map->sum('net_amount')->toArray(), ], 'penalties' => [ 'amount' => abs($penalties->sum('net_amount')), 'count' => $penalties->count(), ], 'pending_payout' => $this->getPendingPayoutAmount($rider), ]; } /** * Get pending payout amount */ public function getPendingPayoutAmount(Rider $rider): float { return $rider->earnings() ->payable() ->sum('net_amount'); } /** * Get earnings history */ public function getEarningsHistory(Rider $rider, int $limit = 50, ?string $type = null): array { $query = $rider->earnings()->confirmed()->orderBy('earning_date', 'desc'); if ($type) { $query->where('type', $type); } return $query->limit($limit)->get()->map(fn ($earning) => [ 'id' => $earning->uuid, 'type' => $earning->type, 'sub_type' => $earning->sub_type, 'amount' => $earning->net_amount, 'description' => $earning->description, 'delivery_code' => $earning->delivery?->tracking_code, 'date' => $earning->earning_date->format('Y-m-d'), 'is_paid' => $earning->is_paid, ])->toArray(); } /** * Get payout history */ public function getPayoutHistory(Rider $rider, int $limit = 20): array { return $rider->payouts() ->orderBy('created_at', 'desc') ->limit($limit) ->get() ->map(fn ($payout) => [ 'id' => $payout->uuid, 'payout_number' => $payout->payout_number, 'period' => $payout->period_start->format('M d').' - '.$payout->period_end->format('M d, Y'), 'amount' => $payout->net_amount, 'deliveries' => $payout->total_deliveries, 'status' => $payout->status, 'payment_method' => $payout->payment_method, 'paid_at' => $payout->paid_at?->format('Y-m-d H:i'), ]) ->toArray(); } /* |-------------------------------------------------------------------------- | Helper Methods |-------------------------------------------------------------------------- */ protected 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::createFromTimeString($slot['start']); $end = Carbon::createFromTimeString($slot['end']); if ($now->between($start, $end)) { return true; } } return false; } protected function getConsecutiveDeliveryCount(Rider $rider): int { // Get today's deliveries in chronological order $deliveries = $rider->deliveries() ->whereDate('delivered_at', now()->toDateString()) ->orderBy('delivered_at', 'desc') ->get(); $consecutive = 0; foreach ($deliveries as $delivery) { if ($delivery->isCompleted()) { $consecutive++; } else { break; } } return $consecutive; } }