migrate to gtea from bistbucket

This commit is contained in:
2026-03-15 17:08:23 +07:00
commit 129ca2260c
3716 changed files with 566316 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Console\Commands;
use Illuminate\Console\Command;
use Modules\RestaurantDelivery\Enums\DeliveryStatus;
use Modules\RestaurantDelivery\Jobs\AssignRiderJob;
use Modules\RestaurantDelivery\Models\Delivery;
class AssignPendingDeliveries extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'delivery:assign-pending
{--limit=50 : Maximum number of deliveries to process}
{--restaurant= : Filter by restaurant ID}';
/**
* The console command description.
*/
protected $description = 'Assign riders to pending deliveries that need assignment';
/**
* Execute the console command.
*/
public function handle(): int
{
$limit = (int) $this->option('limit');
$restaurantId = $this->option('restaurant');
$this->info('Looking for deliveries that need rider assignment...');
$query = Delivery::query()
->where('status', DeliveryStatus::READY_FOR_PICKUP)
->whereNull('rider_id');
if ($restaurantId) {
$query->where('restaurant_id', $restaurantId);
}
$deliveries = $query->limit($limit)
->orderBy('created_at', 'asc')
->get();
if ($deliveries->isEmpty()) {
$this->info('No deliveries found that need rider assignment.');
return self::SUCCESS;
}
$this->info("Found {$deliveries->count()} deliveries to process.");
$bar = $this->output->createProgressBar($deliveries->count());
$bar->start();
foreach ($deliveries as $delivery) {
dispatch(new AssignRiderJob($delivery));
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Dispatched {$deliveries->count()} assignment jobs to queue.");
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Console\Commands;
use Illuminate\Console\Command;
use Modules\RestaurantDelivery\Jobs\CleanupStaleLocationsJob;
use Modules\RestaurantDelivery\Models\LocationLog;
use Modules\RestaurantDelivery\Models\Rider;
class CleanupStaleData extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'delivery:cleanup
{--days=7 : Number of days to keep location logs}
{--queue : Dispatch as a background job}';
/**
* The console command description.
*/
protected $description = 'Clean up stale location logs and mark offline riders';
/**
* Execute the console command.
*/
public function handle(): int
{
$days = (int) $this->option('days');
$queue = $this->option('queue');
if ($queue) {
dispatch(new CleanupStaleLocationsJob($days));
$this->info('Cleanup job dispatched to queue.');
return self::SUCCESS;
}
$this->info("Cleaning up data older than {$days} days...");
// Delete old location logs
$deletedLogs = LocationLog::where('recorded_at', '<', now()->subDays($days))
->delete();
$this->info("Deleted {$deletedLogs} old location log entries.");
// Mark stale riders as offline
$offlineThreshold = config('restaurant-delivery.firebase.location.offline_threshold', 120);
$ridersMarkedOffline = Rider::where('is_online', true)
->where('last_location_update', '<', now()->subSeconds($offlineThreshold))
->update(['is_online' => false]);
$this->info("Marked {$ridersMarkedOffline} riders as offline (no update for {$offlineThreshold}s).");
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Modules\RestaurantDelivery\Models\RiderEarning;
class GenerateEarningsReport extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'delivery:earnings-report
{--period=week : Report period (today, week, month, year)}
{--restaurant= : Filter by restaurant ID}
{--export= : Export to file (csv, json)}';
/**
* The console command description.
*/
protected $description = 'Generate earnings report for riders';
/**
* Execute the console command.
*/
public function handle(): int
{
$period = $this->option('period');
$restaurantId = $this->option('restaurant');
$export = $this->option('export');
$startDate = match ($period) {
'today' => now()->startOfDay(),
'week' => now()->startOfWeek(),
'month' => now()->startOfMonth(),
'year' => now()->startOfYear(),
default => now()->startOfWeek(),
};
$this->info("Generating earnings report for period: {$period}");
$this->info("From: {$startDate->format('Y-m-d H:i:s')}");
$this->newLine();
$query = RiderEarning::query()
->select([
'rider_id',
DB::raw('COUNT(*) as total_deliveries'),
DB::raw('SUM(gross_amount) as gross_earnings'),
DB::raw('SUM(commission_amount) as total_commission'),
DB::raw('SUM(net_amount) as net_earnings'),
DB::raw('SUM(tip_amount) as total_tips'),
DB::raw('SUM(bonus_amount) as total_bonuses'),
DB::raw('SUM(penalty_amount) as total_penalties'),
])
->where('earned_at', '>=', $startDate)
->groupBy('rider_id');
if ($restaurantId) {
$query->where('restaurant_id', $restaurantId);
}
$report = $query->with('rider:id,first_name,last_name,phone')
->get();
if ($report->isEmpty()) {
$this->warn('No earnings data found for the specified period.');
return self::SUCCESS;
}
$currency = config('restaurant-delivery.pricing.currency', 'BDT');
$data = $report->map(function ($row) use ($currency) {
return [
'Rider ID' => $row->rider_id,
'Name' => $row->rider?->full_name ?? 'Unknown',
'Deliveries' => $row->total_deliveries,
'Gross' => "{$currency} ".number_format((float) $row->gross_earnings, 2),
'Commission' => "{$currency} ".number_format((float) $row->total_commission, 2),
'Tips' => "{$currency} ".number_format((float) $row->total_tips, 2),
'Bonuses' => "{$currency} ".number_format((float) $row->total_bonuses, 2),
'Penalties' => "{$currency} ".number_format((float) $row->total_penalties, 2),
'Net' => "{$currency} ".number_format((float) $row->net_earnings, 2),
];
});
// Display table
$this->table(array_keys($data->first()), $data->toArray());
// Show totals
$this->newLine();
$this->info('Totals:');
$this->line(' Total Deliveries: '.$report->sum('total_deliveries'));
$this->line(" Gross Earnings: {$currency} ".number_format((float) $report->sum('gross_earnings'), 2));
$this->line(" Total Commission: {$currency} ".number_format((float) $report->sum('total_commission'), 2));
$this->line(" Net Earnings: {$currency} ".number_format((float) $report->sum('net_earnings'), 2));
// Export if requested
if ($export) {
$filename = "earnings_report_{$period}_".now()->format('Y-m-d_His').".{$export}";
if ($export === 'csv') {
$this->exportToCsv($data->toArray(), $filename);
} elseif ($export === 'json') {
$this->exportToJson($report->toArray(), $filename);
}
$this->info("Report exported to: {$filename}");
}
return self::SUCCESS;
}
/**
* Export data to CSV.
*/
protected function exportToCsv(array $data, string $filename): void
{
$handle = fopen(storage_path("app/{$filename}"), 'w');
if (! empty($data)) {
fputcsv($handle, array_keys($data[0]));
foreach ($data as $row) {
fputcsv($handle, $row);
}
}
fclose($handle);
}
/**
* Export data to JSON.
*/
protected function exportToJson(array $data, string $filename): void
{
file_put_contents(
storage_path("app/{$filename}"),
json_encode($data, JSON_PRETTY_PRINT)
);
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Console\Commands;
use Illuminate\Console\Command;
use Modules\RestaurantDelivery\Jobs\ProcessPayoutJob;
use Modules\RestaurantDelivery\Models\Rider;
class ProcessRiderPayouts extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'delivery:process-payouts
{--rider= : Process payout for specific rider ID}
{--restaurant= : Filter by restaurant ID}
{--payment-method= : Override payment method}
{--dry-run : Show what would be processed without actually processing}';
/**
* The console command description.
*/
protected $description = 'Process pending payouts for riders';
/**
* Execute the console command.
*/
public function handle(): int
{
$riderId = $this->option('rider');
$restaurantId = $this->option('restaurant');
$paymentMethod = $this->option('payment-method');
$dryRun = $this->option('dry-run');
$this->info('Processing rider payouts...');
$query = Rider::query()
->where('status', '!=', 'suspended')
->whereHas('earnings', function ($q) {
$q->where('status', 'pending')
->whereNull('payout_id');
});
if ($riderId) {
$query->where('id', $riderId);
}
if ($restaurantId) {
$query->where('restaurant_id', $restaurantId);
}
$riders = $query->with(['earnings' => function ($q) {
$q->where('status', 'pending')
->whereNull('payout_id');
}])->get();
if ($riders->isEmpty()) {
$this->info('No riders with pending earnings found.');
return self::SUCCESS;
}
$minimumPayout = config('restaurant-delivery.earnings.payout.minimum_amount', 500);
$currency = config('restaurant-delivery.pricing.currency', 'BDT');
$this->info("Found {$riders->count()} riders with pending earnings.");
$this->info("Minimum payout threshold: {$currency} {$minimumPayout}");
$this->newLine();
$processed = 0;
$skipped = 0;
$this->table(
['Rider ID', 'Name', 'Pending Amount', 'Status'],
$riders->map(function ($rider) use ($minimumPayout, $currency, $dryRun, $paymentMethod, &$processed, &$skipped) {
$pendingAmount = $rider->earnings->sum('net_amount');
if ($pendingAmount < $minimumPayout) {
$skipped++;
return [
$rider->id,
$rider->full_name,
"{$currency} ".number_format($pendingAmount, 2),
'Below minimum',
];
}
if (! $dryRun) {
dispatch(new ProcessPayoutJob($rider, $paymentMethod));
}
$processed++;
return [
$rider->id,
$rider->full_name,
"{$currency} ".number_format($pendingAmount, 2),
$dryRun ? 'Would process' : 'Processing',
];
})->toArray()
);
$this->newLine();
if ($dryRun) {
$this->warn("DRY RUN: Would process {$processed} payouts, skip {$skipped} (below minimum).");
} else {
$this->info("Dispatched {$processed} payout jobs, skipped {$skipped} (below minimum).");
}
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Console\Commands;
use Illuminate\Console\Command;
use Modules\RestaurantDelivery\Enums\DeliveryStatus;
use Modules\RestaurantDelivery\Models\Delivery;
use Modules\RestaurantDelivery\Models\Rider;
use Modules\RestaurantDelivery\Services\Firebase\FirebaseService;
class SyncFirebaseData extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'delivery:sync-firebase
{--type=all : Type to sync (all, riders, deliveries)}
{--direction=to-firebase : Sync direction (to-firebase, from-firebase)}';
/**
* The console command description.
*/
protected $description = 'Synchronize data between database and Firebase';
/**
* Execute the console command.
*/
public function handle(FirebaseService $firebase): int
{
if (! $firebase->isEnabled()) {
$this->error('Firebase is not enabled. Check your configuration.');
return self::FAILURE;
}
$type = $this->option('type');
$direction = $this->option('direction');
$this->info("Syncing {$type} {$direction}...");
if ($type === 'all' || $type === 'riders') {
$this->syncRiders($firebase, $direction);
}
if ($type === 'all' || $type === 'deliveries') {
$this->syncDeliveries($firebase, $direction);
}
$this->info('Sync completed successfully.');
return self::SUCCESS;
}
/**
* Sync rider data.
*/
protected function syncRiders(FirebaseService $firebase, string $direction): void
{
$this->info('Syncing rider locations...');
$riders = Rider::where('is_online', true)
->whereNotNull('current_latitude')
->whereNotNull('current_longitude')
->get();
$bar = $this->output->createProgressBar($riders->count());
$bar->start();
foreach ($riders as $rider) {
if ($direction === 'to-firebase') {
$firebase->updateRiderLocation(
$rider->id,
(float) $rider->current_latitude,
(float) $rider->current_longitude
);
$firebase->updateRiderStatus($rider->id, $rider->status);
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Synced {$riders->count()} online riders.");
}
/**
* Sync delivery data.
*/
protected function syncDeliveries(FirebaseService $firebase, string $direction): void
{
$this->info('Syncing active deliveries...');
$deliveries = Delivery::whereIn('status', array_map(
fn ($s) => $s->value,
DeliveryStatus::riderActiveStatuses()
))
->whereNotNull('rider_id')
->with('rider')
->get();
$bar = $this->output->createProgressBar($deliveries->count());
$bar->start();
foreach ($deliveries as $delivery) {
if ($direction === 'to-firebase') {
$firebase->initializeDeliveryTracking($delivery->id, [
'status' => $delivery->status->value,
'rider_id' => $delivery->rider_id,
'pickup_latitude' => $delivery->pickup_latitude,
'pickup_longitude' => $delivery->pickup_longitude,
'drop_latitude' => $delivery->drop_latitude,
'drop_longitude' => $delivery->drop_longitude,
]);
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Synced {$deliveries->count()} active deliveries.");
}
}