softdeletes für packages eingerichtet
This commit is contained in:
@@ -11,7 +11,11 @@ use Filament\Actions;
|
|||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Actions\BulkActionGroup;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\ForceDeleteAction;
|
||||||
|
use Filament\Actions\ForceDeleteBulkAction;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Actions\RestoreAction;
|
||||||
|
use Filament\Actions\RestoreBulkAction;
|
||||||
use Filament\Actions\ViewAction;
|
use Filament\Actions\ViewAction;
|
||||||
use Filament\Forms\Components\CheckboxList;
|
use Filament\Forms\Components\CheckboxList;
|
||||||
use Filament\Forms\Components\MarkdownEditor;
|
use Filament\Forms\Components\MarkdownEditor;
|
||||||
@@ -29,8 +33,12 @@ use Filament\Schemas\Schema;
|
|||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Columns\BadgeColumn;
|
use Filament\Tables\Columns\BadgeColumn;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\TrashedFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rules\Unique;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class PackageResource extends Resource
|
class PackageResource extends Resource
|
||||||
@@ -86,7 +94,10 @@ class PackageResource extends Resource
|
|||||||
->label('Slug')
|
->label('Slug')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(191)
|
->maxLength(191)
|
||||||
->unique(ignoreRecord: true),
|
->unique(
|
||||||
|
ignoreRecord: true,
|
||||||
|
modifyRuleUsing: fn (Unique $rule) => $rule->withoutTrashed()
|
||||||
|
),
|
||||||
Select::make('type')
|
Select::make('type')
|
||||||
->label('Paket-Typ')
|
->label('Paket-Typ')
|
||||||
->options([
|
->options([
|
||||||
@@ -272,6 +283,7 @@ class PackageResource extends Resource
|
|||||||
'endcustomer' => 'Endkunde',
|
'endcustomer' => 'Endkunde',
|
||||||
'reseller' => 'Reseller',
|
'reseller' => 'Reseller',
|
||||||
]),
|
]),
|
||||||
|
TrashedFilter::make(),
|
||||||
])
|
])
|
||||||
->actions([
|
->actions([
|
||||||
Actions\Action::make('syncPaddle')
|
Actions\Action::make('syncPaddle')
|
||||||
@@ -305,15 +317,31 @@ class PackageResource extends Resource
|
|||||||
}),
|
}),
|
||||||
ViewAction::make(),
|
ViewAction::make(),
|
||||||
EditAction::make(),
|
EditAction::make(),
|
||||||
DeleteAction::make(),
|
DeleteAction::make()
|
||||||
|
->visible(fn (Package $record) => ! $record->trashed()),
|
||||||
|
RestoreAction::make()
|
||||||
|
->visible(fn (Package $record) => $record->trashed()),
|
||||||
|
ForceDeleteAction::make()
|
||||||
|
->visible(fn (Package $record) => $record->trashed())
|
||||||
|
->requiresConfirmation(),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
DeleteBulkAction::make(),
|
DeleteBulkAction::make(),
|
||||||
|
RestoreBulkAction::make(),
|
||||||
|
ForceDeleteBulkAction::make()->requiresConfirmation(),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getEloquentQuery(): Builder
|
||||||
|
{
|
||||||
|
return parent::getEloquentQuery()
|
||||||
|
->withoutGlobalScopes([
|
||||||
|
SoftDeletingScope::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class AbandonedCheckout extends Model
|
|||||||
|
|
||||||
public function package(): BelongsTo
|
public function package(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Package::class);
|
return $this->belongsTo(Package::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class CheckoutSession extends Model
|
|||||||
|
|
||||||
public function package(): BelongsTo
|
public function package(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Package::class);
|
return $this->belongsTo(Package::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeActive($query)
|
public function scopeActive($query)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class EventPackage extends Model
|
|||||||
|
|
||||||
public function package(): BelongsTo
|
public function package(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Package::class);
|
return $this->belongsTo(Package::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isActive(): bool
|
public function isActive(): bool
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
class Package extends Model
|
class Package extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class PackagePurchase extends Model
|
|||||||
|
|
||||||
public function package(): BelongsTo
|
public function package(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Package::class);
|
return $this->belongsTo(Package::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEndcustomerEvent(): bool
|
public function isEndcustomerEvent(): bool
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ class Tenant extends Model
|
|||||||
public function getActiveResellerPackage(): ?TenantPackage
|
public function getActiveResellerPackage(): ?TenantPackage
|
||||||
{
|
{
|
||||||
return $this->activeResellerPackage()
|
return $this->activeResellerPackage()
|
||||||
->whereHas('package', fn ($query) => $query->where('type', 'reseller'))
|
->whereHas('package', fn ($query) => $query->withTrashed()->where('type', 'reseller'))
|
||||||
->where('active', true)
|
->where('active', true)
|
||||||
->orderByDesc('expires_at')
|
->orderByDesc('expires_at')
|
||||||
->first();
|
->first();
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TenantPackage extends Model
|
|||||||
|
|
||||||
public function package(): BelongsTo
|
public function package(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Package::class);
|
return $this->belongsTo(Package::class)->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isActive(): bool
|
public function isActive(): bool
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ class CheckoutWebhookService
|
|||||||
protected function resolvePackageFromSubscription(array $data, array $metadata, string $subscriptionId): ?Package
|
protected function resolvePackageFromSubscription(array $data, array $metadata, string $subscriptionId): ?Package
|
||||||
{
|
{
|
||||||
if (isset($metadata['package_id'])) {
|
if (isset($metadata['package_id'])) {
|
||||||
$package = Package::find((int) $metadata['package_id']);
|
$package = Package::withTrashed()->find((int) $metadata['package_id']);
|
||||||
if ($package) {
|
if ($package) {
|
||||||
return $package;
|
return $package;
|
||||||
}
|
}
|
||||||
@@ -344,7 +344,7 @@ class CheckoutWebhookService
|
|||||||
$priceId = Arr::get($data, 'items.0.price_id') ?? Arr::get($data, 'items.0.price.id');
|
$priceId = Arr::get($data, 'items.0.price_id') ?? Arr::get($data, 'items.0.price.id');
|
||||||
|
|
||||||
if ($priceId) {
|
if ($priceId) {
|
||||||
$package = Package::where('paddle_price_id', $priceId)->first();
|
$package = Package::withTrashed()->where('paddle_price_id', $priceId)->first();
|
||||||
if ($package) {
|
if ($package) {
|
||||||
return $package;
|
return $package;
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ class CheckoutWebhookService
|
|||||||
$priceId = Arr::get($subscription, 'data.items.0.price_id') ?? Arr::get($subscription, 'data.items.0.price.id');
|
$priceId = Arr::get($subscription, 'data.items.0.price_id') ?? Arr::get($subscription, 'data.items.0.price.id');
|
||||||
|
|
||||||
if ($priceId) {
|
if ($priceId) {
|
||||||
return Package::where('paddle_price_id', $priceId)->first();
|
return Package::withTrashed()->where('paddle_price_id', $priceId)->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('packages', function (Blueprint $table) {
|
||||||
|
if (! Schema::hasColumn('packages', 'deleted_at')) {
|
||||||
|
$table->softDeletes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('packages', function (Blueprint $table) {
|
||||||
|
if (Schema::hasColumn('packages', 'deleted_at')) {
|
||||||
|
$table->dropSoftDeletes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"title": "Startseite - Fotospiel",
|
"title": "Startseite - Fotospiel",
|
||||||
"hero_tagline": "Eventfotos ohne App-Zwang",
|
"hero_tagline": "Eventfotos ohne App-Zwang",
|
||||||
"hero_title": "Dein Event. Eure Fotos. Echtzeit bereit.",
|
"hero_title": "Dein Event. Eure Fotos.",
|
||||||
"hero_description": "Fotospiel bündelt QR-Zugänge, Live-Galerien und Moderation in einer einzigen Plattform – für Hochzeiten, Firmenfeiern und jedes Fest, das Erinnerungen verdient.",
|
"hero_description": "Fotospiel bündelt QR-Zugänge, Live-Galerien und Moderation in einer einzigen Plattform – für Hochzeiten, Firmenfeiern und jedes Fest, das Erinnerungen verdient.",
|
||||||
"hero_bullets": [
|
"hero_bullets": [
|
||||||
"Live-Galerie in Sekunden startklar",
|
"Live-Galerie in Sekunden startklar",
|
||||||
|
|||||||
122
tests/Feature/Packages/PackageSoftDeleteTest.php
Normal file
122
tests/Feature/Packages/PackageSoftDeleteTest.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Packages;
|
||||||
|
|
||||||
|
use App\Models\Package;
|
||||||
|
use App\Models\PackagePurchase;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\TenantPackage;
|
||||||
|
use App\Services\Checkout\CheckoutAssignmentService;
|
||||||
|
use App\Services\Checkout\CheckoutSessionService;
|
||||||
|
use App\Services\Checkout\CheckoutWebhookService;
|
||||||
|
use App\Services\Paddle\PaddleSubscriptionService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PackageSoftDeleteTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
|
||||||
|
Mockery::close();
|
||||||
|
Carbon::setTestNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_soft_deleted_package_remains_accessible_via_purchase_relations(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$package = Package::factory()->reseller()->create([
|
||||||
|
'max_events_per_year' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenantPackage = TenantPackage::factory()
|
||||||
|
->for($tenant)
|
||||||
|
->for($package)
|
||||||
|
->create([
|
||||||
|
'used_events' => 1,
|
||||||
|
'active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$purchase = PackagePurchase::factory()
|
||||||
|
->for($tenant)
|
||||||
|
->for($package)
|
||||||
|
->create([
|
||||||
|
'type' => 'reseller_subscription',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$package->delete();
|
||||||
|
|
||||||
|
$this->assertNull(Package::find($package->id));
|
||||||
|
$this->assertTrue(Package::onlyTrashed()->where('id', $package->id)->exists());
|
||||||
|
|
||||||
|
$tenantPackage->refresh()->load('package');
|
||||||
|
$this->assertNotNull($tenantPackage->package);
|
||||||
|
$this->assertTrue($tenantPackage->package->is($package));
|
||||||
|
|
||||||
|
$purchase->refresh()->load('package');
|
||||||
|
$this->assertNotNull($purchase->package);
|
||||||
|
$this->assertTrue($purchase->package->is($package));
|
||||||
|
|
||||||
|
$tenant->refresh();
|
||||||
|
$activePackage = $tenant->getActiveResellerPackage();
|
||||||
|
$this->assertNotNull($activePackage);
|
||||||
|
$this->assertTrue($activePackage->is($tenantPackage));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_paddle_subscription_event_handles_soft_deleted_package(): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$package = Package::factory()->reseller()->create([
|
||||||
|
'price' => 29.00,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$package->delete();
|
||||||
|
|
||||||
|
$sessionService = Mockery::mock(CheckoutSessionService::class);
|
||||||
|
$assignmentService = Mockery::mock(CheckoutAssignmentService::class);
|
||||||
|
$subscriptionService = Mockery::mock(PaddleSubscriptionService::class);
|
||||||
|
|
||||||
|
$service = new CheckoutWebhookService(
|
||||||
|
$sessionService,
|
||||||
|
$assignmentService,
|
||||||
|
$subscriptionService
|
||||||
|
);
|
||||||
|
|
||||||
|
Carbon::setTestNow(now());
|
||||||
|
|
||||||
|
$event = [
|
||||||
|
'event_type' => 'subscription.updated',
|
||||||
|
'data' => [
|
||||||
|
'id' => 'sub_123',
|
||||||
|
'status' => 'active',
|
||||||
|
'metadata' => [
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'package_id' => $package->id,
|
||||||
|
],
|
||||||
|
'next_billing_date' => now()->addMonth()->toIso8601String(),
|
||||||
|
'customer_id' => 'cus_456',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertTrue($service->handlePaddleEvent($event));
|
||||||
|
|
||||||
|
$tenantPackage = TenantPackage::where('tenant_id', $tenant->id)
|
||||||
|
->where('package_id', $package->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($tenantPackage);
|
||||||
|
$this->assertNotNull($tenantPackage->package);
|
||||||
|
$this->assertTrue($tenantPackage->package->is($package));
|
||||||
|
$this->assertSame('sub_123', $tenantPackage->paddle_subscription_id);
|
||||||
|
$this->assertTrue($tenantPackage->active);
|
||||||
|
|
||||||
|
$tenant->refresh();
|
||||||
|
$this->assertSame('active', $tenant->subscription_status);
|
||||||
|
$this->assertSame('cus_456', $tenant->paddle_customer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user