A tests, I product action
This commit is contained in:
parent
c6c159a4ff
commit
a4fedcdb58
14
README.md
14
README.md
|
|
@ -60,13 +60,21 @@ $product = Product::create([
|
||||||
'slug' => 'amazing-t-shirt',
|
'slug' => 'amazing-t-shirt',
|
||||||
'sku' => 'TSH-001',
|
'sku' => 'TSH-001',
|
||||||
'type' => 'simple',
|
'type' => 'simple',
|
||||||
'price' => 29.99,
|
|
||||||
'regular_price' => 29.99,
|
|
||||||
'manage_stock' => true,
|
'manage_stock' => true,
|
||||||
'stock_quantity' => 100,
|
|
||||||
'status' => 'published',
|
'status' => 'published',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$product->prices()->create([
|
||||||
|
'currency' => 'USD',
|
||||||
|
'unit_amount' => 1999, // $19.99
|
||||||
|
'sale_unit_amount' => 1499, // $14.99
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product->stocks()->create([
|
||||||
|
'quantity' => 100,
|
||||||
|
]);
|
||||||
|
|
||||||
// Add translated name
|
// Add translated name
|
||||||
$product->setLocalized('name', 'Amazing T-Shirt', 'en');
|
$product->setLocalized('name', 'Amazing T-Shirt', 'en');
|
||||||
$product->setLocalized('description', 'A comfortable cotton t-shirt', 'en');
|
$product->setLocalized('description', 'A comfortable cotton t-shirt', 'en');
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
return [
|
return [
|
||||||
// Table names (customizable for multi-tenancy)
|
// Table names (customizable for multi-tenancy)
|
||||||
'tables' => [
|
'tables' => [
|
||||||
'products' => 'products',
|
'cart_items' => 'cart_items',
|
||||||
'product_prices' => 'product_prices',
|
'carts' => 'carts',
|
||||||
'product_categories' => 'product_categories',
|
'payment_methods' => 'payment_methods',
|
||||||
|
'payment_provider_identities' => 'payment_provider_identities',
|
||||||
|
'product_action_runs' => 'product_action_runs',
|
||||||
'product_attributes' => 'product_attributes',
|
'product_attributes' => 'product_attributes',
|
||||||
|
'product_categories' => 'product_categories',
|
||||||
|
'product_prices' => 'product_prices',
|
||||||
'product_purchases' => 'product_purchases',
|
'product_purchases' => 'product_purchases',
|
||||||
'product_stocks' => 'product_stocks',
|
'product_stocks' => 'product_stocks',
|
||||||
'carts' => 'carts',
|
'products' => 'products',
|
||||||
'cart_items' => 'cart_items',
|
|
||||||
'payment_provider_identities' => 'payment_provider_identities',
|
|
||||||
'payment_methods' => 'payment_methods',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
// Model classes (allow overriding in main instance)
|
// Model classes (allow overriding in main instance)
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ return new class extends Migration
|
||||||
// Product category pivot table
|
// Product category pivot table
|
||||||
if (!Schema::hasTable(config('shop.tables.product_category_product', 'product_category_product'))) {
|
if (!Schema::hasTable(config('shop.tables.product_category_product', 'product_category_product'))) {
|
||||||
Schema::create(config('shop.tables.product_category_product', 'product_category_product'), function (Blueprint $table) {
|
Schema::create(config('shop.tables.product_category_product', 'product_category_product'), function (Blueprint $table) {
|
||||||
$table->uuid('product_id');
|
$table->uuid('product_id')->nullable();
|
||||||
$table->uuid('product_category_id');
|
$table->uuid('product_category_id');
|
||||||
$table->integer('sort_order')->default(0);
|
$table->integer('sort_order')->default(0);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
@ -210,15 +210,20 @@ return new class extends Migration
|
||||||
Schema::create(config('shop.tables.product_actions', 'product_actions'), function (Blueprint $table) {
|
Schema::create(config('shop.tables.product_actions', 'product_actions'), function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->uuid('id')->primary();
|
||||||
$table->uuid('product_id');
|
$table->uuid('product_id');
|
||||||
$table->string('action_type');
|
$table->json('events')->default('[]'); // e.g. ["purchased","paid","refunded","viewed"]
|
||||||
$table->string('event')->default('purchased'); // purchased, refunded, etc.
|
$table->string('class'); // e.g. \App\...
|
||||||
|
$table->string('method')->nullable(); // null means __invoke via constructor params
|
||||||
|
$table->boolean('defer')->default(true); // queued when true, sync otherwise
|
||||||
$table->json('parameters')->nullable();
|
$table->json('parameters')->nullable();
|
||||||
$table->boolean('active')->default(true);
|
$table->boolean('active')->default(true);
|
||||||
$table->integer('sort_order')->default(0);
|
$table->integer('sort_order')->default(0);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->index(['product_id', 'event', 'active']);
|
$table->index(['product_id', 'active']);
|
||||||
$table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade');
|
$table->foreign('product_id')
|
||||||
|
->references('id')
|
||||||
|
->on(config('shop.tables.products', 'products'))
|
||||||
|
->nullOnDelete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,6 +343,18 @@ return new class extends Migration
|
||||||
->onDelete('cascade');
|
->onDelete('cascade');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProductActionRuns table
|
||||||
|
if (!Schema::hasTable(config('shop.tables.product_action_runs', 'product_action_runs'))) {
|
||||||
|
Schema::create(config('shop.tables.product_action_runs', 'product_action_runs'), function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('action');
|
||||||
|
$table->unsignedBigInteger('product_purchase_id')->nullable();
|
||||||
|
$table->boolean('success')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->foreign('product_purchase_id')->references('id')->on(config('shop.tables.product_purchases', 'product_purchases'))->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -350,6 +367,7 @@ return new class extends Migration
|
||||||
Schema::dropIfExists(config('shop.tables.cart_discounts', 'cart_discounts'));
|
Schema::dropIfExists(config('shop.tables.cart_discounts', 'cart_discounts'));
|
||||||
Schema::dropIfExists(config('shop.tables.cart_items', 'cart_items'));
|
Schema::dropIfExists(config('shop.tables.cart_items', 'cart_items'));
|
||||||
Schema::dropIfExists(config('shop.tables.carts', 'carts'));
|
Schema::dropIfExists(config('shop.tables.carts', 'carts'));
|
||||||
|
Schema::dropIfExists(config('shop.tables.product_action_runs', 'product_action_runs'));
|
||||||
Schema::dropIfExists(config('shop.tables.product_purchases', 'product_purchases'));
|
Schema::dropIfExists(config('shop.tables.product_purchases', 'product_purchases'));
|
||||||
Schema::dropIfExists(config('shop.tables.product_actions', 'product_actions'));
|
Schema::dropIfExists(config('shop.tables.product_actions', 'product_actions'));
|
||||||
Schema::dropIfExists(config('shop.tables.product_category_product', 'product_category_product'));
|
Schema::dropIfExists(config('shop.tables.product_category_product', 'product_category_product'));
|
||||||
|
|
@ -358,7 +376,7 @@ return new class extends Migration
|
||||||
Schema::dropIfExists(config('shop.tables.product_stocks', 'product_stocks'));
|
Schema::dropIfExists(config('shop.tables.product_stocks', 'product_stocks'));
|
||||||
Schema::dropIfExists(config('shop.tables.product_attributes', 'product_attributes'));
|
Schema::dropIfExists(config('shop.tables.product_attributes', 'product_attributes'));
|
||||||
Schema::dropIfExists(config('shop.tables.product_prices', 'product_prices'));
|
Schema::dropIfExists(config('shop.tables.product_prices', 'product_prices'));
|
||||||
Schema::dropIfExists('product_category_product');
|
Schema::dropIfExists(config('shop.tables.product_category_product', 'product_category_product'));
|
||||||
Schema::dropIfExists(config('shop.tables.product_categories', 'product_categories'));
|
Schema::dropIfExists(config('shop.tables.product_categories', 'product_categories'));
|
||||||
Schema::dropIfExists(config('shop.tables.products', 'products'));
|
Schema::dropIfExists(config('shop.tables.products', 'products'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -316,22 +316,19 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function syncPricesDown()
|
|
||||||
{
|
|
||||||
if (config('shop.stripe.enabled') && config('shop.stripe.sync_prices')) {
|
|
||||||
StripeService::syncProductPricesDown($this);
|
|
||||||
}
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getAvailableActions(): array
|
public static function getAvailableActions(): array
|
||||||
{
|
{
|
||||||
return ProductAction::getAvailableActions();
|
return ProductAction::getAvailableActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function callActions(string $event = 'purchased', ?ProductPurchase $productPurchase = null, array $additionalData = []): void
|
public function callActions(string $event = 'purchased', ?ProductPurchase $productPurchase = null, array $additionalData = [])
|
||||||
{
|
{
|
||||||
ProductAction::callForProduct($this, $event, $productPurchase, $additionalData);
|
return ProductAction::callForProduct(
|
||||||
|
$this,
|
||||||
|
$event,
|
||||||
|
$productPurchase,
|
||||||
|
$additionalData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function relatedProducts(): BelongsToMany
|
public function relatedProducts(): BelongsToMany
|
||||||
|
|
@ -529,7 +526,7 @@ class Product extends Model implements Purchasable, Cartable
|
||||||
->where('product_id', $this->id);
|
->where('product_id', $this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasPrice() : bool
|
public function hasPrice(): bool
|
||||||
{
|
{
|
||||||
return $this->prices()->exists();
|
return $this->prices()->exists();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,20 @@ class ProductAction extends Model
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'product_id',
|
'product_id',
|
||||||
'event',
|
'events',
|
||||||
'action_type',
|
'class',
|
||||||
|
'method',
|
||||||
|
'defer',
|
||||||
'parameters',
|
'parameters',
|
||||||
'active',
|
'active',
|
||||||
'sort_order',
|
'sort_order',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
'events' => 'array',
|
||||||
'parameters' => 'array',
|
'parameters' => 'array',
|
||||||
'active' => 'boolean',
|
'active' => 'boolean',
|
||||||
|
'defer' => 'boolean',
|
||||||
'sort_order' => 'integer',
|
'sort_order' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -37,6 +41,43 @@ class ProductAction extends Model
|
||||||
return $this->belongsTo(config('shop.models.product', Product::class));
|
return $this->belongsTo(config('shop.models.product', Product::class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function runs()
|
||||||
|
{
|
||||||
|
return $this->morphMany(ProductActionRun::class, 'action');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility accessor: expose first event as 'event'
|
||||||
|
public function getEventAttribute(): ?string
|
||||||
|
{
|
||||||
|
$events = $this->events ?? [];
|
||||||
|
return is_array($events) ? ($events[0] ?? null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compatibility mutators for legacy fields used in tests/factories
|
||||||
|
public function setEventAttribute($value): void
|
||||||
|
{
|
||||||
|
// Ensure events array reflects provided single event
|
||||||
|
$this->events = is_null($value) ? [] : [(string) $value];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setActionTypeAttribute($value): void
|
||||||
|
{
|
||||||
|
if (is_string($value)) {
|
||||||
|
// If a fully-qualified class is passed, use it; otherwise prefix with namespace config
|
||||||
|
if (str_starts_with($value, '\\') || str_contains($value, '\\')) {
|
||||||
|
$this->class = $value;
|
||||||
|
} else {
|
||||||
|
$namespace = config('shop.actions.namespace', 'App\\Jobs\\ProductAction');
|
||||||
|
$this->class = $namespace . '\\' . $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function callForProduct(
|
public static function callForProduct(
|
||||||
Product $product,
|
Product $product,
|
||||||
string $event,
|
string $event,
|
||||||
|
|
@ -44,7 +85,7 @@ class ProductAction extends Model
|
||||||
array $additionalData = []
|
array $additionalData = []
|
||||||
): void {
|
): void {
|
||||||
$actions = $product->actions()
|
$actions = $product->actions()
|
||||||
->where('event', $event)
|
->whereJsonContains('events', $event)
|
||||||
->where('active', true)
|
->where('active', true)
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->get();
|
->get();
|
||||||
|
|
@ -53,21 +94,12 @@ class ProductAction extends Model
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$available_actions = self::getAvailableActions();
|
|
||||||
|
|
||||||
foreach ($actions as $action) {
|
foreach ($actions as $action) {
|
||||||
|
$success = false;
|
||||||
try {
|
try {
|
||||||
if (!isset($available_actions[$action->action_type])) {
|
$class = $action->class;
|
||||||
Log::warning('Product action not found', [
|
$method = $action->method;
|
||||||
'product_id' => $product->id,
|
$defer = (bool) $action->defer;
|
||||||
'event' => $event,
|
|
||||||
'action_type' => $action->action_type,
|
|
||||||
]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$namespace = config('shop.actions.namespace', 'App\\Jobs\\ProductAction');
|
|
||||||
$action_job = $namespace . '\\' . $action->action_type;
|
|
||||||
|
|
||||||
$params = [
|
$params = [
|
||||||
'product' => $product,
|
'product' => $product,
|
||||||
|
|
@ -77,46 +109,140 @@ class ProductAction extends Model
|
||||||
...$additionalData,
|
...$additionalData,
|
||||||
];
|
];
|
||||||
|
|
||||||
dispatch(new $action_job(...$params));
|
// Skip if class is not defined
|
||||||
} catch (\Exception $e) {
|
if (empty($class) || !is_string($class)) {
|
||||||
|
Log::warning('Product action class missing', [
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'event' => $event,
|
||||||
|
'action_id' => $action->id ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer via queue or call synchronously
|
||||||
|
if ($defer) {
|
||||||
|
// If a method is provided, dispatch a closure job calling the static method
|
||||||
|
if ($method) {
|
||||||
|
defer(
|
||||||
|
fn() =>
|
||||||
|
dispatch(function () use ($class, $method, $params, $action, $productPurchase) {
|
||||||
|
if (!class_exists($class)) {
|
||||||
|
Log::warning('Product action class not found for deferred static call', ['class' => $class, 'method' => $method]);
|
||||||
|
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$class::$method(...$params);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Assume class is a Job or invokable and can be dispatched directly
|
||||||
|
if (class_exists($class)) {
|
||||||
|
dispatch(new $class(...$params));
|
||||||
|
} else {
|
||||||
|
defer(
|
||||||
|
fn() =>
|
||||||
|
dispatch(function () use ($class, $action, $productPurchase) {
|
||||||
|
Log::warning('Product action class not found for deferred job', ['class' => $class]);
|
||||||
|
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For deferred jobs, we assume success since they were dispatched
|
||||||
|
$success = true;
|
||||||
|
} else {
|
||||||
|
if ($method) {
|
||||||
|
// Call static method directly
|
||||||
|
if (class_exists($class)) {
|
||||||
|
$class::$method(...$params);
|
||||||
|
$success = true;
|
||||||
|
} else {
|
||||||
|
Log::warning('Product action class not found for static call', ['class' => $class, 'method' => $method]);
|
||||||
|
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Instantiate and invoke if invokable
|
||||||
|
if (class_exists($class)) {
|
||||||
|
$instance = new $class(...$params);
|
||||||
|
if (is_callable($instance)) {
|
||||||
|
$instance();
|
||||||
|
$success = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log::warning('Product action class not found for direct instantiation', ['class' => $class]);
|
||||||
|
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful action run
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => $success,
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
Log::error('Error calling product action', [
|
Log::error('Error calling product action', [
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => $event,
|
'event' => $event,
|
||||||
'action_type' => $action->action_type ?? 'unknown',
|
'class' => $action->class ?? 'unknown',
|
||||||
|
'method' => $action->method ?? null,
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'trace' => $e->getTraceAsString(),
|
'trace' => $e->getTraceAsString(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Log failed action run
|
||||||
|
ProductActionRun::create([
|
||||||
|
'action_id' => $action->id,
|
||||||
|
'action_type' => ProductAction::class,
|
||||||
|
'product_purchase_id' => $productPurchase?->id,
|
||||||
|
'success' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
report($e);
|
report($e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(
|
|
||||||
Product $product,
|
|
||||||
?ProductPurchase $productPurchase = null,
|
|
||||||
array $additionalData = []
|
|
||||||
): void {
|
|
||||||
$namespace = config('shop.actions.namespace', 'App\\Jobs\\ProductAction');
|
|
||||||
$action_job = $namespace . '\\' . $this->action_type;
|
|
||||||
|
|
||||||
if (!class_exists($action_job)) {
|
|
||||||
throw new \Exception("Action class {$action_job} not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
$params = [
|
|
||||||
'product' => $product,
|
|
||||||
'productPurchase' => $productPurchase,
|
|
||||||
'event' => $this->event,
|
|
||||||
...($this->parameters ?? []),
|
|
||||||
...$additionalData,
|
|
||||||
];
|
|
||||||
|
|
||||||
dispatch(new $action_job(...$params));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getAvailableActions(): array
|
|
||||||
{
|
|
||||||
return config('shop.actions.available', []);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
class ProductActionRun extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'product_action_runs';
|
||||||
|
protected $fillable = [
|
||||||
|
'action_id',
|
||||||
|
'action_type',
|
||||||
|
'product_purchase_id',
|
||||||
|
'success',
|
||||||
|
];
|
||||||
|
protected $casts = [
|
||||||
|
'success' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function action(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function productPurchase()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductPurchase::class, 'product_purchase_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -78,9 +78,45 @@ class ProductPurchase extends Model
|
||||||
protected static function booted()
|
protected static function booted()
|
||||||
{
|
{
|
||||||
static::created(function ($productPurchase) {
|
static::created(function ($productPurchase) {
|
||||||
if ($productPurchase->status === 'completed' && $product = $productPurchase->product) {
|
$product = ($productPurchase->purchasable instanceof Product)
|
||||||
|
? $productPurchase->purchasable
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$product ??= ($productPurchase->purchasable instanceof ProductPrice)
|
||||||
|
? $productPurchase->purchasable?->product
|
||||||
|
: $product;
|
||||||
|
|
||||||
|
if ($productPurchase->status === 'completed' && $product) {
|
||||||
|
$product->callActions('purchased', $productPurchase);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// updated purchase from unpaid to paid
|
||||||
|
static::updated(function ($productPurchase) {
|
||||||
|
$product = ($productPurchase->purchasable instanceof Product)
|
||||||
|
? $productPurchase->purchasable
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$product ??= ($productPurchase->purchasable instanceof ProductPrice)
|
||||||
|
? $productPurchase->purchasable?->product
|
||||||
|
: $product;
|
||||||
|
|
||||||
|
|
||||||
|
if ($productPurchase->status === 'completed' && $product) {
|
||||||
$product->callActions('purchased', $productPurchase);
|
$product->callActions('purchased', $productPurchase);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function actionRuns()
|
||||||
|
{
|
||||||
|
return $this->hasManyThrough(
|
||||||
|
ProductActionRun::class,
|
||||||
|
ProductAction::class,
|
||||||
|
'product_id', // Foreign key on ProductAction table...
|
||||||
|
'action_id', // Foreign key on ProductActionRun table...
|
||||||
|
'purchasable_id', // Local key on ProductPurchase table...
|
||||||
|
'id' // Local key on ProductAction table...
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Traits;
|
namespace Blax\Shop\Traits;
|
||||||
|
|
||||||
|
use Blax\Shop\Contracts\Purchasable;
|
||||||
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
|
||||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
use Blax\Shop\Exceptions\NotPurchasable;
|
use Blax\Shop\Exceptions\NotPurchasable;
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,8 @@ class HasShoppingCapabilitiesTest extends TestCase
|
||||||
|
|
||||||
// Create a product action
|
// Create a product action
|
||||||
$product->actions()->create([
|
$product->actions()->create([
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'TestAction',
|
'class' => 'TestAction',
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,18 @@ class ProductActionTest extends TestCase
|
||||||
{
|
{
|
||||||
$product = Product::factory()->create();
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = $product->actions()->create([
|
||||||
'product_id' => $product->id,
|
'events' => ['purchased', 'refunded'],
|
||||||
'event' => 'purchased',
|
'class' => 'App\\Actions\\SendWelcomeEmail',
|
||||||
'action_type' => 'App\\Actions\\SendWelcomeEmail',
|
|
||||||
'active' => true,
|
|
||||||
'sort_order' => 10,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertDatabaseHas('product_actions', [
|
$this->assertDatabaseHas('product_actions', [
|
||||||
'id' => $action->id,
|
'id' => $action->id,
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->assertContains('purchased', $action->events ?? []);
|
||||||
|
$this->assertContains('refunded', $action->events ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -40,16 +39,14 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\SendWelcomeEmail',
|
'class' => 'App\\Actions\\SendWelcomeEmail',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\GrantAccess',
|
'class' => 'App\\Actions\\GrantAccess',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertCount(2, $product->fresh()->actions);
|
$this->assertCount(2, $product->fresh()->actions);
|
||||||
|
|
@ -62,9 +59,8 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'event' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\TestAction',
|
'class' => 'App\\Actions\\TestAction',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals($product->id, $action->product->id);
|
$this->assertEquals($product->id, $action->product->id);
|
||||||
|
|
@ -77,11 +73,12 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\TestAction',
|
'class' => 'App\\Actions\\TestAction',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$action->refresh();
|
||||||
|
|
||||||
$this->assertTrue($action->active);
|
$this->assertTrue($action->active);
|
||||||
|
|
||||||
$action->update(['active' => false]);
|
$action->update(['active' => false]);
|
||||||
|
|
@ -96,14 +93,13 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\SendEmail',
|
'class' => 'App\\Actions\\SendEmail',
|
||||||
'parameters' => [
|
'parameters' => [
|
||||||
'template' => 'welcome',
|
'template' => 'welcome',
|
||||||
'delay' => 60,
|
'delay' => 60,
|
||||||
'subject' => 'Welcome to our service',
|
'subject' => 'Welcome to our service',
|
||||||
],
|
],
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$action = $action->fresh();
|
$action = $action->fresh();
|
||||||
|
|
@ -120,18 +116,14 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action1 = ProductAction::create([
|
$action1 = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\FirstAction',
|
'class' => 'App\\Actions\\FirstAction',
|
||||||
'sort_order' => 1,
|
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$action2 = ProductAction::create([
|
$action2 = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\SecondAction',
|
'class' => 'App\\Actions\\SecondAction',
|
||||||
'sort_order' => 2,
|
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$sorted = ProductAction::where('product_id', $product->id)
|
$sorted = ProductAction::where('product_id', $product->id)
|
||||||
|
|
@ -149,20 +141,20 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$purchasedAction = ProductAction::create([
|
$purchasedAction = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\OnPurchase',
|
'class' => 'App\\Actions\\OnPurchase',
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$refundedAction = ProductAction::create([
|
$refundedAction = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'refunded',
|
'events' => ['refunded'],
|
||||||
'action_type' => 'App\\Actions\\OnRefund',
|
'class' => 'App\\Actions\\OnRefund',
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('purchased', $purchasedAction->event);
|
$this->assertContains('purchased', $purchasedAction->events);
|
||||||
$this->assertEquals('refunded', $refundedAction->event);
|
$this->assertContains('refunded', $refundedAction->events);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
@ -172,27 +164,24 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\OnPurchase',
|
'class' => 'App\\Actions\\OnPurchase',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\AnotherPurchase',
|
'class' => 'App\\Actions\\OnPurchase',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'refunded',
|
'events' => ['refunded'],
|
||||||
'action_type' => 'App\\Actions\\OnRefund',
|
'class' => 'App\\Actions\\OnPurchase',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$purchaseActions = ProductAction::where('product_id', $product->id)
|
$purchaseActions = ProductAction::where('product_id', $product->id)
|
||||||
->where('event', 'purchased')
|
->whereJsonContains('events', 'purchased')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$this->assertCount(2, $purchaseActions);
|
$this->assertCount(2, $purchaseActions);
|
||||||
|
|
@ -205,15 +194,14 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\EnabledAction',
|
'class' => 'App\\Actions\\EnabledAction',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\DisabledAction',
|
'class' => 'App\\Actions\\DisabledAction',
|
||||||
'active' => false,
|
'active' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -232,16 +220,14 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product1->id,
|
'product_id' => $product1->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\CommonAction',
|
'class' => 'App\\Actions\\CommonAction',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ProductAction::create([
|
ProductAction::create([
|
||||||
'product_id' => $product2->id,
|
'product_id' => $product2->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\CommonAction',
|
'class' => 'App\\Actions\\CommonAction',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertCount(1, $product1->actions);
|
$this->assertCount(1, $product1->actions);
|
||||||
|
|
@ -255,12 +241,11 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\TestAction',
|
'class' => 'App\\Actions\\TestAction',
|
||||||
'parameters' => [
|
'parameters' => [
|
||||||
'key' => 'old_value'
|
'key' => 'old_value'
|
||||||
],
|
],
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$action->update([
|
$action->update([
|
||||||
|
|
@ -284,8 +269,8 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'event' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\TestAction',
|
'class' => 'App\\Actions\\TestAction',
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -303,9 +288,8 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$action = ProductAction::create([
|
$action = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\SimpleAction',
|
'class' => 'App\\Actions\\SimpleAction',
|
||||||
'active' => true,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertNull($action->parameters);
|
$this->assertNull($action->parameters);
|
||||||
|
|
@ -318,24 +302,24 @@ class ProductActionTest extends TestCase
|
||||||
|
|
||||||
$high = ProductAction::create([
|
$high = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\HighPriority',
|
'class' => 'App\\Actions\\HighPriority',
|
||||||
'sort_order' => 100,
|
'sort_order' => 100,
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$medium = ProductAction::create([
|
$medium = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\MediumPriority',
|
'class' => 'App\\Actions\\MediumPriority',
|
||||||
'sort_order' => 50,
|
'sort_order' => 50,
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$low = ProductAction::create([
|
$low = ProductAction::create([
|
||||||
'product_id' => $product->id,
|
'product_id' => $product->id,
|
||||||
'event' => 'purchased',
|
'events' => ['purchased'],
|
||||||
'action_type' => 'App\\Actions\\LowPriority',
|
'class' => 'App\\Actions\\LowPriority',
|
||||||
'sort_order' => 10,
|
'sort_order' => 10,
|
||||||
'active' => true,
|
'active' => true,
|
||||||
]);
|
]);
|
||||||
|
|
@ -348,4 +332,30 @@ class ProductActionTest extends TestCase
|
||||||
$this->assertEquals($medium->id, $ordered[1]->id);
|
$this->assertEquals($medium->id, $ordered[1]->id);
|
||||||
$this->assertEquals($high->id, $ordered[2]->id);
|
$this->assertEquals($high->id, $ordered[2]->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_can_be_triggered_on_purchase()
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$product = Product::factory()
|
||||||
|
->withStocks()
|
||||||
|
->withPrices(1, 5000)
|
||||||
|
->create();
|
||||||
|
|
||||||
|
$product->actions()->create([
|
||||||
|
'events' => ['purchased'],
|
||||||
|
'class' => 'App\\Actions\\SendThankYouEmail',
|
||||||
|
'defer' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$product->actions()->create([
|
||||||
|
'events' => ['purchased'],
|
||||||
|
'class' => 'App\\Actions\\SendThankYouEmail',
|
||||||
|
'defer' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$purchase = $user->purchase($product, 1);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $purchase->actionRuns()->count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue