351 lines
10 KiB
PHP
351 lines
10 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\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Str;
|
|
use Modules\RestaurantDelivery\Traits\HasRestaurant;
|
|
use Modules\RestaurantDelivery\Traits\HasUuid;
|
|
|
|
class RiderPayout extends Model
|
|
{
|
|
use HasFactory, HasRestaurant, HasUuid, SoftDeletes;
|
|
|
|
public const TABLE_NAME = 'restaurant_rider_payouts';
|
|
|
|
protected $table = self::TABLE_NAME;
|
|
|
|
protected $fillable = [
|
|
'payout_number',
|
|
'rider_id',
|
|
'restaurant_id',
|
|
'period_start',
|
|
'period_end',
|
|
'period_type',
|
|
'total_deliveries_amount',
|
|
'total_tips_amount',
|
|
'total_bonuses_amount',
|
|
'total_penalties_amount',
|
|
'total_adjustments_amount',
|
|
'gross_amount',
|
|
'platform_fees',
|
|
'tax_deductions',
|
|
'other_deductions',
|
|
'net_amount',
|
|
'currency',
|
|
'total_deliveries',
|
|
'total_tips_count',
|
|
'total_bonuses_count',
|
|
'total_penalties_count',
|
|
'status',
|
|
'payment_method',
|
|
'payment_reference',
|
|
'payment_details',
|
|
'processed_at',
|
|
'paid_at',
|
|
'failed_at',
|
|
'failure_reason',
|
|
'retry_count',
|
|
'approved_by',
|
|
'approved_at',
|
|
'processed_by',
|
|
'notes',
|
|
'meta',
|
|
];
|
|
|
|
protected $casts = [
|
|
'period_start' => '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;
|
|
}
|
|
}
|