Files
temporalio-test/app/Temporal/EloquentQuery/EloquentQueryActivity.php
2026-05-09 01:18:51 +02:00

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,
];
}
}