580 lines
17 KiB
PHP
580 lines
17 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Modules\RestaurantDelivery\Services\Earnings;
|
||
|
|
|
||
|
|
use Illuminate\Support\Carbon;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
use Modules\RestaurantDelivery\Models\Delivery;
|
||
|
|
use Modules\RestaurantDelivery\Models\Rider;
|
||
|
|
use Modules\RestaurantDelivery\Models\RiderBonus;
|
||
|
|
use Modules\RestaurantDelivery\Models\RiderEarning;
|
||
|
|
use Modules\RestaurantDelivery\Models\RiderPayout;
|
||
|
|
|
||
|
|
class EarningsService
|
||
|
|
{
|
||
|
|
protected array $config;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
$this->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;
|
||
|
|
}
|
||
|
|
}
|