'date', 'period_end' => 'date', 'total_deliveries_amount' => 'decimal:2', 'total_tips_amount' => 'decimal:2', 'total_bonuses_amount' => 'decimal:2', 'total_penalties_amount' => 'decimal:2', 'total_adjustments_amount' => 'decimal:2', 'gross_amount' => 'decimal:2', 'platform_fees' => 'decimal:2', 'tax_deductions' => 'decimal:2', 'other_deductions' => 'decimal:2', 'net_amount' => 'decimal:2', 'payment_details' => 'array', 'processed_at' => 'datetime', 'paid_at' => 'datetime', 'failed_at' => 'datetime', 'approved_at' => 'datetime', 'meta' => 'array', ]; protected static function boot() { parent::boot(); static::creating(function ($payout) { if (empty($payout->payout_number)) { $payout->payout_number = static::generatePayoutNumber(); } }); } /* |-------------------------------------------------------------------------- | Relationships |-------------------------------------------------------------------------- */ public function rider(): BelongsTo { return $this->belongsTo(Rider::class, 'rider_id'); } public function earnings(): HasMany { return $this->hasMany(RiderEarning::class, 'payout_id'); } public function tips(): HasMany { return $this->hasMany(DeliveryTip::class, 'payout_id'); } public function approvedBy(): BelongsTo { return $this->belongsTo(config('auth.providers.users.model'), 'approved_by'); } public function processedBy(): BelongsTo { return $this->belongsTo(config('auth.providers.users.model'), 'processed_by'); } /* |-------------------------------------------------------------------------- | Scopes |-------------------------------------------------------------------------- */ public function scopePending($query) { return $query->where('status', 'pending'); } public function scopeProcessing($query) { return $query->where('status', 'processing'); } public function scopeCompleted($query) { return $query->where('status', 'completed'); } public function scopeFailed($query) { return $query->where('status', 'failed'); } public function scopeForPeriod($query, $startDate, $endDate) { return $query->whereBetween('period_start', [$startDate, $endDate]); } public function scopeWeekly($query) { return $query->where('period_type', 'weekly'); } public function scopeMonthly($query) { return $query->where('period_type', 'monthly'); } /* |-------------------------------------------------------------------------- | Methods |-------------------------------------------------------------------------- */ public static function generatePayoutNumber(): string { do { $number = 'PAY-'.date('Ymd').'-'.strtoupper(Str::random(6)); } while (static::where('payout_number', $number)->exists()); return $number; } public function approve(int $approvedBy): void { $this->update([ 'status' => 'processing', 'approved_by' => $approvedBy, 'approved_at' => now(), ]); } public function process(int $processedBy): void { $this->update([ 'processed_by' => $processedBy, 'processed_at' => now(), ]); } public function markAsCompleted(string $paymentReference, array $paymentDetails = []): void { $this->update([ 'status' => 'completed', 'payment_reference' => $paymentReference, 'payment_details' => $paymentDetails, 'paid_at' => now(), ]); // Mark all associated earnings as paid $this->earnings()->update([ 'is_paid' => true, 'paid_at' => now(), 'status' => 'paid', ]); // Mark all associated tips as transferred $this->tips()->update([ 'is_transferred' => true, 'transferred_at' => now(), 'payment_status' => 'transferred', ]); } public function markAsFailed(string $reason): void { $this->update([ 'status' => 'failed', 'failed_at' => now(), 'failure_reason' => $reason, ]); } public function retry(): void { $this->update([ 'status' => 'processing', 'retry_count' => $this->retry_count + 1, 'failed_at' => null, 'failure_reason' => null, ]); } public function cancel(): void { $this->update(['status' => 'cancelled']); // Unlink earnings from this payout $this->earnings()->update(['payout_id' => null]); $this->tips()->update(['payout_id' => null]); } public function isPending(): bool { return $this->status === 'pending'; } public function isProcessing(): bool { return $this->status === 'processing'; } public function isCompleted(): bool { return $this->status === 'completed'; } public function isFailed(): bool { return $this->status === 'failed'; } public function canRetry(): bool { return $this->isFailed() && $this->retry_count < 3; } /* |-------------------------------------------------------------------------- | Static Factory Methods |-------------------------------------------------------------------------- */ public static function createForRider( Rider $rider, \DateTime $periodStart, \DateTime $periodEnd, string $periodType = 'weekly' ): ?self { // Get all unpaid earnings for the period $earnings = RiderEarning::where('rider_id', $rider->id) ->payable() ->forPeriod($periodStart, $periodEnd) ->get(); // Get all untransferred tips $tips = DeliveryTip::where('rider_id', $rider->id) ->notTransferred() ->forPeriod($periodStart, $periodEnd) ->get(); if ($earnings->isEmpty() && $tips->isEmpty()) { return null; } // Calculate amounts by type $deliveriesAmount = $earnings->where('type', 'delivery')->sum('net_amount'); $bonusesAmount = $earnings->where('type', 'bonus')->sum('net_amount'); $penaltiesAmount = abs($earnings->where('type', 'penalty')->sum('net_amount')); $adjustmentsAmount = $earnings->where('type', 'adjustment')->sum('net_amount'); $tipsAmount = $tips->sum('rider_amount'); $grossAmount = $deliveriesAmount + $bonusesAmount + $tipsAmount + $adjustmentsAmount; $netAmount = $grossAmount - $penaltiesAmount; // Check minimum payout amount $minimumAmount = config('restaurant-delivery.earnings.payout.minimum_amount', 500); if ($netAmount < $minimumAmount) { return null; } $payout = static::create([ 'rider_id' => $rider->id, 'restaurant_id' => getUserRestaurantId(), 'period_start' => $periodStart, 'period_end' => $periodEnd, 'period_type' => $periodType, 'total_deliveries_amount' => $deliveriesAmount, 'total_tips_amount' => $tipsAmount, 'total_bonuses_amount' => $bonusesAmount, 'total_penalties_amount' => $penaltiesAmount, 'total_adjustments_amount' => $adjustmentsAmount, 'gross_amount' => $grossAmount, 'platform_fees' => 0, 'tax_deductions' => 0, 'other_deductions' => $penaltiesAmount, 'net_amount' => $netAmount, 'currency' => config('restaurant-delivery.pricing.currency', 'BDT'), 'total_deliveries' => $earnings->where('type', 'delivery')->count(), 'total_tips_count' => $tips->count(), 'total_bonuses_count' => $earnings->where('type', 'bonus')->count(), 'total_penalties_count' => $earnings->where('type', 'penalty')->count(), 'status' => 'pending', 'payment_method' => $rider->mobile_wallet_provider ?? 'bank_transfer', ]); // Link earnings and tips to this payout $earnings->each(fn ($e) => $e->update(['payout_id' => $payout->id])); $tips->each(fn ($t) => $t->update(['payout_id' => $payout->id])); return $payout; } }