A tests, I product action

This commit is contained in:
a6a2f5842 2025-11-29 20:09:19 +01:00
parent c6c159a4ff
commit a4fedcdb58
10 changed files with 376 additions and 149 deletions

View File

@ -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');

View File

@ -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)

View File

@ -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'));
} }

View File

@ -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();
} }

View File

@ -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', []);
}
} }

View File

@ -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');
}
}

View File

@ -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...
);
}
} }

View File

@ -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;

View File

@ -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,
]); ]);

View File

@ -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());
}
} }