394 lines
14 KiB
PHP
394 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Temporal\EloquentQuery;
|
|
|
|
use App\Models\Order;
|
|
use App\Models\OrderItem;
|
|
use App\Models\Product;
|
|
use App\Models\User;
|
|
use App\Temporal\Shared\FaultSimulator;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Temporal\Activity;
|
|
|
|
class EloquentQueryActivity implements EloquentQueryActivityInterface
|
|
{
|
|
public function runInventoryAnalytics(array $simulationConfig = []): array
|
|
{
|
|
FaultSimulator::maybeApply($simulationConfig, 'runInventoryAnalytics');
|
|
$startTime = microtime(true);
|
|
$queriesRun = 0;
|
|
|
|
// Global stats with aggregation functions
|
|
$globalStats = Product::selectRaw('
|
|
COUNT(*) as total_products,
|
|
SUM(stock_quantity) as total_stock,
|
|
AVG(price) as avg_price,
|
|
MIN(price) as min_price,
|
|
MAX(price) as max_price,
|
|
SUM(price * stock_quantity) as total_inventory_value
|
|
')->first();
|
|
$queriesRun++;
|
|
|
|
// Per-category breakdown
|
|
$categoryBreakdown = Product::select('category')
|
|
->selectRaw('COUNT(*) as product_count')
|
|
->selectRaw('SUM(stock_quantity) as category_stock')
|
|
->selectRaw('AVG(price) as avg_price')
|
|
->selectRaw('SUM(price * stock_quantity) as category_value')
|
|
->groupBy('category')
|
|
->orderByDesc('category_value')
|
|
->get()
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// Stock level distribution using CASE WHEN
|
|
$stockDistribution = Product::selectRaw("
|
|
CASE
|
|
WHEN stock_quantity = 0 THEN 'out_of_stock'
|
|
WHEN stock_quantity BETWEEN 1 AND 10 THEN 'low_stock'
|
|
WHEN stock_quantity BETWEEN 11 AND 50 THEN 'medium_stock'
|
|
ELSE 'high_stock'
|
|
END as stock_level
|
|
")
|
|
->selectRaw('COUNT(*) as count')
|
|
->groupBy('stock_level')
|
|
->get()
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
return [
|
|
'queriesRun' => $queriesRun,
|
|
'rowsAffected' => 0,
|
|
'executionTimeMs' => round((microtime(true) - $startTime) * 1000, 2),
|
|
'data' => [
|
|
'globalStats' => $globalStats ? $globalStats->toArray() : [],
|
|
'categoryBreakdown' => $categoryBreakdown,
|
|
'stockDistribution' => $stockDistribution,
|
|
],
|
|
'attempt' => Activity::getInfo()->attempt,
|
|
];
|
|
}
|
|
|
|
public function runOrderDeepLoad(array $simulationConfig = []): array
|
|
{
|
|
FaultSimulator::maybeApply($simulationConfig, 'runOrderDeepLoad');
|
|
$startTime = microtime(true);
|
|
$queriesRun = 0;
|
|
|
|
// Eager load orders with items and products, count items
|
|
$orders = Order::with(['items.product'])
|
|
->withCount('items')
|
|
->get();
|
|
$queriesRun += 3; // orders + items + products (eager loading)
|
|
|
|
// Collection mapping — build summary per order
|
|
$orderSummaries = $orders->map(function ($order) {
|
|
$items = $order->items;
|
|
$heaviest = $items->sortByDesc('quantity')->first();
|
|
|
|
return [
|
|
'id' => $order->id,
|
|
'order_number' => $order->order_number,
|
|
'customer_name' => $order->customer_name,
|
|
'items_count' => $order->items_count,
|
|
'total_amount' => $order->total_amount,
|
|
'heaviest_line_item' => $heaviest ? [
|
|
'product' => $heaviest->product?->name,
|
|
'quantity' => $heaviest->quantity,
|
|
] : null,
|
|
];
|
|
})->toArray();
|
|
|
|
// Products that have been ordered vs never ordered
|
|
$orderedProductCount = Product::whereHas('orderItems')->count();
|
|
$queriesRun++;
|
|
|
|
$neverOrderedCount = Product::whereDoesntHave('orderItems')->count();
|
|
$queriesRun++;
|
|
|
|
return [
|
|
'queriesRun' => $queriesRun,
|
|
'rowsAffected' => 0,
|
|
'executionTimeMs' => round((microtime(true) - $startTime) * 1000, 2),
|
|
'data' => [
|
|
'orderCount' => $orders->count(),
|
|
'orderSummaries' => array_slice($orderSummaries, 0, 10),
|
|
'orderedProductCount' => $orderedProductCount,
|
|
'neverOrderedCount' => $neverOrderedCount,
|
|
],
|
|
'attempt' => Activity::getInfo()->attempt,
|
|
];
|
|
}
|
|
|
|
public function runProductScoring(array $simulationConfig = []): array
|
|
{
|
|
FaultSimulator::maybeApply($simulationConfig, 'runProductScoring');
|
|
$startTime = microtime(true);
|
|
$queriesRun = 0;
|
|
|
|
// Premium products: high price, in stock, ordered by price desc
|
|
$premiumProducts = Product::where('price', '>', 50)
|
|
->where('stock_quantity', '>', 0)
|
|
->where('status', 'active')
|
|
->orderByDesc('price')
|
|
->limit(10)
|
|
->get(['id', 'name', 'price', 'stock_quantity', 'category'])
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// At-risk inventory: nested where with orWhere closures
|
|
$atRiskProducts = Product::where(function ($q) {
|
|
$q->where('stock_quantity', 0)
|
|
->where('status', 'active');
|
|
})->orWhere(function ($q) {
|
|
$q->where('stock_quantity', '<', 5)
|
|
->where('stock_quantity', '>', 0)
|
|
->whereHas('orderItems');
|
|
})->get(['id', 'name', 'stock_quantity', 'category', 'status'])
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// Category tiers: groupBy with having
|
|
$categoryTiers = Product::select('category')
|
|
->selectRaw('COUNT(*) as product_count')
|
|
->selectRaw('AVG(price) as avg_price')
|
|
->selectRaw('SUM(stock_quantity) as total_stock')
|
|
->groupBy('category')
|
|
->having('avg_price', '>', 50)
|
|
->get()
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// Demand score: leftJoin with computed ratio
|
|
$demandScores = DB::table('products')
|
|
->leftJoin('order_items', 'products.id', '=', 'order_items.product_id')
|
|
->select(
|
|
'products.id',
|
|
'products.name',
|
|
'products.stock_quantity',
|
|
'products.category'
|
|
)
|
|
->selectRaw('COALESCE(SUM(order_items.quantity), 0) as total_ordered')
|
|
->selectRaw('CASE WHEN products.stock_quantity > 0 THEN ROUND(COALESCE(SUM(order_items.quantity), 0)::numeric / products.stock_quantity, 2) ELSE 0 END as demand_ratio')
|
|
->groupBy('products.id', 'products.name', 'products.stock_quantity', 'products.category')
|
|
->orderByDesc('demand_ratio')
|
|
->limit(15)
|
|
->get()
|
|
->map(fn ($r) => (array) $r)
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
return [
|
|
'queriesRun' => $queriesRun,
|
|
'rowsAffected' => 0,
|
|
'executionTimeMs' => round((microtime(true) - $startTime) * 1000, 2),
|
|
'data' => [
|
|
'premiumProducts' => $premiumProducts,
|
|
'atRiskProducts' => array_slice($atRiskProducts, 0, 10),
|
|
'categoryTiers' => $categoryTiers,
|
|
'demandScores' => $demandScores,
|
|
],
|
|
'attempt' => Activity::getInfo()->attempt,
|
|
];
|
|
}
|
|
|
|
public function runStockAudit(array $simulationConfig = []): array
|
|
{
|
|
FaultSimulator::maybeApply($simulationConfig, 'runStockAudit');
|
|
$startTime = microtime(true);
|
|
$queriesRun = 0;
|
|
$rowsAffected = 0;
|
|
$negativeFixed = 0;
|
|
$timestamped = 0;
|
|
|
|
Product::orderBy('id')->chunk(100, function ($products) use (&$queriesRun, &$rowsAffected, &$negativeFixed, &$timestamped) {
|
|
$queriesRun++; // each chunk is a query
|
|
|
|
Activity::heartbeat([
|
|
'processed' => $queriesRun * 100,
|
|
'negativeFixed' => $negativeFixed,
|
|
'timestamped' => $timestamped,
|
|
]);
|
|
|
|
foreach ($products as $product) {
|
|
$changed = false;
|
|
$updates = [];
|
|
|
|
// Fix negative stock
|
|
if ($product->stock_quantity < 0) {
|
|
$updates['stock_quantity'] = 0;
|
|
$negativeFixed++;
|
|
$changed = true;
|
|
}
|
|
|
|
// Stamp missing imported_at
|
|
if ($product->imported_at === null) {
|
|
$updates['imported_at'] = now();
|
|
$timestamped++;
|
|
$changed = true;
|
|
}
|
|
|
|
if ($changed) {
|
|
$product->update($updates);
|
|
$rowsAffected++;
|
|
$queriesRun++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return [
|
|
'queriesRun' => $queriesRun,
|
|
'rowsAffected' => $rowsAffected,
|
|
'executionTimeMs' => round((microtime(true) - $startTime) * 1000, 2),
|
|
'data' => [
|
|
'negativeStockFixed' => $negativeFixed,
|
|
'missingTimestampFixed' => $timestamped,
|
|
'note' => 'Idempotent — subsequent runs make 0 changes if data is clean',
|
|
],
|
|
'attempt' => Activity::getInfo()->attempt,
|
|
];
|
|
}
|
|
|
|
public function runPriceRecalculation(array $simulationConfig = []): array
|
|
{
|
|
FaultSimulator::maybeApply($simulationConfig, 'runPriceRecalculation');
|
|
$startTime = microtime(true);
|
|
$queriesRun = 0;
|
|
$rowsAffected = 0;
|
|
$discrepancies = [];
|
|
|
|
// Recalculate each order total from its items within a transaction
|
|
$orderIds = Order::pluck('id')->toArray();
|
|
$queriesRun++;
|
|
|
|
foreach ($orderIds as $orderId) {
|
|
DB::transaction(function () use ($orderId, &$queriesRun, &$rowsAffected, &$discrepancies) {
|
|
$order = Order::lockForUpdate()->find($orderId);
|
|
$queriesRun++;
|
|
|
|
if (!$order) {
|
|
return;
|
|
}
|
|
|
|
$recalculated = OrderItem::where('order_id', $orderId)
|
|
->selectRaw('SUM(quantity * unit_price) as computed_total')
|
|
->value('computed_total') ?? 0;
|
|
$queriesRun++;
|
|
|
|
$oldTotal = (float) $order->total_amount;
|
|
$newTotal = round((float) $recalculated, 2);
|
|
|
|
if (abs($oldTotal - $newTotal) > 0.01) {
|
|
$discrepancies[] = [
|
|
'order_id' => $orderId,
|
|
'old_total' => $oldTotal,
|
|
'new_total' => $newTotal,
|
|
'diff' => round($newTotal - $oldTotal, 2),
|
|
];
|
|
$order->update(['total_amount' => $newTotal]);
|
|
$rowsAffected++;
|
|
$queriesRun++;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Demonstrate updateOrCreate (upsert) pattern
|
|
Product::updateOrCreate(
|
|
['sku' => '_RECONCILIATION_MARKER'],
|
|
[
|
|
'name' => 'Reconciliation Marker',
|
|
'price' => 0,
|
|
'stock_quantity' => count($orderIds),
|
|
'category' => 'system',
|
|
'status' => 'inactive',
|
|
'description' => 'Last reconciliation: ' . now()->toISOString(),
|
|
'imported_at' => now(),
|
|
]
|
|
);
|
|
$queriesRun++;
|
|
$rowsAffected++;
|
|
|
|
return [
|
|
'queriesRun' => $queriesRun,
|
|
'rowsAffected' => $rowsAffected,
|
|
'executionTimeMs' => round((microtime(true) - $startTime) * 1000, 2),
|
|
'data' => [
|
|
'ordersChecked' => count($orderIds),
|
|
'discrepancies' => $discrepancies,
|
|
'discrepancyCount' => count($discrepancies),
|
|
'reconciliationMarkerSet' => true,
|
|
],
|
|
'attempt' => Activity::getInfo()->attempt,
|
|
];
|
|
}
|
|
|
|
public function runSummaryReport(array $simulationConfig = []): array
|
|
{
|
|
FaultSimulator::maybeApply($simulationConfig, 'runSummaryReport');
|
|
$startTime = microtime(true);
|
|
$queriesRun = 0;
|
|
|
|
// Top-selling products via DB::table join
|
|
$topSelling = DB::table('order_items')
|
|
->select('products.id', 'products.name', 'products.category')
|
|
->selectRaw('SUM(order_items.quantity) as total_sold')
|
|
->selectRaw('SUM(order_items.quantity * order_items.unit_price) as total_revenue')
|
|
->join('products', 'order_items.product_id', '=', 'products.id')
|
|
->groupBy('products.id', 'products.name', 'products.category')
|
|
->orderByDesc('total_revenue')
|
|
->limit(10)
|
|
->get()
|
|
->map(fn ($r) => (array) $r)
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// Revenue per category
|
|
$revenueByCategory = DB::table('order_items')
|
|
->join('products', 'order_items.product_id', '=', 'products.id')
|
|
->select('products.category')
|
|
->selectRaw('SUM(order_items.quantity * order_items.unit_price) as category_revenue')
|
|
->selectRaw('COUNT(DISTINCT order_items.order_id) as order_count')
|
|
->groupBy('products.category')
|
|
->orderByDesc('category_revenue')
|
|
->get()
|
|
->map(fn ($r) => (array) $r)
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// Customer spend ranking
|
|
$customerRanking = Order::select('customer_name')
|
|
->selectRaw('COUNT(*) as order_count')
|
|
->selectRaw('SUM(total_amount) as total_spent')
|
|
->selectRaw('AVG(total_amount) as avg_order_value')
|
|
->groupBy('customer_name')
|
|
->orderByDesc('total_spent')
|
|
->limit(10)
|
|
->get()
|
|
->toArray();
|
|
$queriesRun++;
|
|
|
|
// Cross-model health summary
|
|
$healthSummary = [
|
|
'products' => Product::count(),
|
|
'active_products' => Product::where('status', 'active')->count(),
|
|
'orders' => Order::count(),
|
|
'order_items' => OrderItem::count(),
|
|
'users' => User::count(),
|
|
];
|
|
$queriesRun += 5;
|
|
|
|
return [
|
|
'queriesRun' => $queriesRun,
|
|
'rowsAffected' => 0,
|
|
'executionTimeMs' => round((microtime(true) - $startTime) * 1000, 2),
|
|
'data' => [
|
|
'topSelling' => $topSelling,
|
|
'revenueByCategory' => $revenueByCategory,
|
|
'customerRanking' => $customerRanking,
|
|
'healthSummary' => $healthSummary,
|
|
],
|
|
'attempt' => Activity::getInfo()->attempt,
|
|
];
|
|
}
|
|
}
|