übergang auf pakete, integration von stripe und paypal, blog hinzugefügt.
This commit is contained in:
78
app/Console/Commands/MigrateLegacyPurchases.php
Normal file
78
app/Console/Commands/MigrateLegacyPurchases.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\TenantPackage;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MigrateLegacyPurchases extends Command
|
||||
{
|
||||
protected $signature = 'packages:migrate-legacy';
|
||||
protected $description = 'Migrate legacy purchases to new system with temp tenants';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$legacyPurchases = PackagePurchase::whereNull('tenant_id')->get();
|
||||
|
||||
if ($legacyPurchases->isEmpty()) {
|
||||
$this->info('No legacy purchases found.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info("Found {$legacyPurchases->count()} legacy purchases.");
|
||||
|
||||
foreach ($legacyPurchases as $purchase) {
|
||||
if (!$purchase->user_id) {
|
||||
// Create temp user if no user
|
||||
$tempUser = User::create([
|
||||
'name' => 'Legacy User ' . $purchase->id,
|
||||
'email' => 'legacy' . $purchase->id . '@fotospiel.local',
|
||||
'password' => Hash::make('legacy'),
|
||||
'username' => 'legacy' . $purchase->id,
|
||||
'first_name' => 'Legacy',
|
||||
'last_name' => 'User',
|
||||
'address' => 'Legacy Address',
|
||||
'phone' => '000000000',
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$tempTenant = Tenant::create([
|
||||
'user_id' => $tempUser->id,
|
||||
'name' => 'Legacy Tenant ' . $purchase->id,
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$purchase->update([
|
||||
'user_id' => $tempUser->id,
|
||||
'tenant_id' => $tempTenant->id,
|
||||
]);
|
||||
|
||||
// Assign default free package
|
||||
TenantPackage::create([
|
||||
'tenant_id' => $tempTenant->id,
|
||||
'package_id' => 1, // Assume free package ID 1
|
||||
'expires_at' => now()->addYear(),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->info("Created temp user/tenant for purchase {$purchase->id}");
|
||||
} else {
|
||||
$user = User::find($purchase->user_id);
|
||||
if ($user && $user->tenant) {
|
||||
$purchase->update(['tenant_id' => $user->tenant->id]);
|
||||
$this->info("Assigned tenant for purchase {$purchase->id}");
|
||||
} else {
|
||||
$this->error("Could not assign tenant for purchase {$purchase->id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Legacy migration completed.');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
9
app/Enums/PackageType.php
Normal file
9
app/Enums/PackageType.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PackageType: string
|
||||
{
|
||||
case ENDCUSTOMER = 'endcustomer';
|
||||
case RESELLER = 'reseller';
|
||||
}
|
||||
87
app/Filament/Pages/Auth/Login.php
Normal file
87
app/Filament/Pages/Auth/Login.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class Login extends Page implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
protected string $view = 'filament.pages.auth.login';
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('data.username_or_email')
|
||||
->label('Username or Email')
|
||||
->required()
|
||||
->autofocus(),
|
||||
TextInput::make('data.password')
|
||||
->password()
|
||||
->required()
|
||||
->extraAttributes(['tabindex' => 2]),
|
||||
Checkbox::make('data.remember')
|
||||
->label('Remember me'),
|
||||
];
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
$credentials = $this->getCredentialsFromFormData($data);
|
||||
|
||||
if (! Auth::attempt($credentials, $data['remember'] ?? false)) {
|
||||
throw ValidationException::withMessages([
|
||||
'data.username_or_email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user->email_verified_at) {
|
||||
Auth::logout();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'data.username_or_email' => 'Your email address is not verified. Please check your email for a verification link.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $user->tenant) {
|
||||
Auth::logout();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'data.username_or_email' => 'No tenant associated with your account. Contact support.',
|
||||
]);
|
||||
}
|
||||
|
||||
session()->regenerate();
|
||||
|
||||
$this->redirect($this->getRedirectUrl());
|
||||
}
|
||||
|
||||
protected function getCredentialsFromFormData(array $data): array
|
||||
{
|
||||
$usernameOrEmail = $data['username_or_email'];
|
||||
$password = $data['password'];
|
||||
|
||||
$credentials = ['password' => $password];
|
||||
|
||||
if (filter_var($usernameOrEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$credentials['email'] = $usernameOrEmail;
|
||||
} else {
|
||||
$credentials['username'] = $usernameOrEmail;
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use Filament\Auth\Pages\EditProfile as BaseEditProfile;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class SuperAdminProfile extends BaseEditProfile
|
||||
{
|
||||
protected function getUsernameFormComponent(): Component
|
||||
{
|
||||
return TextInput::make('username')
|
||||
->label(__('Username'))
|
||||
->maxLength(32)
|
||||
->unique(ignoreRecord: true);
|
||||
}
|
||||
|
||||
protected function getPreferredLocaleFormComponent(): Component
|
||||
{
|
||||
$supported = collect(explode(',', (string) env('APP_SUPPORTED_LOCALES', 'de,en')))
|
||||
->map(fn ($l) => trim((string) $l))
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (empty($supported)) {
|
||||
$supported = array_values(array_unique(array_filter([
|
||||
config('app.locale'),
|
||||
config('app.fallback_locale'),
|
||||
])));
|
||||
}
|
||||
|
||||
$options = collect($supported)->mapWithKeys(fn ($l) => [$l => strtoupper($l)])->all();
|
||||
|
||||
return Select::make('preferred_locale')
|
||||
->label(__('Language'))
|
||||
->required()
|
||||
->options($options);
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
$this->getNameFormComponent(),
|
||||
$this->getEmailFormComponent(),
|
||||
$this->getUsernameFormComponent(),
|
||||
$this->getPreferredLocaleFormComponent(),
|
||||
$this->getPasswordFormComponent(),
|
||||
$this->getPasswordConfirmationFormComponent(),
|
||||
$this->getCurrentPasswordFormComponent(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,11 @@ class EventPurchaseResource extends Resource
|
||||
{
|
||||
protected static ?string $model = EventPurchase::class;
|
||||
|
||||
public static function getNavigationIcon(): string
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
return 'heroicon-o-shopping-cart';
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Billing';
|
||||
}
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PackageResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePackage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PackageResource::class;
|
||||
}
|
||||
20
app/Filament/Resources/PackageResource/Pages/EditPackage.php
Normal file
20
app/Filament/Resources/PackageResource/Pages/EditPackage.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPackage extends EditRecord
|
||||
{
|
||||
protected static string $resource = PackageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPackages extends ListRecords
|
||||
{
|
||||
protected static string $resource = PackageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
199
app/Filament/Resources/PurchaseResource.php
Normal file
199
app/Filament/Resources/PurchaseResource.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\PurchaseResource\Pages;
|
||||
use App\Models\PackagePurchase;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
class PurchaseResource extends Resource
|
||||
{
|
||||
protected static ?string $model = PackagePurchase::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shopping-cart';
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return 'Billing';
|
||||
}
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Select::make('tenant_id')
|
||||
->label('Tenant')
|
||||
->relationship('tenant', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->nullable(),
|
||||
Select::make('event_id')
|
||||
->label('Event')
|
||||
->relationship('event', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->nullable(),
|
||||
Select::make('package_id')
|
||||
->label('Package')
|
||||
->relationship('package', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
TextInput::make('provider_id')
|
||||
->label('Provider ID')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('price')
|
||||
->label('Price')
|
||||
->numeric()
|
||||
->step(0.01)
|
||||
->prefix('€')
|
||||
->required(),
|
||||
Select::make('type')
|
||||
->label('Type')
|
||||
->options([
|
||||
'endcustomer_event' => 'Endcustomer Event',
|
||||
'reseller_subscription' => 'Reseller Subscription',
|
||||
])
|
||||
->required(),
|
||||
Textarea::make('metadata')
|
||||
->label('Metadata')
|
||||
->json()
|
||||
->columnSpanFull(),
|
||||
Toggle::make('refunded')
|
||||
->label('Refunded')
|
||||
->default(false),
|
||||
])
|
||||
->columns(2);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
BadgeColumn::make('type')
|
||||
->label('Type')
|
||||
->color(fn (string $state): string => match($state) {
|
||||
'endcustomer_event' => 'info',
|
||||
'reseller_subscription' => 'success',
|
||||
default => 'gray',
|
||||
}),
|
||||
TextColumn::make('tenant.name')
|
||||
->label('Tenant')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('event.name')
|
||||
->label('Event')
|
||||
->searchable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('package.name')
|
||||
->label('Package')
|
||||
->badge()
|
||||
->color('success'),
|
||||
TextColumn::make('price')
|
||||
->label('Price')
|
||||
->money('EUR')
|
||||
->sortable(),
|
||||
TextColumn::make('purchased_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
BadgeColumn::make('refunded')
|
||||
->label('Status')
|
||||
->color(fn (bool $state): string => $state ? 'danger' : 'success'),
|
||||
TextColumn::make('provider_id')
|
||||
->copyable()
|
||||
->toggleable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->options([
|
||||
'endcustomer_event' => 'Endcustomer Event',
|
||||
'reseller_subscription' => 'Reseller Subscription',
|
||||
]),
|
||||
Filter::make('purchased_at')
|
||||
->form([
|
||||
DateTimePicker::make('started_from'),
|
||||
DateTimePicker::make('ended_before'),
|
||||
])
|
||||
->query(function (Builder $query, array $data): Builder {
|
||||
return $query
|
||||
->when(
|
||||
$data['started_from'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('purchased_at', '>=', $date),
|
||||
)
|
||||
->when(
|
||||
$data['ended_before'],
|
||||
fn (Builder $query, $date): Builder => $query->whereDate('purchased_at', '<=', $date),
|
||||
);
|
||||
}),
|
||||
SelectFilter::make('tenant_id')
|
||||
->label('Tenant')
|
||||
->relationship('tenant', 'name')
|
||||
->searchable(),
|
||||
])
|
||||
->actions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
Action::make('refund')
|
||||
->label('Refund')
|
||||
->color('danger')
|
||||
->icon('heroicon-o-arrow-uturn-left')
|
||||
->requiresConfirmation()
|
||||
->visible(fn (PackagePurchase $record): bool => !$record->refunded)
|
||||
->action(function (PackagePurchase $record) {
|
||||
$record->update(['refunded' => true]);
|
||||
// TODO: Call Stripe/PayPal API for actual refund
|
||||
Log::info('Refund processed for purchase ID: ' . $record->id);
|
||||
}),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->emptyStateHeading('No Purchases Found')
|
||||
->emptyStateDescription('Create your first purchase.');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListPurchases::route('/'),
|
||||
'create' => Pages\CreatePurchase::route('/create'),
|
||||
'view' => Pages\ViewPurchase::route('/{record}'),
|
||||
'edit' => Pages\EditPurchase::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
// Add RelationManagers if needed
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PurchaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PurchaseResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreatePurchase extends CreateRecord
|
||||
{
|
||||
protected static string $resource = PurchaseResource::class;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PurchaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PurchaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditPurchase extends EditRecord
|
||||
{
|
||||
protected static string $resource = PurchaseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PurchaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PurchaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListPurchases extends ListRecords
|
||||
{
|
||||
protected static string $resource = PurchaseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\PurchaseResource\Pages;
|
||||
|
||||
use App\Filament\Resources\PurchaseResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewPurchase extends ViewRecord
|
||||
{
|
||||
protected static string $resource = PurchaseResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
Actions\Action::make('refund')
|
||||
->label('Refund')
|
||||
->color('danger')
|
||||
->icon('heroicon-o-arrow-uturn-left')
|
||||
->requiresConfirmation()
|
||||
->visible(fn ($record): bool => !$record->refunded)
|
||||
->action(function ($record) {
|
||||
$record->update(['refunded' => true]);
|
||||
// TODO: Call Stripe/PayPal API for actual refund
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
119
app/Filament/Resources/TenantPackageResource.php
Normal file
119
app/Filament/Resources/TenantPackageResource.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource\Pages;
|
||||
use App\Models\TenantPackage;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Icons\Icon;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\DeleteAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
class TenantPackageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = TenantPackage::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-shopping-bag';
|
||||
|
||||
protected static ?string $navigationLabel = 'Packages';
|
||||
|
||||
protected static ?string $slug = 'tenant-packages';
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Package Details')
|
||||
->schema([
|
||||
Select::make('package_id')
|
||||
->relationship('package', 'name')
|
||||
->required()
|
||||
->searchable(),
|
||||
Select::make('tenant_id')
|
||||
->relationship('tenant', 'name')
|
||||
->required()
|
||||
->default(fn () => Auth::user()->tenant_id)
|
||||
->disabled(),
|
||||
DateTimePicker::make('expires_at')
|
||||
->required(),
|
||||
Toggle::make('is_active')
|
||||
->default(true),
|
||||
])
|
||||
->columns(1),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('package.name')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('tenant.name')
|
||||
->badge()
|
||||
->color('success'),
|
||||
TextColumn::make('expires_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
IconColumn::make('is_active')
|
||||
->boolean(),
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
]),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('tenant_id', Auth::user()->tenant_id));
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTenantPackages::route('/'),
|
||||
'create' => Pages\CreateTenantPackage::route('/create'),
|
||||
'view' => Pages\ViewTenantPackage::route('/{record}'),
|
||||
'edit' => Pages\EditTenantPackage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TenantPackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CreateTenantPackage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TenantPackageResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['tenant_id'] = Auth::user()->tenant_id;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TenantPackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditTenantPackage extends EditRecord
|
||||
{
|
||||
protected static string $resource = TenantPackageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\ViewAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TenantPackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ListTenantPackages extends ListRecords
|
||||
{
|
||||
protected static string $resource = TenantPackageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return parent::getTableQuery()->where('tenant_id', Auth::user()->tenant_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TenantPackageResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantPackageResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewTenantPackage extends ViewRecord
|
||||
{
|
||||
protected static string $resource = TenantPackageResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\EditAction::make(),
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ class TenantResource extends Resource
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
\Illuminate\Support\Facades\Log::info('TenantResource form() method called');
|
||||
|
||||
return $form->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('admin.tenants.fields.name'))
|
||||
@@ -85,22 +87,25 @@ class TenantResource extends Resource
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
\Illuminate\Support\Facades\Log::info('TenantResource table() method called');
|
||||
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('name')->searchable()->sortable(),
|
||||
Tables\Columns\TextColumn::make('slug')->searchable(),
|
||||
Tables\Columns\TextColumn::make('contact_email'),
|
||||
Tables\Columns\TextColumn::make('activeResellerPackage.name')
|
||||
Tables\Columns\TextColumn::make('active_reseller_package_id')
|
||||
->label(__('admin.tenants.fields.active_package'))
|
||||
->badge()
|
||||
->color('success'),
|
||||
Tables\Columns\TextColumn::make('remaining_events')
|
||||
->color('success')
|
||||
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->name ?? 'Kein aktives Package'),
|
||||
Tables\Columns\TextColumn::make('active_reseller_package_id')
|
||||
->label(__('admin.tenants.fields.remaining_events'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state < 1 ? 'danger' : 'success')
|
||||
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->remaining_events ?? 0),
|
||||
Tables\Columns\TextColumn::make('activeResellerPackage.expires_at')
|
||||
Tables\Columns\TextColumn::make('active_reseller_package_id')
|
||||
->dateTime()
|
||||
->label(__('admin.tenants.fields.package_expires_at'))
|
||||
->badge()
|
||||
@@ -178,6 +183,8 @@ class TenantResource extends Resource
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
\Illuminate\Support\Facades\Log::info('TenantResource getRelations() method called');
|
||||
|
||||
return [
|
||||
TenantPackagesRelationManager::class,
|
||||
PackagePurchasesRelationManager::class,
|
||||
|
||||
132
app/Filament/Resources/UserResource.php
Normal file
132
app/Filament/Resources/UserResource.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\UserResource\Pages;
|
||||
use App\Models\User;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Icons\Icon;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\ActionGroup;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use BackedEnum;
|
||||
class UserResource extends Resource
|
||||
{
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-circle';
|
||||
|
||||
protected static ?string $navigationLabel = 'Users';
|
||||
|
||||
protected static ?string $slug = 'users';
|
||||
|
||||
public static function form(Schema $form): Schema
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Section::make('Personal Information')
|
||||
->schema([
|
||||
TextInput::make('first_name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('last_name')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('username')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
TextInput::make('email')
|
||||
->email()
|
||||
->required()
|
||||
->unique(ignoreRecord: true),
|
||||
Textarea::make('address')
|
||||
->required()
|
||||
->rows(3),
|
||||
TextInput::make('phone')
|
||||
->required()
|
||||
->tel(),
|
||||
])
|
||||
->columns(2),
|
||||
Section::make('Password')
|
||||
->schema([
|
||||
TextInput::make('password')
|
||||
->password()
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->dehydrated(fn (?string $state): bool => filled($state))
|
||||
->same('password_confirmation'),
|
||||
TextInput::make('password_confirmation')
|
||||
->password()
|
||||
->required(fn (string $operation): bool => $operation === 'create')
|
||||
->dehydrated(false),
|
||||
])
|
||||
->columns(1)
|
||||
->visible(fn (): bool => Auth::user()?->id === Request::route('record')),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('fullName')
|
||||
->searchable(),
|
||||
TextColumn::make('email')
|
||||
->searchable(),
|
||||
TextColumn::make('username')
|
||||
->searchable(),
|
||||
TextColumn::make('phone'),
|
||||
TextColumn::make('tenant.name')
|
||||
->label('Tenant'),
|
||||
TextColumn::make('email_verified_at')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
ActionGroup::make([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
]),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->where('tenant_id', Auth::user()->tenant_id));
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListUsers::route('/'),
|
||||
'edit' => Pages\EditUser::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
34
app/Filament/Resources/UserResource/Pages/EditUser.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class EditUser extends EditRecord
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = Hash::make($data['password']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
}
|
||||
28
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
28
app/Filament/Resources/UserResource/Pages/ListUsers.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\UserResource\Pages;
|
||||
|
||||
use App\Filament\Resources\UserResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ListUsers extends ListRecords
|
||||
{
|
||||
protected static string $resource = UserResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordClasses(fn (User $record) => $record->id === auth()->id() ? 'border-2 border-blue-500' : '')
|
||||
->poll('30s');
|
||||
}
|
||||
}
|
||||
41
app/Filament/SuperAdmin/Pages/Auth/EditProfile.php
Normal file
41
app/Filament/SuperAdmin/Pages/Auth/EditProfile.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\SuperAdmin\Pages\Auth;
|
||||
|
||||
use Filament\Auth\Pages\EditProfile as BaseEditProfile;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class EditProfile extends BaseEditProfile
|
||||
{
|
||||
public function mount(): void
|
||||
{
|
||||
Log::info('EditProfile class loaded for superadmin');
|
||||
parent::mount();
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
$this->getNameFormComponent(),
|
||||
$this->getEmailFormComponent(),
|
||||
TextInput::make('username')
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(255),
|
||||
Select::make('preferred_locale')
|
||||
->options([
|
||||
'de' => 'Deutsch',
|
||||
'en' => 'English',
|
||||
])
|
||||
->default('de')
|
||||
->required(),
|
||||
$this->getPasswordFormComponent(),
|
||||
$this->getPasswordConfirmationFormComponent(),
|
||||
$this->getCurrentPasswordFormComponent(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
app/Filament/SuperAdmin/Pages/Auth/Login.php
Normal file
79
app/Filament/SuperAdmin/Pages/Auth/Login.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\SuperAdmin\Pages\Auth;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Auth\Pages\Login as BaseLogin;
|
||||
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class Login extends BaseLogin implements HasForms
|
||||
{
|
||||
use InteractsWithForms;
|
||||
|
||||
public function authenticate(): ?LoginResponse
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
|
||||
$credentials = $this->getCredentialsFromFormData($data);
|
||||
|
||||
if (! Auth::attempt($credentials, $data['remember'] ?? false)) {
|
||||
throw ValidationException::withMessages([
|
||||
'data.email' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if (! $user->email_verified_at) {
|
||||
Auth::logout();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'data.email' => 'Your email address is not verified. Please check your email for a verification link.',
|
||||
]);
|
||||
}
|
||||
|
||||
// SuperAdmin-spezifisch: Prüfe auf SuperAdmin-Rolle, keine Tenant-Prüfung
|
||||
if ($user->role !== 'superadmin') {
|
||||
Auth::logout();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'data.email' => 'You do not have access to the SuperAdmin panel. Contact support.',
|
||||
]);
|
||||
}
|
||||
|
||||
session()->regenerate();
|
||||
|
||||
return $this->getLoginResponse();
|
||||
}
|
||||
|
||||
protected function getCredentialsFromFormData(array $data): array
|
||||
{
|
||||
return [
|
||||
'email' => $data['email'],
|
||||
'password' => $data['password'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getFormSchema(): array
|
||||
{
|
||||
return [
|
||||
TextInput::make('data.email')
|
||||
->label('Email')
|
||||
->email()
|
||||
->required()
|
||||
->autofocus(),
|
||||
TextInput::make('data.password')
|
||||
->label('Password')
|
||||
->password()
|
||||
->required()
|
||||
->extraAttributes(['tabindex' => 2]),
|
||||
Checkbox::make('data.remember')
|
||||
->label('Remember me'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,7 @@ class EventController extends Controller
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
}
|
||||
|
||||
if (!$tenant->canCreateEvent()) {
|
||||
return response()->json([
|
||||
'error' => 'No available package for creating events. Please purchase a package.',
|
||||
], 402);
|
||||
}
|
||||
// Package check is now handled by middleware
|
||||
|
||||
$validated = $request->validated();
|
||||
$tenantId = $tenant->id;
|
||||
|
||||
94
app/Http/Controllers/Auth/MarketingRegisterController.php
Normal file
94
app/Http/Controllers/Auth/MarketingRegisterController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Package;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Models\User;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MarketingRegisterController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the registration form.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$package = null;
|
||||
if ($request->has('package_id')) {
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
}
|
||||
|
||||
return view('marketing.register', compact('package'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'username' => ['required', 'string', 'max:255', 'unique:' . User::class, 'alpha_dash'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:' . User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'address' => ['required', 'string'],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
'privacy_consent' => ['required', 'accepted'],
|
||||
'package_id' => ['nullable', 'exists:packages,id'],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'username' => $request->username,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'first_name' => $request->first_name,
|
||||
'last_name' => $request->last_name,
|
||||
'address' => $request->address,
|
||||
'phone' => $request->phone,
|
||||
'preferred_locale' => $request->preferred_locale ?? 'de',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $request->name,
|
||||
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
// If package_id provided and free, assign immediately
|
||||
if ($request->package_id) {
|
||||
$package = Package::find($request->package_id);
|
||||
if ($package && $package->price == 0) {
|
||||
TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'active' => true,
|
||||
'expires_at' => now()->addYear(), // or based on package duration
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return $user->hasVerifiedEmail()
|
||||
? redirect()->intended('/admin')
|
||||
: redirect()->route('verification.notice');
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
@@ -42,6 +44,13 @@ class RegisteredUserController extends Controller
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $request->name,
|
||||
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Checkout\Session;
|
||||
@@ -18,12 +19,16 @@ use PayPal\Rest\ApiContext;
|
||||
use PayPal\Auth\OAuthTokenCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\EventPurchase;
|
||||
use App\Models\Package;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Models\PackagePurchase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class MarketingController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.key'));
|
||||
Stripe::setApiKey(config('services.stripe.key'));
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -53,74 +58,155 @@ class MarketingController extends Controller
|
||||
return redirect()->back()->with('success', 'Nachricht gesendet!');
|
||||
}
|
||||
|
||||
public function checkout(Request $request, $package)
|
||||
/**
|
||||
* Handle package purchase flow.
|
||||
*/
|
||||
public function buyPackages(Request $request, $packageId)
|
||||
{
|
||||
$packages = [
|
||||
'basic' => ['name' => 'Basic', 'price' => 0, 'events' => 1],
|
||||
'standard' => ['name' => 'Standard', 'price' => 9900, 'events' => 10], // cents
|
||||
'premium' => ['name' => 'Premium', 'price' => 19900, 'events' => 50],
|
||||
];
|
||||
$package = Package::findOrFail($packageId);
|
||||
|
||||
if (!isset($packages[$package])) {
|
||||
abort(404);
|
||||
if (!Auth::check()) {
|
||||
return redirect()->route('register', ['package_id' => $package->id])
|
||||
->with('message', __('marketing.packages.register_required'));
|
||||
}
|
||||
|
||||
$pkg = $packages[$package];
|
||||
$user = Auth::user();
|
||||
if (!$user->email_verified_at) {
|
||||
return redirect()->route('verification.notice')
|
||||
->with('message', __('auth.verification_required'));
|
||||
}
|
||||
|
||||
if ($pkg['price'] == 0) {
|
||||
// Free package: create tenant and event
|
||||
$tenant = Tenant::create([
|
||||
'name' => $request->input('tenant_name', 'New Tenant'),
|
||||
'slug' => Str::slug('new-' . now()),
|
||||
'email' => $request->input('email'),
|
||||
'events_remaining' => $pkg['events'],
|
||||
]);
|
||||
$tenant = $user->tenant;
|
||||
if (!$tenant) {
|
||||
abort(500, 'Tenant not found');
|
||||
}
|
||||
|
||||
// Create initial event
|
||||
$event = $tenant->events()->create([
|
||||
'name' => $request->input('event_name', 'My Event'),
|
||||
'slug' => Str::slug($request->input('event_name', 'my-event')),
|
||||
'status' => 'active',
|
||||
]);
|
||||
if ($package->price == 0) {
|
||||
TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
],
|
||||
[
|
||||
'active' => true,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]
|
||||
);
|
||||
|
||||
$purchase = EventPurchase::create([
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'events_purchased' => $pkg['events'],
|
||||
'amount' => 0,
|
||||
'currency' => 'EUR',
|
||||
'provider' => 'free',
|
||||
'status' => 'completed',
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => 'free',
|
||||
'price' => 0,
|
||||
'type' => $package->type,
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
return redirect("/admin/tenants/{$tenant->id}/edit")->with('success', 'Konto erstellt! Willkommen bei Fotospiel.');
|
||||
return redirect('/admin')->with('success', __('marketing.packages.free_assigned'));
|
||||
}
|
||||
|
||||
$stripe = new \Stripe\StripeClient(config('services.stripe.secret'));
|
||||
if ($request->input('provider') === 'paypal') {
|
||||
return $this->paypalCheckout($request, $packageId);
|
||||
}
|
||||
|
||||
return $this->checkout($request, $packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout for Stripe with auth metadata.
|
||||
*/
|
||||
public function checkout(Request $request, $packageId)
|
||||
{
|
||||
$package = Package::findOrFail($packageId);
|
||||
$user = Auth::user();
|
||||
$tenant = $user->tenant;
|
||||
|
||||
$stripe = new StripeClient(config('services.stripe.secret'));
|
||||
$session = $stripe->checkout->sessions->create([
|
||||
'payment_method_types' => ['card'],
|
||||
'line_items' => [[
|
||||
'price_data' => [
|
||||
'currency' => 'eur',
|
||||
'product_data' => [
|
||||
'name' => $pkg['name'] . ' Package',
|
||||
'name' => $package->name,
|
||||
],
|
||||
'unit_amount' => $pkg['price'],
|
||||
'unit_amount' => $package->price * 100,
|
||||
],
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'payment',
|
||||
'success_url' => route('marketing.success', $package),
|
||||
'cancel_url' => route('marketing'),
|
||||
'success_url' => route('marketing.success', $packageId),
|
||||
'cancel_url' => route('packages'),
|
||||
'metadata' => [
|
||||
'package' => $package,
|
||||
'events' => $pkg['events'],
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => $package->type,
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect($session->url, 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal checkout with auth metadata.
|
||||
*/
|
||||
public function paypalCheckout(Request $request, $packageId)
|
||||
{
|
||||
$package = Package::findOrFail($packageId);
|
||||
$user = Auth::user();
|
||||
$tenant = $user->tenant;
|
||||
|
||||
$apiContext = new ApiContext(
|
||||
new OAuthTokenCredential(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
)
|
||||
);
|
||||
|
||||
$payment = new Payment();
|
||||
$payer = new Payer();
|
||||
$payer->setPaymentMethod('paypal');
|
||||
|
||||
$amountObj = new Amount();
|
||||
$amountObj->setCurrency('EUR');
|
||||
$amountObj->setTotal($package->price);
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setAmount($amountObj);
|
||||
|
||||
$redirectUrls = new RedirectUrls();
|
||||
$redirectUrls->setReturnUrl(route('marketing.success', $packageId));
|
||||
$redirectUrls->setCancelUrl(route('packages'));
|
||||
|
||||
$customData = json_encode([
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => $package->type,
|
||||
]);
|
||||
|
||||
$payment->setIntent('sale')
|
||||
->setPayer($payer)
|
||||
->setTransactions([$transaction])
|
||||
->setRedirectUrls($redirectUrls)
|
||||
->setNoteToPayer('Package: ' . $package->name)
|
||||
->setCustom($customData);
|
||||
|
||||
try {
|
||||
$payment->create($apiContext);
|
||||
|
||||
session(['paypal_payment_id' => $payment->getId()]);
|
||||
|
||||
return redirect($payment->getApprovalLink());
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal checkout error: ' . $e->getMessage());
|
||||
return back()->with('error', 'Zahlung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
public function stripeCheckout($sessionId)
|
||||
{
|
||||
// Handle Stripe success
|
||||
@@ -154,82 +240,4 @@ class MarketingController extends Controller
|
||||
|
||||
return view('marketing.blog-show', compact('post'));
|
||||
}
|
||||
|
||||
public function paypalCheckout(Request $request, $package)
|
||||
{
|
||||
$packages = [
|
||||
'basic' => ['name' => 'Basic', 'price' => 0, 'events' => 1],
|
||||
'standard' => ['name' => 'Standard', 'price' => 99, 'events' => 10],
|
||||
'premium' => ['name' => 'Premium', 'price' => 199, 'events' => 50],
|
||||
];
|
||||
|
||||
if (!isset($packages[$package])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$pkg = $packages[$package];
|
||||
|
||||
if ($pkg['price'] == 0) {
|
||||
// Free package: create tenant and event
|
||||
$tenant = Tenant::create([
|
||||
'name' => $request->input('tenant_name', 'New Tenant'),
|
||||
'slug' => Str::slug('new-' . now()),
|
||||
'email' => $request->input('email'),
|
||||
'events_remaining' => $pkg['events'],
|
||||
]);
|
||||
|
||||
// Create initial event
|
||||
$event = $tenant->events()->create([
|
||||
'name' => $request->input('event_name', 'My Event'),
|
||||
'slug' => Str::slug($request->input('event_name', 'my-event')),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$purchase = EventPurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'events_purchased' => $pkg['events'],
|
||||
'amount' => 0,
|
||||
'currency' => 'EUR',
|
||||
'provider' => 'free',
|
||||
'status' => 'completed',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect("/admin/tenants/{$tenant->id}/edit")->with('success', 'Konto erstellt! Willkommen bei Fotospiel.');
|
||||
}
|
||||
|
||||
$apiContext = new ApiContext(
|
||||
new OAuthTokenCredential(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
)
|
||||
);
|
||||
|
||||
$payment = new Payment();
|
||||
$payer = new Payer();
|
||||
$payer->setPaymentMethod('paypal');
|
||||
|
||||
$amountObj = new Amount();
|
||||
$amountObj->setCurrency('EUR');
|
||||
$amountObj->setTotal($pkg['price']);
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setAmount($amountObj);
|
||||
|
||||
$redirectUrls = new RedirectUrls();
|
||||
$redirectUrls->setReturnUrl(route('marketing.success', $package));
|
||||
$redirectUrls->setCancelUrl(route('marketing'));
|
||||
|
||||
$payment->setIntent('sale')
|
||||
->setPayer($payer)
|
||||
->setTransactions([$transaction])
|
||||
->setRedirectUrls($redirectUrls);
|
||||
|
||||
try {
|
||||
$payment->create($apiContext);
|
||||
return redirect($payment->getApprovalLink());
|
||||
} catch (Exception $e) {
|
||||
return back()->with('error', 'Zahlung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
235
app/Http/Controllers/PayPalController.php
Normal file
235
app/Http/Controllers/PayPalController.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Models\Package;
|
||||
|
||||
use PaypalServerSdkLib\PaypalServerSdkClientBuilder;
|
||||
use PaypalServerSdkLib\Auth\ClientCredentialsAuthCredentialsBuilder;
|
||||
use PaypalServerSdkLib\Environment;
|
||||
use PaypalServerSdkLib\Logging\LoggingConfigurationBuilder;
|
||||
use PaypalServerSdkLib\Logging\RequestLoggingConfigurationBuilder;
|
||||
use PaypalServerSdkLib\Logging\ResponseLoggingConfigurationBuilder;
|
||||
use PaypalServerSdkLib\Logging\LogLevel;
|
||||
use PaypalServerSdkLib\Orders\OrderRequestBuilder;
|
||||
use PaypalServerSdkLib\Orders\CheckoutPaymentIntent;
|
||||
use PaypalServerSdkLib\Orders\PurchaseUnitRequestBuilder;
|
||||
use PaypalServerSdkLib\Orders\AmountWithBreakdownBuilder;
|
||||
use PaypalServerSdkLib\Orders\ApplicationContextBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\SubscriptionRequestBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\SubscriberBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\NameBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\ApplicationContextSubscriptionBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\ShippingPreference;
|
||||
|
||||
class PayPalController extends Controller
|
||||
{
|
||||
private $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$clientId = config('services.paypal.client_id');
|
||||
$clientSecret = config('services.paypal.secret');
|
||||
|
||||
$this->client = PaypalServerSdkClientBuilder::init()
|
||||
->clientCredentialsAuthCredentials(
|
||||
ClientCredentialsAuthCredentialsBuilder::init($clientId, $clientSecret)
|
||||
)
|
||||
->environment(config('app.env') === 'production' ? Environment::PRODUCTION : Environment::SANDBOX)
|
||||
->loggingConfiguration(
|
||||
LoggingConfigurationBuilder::init()
|
||||
->level(LogLevel::INFO)
|
||||
->requestConfiguration(RequestLoggingConfigurationBuilder::init()->body(true))
|
||||
->responseConfiguration(ResponseLoggingConfigurationBuilder::init()->headers(true))
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function createOrder(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tenant_id' => 'required|exists:tenants,id',
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::findOrFail($request->tenant_id);
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
|
||||
$ordersController = $this->client->getOrdersController();
|
||||
|
||||
$requestBody = OrderRequestBuilder::init(CheckoutPaymentIntent::CAPTURE)
|
||||
->purchaseUnits([
|
||||
PurchaseUnitRequestBuilder::init()
|
||||
->amount(
|
||||
AmountWithBreakdownBuilder::init('EUR', number_format($package->price, 2, '.', ''))
|
||||
->build()
|
||||
)
|
||||
->description('Package: ' . $package->name)
|
||||
->customId($tenant->id . '_' . $package->id . '_endcustomer_event')
|
||||
->build()
|
||||
])
|
||||
->applicationContext(
|
||||
ApplicationContextBuilder::init()
|
||||
->shippingPreference(ShippingPreference::NO_SHIPPING)
|
||||
->userAction('PAY_NOW')
|
||||
->build()
|
||||
)
|
||||
->build();
|
||||
|
||||
$collect = [
|
||||
'body' => $requestBody,
|
||||
'prefer' => 'return=representation'
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $ordersController->createOrder($collect);
|
||||
|
||||
if ($response->statusCode === 201) {
|
||||
$result = $response->result;
|
||||
$approveLink = collect($result->links)->first(fn($link) => $link->rel === 'approve')?->href;
|
||||
|
||||
return response()->json([
|
||||
'id' => $result->id,
|
||||
'approve_url' => $approveLink,
|
||||
]);
|
||||
}
|
||||
|
||||
Log::error('PayPal order creation failed', ['response' => $response]);
|
||||
return response()->json(['error' => 'Order creation failed'], 400);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal order creation exception', ['error' => $e->getMessage()]);
|
||||
return response()->json(['error' => 'Order creation failed'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function captureOrder(Request $request)
|
||||
{
|
||||
$request->validate(['order_id' => 'required']);
|
||||
|
||||
$ordersController = $this->client->getOrdersController();
|
||||
|
||||
$collect = [
|
||||
'id' => $request->order_id,
|
||||
'prefer' => 'return=representation'
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $ordersController->captureOrder($collect);
|
||||
|
||||
if ($response->statusCode === 201) {
|
||||
$result = $response->result;
|
||||
$customId = $result->purchaseUnits[0]->customId ?? null;
|
||||
|
||||
if ($customId) {
|
||||
[$tenantId, $packageId, $type] = explode('_', $customId);
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
$package = Package::findOrFail($packageId);
|
||||
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $result->id,
|
||||
'price' => $result->purchaseUnits[0]->amount->value,
|
||||
'type' => $type ?? 'endcustomer_event',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
Log::info('PayPal order captured and purchase created: ' . $result->id);
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'captured', 'order' => $result]);
|
||||
}
|
||||
|
||||
Log::error('PayPal order capture failed', ['response' => $response]);
|
||||
return response()->json(['error' => 'Capture failed'], 400);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal order capture exception', ['error' => $e->getMessage()]);
|
||||
return response()->json(['error' => 'Capture failed'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function createSubscription(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tenant_id' => 'required|exists:tenants,id',
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'plan_id' => 'required', // PayPal plan ID for the package
|
||||
]);
|
||||
|
||||
$tenant = Tenant::findOrFail($request->tenant_id);
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
|
||||
$subscriptionsController = $this->client->getSubscriptionsController();
|
||||
|
||||
$requestBody = SubscriptionRequestBuilder::init()
|
||||
->planId($request->plan_id)
|
||||
->subscriber(
|
||||
SubscriberBuilder::init()
|
||||
->name(
|
||||
NameBuilder::init()
|
||||
->givenName($tenant->name ?? 'Tenant')
|
||||
->build()
|
||||
)
|
||||
->emailAddress($tenant->email)
|
||||
->build()
|
||||
)
|
||||
->customId($tenant->id . '_' . $package->id . '_reseller_subscription')
|
||||
->applicationContext(
|
||||
ApplicationContextSubscriptionBuilder::init()
|
||||
->shippingPreference(ShippingPreference::NO_SHIPPING)
|
||||
->userAction('SUBSCRIBE_NOW')
|
||||
->build()
|
||||
)
|
||||
->build();
|
||||
|
||||
$collect = [
|
||||
'body' => $requestBody,
|
||||
'prefer' => 'return=representation'
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $subscriptionsController->createSubscription($collect);
|
||||
|
||||
if ($response->statusCode === 201) {
|
||||
$result = $response->result;
|
||||
$subscriptionId = $result->id;
|
||||
|
||||
TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $subscriptionId,
|
||||
'price' => $package->price,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
$approveLink = collect($result->links)->first(fn($link) => $link->rel === 'approve')?->href;
|
||||
|
||||
return response()->json([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'approve_url' => $approveLink,
|
||||
]);
|
||||
}
|
||||
|
||||
Log::error('PayPal subscription creation failed', ['response' => $response]);
|
||||
return response()->json(['error' => 'Subscription creation failed'], 400);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal subscription creation exception', ['error' => $e->getMessage()]);
|
||||
return response()->json(['error' => 'Subscription creation failed'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,32 +16,60 @@ class PayPalWebhookController extends Controller
|
||||
$payerEmail = $input['payer_email'] ?? null;
|
||||
$paymentStatus = $input['payment_status'] ?? null;
|
||||
$mcGross = $input['mc_gross'] ?? 0;
|
||||
$packageId = $input['custom'] ?? null;
|
||||
$custom = $input['custom'] ?? null;
|
||||
|
||||
if ($paymentStatus === 'Completed' && $mcGross > 0) {
|
||||
// Verify IPN with PayPal (simplified; use SDK for full verification)
|
||||
// $verified = $this->verifyIPN($input);
|
||||
|
||||
// Find or create tenant (for public checkout, perhaps create new or use session)
|
||||
// For now, assume tenant_id from custom or session
|
||||
$tenantId = $packageId ? Tenant::where('slug', $packageId)->first()->id ?? 1 : 1;
|
||||
// Parse custom for user_id or tenant_id
|
||||
$data = json_decode($custom, true);
|
||||
$userId = $data['user_id'] ?? null;
|
||||
$tenantId = $data['tenant_id'] ?? null;
|
||||
$packageId = $data['package_id'] ?? null;
|
||||
|
||||
// Create purchase and increment credits
|
||||
$purchase = EventPurchase::create([
|
||||
'tenant_id' => $tenantId, // Implement tenant resolution
|
||||
'events_purchased' => $mcGross / 49, // Example: 49€ per event credit
|
||||
'amount' => $mcGross,
|
||||
'currency' => $input['mc_currency'] ?? 'EUR',
|
||||
'provider' => 'paypal',
|
||||
'external_receipt_id' => $ipnMessage,
|
||||
'status' => 'completed',
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id in PayPal IPN: ' . $userId);
|
||||
return response('OK', 200);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId || !$packageId) {
|
||||
Log::error('Missing tenant or package in PayPal IPN custom data');
|
||||
return response('OK', 200);
|
||||
}
|
||||
|
||||
// Create PackagePurchase
|
||||
\App\Models\PackagePurchase::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $ipnMessage,
|
||||
'price' => $mcGross,
|
||||
'type' => $data['type'] ?? 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::find($tenantId);
|
||||
$tenant->incrementCredits($purchase->events_purchased, 'paypal_purchase', 'PayPal IPN', $purchase->id);
|
||||
// Update TenantPackage if subscription
|
||||
if ($data['type'] ?? '' === 'reseller_subscription') {
|
||||
\App\Models\TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
],
|
||||
[
|
||||
'active' => true,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Log::info('PayPal IPN processed', $input);
|
||||
Log::info('PayPal IPN processed for tenant ' . $tenantId . ', package ' . $packageId, $input);
|
||||
}
|
||||
|
||||
return response('OK', 200);
|
||||
|
||||
67
app/Http/Controllers/ProfileController.php
Normal file
67
app/Http/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
// Authorized via auth middleware
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'username' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:users,username,' . $user->id],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'address' => ['required', 'string'],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
$user->update($request->only([
|
||||
'name', 'username', 'email', 'first_name', 'last_name', 'address', 'phone'
|
||||
]));
|
||||
|
||||
return back()->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function updatePassword(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
// Authorized via auth middleware
|
||||
|
||||
$request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
return back()->with('status', 'password-updated');
|
||||
}
|
||||
}
|
||||
107
app/Http/Controllers/StripeController.php
Normal file
107
app/Http/Controllers/StripeController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\PaymentIntent;
|
||||
use Stripe\Subscription;
|
||||
|
||||
class StripeController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function createPaymentIntent(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tenant_id' => 'required|exists:tenants,id',
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::findOrFail($request->tenant_id);
|
||||
$package = \App\Models\Package::findOrFail($request->package_id);
|
||||
|
||||
$paymentIntent = PaymentIntent::create([
|
||||
'amount' => $request->amount * 100, // cents
|
||||
'currency' => 'eur',
|
||||
'metadata' => [
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => 'endcustomer_event', // or reseller
|
||||
],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'client_secret' => $paymentIntent->client_secret,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createSubscription(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tenant_id' => 'required|exists:tenants,id',
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'payment_method_id' => 'required',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::findOrFail($request->tenant_id);
|
||||
$package = \App\Models\Package::findOrFail($request->package_id);
|
||||
|
||||
$subscription = Subscription::create([
|
||||
'customer' => $tenant->stripe_customer_id ?? $this->createCustomer($tenant),
|
||||
'items' => [[
|
||||
'price' => $package->stripe_price_id, // Assume package has stripe_price_id
|
||||
]],
|
||||
'payment_method' => $request->payment_method_id,
|
||||
'default_payment_method' => $request->payment_method_id,
|
||||
'expand' => ['latest_invoice.payment_intent'],
|
||||
'metadata' => [
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => 'reseller_subscription',
|
||||
],
|
||||
]);
|
||||
|
||||
// Create TenantPackage and PackagePurchase
|
||||
\App\Models\TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $subscription->id,
|
||||
'price' => $package->price,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'subscription_id' => $subscription->id,
|
||||
'client_secret' => $subscription->latest_invoice->payment_intent->client_secret ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createCustomer(Tenant $tenant)
|
||||
{
|
||||
$customer = \Stripe\Customer::create([
|
||||
'email' => $tenant->email,
|
||||
'metadata' => ['tenant_id' => $tenant->id],
|
||||
]);
|
||||
|
||||
$tenant->update(['stripe_customer_id' => $customer->id]);
|
||||
|
||||
return $customer->id;
|
||||
}
|
||||
}
|
||||
@@ -2,90 +2,169 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EventPurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\TenantPackage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Webhook;
|
||||
|
||||
class StripeWebhookController extends Controller
|
||||
{
|
||||
public function handle(Request $request)
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request)
|
||||
{
|
||||
$payload = $request->getContent();
|
||||
$sig = $request->header('Stripe-Signature');
|
||||
$secret = config('services.stripe.webhook');
|
||||
$sigHeader = $request->header('Stripe-Signature');
|
||||
$endpointSecret = config('services.stripe.webhook_secret');
|
||||
|
||||
if (!$secret || !$sig) {
|
||||
abort(400, 'Missing signature');
|
||||
try {
|
||||
$event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Stripe webhook signature verification failed: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Invalid signature'], 400);
|
||||
}
|
||||
|
||||
$expectedSig = 'v1=' . hash_hmac('sha256', $payload, $secret);
|
||||
switch ($event['type']) {
|
||||
case 'payment_intent.succeeded':
|
||||
$paymentIntent = $event['data']['object'];
|
||||
$this->handlePaymentIntentSucceeded($paymentIntent);
|
||||
break;
|
||||
|
||||
if (!hash_equals($expectedSig, $sig)) {
|
||||
abort(400, 'Invalid signature');
|
||||
case 'invoice.payment_succeeded':
|
||||
$invoice = $event['data']['object'];
|
||||
$this->handleInvoicePaymentSucceeded($invoice);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_failed':
|
||||
$invoice = $event['data']['object'];
|
||||
$this->handleInvoicePaymentFailed($invoice);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('Unhandled Stripe event type: ' . $event['type']);
|
||||
}
|
||||
|
||||
$event = json_decode($payload, true);
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error('Invalid JSON in Stripe webhook: ' . json_last_error_msg());
|
||||
return response('', 200);
|
||||
private function handlePaymentIntentSucceeded($paymentIntent)
|
||||
{
|
||||
$metadata = $paymentIntent['metadata'];
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe payment intent: ' . $paymentIntent['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event['type'] === 'checkout.session.completed') {
|
||||
$session = $event['data']['object'];
|
||||
$receiptId = $session['id'];
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
$type = $metadata['type'] ?? 'endcustomer_event';
|
||||
|
||||
// Idempotency check
|
||||
if (EventPurchase::where('external_receipt_id', $receiptId)->exists()) {
|
||||
return response('', 200);
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantId = $session['metadata']['tenant_id'] ?? null;
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::warning('No tenant_id in Stripe metadata', ['receipt_id' => $receiptId]);
|
||||
// Dispatch job for retry or manual resolution
|
||||
\App\Jobs\ValidateStripeWebhookJob::dispatch($payload, $sig);
|
||||
return response('', 200);
|
||||
}
|
||||
|
||||
$tenant = Tenant::find($tenantId);
|
||||
|
||||
if (!$tenant) {
|
||||
Log::error('Tenant not found for Stripe webhook', ['tenant_id' => $tenantId]);
|
||||
\App\Jobs\ValidateStripeWebhookJob::dispatch($payload, $sig);
|
||||
return response('', 200);
|
||||
}
|
||||
|
||||
$amount = $session['amount_total'] / 100;
|
||||
$currency = $session['currency'];
|
||||
$eventsPurchased = (int) ($session['metadata']['events_purchased'] ?? 1);
|
||||
|
||||
DB::transaction(function () use ($tenant, $amount, $currency, $eventsPurchased, $receiptId) {
|
||||
$purchase = EventPurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'events_purchased' => $eventsPurchased,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'provider' => 'stripe',
|
||||
'external_receipt_id' => $receiptId,
|
||||
'status' => 'completed',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
$tenant->incrementCredits($eventsPurchased, 'purchase', null, $purchase->id);
|
||||
});
|
||||
|
||||
Log::info('Processed Stripe purchase', ['receipt_id' => $receiptId, 'tenant_id' => $tenantId]);
|
||||
} else {
|
||||
// For other event types, log or dispatch job if needed
|
||||
Log::info('Unhandled Stripe event', ['type' => $event['type']]);
|
||||
// Optionally dispatch job for processing other events
|
||||
\App\Jobs\ValidateStripeWebhookJob::dispatch($payload, $sig);
|
||||
}
|
||||
|
||||
return response('', 200);
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe payment intent: ' . $paymentIntent['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PackagePurchase for one-off payment
|
||||
\App\Models\PackagePurchase::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $paymentIntent['id'],
|
||||
'price' => $paymentIntent['amount_received'] / 100,
|
||||
'type' => $type,
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
if ($type === 'endcustomer_event') {
|
||||
// For event packages, assume event_id from metadata or handle separately
|
||||
// TODO: Link to specific event if provided
|
||||
}
|
||||
|
||||
Log::info('Package purchase created via Stripe payment intent: ' . $paymentIntent['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentSucceeded($invoice)
|
||||
{
|
||||
$subscription = $invoice['subscription'];
|
||||
$metadata = $invoice['metadata'];
|
||||
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe invoice: ' . $invoice['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe invoice: ' . $invoice['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update or create TenantPackage for subscription
|
||||
\App\Models\TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
],
|
||||
[
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(), // Renew annually
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
// Create or update PackagePurchase
|
||||
\App\Models\PackagePurchase::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $subscription,
|
||||
],
|
||||
[
|
||||
'price' => $invoice['amount_paid'] / 100,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]
|
||||
);
|
||||
|
||||
Log::info('Subscription renewed via Stripe invoice: ' . $invoice['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentFailed($invoice)
|
||||
{
|
||||
$subscription = $invoice['subscription'];
|
||||
Log::warning('Stripe invoice payment failed: ' . $invoice['id'] . ' for subscription ' . $subscription);
|
||||
|
||||
// TODO: Deactivate package or notify tenant
|
||||
// e.g., TenantPackage::where('provider_id', $subscription)->update(['active' => false]);
|
||||
}
|
||||
}
|
||||
91
app/Http/Middleware/PackageMiddleware.php
Normal file
91
app/Http/Middleware/PackageMiddleware.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\Tenant;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PackageMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$tenant = $request->attributes->get('tenant');
|
||||
if (! $tenant instanceof Tenant) {
|
||||
$tenant = $this->resolveTenant($request);
|
||||
$request->attributes->set('tenant', $tenant);
|
||||
$request->attributes->set('tenant_id', $tenant->id);
|
||||
$request->merge([
|
||||
'tenant' => $tenant,
|
||||
'tenant_id' => $tenant->id,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->requiresPackageCheck($request) && !$this->canPerformAction($request, $tenant)) {
|
||||
return response()->json([
|
||||
'error' => 'Package limits exceeded. Please purchase or upgrade a package.',
|
||||
], 402);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function requiresPackageCheck(Request $request): bool
|
||||
{
|
||||
return $request->isMethod('post') && (
|
||||
$request->routeIs('api.v1.tenant.events.store') ||
|
||||
$request->routeIs('api.v1.tenant.photos.store') // Assuming photo upload route
|
||||
);
|
||||
}
|
||||
|
||||
private function canPerformAction(Request $request, Tenant $tenant): bool
|
||||
{
|
||||
if ($request->routeIs('api.v1.tenant.events.store')) {
|
||||
// Check tenant package for event creation
|
||||
$resellerPackage = $tenant->activeResellerPackage();
|
||||
if ($resellerPackage) {
|
||||
return $resellerPackage->used_events < $resellerPackage->package->max_events_per_year;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($request->routeIs('api.v1.tenant.photos.store')) {
|
||||
$eventId = $request->input('event_id');
|
||||
if (!$eventId) {
|
||||
return false;
|
||||
}
|
||||
$event = Event::findOrFail($eventId);
|
||||
if ($event->tenant_id !== $tenant->id) {
|
||||
return false;
|
||||
}
|
||||
$eventPackage = $event->eventPackage;
|
||||
if (!$eventPackage) {
|
||||
return false;
|
||||
}
|
||||
return $eventPackage->used_photos < $eventPackage->package->max_photos;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function resolveTenant(Request $request): Tenant
|
||||
{
|
||||
$user = $request->user();
|
||||
if ($user && isset($user->tenant) && $user->tenant instanceof Tenant) {
|
||||
return $user->tenant;
|
||||
}
|
||||
|
||||
$tenantId = $request->attributes->get('tenant_id');
|
||||
if (! $tenantId && $user && isset($user->tenant_id)) {
|
||||
$tenantId = $user->tenant_id;
|
||||
}
|
||||
|
||||
if (! $tenantId) {
|
||||
abort(401, 'Unauthenticated');
|
||||
}
|
||||
|
||||
return Tenant::findOrFail($tenantId);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class LoginRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'login' => ['required', 'string'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
@@ -40,11 +40,14 @@ class LoginRequest extends FormRequest
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
||||
$credentials = $this->only('login', 'password');
|
||||
$credentials['login'] = $this->input('login');
|
||||
|
||||
if (! Auth::attempt($credentials, $this->boolean('remember'))) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('auth.failed'),
|
||||
'login' => __('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -79,7 +82,7 @@ class LoginRequest extends FormRequest
|
||||
*/
|
||||
public function throttleKey(): string
|
||||
{
|
||||
return $this->string('email')
|
||||
return $this->string('login')
|
||||
->lower()
|
||||
->append('|'.$this->ip())
|
||||
->transliterate()
|
||||
|
||||
42
app/Mail/Welcome.php
Normal file
42
app/Mail/Welcome.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class Welcome extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public User $user)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Welcome to Fotospiel!',
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.welcome',
|
||||
with: [
|
||||
'user' => $this->user,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ class Event extends Model
|
||||
'date' => 'datetime',
|
||||
'settings' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'name' => 'array',
|
||||
'description' => 'array',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
|
||||
@@ -40,13 +40,7 @@ class Package extends Model
|
||||
'features' => 'array',
|
||||
];
|
||||
|
||||
protected function features(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn (mixed $value) => $value ? json_decode($value, true) : [],
|
||||
set: fn (array $value) => json_encode($value),
|
||||
);
|
||||
}
|
||||
// features handled by $casts = ['features' => 'array']
|
||||
|
||||
public function eventPackages(): HasMany
|
||||
{
|
||||
|
||||
@@ -78,6 +78,10 @@ class PackagePurchase extends Model
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($purchase) {
|
||||
if (!$purchase->tenant_id) {
|
||||
throw new \Exception('Tenant ID is required for package purchases.');
|
||||
}
|
||||
|
||||
if (!$purchase->purchased_at) {
|
||||
$purchase->purchased_at = now();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ class Task extends Model
|
||||
protected $casts = [
|
||||
'due_date' => 'datetime',
|
||||
'is_completed' => 'bool',
|
||||
'title' => 'array',
|
||||
'description' => 'array',
|
||||
'example_text' => 'array',
|
||||
];
|
||||
|
||||
public function emotion(): BelongsTo
|
||||
|
||||
@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Tenant extends Model
|
||||
@@ -51,9 +53,9 @@ class Tenant extends Model
|
||||
return $this->hasMany(TenantPackage::class);
|
||||
}
|
||||
|
||||
public function activeResellerPackage()
|
||||
public function activeResellerPackage(): HasOne
|
||||
{
|
||||
return $this->tenantPackages()->where('active', true)->first();
|
||||
return $this->hasOne(TenantPackage::class)->where('active', true);
|
||||
}
|
||||
|
||||
public function canCreateEvent(): bool
|
||||
@@ -93,4 +95,9 @@ class Tenant extends Model
|
||||
get: fn () => $this->activeResellerPackage() !== null,
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
@@ -25,6 +27,10 @@ class User extends Authenticatable
|
||||
'password',
|
||||
'username',
|
||||
'preferred_locale',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'address',
|
||||
'phone',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -50,8 +56,15 @@ class User extends Authenticatable
|
||||
];
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
protected function fullName(): Attribute
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
return Attribute::make(
|
||||
get: fn () => $this->first_name . ' ' . $this->last_name,
|
||||
);
|
||||
}
|
||||
|
||||
public function tenant(): HasOne
|
||||
{
|
||||
return $this->hasOne(Tenant::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ class AdminPanelProvider extends PanelProvider
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->login()
|
||||
->login(\App\Filament\Pages\Auth\Login::class)
|
||||
->colors([
|
||||
'primary' => Color::Pink,
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||
->discoverPages(in: app_path('Filament/Admin/Pages'), for: 'App\\Filament\\Admin\\Pages')
|
||||
->pages([
|
||||
Pages\Dashboard::class,
|
||||
])
|
||||
@@ -61,8 +61,10 @@ class AdminPanelProvider extends PanelProvider
|
||||
Authenticate::class,
|
||||
])
|
||||
->resources([
|
||||
// Blog-Resources moved to SuperAdminPanel
|
||||
\App\Filament\Resources\UserResource::class,
|
||||
\App\Filament\Resources\TenantPackageResource::class,
|
||||
])
|
||||
->tenant(\App\Models\Tenant::class)
|
||||
// Remove blog models as they are global and handled in SuperAdmin
|
||||
;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use App\Filament\Resources\LegalPageResource;
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use App\Filament\Pages\SuperAdminProfile;
|
||||
use App\Models\User;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\BlogPost;
|
||||
@@ -30,28 +29,31 @@ use App\Filament\Widgets\TopTenantsByUploads;
|
||||
use Stephenjude\FilamentBlog\Filament\Resources\CategoryResource;
|
||||
use Stephenjude\FilamentBlog\Filament\Resources\PostResource;
|
||||
use Stephenjude\FilamentBlog\BlogPlugin;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SuperAdminPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
\Illuminate\Support\Facades\Log::info('SuperAdminPanelProvider panel method called');
|
||||
|
||||
return $panel
|
||||
->default()
|
||||
->id('superadmin')
|
||||
->path('super-admin')
|
||||
->login()
|
||||
->colors([
|
||||
'primary' => Color::Pink,
|
||||
])
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
|
||||
->discoverPages(in: app_path('Filament/SuperAdmin/Pages'), for: 'App\\Filament\\SuperAdmin\\Pages')
|
||||
->pages([
|
||||
Pages\Dashboard::class,
|
||||
])
|
||||
->login(\App\Filament\SuperAdmin\Pages\Auth\Login::class)
|
||||
->plugin(
|
||||
BlogPlugin::make()
|
||||
)
|
||||
->profile(SuperAdminProfile::class)
|
||||
->profile()
|
||||
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
|
||||
->widgets([
|
||||
Widgets\AccountWidget::class,
|
||||
@@ -74,13 +76,10 @@ class SuperAdminPanelProvider extends PanelProvider
|
||||
Authenticate::class,
|
||||
])
|
||||
->resources([
|
||||
TenantResource::class,
|
||||
// Temporär deaktiviert: TenantResource - verdächtigt für frühen Fehler
|
||||
// TenantResource::class,
|
||||
LegalPageResource::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
'superadmin.auth',
|
||||
])
|
||||
->authGuard('web')
|
||||
// SuperAdmin-Zugriff durch custom Middleware, globale Sichtbarkeit ohne Tenant-Isolation
|
||||
// Blog-Resources werden durch das Plugin-ServiceProvider automatisch registriert
|
||||
|
||||
Reference in New Issue
Block a user