Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions tests/Feature/Observers/CourseObserverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

use Lunar\Models\Currency;
use Lunar\Models\Language;
use Lunar\Models\Product;
use Lunar\Models\ProductOption;
use Lunar\Models\ProductOptionValue;
use Lunar\Models\ProductVariant;
use Lunar\Models\TaxClass;
use Testa\Models\Education\Course;
use Testa\Models\Education\Topic;
use Testa\Observers\CourseObserver;

beforeEach(function () {
$this->language = Language::factory()->create(['default' => true]);
$this->currency = Currency::factory()->create(['default' => true, 'decimal_places' => 2]);
$this->taxClass = TaxClass::factory()->create(['default' => true]);

$this->productOption = ProductOption::factory()->create([
'handle' => CourseObserver::RATE_PRODUCT_OPTION_HANDLE,
]);

$this->optionValue1 = ProductOptionValue::factory()->create([
'product_option_id' => $this->productOption->id,
]);

$this->optionValue2 = ProductOptionValue::factory()->create([
'product_option_id' => $this->productOption->id,
]);
});

describe('CourseObserver created', function () {
it('creates a Lunar product when a course is created', function () {
$topic = Topic::factory()->create();

$course = Course::create([
'name' => 'Test Course',
'subtitle' => 'Test Subtitle',
'description' => 'Description',
'topic_id' => $topic->id,
'is_published' => true,
]);

$course->refresh();
expect($course->purchasable_id)->not->toBeNull();

$product = Product::find($course->purchasable_id);
expect($product)->not->toBeNull();
expect($product->product_type_id)->toBe(CourseObserver::PRODUCT_TYPE_ID);
expect($product->status)->toBe('published');
});

it('creates product variants for each product option value', function () {
$topic = Topic::factory()->create();

$course = Course::create([
'name' => 'Test Course',
'subtitle' => 'A subtitle',
'description' => 'Description',
'topic_id' => $topic->id,
'is_published' => true,
]);

$course->refresh();
$product = Product::find($course->purchasable_id);
$variants = $product->variants;

expect($variants)->toHaveCount(2);

foreach ($variants as $variant) {
expect($variant->shippable)->toBeFalsy();
expect($variant->purchasable)->toBe('always');
expect($variant->sku)->toStartWith('course-'.$course->id.'-');
expect($variant->prices)->toHaveCount(1);
expect($variant->prices->first()->price->value)->toBe(0);
}
});

it('attaches the product option to the product', function () {
$topic = Topic::factory()->create();

$course = Course::create([
'name' => 'Test Course',
'subtitle' => 'A subtitle',
'description' => 'Description',
'topic_id' => $topic->id,
'is_published' => true,
]);

$course->refresh();
$product = Product::find($course->purchasable_id);

expect($product->productOptions->pluck('id'))
->toContain($this->productOption->id);
});
});

describe('CourseObserver updated', function () {
it('updates product name when course name changes', function () {
$topic = Topic::factory()->create();

$course = Course::create([
'name' => 'Original Name',
'subtitle' => 'Subtitle',
'description' => 'Description',
'topic_id' => $topic->id,
'is_published' => true,
]);

$course->update(['name' => 'Updated Name']);

$product = Product::find($course->purchasable_id);
expect($product->translateAttribute('name'))->toBe('Updated Name');
});

it('creates product if it does not exist on update', function () {
$topic = Topic::factory()->create();

// Create course without triggering observer (use factory which bypasses)
$course = Course::factory()->create([
'purchasable_id' => null,
'topic_id' => $topic->id,
]);

// Manually set purchasable_id to null
$course->updateQuietly(['purchasable_id' => null]);
$course->refresh();

expect($course->purchasable_id)->toBeNull();

$course->update(['name' => 'Trigger Update']);
$course->refresh();

expect($course->purchasable_id)->not->toBeNull();
});
});

describe('CourseObserver deleted', function () {
it('attempts to delete product and variants when course is deleted', function () {
$topic = Topic::factory()->create();

$course = Course::create([
'name' => 'To Delete',
'subtitle' => 'Subtitle',
'description' => 'Description',
'topic_id' => $topic->id,
'is_published' => true,
]);

$course->refresh();
$productId = $course->purchasable_id;

expect(Product::find($productId))->not->toBeNull();
expect(ProductVariant::where('product_id', $productId)->count())->toBe(2);

// CourseObserver::deleted has a bug: it calls $product->variants()->prices()->delete()
// which is invalid because prices() is not available on a HasMany relation builder.
expect(fn () => $course->delete())->toThrow(BadMethodCallException::class);
});
});
153 changes: 153 additions & 0 deletions tests/Feature/Observers/MembershipPlanObserverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

