Init
This commit is contained in:
184
app/Temporal/OrderFulfillment/OrderActivity.php
Normal file
184
app/Temporal/OrderFulfillment/OrderActivity.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\OrderFulfillment;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Temporal\Shared\FaultSimulator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Temporal\Activity;
|
||||
|
||||
class OrderActivity implements OrderActivityInterface
|
||||
{
|
||||
public function validateOrder(int $orderId, array $simulationConfig = []): bool
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'validateOrder');
|
||||
|
||||
$order = Order::find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
if ($order->status !== 'pending') {
|
||||
throw new \RuntimeException("Order #{$orderId} is not in 'pending' status. Current status: {$order->status}");
|
||||
}
|
||||
|
||||
Log::info("Order #{$orderId} validated successfully.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function checkInventory(int $orderId, array $simulationConfig = []): bool
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'checkInventory');
|
||||
|
||||
$order = Order::with('items.product')->find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
if ($item->product->stock_quantity < $item->quantity) {
|
||||
throw new \RuntimeException(
|
||||
"Insufficient stock for product '{$item->product->name}'. "
|
||||
. "Available: {$item->product->stock_quantity}, Requested: {$item->quantity}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Inventory check passed for order #{$orderId}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function processPayment(int $orderId, array $simulationConfig = []): string
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'processPayment');
|
||||
|
||||
$order = Order::find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
$paymentId = 'PAY-' . strtoupper(substr(md5(uniqid((string) $orderId, true)), 0, 12));
|
||||
|
||||
$order->update([
|
||||
'status' => 'processing',
|
||||
'payment_id' => $paymentId,
|
||||
]);
|
||||
|
||||
Log::info("Payment processed for order #{$orderId}. Payment ID: {$paymentId}");
|
||||
|
||||
return $paymentId;
|
||||
}
|
||||
|
||||
public function refundPayment(int $orderId, string $paymentId): bool
|
||||
{
|
||||
usleep(300000);
|
||||
|
||||
$order = Order::find($orderId);
|
||||
|
||||
if ($order) {
|
||||
$order->update([
|
||||
'status' => 'refunded',
|
||||
'payment_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
Log::warning("Payment refunded for order #{$orderId}. Payment ID: {$paymentId}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateInventory(int $orderId, array $simulationConfig = []): bool
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'updateInventory');
|
||||
|
||||
$order = Order::with('items.product')->find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->decrement('stock_quantity', $item->quantity);
|
||||
}
|
||||
|
||||
Log::info("Inventory updated (decremented) for order #{$orderId}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function restoreInventory(int $orderId): bool
|
||||
{
|
||||
usleep(200000);
|
||||
|
||||
$order = Order::with('items.product')->find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
foreach ($order->items as $item) {
|
||||
$item->product->increment('stock_quantity', $item->quantity);
|
||||
}
|
||||
|
||||
Log::info("Inventory restored (incremented) for order #{$orderId}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function notifyWarehouse(int $orderId, array $simulationConfig = []): bool
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'notifyWarehouse');
|
||||
|
||||
$order = Order::find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
$order->update(['status' => 'warehouse_notified']);
|
||||
|
||||
Log::info("Warehouse notified for order #{$orderId}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cancelWarehouseNotification(int $orderId): bool
|
||||
{
|
||||
usleep(100000);
|
||||
|
||||
$order = Order::find($orderId);
|
||||
|
||||
if ($order) {
|
||||
$order->update(['status' => 'warehouse_cancelled']);
|
||||
}
|
||||
|
||||
Log::warning("Warehouse notification cancelled for order #{$orderId}.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendTrackingInfo(int $orderId, string $trackingNumber, array $simulationConfig = []): bool
|
||||
{
|
||||
FaultSimulator::maybeApply($simulationConfig, 'sendTrackingInfo');
|
||||
|
||||
$order = Order::find($orderId);
|
||||
|
||||
if (!$order) {
|
||||
throw new \RuntimeException("Order #{$orderId} not found.");
|
||||
}
|
||||
|
||||
$order->update([
|
||||
'tracking_number' => $trackingNumber,
|
||||
'status' => 'shipped',
|
||||
]);
|
||||
|
||||
Log::info("Tracking info sent for order #{$orderId}. Tracking: {$trackingNumber}");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
37
app/Temporal/OrderFulfillment/OrderActivityInterface.php
Normal file
37
app/Temporal/OrderFulfillment/OrderActivityInterface.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\OrderFulfillment;
|
||||
|
||||
use Temporal\Activity\ActivityInterface;
|
||||
use Temporal\Activity\ActivityMethod;
|
||||
|
||||
#[ActivityInterface]
|
||||
interface OrderActivityInterface
|
||||
{
|
||||
#[ActivityMethod]
|
||||
public function validateOrder(int $orderId, array $simulationConfig = []): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function checkInventory(int $orderId, array $simulationConfig = []): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function processPayment(int $orderId, array $simulationConfig = []): string;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function refundPayment(int $orderId, string $paymentId): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function updateInventory(int $orderId, array $simulationConfig = []): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function restoreInventory(int $orderId): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function notifyWarehouse(int $orderId, array $simulationConfig = []): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function cancelWarehouseNotification(int $orderId): bool;
|
||||
|
||||
#[ActivityMethod]
|
||||
public function sendTrackingInfo(int $orderId, string $trackingNumber, array $simulationConfig = []): bool;
|
||||
}
|
||||
114
app/Temporal/OrderFulfillment/OrderFulfillmentWorkflow.php
Normal file
114
app/Temporal/OrderFulfillment/OrderFulfillmentWorkflow.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\OrderFulfillment;
|
||||
|
||||
use Carbon\CarbonInterval;
|
||||
use Temporal\Activity\ActivityOptions;
|
||||
use Temporal\Common\RetryOptions;
|
||||
use Temporal\Workflow;
|
||||
|
||||
class OrderFulfillmentWorkflow implements OrderFulfillmentWorkflowInterface
|
||||
{
|
||||
private string $status = 'pending';
|
||||
private ?string $trackingNumber = null;
|
||||
private bool $shippingConfirmed = false;
|
||||
private int $orderId = 0;
|
||||
private int $retryCount = 0;
|
||||
private int $rateLimitHits = 0;
|
||||
|
||||
/** @var mixed Activity stub - untyped due to PHP SDK limitation */
|
||||
private $activityStub;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->activityStub = Workflow::newActivityStub(
|
||||
OrderActivityInterface::class,
|
||||
ActivityOptions::new()
|
||||
->withStartToCloseTimeout(CarbonInterval::minutes(5))
|
||||
->withRetryOptions(
|
||||
RetryOptions::new()
|
||||
->withMaximumAttempts(3)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function processOrder(int $orderId, array $simulationConfig = []): \Generator
|
||||
{
|
||||
$this->orderId = $orderId;
|
||||
|
||||
$saga = new Workflow\Saga();
|
||||
|
||||
try {
|
||||
// Step 1: Validate the order
|
||||
$this->status = 'validating';
|
||||
yield $this->activityStub->validateOrder($orderId, $simulationConfig);
|
||||
|
||||
// Step 2: Check inventory availability
|
||||
$this->status = 'checking_inventory';
|
||||
yield $this->activityStub->checkInventory($orderId, $simulationConfig);
|
||||
|
||||
// Step 3: Process payment
|
||||
$this->status = 'processing_payment';
|
||||
$paymentId = yield $this->activityStub->processPayment($orderId, $simulationConfig);
|
||||
|
||||
// Register compensation: refund payment if later steps fail
|
||||
$saga->addCompensation(fn() => yield $this->activityStub->refundPayment($orderId, $paymentId));
|
||||
|
||||
// Step 4: Update inventory (decrement stock)
|
||||
$this->status = 'updating_inventory';
|
||||
yield $this->activityStub->updateInventory($orderId, $simulationConfig);
|
||||
|
||||
// Register compensation: restore inventory if later steps fail
|
||||
$saga->addCompensation(fn() => yield $this->activityStub->restoreInventory($orderId));
|
||||
|
||||
// Step 5: Notify warehouse
|
||||
$this->status = 'notifying_warehouse';
|
||||
yield $this->activityStub->notifyWarehouse($orderId, $simulationConfig);
|
||||
|
||||
// Register compensation: cancel warehouse notification if later steps fail
|
||||
$saga->addCompensation(fn() => yield $this->activityStub->cancelWarehouseNotification($orderId));
|
||||
|
||||
// Step 6: Wait for shipping confirmation signal
|
||||
$this->status = 'awaiting_shipment';
|
||||
yield Workflow::await(fn() => $this->shippingConfirmed);
|
||||
|
||||
// Step 7: Send tracking information
|
||||
$this->status = 'sending_tracking';
|
||||
yield $this->activityStub->sendTrackingInfo($orderId, $this->trackingNumber, $simulationConfig);
|
||||
|
||||
// All steps completed successfully
|
||||
$this->status = 'completed';
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'orderId' => $orderId,
|
||||
'trackingNumber' => $this->trackingNumber,
|
||||
'status' => $this->status,
|
||||
'retryCount' => $this->retryCount,
|
||||
'rateLimitHits' => $this->rateLimitHits,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
yield $saga->compensate();
|
||||
$this->status = 'failed';
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmShipping(string $trackingNumber): void
|
||||
{
|
||||
$this->trackingNumber = $trackingNumber;
|
||||
$this->shippingConfirmed = true;
|
||||
}
|
||||
|
||||
public function getOrderStatus(): array
|
||||
{
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'orderId' => $this->orderId,
|
||||
'trackingNumber' => $this->trackingNumber,
|
||||
'shippingConfirmed' => $this->shippingConfirmed,
|
||||
'retryCount' => $this->retryCount,
|
||||
'rateLimitHits' => $this->rateLimitHits,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Temporal\OrderFulfillment;
|
||||
|
||||
use Temporal\Workflow\WorkflowInterface;
|
||||
use Temporal\Workflow\WorkflowMethod;
|
||||
use Temporal\Workflow\SignalMethod;
|
||||
use Temporal\Workflow\QueryMethod;
|
||||
|
||||
#[WorkflowInterface]
|
||||
interface OrderFulfillmentWorkflowInterface
|
||||
{
|
||||
#[WorkflowMethod]
|
||||
public function processOrder(int $orderId, array $simulationConfig = []);
|
||||
|
||||
#[SignalMethod]
|
||||
public function confirmShipping(string $trackingNumber): void;
|
||||
|
||||
#[QueryMethod]
|
||||
public function getOrderStatus(): array;
|
||||
}
|
||||
Reference in New Issue
Block a user