387 lines
11 KiB
PHP
387 lines
11 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Modules\RestaurantDelivery\Models;
|
||
|
|
|
||
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
|
|
use Illuminate\Database\Eloquent\Model;
|
||
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||
|
|
use Modules\RestaurantDelivery\Traits\HasRestaurant;
|
||
|
|
use Modules\RestaurantDelivery\Traits\HasUuid;
|
||
|
|
|
||
|
|
class RiderEarning extends Model
|
||
|
|
{
|
||
|
|
use HasFactory, HasRestaurant, HasUuid, SoftDeletes;
|
||
|
|
|
||
|
|
public const TABLE_NAME = 'restaurant_rider_earnings';
|
||
|
|
|
||
|
|
protected $table = self::TABLE_NAME;
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
'rider_id',
|
||
|
|
'delivery_id',
|
||
|
|
'restaurant_id',
|
||
|
|
'type',
|
||
|
|
'sub_type',
|
||
|
|
'gross_amount',
|
||
|
|
'platform_fee',
|
||
|
|
'tax',
|
||
|
|
'deductions',
|
||
|
|
'net_amount',
|
||
|
|
'currency',
|
||
|
|
'calculation_breakdown',
|
||
|
|
'description',
|
||
|
|
'status',
|
||
|
|
'confirmed_at',
|
||
|
|
'payout_id',
|
||
|
|
'is_paid',
|
||
|
|
'paid_at',
|
||
|
|
'earning_date',
|
||
|
|
'earning_week',
|
||
|
|
'earning_month',
|
||
|
|
'earning_year',
|
||
|
|
];
|
||
|
|
|
||
|
|
protected $casts = [
|
||
|
|
'gross_amount' => '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;
|
||
|
|
}
|
||
|
|
}
|