From a4fedcdb58fafa3991da00626db795678187fbbe Mon Sep 17 00:00:00 2001 From: a6a2f5842 Date: Sat, 29 Nov 2025 20:09:19 +0100 Subject: [PATCH] A tests, I product action --- README.md | 14 +- config/shop.php | 15 +- .../create_blax_shop_tables.php.stub | 30 ++- src/Models/Product.php | 21 +- src/Models/ProductAction.php | 220 ++++++++++++++---- src/Models/ProductActionRun.php | 30 +++ src/Models/ProductPurchase.php | 38 ++- src/Traits/HasShoppingCapabilities.php | 1 + tests/Feature/HasShoppingCapabilitiesTest.php | 4 +- tests/Feature/ProductActionTest.php | 152 ++++++------ 10 files changed, 376 insertions(+), 149 deletions(-) create mode 100644 src/Models/ProductActionRun.php diff --git a/README.md b/README.md index df4cc43..5a474d5 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,21 @@ $product = Product::create([ 'slug' => 'amazing-t-shirt', 'sku' => 'TSH-001', 'type' => 'simple', - 'price' => 29.99, - 'regular_price' => 29.99, 'manage_stock' => true, - 'stock_quantity' => 100, '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 $product->setLocalized('name', 'Amazing T-Shirt', 'en'); $product->setLocalized('description', 'A comfortable cotton t-shirt', 'en'); diff --git a/config/shop.php b/config/shop.php index 8462694..630431d 100644 --- a/config/shop.php +++ b/config/shop.php @@ -3,16 +3,17 @@ return [ // Table names (customizable for multi-tenancy) 'tables' => [ - 'products' => 'products', - 'product_prices' => 'product_prices', - 'product_categories' => 'product_categories', + 'cart_items' => 'cart_items', + 'carts' => 'carts', + 'payment_methods' => 'payment_methods', + 'payment_provider_identities' => 'payment_provider_identities', + 'product_action_runs' => 'product_action_runs', 'product_attributes' => 'product_attributes', + 'product_categories' => 'product_categories', + 'product_prices' => 'product_prices', 'product_purchases' => 'product_purchases', 'product_stocks' => 'product_stocks', - 'carts' => 'carts', - 'cart_items' => 'cart_items', - 'payment_provider_identities' => 'payment_provider_identities', - 'payment_methods' => 'payment_methods', + 'products' => 'products', ], // Model classes (allow overriding in main instance) diff --git a/database/migrations/create_blax_shop_tables.php.stub b/database/migrations/create_blax_shop_tables.php.stub index bc79f84..f57ec81 100644 --- a/database/migrations/create_blax_shop_tables.php.stub +++ b/database/migrations/create_blax_shop_tables.php.stub @@ -98,7 +98,7 @@ return new class extends Migration // Product category pivot table 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) { - $table->uuid('product_id'); + $table->uuid('product_id')->nullable(); $table->uuid('product_category_id'); $table->integer('sort_order')->default(0); $table->timestamps(); @@ -210,15 +210,20 @@ return new class extends Migration Schema::create(config('shop.tables.product_actions', 'product_actions'), function (Blueprint $table) { $table->uuid('id')->primary(); $table->uuid('product_id'); - $table->string('action_type'); - $table->string('event')->default('purchased'); // purchased, refunded, etc. + $table->json('events')->default('[]'); // e.g. ["purchased","paid","refunded","viewed"] + $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->boolean('active')->default(true); $table->integer('sort_order')->default(0); $table->timestamps(); - $table->index(['product_id', 'event', 'active']); - $table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade'); + $table->index(['product_id', 'active']); + $table->foreign('product_id') + ->references('id') + ->on(config('shop.tables.products', 'products')) + ->nullOnDelete(); }); } @@ -338,6 +343,18 @@ return new class extends Migration ->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_items', 'cart_items')); 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_actions', 'product_actions')); 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_attributes', 'product_attributes')); 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.products', 'products')); } diff --git a/src/Models/Product.php b/src/Models/Product.php index e2c3df1..1f1eff0 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -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 { 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 @@ -411,7 +408,7 @@ class Product extends Model implements Purchasable, Cartable { $stockTable = config('shop.tables.product_stocks', 'product_stocks'); $productTable = config('shop.tables.products', 'products'); - + return $query->where('manage_stock', true) ->whereNotNull('low_stock_threshold') ->whereRaw("(\n SELECT COALESCE(SUM(quantity), 0)\n FROM {$stockTable}\n WHERE {$stockTable}.product_id = {$productTable}.id\n AND {$stockTable}.status IN ('completed', 'pending')\n AND ({$stockTable}.expires_at IS NULL OR {$stockTable}.expires_at > ?)\n ) <= {$productTable}.low_stock_threshold", [now()]); @@ -529,7 +526,7 @@ class Product extends Model implements Purchasable, Cartable ->where('product_id', $this->id); } - public function hasPrice() : bool + public function hasPrice(): bool { return $this->prices()->exists(); } diff --git a/src/Models/ProductAction.php b/src/Models/ProductAction.php index 0903ba2..368c16d 100644 --- a/src/Models/ProductAction.php +++ b/src/Models/ProductAction.php @@ -13,16 +13,20 @@ class ProductAction extends Model protected $fillable = [ 'product_id', - 'event', - 'action_type', + 'events', + 'class', + 'method', + 'defer', 'parameters', 'active', 'sort_order', ]; protected $casts = [ + 'events' => 'array', 'parameters' => 'array', 'active' => 'boolean', + 'defer' => 'boolean', 'sort_order' => 'integer', ]; @@ -37,6 +41,43 @@ class ProductAction extends Model 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( Product $product, string $event, @@ -44,7 +85,7 @@ class ProductAction extends Model array $additionalData = [] ): void { $actions = $product->actions() - ->where('event', $event) + ->whereJsonContains('events', $event) ->where('active', true) ->orderBy('sort_order') ->get(); @@ -53,21 +94,12 @@ class ProductAction extends Model return; } - $available_actions = self::getAvailableActions(); - foreach ($actions as $action) { + $success = false; try { - if (!isset($available_actions[$action->action_type])) { - Log::warning('Product action not found', [ - 'product_id' => $product->id, - 'event' => $event, - 'action_type' => $action->action_type, - ]); - continue; - } - - $namespace = config('shop.actions.namespace', 'App\\Jobs\\ProductAction'); - $action_job = $namespace . '\\' . $action->action_type; + $class = $action->class; + $method = $action->method; + $defer = (bool) $action->defer; $params = [ 'product' => $product, @@ -77,46 +109,140 @@ class ProductAction extends Model ...$additionalData, ]; - dispatch(new $action_job(...$params)); - } catch (\Exception $e) { + // Skip if class is not defined + 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', [ 'product_id' => $product->id, 'event' => $event, - 'action_type' => $action->action_type ?? 'unknown', + 'class' => $action->class ?? 'unknown', + 'method' => $action->method ?? null, 'error' => $e->getMessage(), '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); } } } - - 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', []); - } } diff --git a/src/Models/ProductActionRun.php b/src/Models/ProductActionRun.php new file mode 100644 index 0000000..4c07222 --- /dev/null +++ b/src/Models/ProductActionRun.php @@ -0,0 +1,30 @@ + 'boolean', + ]; + + public function action(): MorphTo + { + return $this->morphTo(); + } + + public function productPurchase() + { + return $this->belongsTo(ProductPurchase::class, 'product_purchase_id'); + } +} diff --git a/src/Models/ProductPurchase.php b/src/Models/ProductPurchase.php index 4bb1bc4..10d1cef 100644 --- a/src/Models/ProductPurchase.php +++ b/src/Models/ProductPurchase.php @@ -78,9 +78,45 @@ class ProductPurchase extends Model protected static function booted() { 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); } }); } + + 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... + ); + } } diff --git a/src/Traits/HasShoppingCapabilities.php b/src/Traits/HasShoppingCapabilities.php index a2b1844..b190c40 100644 --- a/src/Traits/HasShoppingCapabilities.php +++ b/src/Traits/HasShoppingCapabilities.php @@ -2,6 +2,7 @@ namespace Blax\Shop\Traits; +use Blax\Shop\Contracts\Purchasable; use Blax\Shop\Exceptions\MultiplePurchaseOptions; use Blax\Shop\Exceptions\NotEnoughStockException; use Blax\Shop\Exceptions\NotPurchasable; diff --git a/tests/Feature/HasShoppingCapabilitiesTest.php b/tests/Feature/HasShoppingCapabilitiesTest.php index 3b643a7..11d6506 100644 --- a/tests/Feature/HasShoppingCapabilitiesTest.php +++ b/tests/Feature/HasShoppingCapabilitiesTest.php @@ -264,8 +264,8 @@ class HasShoppingCapabilitiesTest extends TestCase // Create a product action $product->actions()->create([ - 'event' => 'purchased', - 'action_type' => 'TestAction', + 'events' => ['purchased'], + 'class' => 'TestAction', 'active' => true, ]); diff --git a/tests/Feature/ProductActionTest.php b/tests/Feature/ProductActionTest.php index 5243e90..c0ff1de 100644 --- a/tests/Feature/ProductActionTest.php +++ b/tests/Feature/ProductActionTest.php @@ -18,19 +18,18 @@ class ProductActionTest extends TestCase { $product = Product::factory()->create(); - $action = ProductAction::create([ - 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\SendWelcomeEmail', - 'active' => true, - 'sort_order' => 10, + $action = $product->actions()->create([ + 'events' => ['purchased', 'refunded'], + 'class' => 'App\\Actions\\SendWelcomeEmail', ]); $this->assertDatabaseHas('product_actions', [ 'id' => $action->id, 'product_id' => $product->id, - 'event' => 'purchased', ]); + + $this->assertContains('purchased', $action->events ?? []); + $this->assertContains('refunded', $action->events ?? []); } /** @test */ @@ -40,16 +39,14 @@ class ProductActionTest extends TestCase ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\SendWelcomeEmail', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\SendWelcomeEmail', ]); ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\GrantAccess', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\GrantAccess', ]); $this->assertCount(2, $product->fresh()->actions); @@ -62,9 +59,8 @@ class ProductActionTest extends TestCase $action = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\TestAction', - 'active' => true, + 'event' => ['purchased'], + 'class' => 'App\\Actions\\TestAction', ]); $this->assertEquals($product->id, $action->product->id); @@ -77,11 +73,12 @@ class ProductActionTest extends TestCase $action = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\TestAction', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\TestAction', ]); + $action->refresh(); + $this->assertTrue($action->active); $action->update(['active' => false]); @@ -96,14 +93,13 @@ class ProductActionTest extends TestCase $action = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\SendEmail', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\SendEmail', 'parameters' => [ 'template' => 'welcome', 'delay' => 60, 'subject' => 'Welcome to our service', ], - 'active' => true, ]); $action = $action->fresh(); @@ -120,18 +116,14 @@ class ProductActionTest extends TestCase $action1 = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\FirstAction', - 'sort_order' => 1, - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\FirstAction', ]); $action2 = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\SecondAction', - 'sort_order' => 2, - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\SecondAction', ]); $sorted = ProductAction::where('product_id', $product->id) @@ -149,20 +141,20 @@ class ProductActionTest extends TestCase $purchasedAction = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\OnPurchase', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\OnPurchase', 'active' => true, ]); $refundedAction = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'refunded', - 'action_type' => 'App\\Actions\\OnRefund', + 'events' => ['refunded'], + 'class' => 'App\\Actions\\OnRefund', 'active' => true, ]); - $this->assertEquals('purchased', $purchasedAction->event); - $this->assertEquals('refunded', $refundedAction->event); + $this->assertContains('purchased', $purchasedAction->events); + $this->assertContains('refunded', $refundedAction->events); } /** @test */ @@ -172,27 +164,24 @@ class ProductActionTest extends TestCase ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\OnPurchase', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\OnPurchase', ]); ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\AnotherPurchase', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\OnPurchase', ]); ProductAction::create([ 'product_id' => $product->id, - 'event' => 'refunded', - 'action_type' => 'App\\Actions\\OnRefund', - 'active' => true, + 'events' => ['refunded'], + 'class' => 'App\\Actions\\OnPurchase', ]); $purchaseActions = ProductAction::where('product_id', $product->id) - ->where('event', 'purchased') + ->whereJsonContains('events', 'purchased') ->get(); $this->assertCount(2, $purchaseActions); @@ -205,15 +194,14 @@ class ProductActionTest extends TestCase ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\EnabledAction', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\EnabledAction', ]); ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\DisabledAction', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\DisabledAction', 'active' => false, ]); @@ -232,16 +220,14 @@ class ProductActionTest extends TestCase ProductAction::create([ 'product_id' => $product1->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\CommonAction', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\CommonAction', ]); ProductAction::create([ 'product_id' => $product2->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\CommonAction', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\CommonAction', ]); $this->assertCount(1, $product1->actions); @@ -255,12 +241,11 @@ class ProductActionTest extends TestCase $action = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\TestAction', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\TestAction', 'parameters' => [ 'key' => 'old_value' ], - 'active' => true, ]); $action->update([ @@ -284,8 +269,8 @@ class ProductActionTest extends TestCase $action = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\TestAction', + 'event' => ['purchased'], + 'class' => 'App\\Actions\\TestAction', 'active' => true, ]); @@ -303,9 +288,8 @@ class ProductActionTest extends TestCase $action = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\SimpleAction', - 'active' => true, + 'events' => ['purchased'], + 'class' => 'App\\Actions\\SimpleAction', ]); $this->assertNull($action->parameters); @@ -318,24 +302,24 @@ class ProductActionTest extends TestCase $high = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\HighPriority', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\HighPriority', 'sort_order' => 100, 'active' => true, ]); $medium = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\MediumPriority', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\MediumPriority', 'sort_order' => 50, 'active' => true, ]); $low = ProductAction::create([ 'product_id' => $product->id, - 'event' => 'purchased', - 'action_type' => 'App\\Actions\\LowPriority', + 'events' => ['purchased'], + 'class' => 'App\\Actions\\LowPriority', 'sort_order' => 10, 'active' => true, ]); @@ -348,4 +332,30 @@ class ProductActionTest extends TestCase $this->assertEquals($medium->id, $ordered[1]->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()); + } }