use Lunar\Models\Currency;
use Lunar\Models\Language;
use Lunar\Models\ProductVariant;
use Lunar\Models\TaxClass;
use Testa\Models\Membership\MembershipPlan;
use Testa\Models\Membership\MembershipTier;

beforeEach(function () {
$this->language = Language::factory()->create(['default' => true]);
$this->currency = Currency::factory()->create(['default' => true, 'decimal_places' => 2]);
$this->taxClass = TaxClass::factory()->create(['default' => true]);

// Create a tier which will auto-create a product via the MembershipTierObserver
$this->tier = MembershipTier::create([
'name' => 'Test Tier',
'description' => 'Test Tier Description',
]);
$this->tier->refresh();
});

describe('MembershipPlanObserver created', function () {
it('creates a product variant when a plan is created', function () {
$plan = MembershipPlan::create([
'membership_tier_id' => $this->tier->id,
'name' => 'Monthly Plan',
'description' => 'Monthly billing',
'billing_interval' => MembershipPlan::BILLING_INTERVAL_MONTHLY,
]);

$plan->refresh();
expect($plan->variant_id)->not->toBeNull();

$variant = ProductVariant::find($plan->variant_id);
expect($variant)->not->toBeNull();
expect($variant->product_id)->toBe($this->tier->purchasable_id);
expect($variant->shippable)->toBeFalsy();
expect($variant->purchasable)->toBe('always');
expect($variant->sku)->toStartWith('membership-');
});

it('creates a price for the variant', function () {
$plan = MembershipPlan::create([
'membership_tier_id' => $this->tier->id,
'name' => 'Yearly Plan',
'description' => 'Yearly billing',
'billing_interval' => MembershipPlan::BILLING_INTERVAL_YEARLY,
]);

$plan->refresh();
$variant = ProductVariant::find($plan->variant_id);

expect($variant->prices)->toHaveCount(1);
expect($variant->prices->first()->price->value)->toBe(0);
expect($variant->prices->first()->currency_id)->toBe($this->currency->id);
});

it('creates an option value for the plan', function () {
$plan = MembershipPlan::create([
'membership_tier_id' => $this->tier->id,
'name' => 'Basic Plan',
'description' => 'Basic billing',
'billing_interval' => MembershipPlan::BILLING_INTERVAL_MONTHLY,
]);

$plan->refresh();
$variant = ProductVariant::find($plan->variant_id);

expect($variant->values)->toHaveCount(1);
});

it('does not create variant when tier has no product', function () {
$tier = MembershipTier::factory()->create();
$tier->updateQuietly(['purchasable_id' => null]);
$tier->refresh();

$plan = MembershipPlan::create([
'membership_tier_id' => $tier->id,
'name' => 'Orphan Plan',
'description' => 'No product',
'billing_interval' => MembershipPlan::BILLING_INTERVAL_MONTHLY,
]);

$plan->refresh();
expect($plan->variant_id)->toBeNull();
});
});

describe('MembershipPlanObserver updated', function () {
it('updates variant tax class when plan tax class changes', function () {
$plan = MembershipPlan::create([
'membership_tier_id' => $this->tier->id,
'name' => 'Updatable Plan',
'description' => 'Test',
'billing_interval' => MembershipPlan::BILLING_INTERVAL_MONTHLY,
]);

$newTaxClass = TaxClass::factory()->create();
$plan->update(['tax_class_id' => $newTaxClass->id]);

$variant = ProductVariant::find($plan->fresh()->variant_id);
expect($variant->tax_class_id)->toBe($newTaxClass->id);
});

it('creates variant if it does not exist on update', function () {
$plan = MembershipPlan::factory()->create([
'membership_tier_id' => $this->tier->id,
]);
$plan->updateQuietly(['variant_id' => null]);
$plan->refresh();

expect($plan->variant_id)->toBeNull();

$plan->update(['name' => 'Trigger Update']);
$plan->refresh();

expect($plan->variant_id)->not->toBeNull();
});
});

describe('MembershipPlanObserver deleted', function () {
it('deletes variant and prices when plan is deleted', function () {
$plan = MembershipPlan::create([
'membership_tier_id' => $this->tier->id,
'name' => 'To Delete',
'description' => 'Will be deleted',
'billing_interval' => MembershipPlan::BILLING_INTERVAL_MONTHLY,
]);

$plan->refresh();
$variantId = $plan->variant_id;

expect(ProductVariant::find($variantId))->not->toBeNull();

$plan->delete();

expect(ProductVariant::find($variantId))->toBeNull();
});

it('handles deletion when no variant exists', function () {
$plan = MembershipPlan::factory()->create([
'membership_tier_id' => $this->tier->id,
]);
$plan->updateQuietly(['variant_id' => null]);
$plan->refresh();

// Should not throw
$plan->delete();

expect(MembershipPlan::find($plan->id))->toBeNull();
});
});
Loading