feat: subscriptions
CI / 🧪 Tests Laravel (push) Successful in 2m48s
CI / 🐳 Build & Push Image (push) Successful in 3m0s

This commit is contained in:
2026-08-07 11:53:46 +02:00
parent 99cb0e63e6
commit 2ece4b6e3e
51 changed files with 1489 additions and 254 deletions
@@ -55,7 +55,7 @@ class CreditProductResource extends Resource
return Action::make('syncStripe')
->label(__('admin.credit_products.actions.sync_stripe'))
->icon(Heroicon::OutlinedArrowPath)
->visible(fn (CreditProduct $record): bool => blank($record->stripe_price_id))
->visible(fn (CreditProduct $record): bool => blank($record->stripe_price_id) && $record->purpose !== null)
->requiresConfirmation()
->action(function (CreditProduct $record): void {
app(StripeCreditProductSyncer::class)->sync($record);
@@ -2,7 +2,9 @@
namespace App\Filament\Resources\CreditProducts\Schemas;
use App\Enums\BillingProductPurpose;
use App\Enums\CreditProductType;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
@@ -23,16 +25,15 @@ class CreditProductForm
->label(__('admin.credit_products.fields.name'))
->required()
->maxLength(255),
Select::make('type')
->label(__('admin.credit_products.fields.type'))
->options(CreditProductType::class)
->default(CreditProductType::ONE_TIME->value)
->required(),
TextInput::make('credits')
->label(__('admin.credit_products.fields.credits'))
->integer()
->minValue(1)
Select::make('purpose')
->label(__('admin.credit_products.fields.purpose'))
->options(BillingProductPurpose::class)
->unique(ignoreRecord: true)
->required(),
Hidden::make('type')
->default(CreditProductType::ONE_TIME->value),
Hidden::make('credits')
->default(0),
TextInput::make('amount')
->label(__('admin.credit_products.fields.amount'))
->helperText(__('admin.credit_products.helpers.amount'))
@@ -2,7 +2,7 @@
namespace App\Filament\Resources\CreditProducts\Tables;
use App\Enums\CreditProductType;
use App\Enums\BillingProductPurpose;
use App\Filament\Resources\CreditProducts\CreditProductResource;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
@@ -24,14 +24,10 @@ class CreditProductsTable
->label(__('admin.credit_products.fields.name'))
->searchable()
->sortable(),
TextColumn::make('type')
->label(__('admin.credit_products.fields.type'))
TextColumn::make('purpose')
->label(__('admin.credit_products.fields.purpose'))
->badge()
->sortable(),
TextColumn::make('credits')
->label(__('admin.credit_products.fields.credits'))
->numeric(decimalPlaces: 0)
->sortable(),
TextColumn::make('amount')
->label(__('admin.credit_products.fields.amount'))
->money(fn ($record): string => $record->currency, divideBy: 100)
@@ -56,9 +52,9 @@ class CreditProductsTable
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
SelectFilter::make('type')
->label(__('admin.credit_products.fields.type'))
->options(CreditProductType::class),
SelectFilter::make('purpose')
->label(__('admin.credit_products.fields.purpose'))
->options(BillingProductPurpose::class),
TernaryFilter::make('is_active')
->label(__('admin.credit_products.fields.is_active')),
])
@@ -0,0 +1,61 @@
<?php
namespace App\Filament\Resources\IdentityVerificationRequests;
use App\Filament\Resources\IdentityVerificationRequests\Pages\ListIdentityVerificationRequests;
use App\Filament\Resources\IdentityVerificationRequests\Pages\ViewIdentityVerificationRequest;
use App\Filament\Resources\IdentityVerificationRequests\Schemas\IdentityVerificationRequestInfolist;
use App\Filament\Resources\IdentityVerificationRequests\Tables\IdentityVerificationRequestsTable;
use App\Models\IdentityVerificationRequest;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class IdentityVerificationRequestResource extends Resource
{
protected static ?string $model = IdentityVerificationRequest::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedIdentification;
protected static ?string $recordTitleAttribute = 'id';
public static function getNavigationLabel(): string
{
return __('admin.identity_verification_requests.navigation.label');
}
public static function getModelLabel(): string
{
return __('admin.identity_verification_requests.navigation.singular');
}
public static function getPluralModelLabel(): string
{
return __('admin.identity_verification_requests.navigation.plural');
}
public static function canCreate(): bool
{
return false;
}
public static function infolist(Schema $schema): Schema
{
return IdentityVerificationRequestInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return IdentityVerificationRequestsTable::configure($table);
}
public static function getPages(): array
{
return [
'index' => ListIdentityVerificationRequests::route('/'),
'view' => ViewIdentityVerificationRequest::route('/{record}'),
];
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Resources\IdentityVerificationRequests\Pages;
use App\Filament\Resources\IdentityVerificationRequests\IdentityVerificationRequestResource;
use Filament\Resources\Pages\ListRecords;
class ListIdentityVerificationRequests extends ListRecords
{
protected static string $resource = IdentityVerificationRequestResource::class;
protected function getHeaderActions(): array
{
return [];
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Filament\Resources\IdentityVerificationRequests\Pages;
use App\Filament\Resources\IdentityVerificationRequests\IdentityVerificationRequestResource;
use App\Filament\Resources\IdentityVerificationRequests\Tables\IdentityVerificationRequestsTable;
use Filament\Resources\Pages\ViewRecord;
class ViewIdentityVerificationRequest extends ViewRecord
{
protected static string $resource = IdentityVerificationRequestResource::class;
protected function getHeaderActions(): array
{
return [
IdentityVerificationRequestsTable::approveAction(),
IdentityVerificationRequestsTable::rejectAction(),
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Filament\Resources\IdentityVerificationRequests\Schemas;
use App\Models\IdentityVerificationRequest;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class IdentityVerificationRequestInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make(__('admin.identity_verification_requests.sections.request'))
->columns(3)
->schema([
TextEntry::make('id')
->label(__('admin.identity_verification_requests.fields.id'))
->copyable(),
TextEntry::make('status')
->label(__('admin.identity_verification_requests.fields.status'))
->badge(),
TextEntry::make('user.email')
->label(__('admin.identity_verification_requests.fields.user'))
->copyable(),
TextEntry::make('submitted_at')
->label(__('admin.identity_verification_requests.fields.submitted_at'))
->dateTime(),
TextEntry::make('reviewed_at')
->label(__('admin.identity_verification_requests.fields.reviewed_at'))
->dateTime()
->placeholder(__('admin.identity_verification_requests.placeholders.empty')),
TextEntry::make('reviewer.email')
->label(__('admin.identity_verification_requests.fields.reviewed_by'))
->placeholder(__('admin.identity_verification_requests.placeholders.empty')),
TextEntry::make('document')
->label(__('admin.identity_verification_requests.fields.document'))
->state(__('admin.identity_verification_requests.actions.open_document'))
->url(fn (IdentityVerificationRequest $record): ?string => $record->temporaryDocumentUrl())
->openUrlInNewTab()
->visible(fn (IdentityVerificationRequest $record): bool => $record->hasDocument()),
TextEntry::make('document_deleted_at')
->label(__('admin.identity_verification_requests.fields.document_deleted_at'))
->dateTime()
->placeholder(__('admin.identity_verification_requests.placeholders.empty')),
TextEntry::make('rejection_reason')
->label(__('admin.identity_verification_requests.fields.rejection_reason'))
->placeholder(__('admin.identity_verification_requests.placeholders.empty'))
->columnSpanFull(),
]),
]);
}
}
@@ -0,0 +1,98 @@
<?php
namespace App\Filament\Resources\IdentityVerificationRequests\Tables;
use App\Actions\ReviewIdentityVerification;
use App\Enums\IdentityVerificationStatus;
use App\Models\IdentityVerificationRequest;
use App\Models\User;
use Filament\Actions\Action;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\Textarea;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class IdentityVerificationRequestsTable
{
public static function configure(Table $table): Table
{
return $table
->defaultSort('submitted_at', 'desc')
->columns([
TextColumn::make('submitted_at')
->label(__('admin.identity_verification_requests.fields.submitted_at'))
->dateTime()
->sortable(),
TextColumn::make('status')
->label(__('admin.identity_verification_requests.fields.status'))
->badge()
->sortable(),
TextColumn::make('user.email')
->label(__('admin.identity_verification_requests.fields.user'))
->searchable(),
TextColumn::make('reviewed_at')
->label(__('admin.identity_verification_requests.fields.reviewed_at'))
->dateTime()
->sortable()
->placeholder(__('admin.identity_verification_requests.placeholders.empty')),
])
->filters([
SelectFilter::make('status')
->label(__('admin.identity_verification_requests.fields.status'))
->options(IdentityVerificationStatus::class),
])
->recordActions([
ViewAction::make(),
self::approveAction(),
self::rejectAction(),
]);
}
public static function approveAction(): Action
{
return Action::make('approve')
->label(__('admin.identity_verification_requests.actions.approve'))
->color('success')
->requiresConfirmation()
->visible(fn (IdentityVerificationRequest $record): bool => $record->status === IdentityVerificationStatus::PENDING)
->action(function (IdentityVerificationRequest $record): void {
app(ReviewIdentityVerification::class)->execute(
$record,
IdentityVerificationStatus::APPROVED,
self::reviewer(),
);
});
}
public static function rejectAction(): Action
{
return Action::make('reject')
->label(__('admin.identity_verification_requests.actions.reject'))
->color('danger')
->schema([
Textarea::make('reason')
->label(__('admin.identity_verification_requests.fields.rejection_reason'))
->required()
->maxLength(2000),
])
->visible(fn (IdentityVerificationRequest $record): bool => $record->status === IdentityVerificationStatus::PENDING)
->action(function (array $data, IdentityVerificationRequest $record): void {
app(ReviewIdentityVerification::class)->execute(
$record,
IdentityVerificationStatus::REJECTED,
self::reviewer(),
$data['reason'],
);
});
}
private static function reviewer(): User
{
$reviewer = auth()->user();
abort_unless($reviewer instanceof User, 403);
return $reviewer;
}
}
@@ -63,8 +63,8 @@ class UserForm
->autocomplete('new-password'),
DateTimePicker::make('email_verified_at')
->label(__('admin.users.fields.email_verified_at')),
DateTimePicker::make('account_verified_at')
->label(__('admin.users.fields.account_verified_at')),
DateTimePicker::make('trial_ends_at')
->label(__('admin.users.fields.trial_ends_at')),
]),
Section::make(__('admin.users.sections.profile'))
->columns(2)
@@ -35,6 +35,14 @@ class UserInfolist
->label(__('admin.users.fields.account_verified_at'))
->dateTime()
->placeholder(__('admin.users.placeholders.not_verified')),
TextEntry::make('certification_purchased_at')
->label(__('admin.users.fields.certification_purchased_at'))
->dateTime()
->placeholder(__('admin.users.placeholders.empty')),
TextEntry::make('trial_ends_at')
->label(__('admin.users.fields.trial_ends_at'))
->dateTime()
->placeholder(__('admin.users.placeholders.empty')),
TextEntry::make('suspended_at')
->label(__('admin.users.fields.suspended_at'))
->dateTime()
@@ -51,6 +51,12 @@ class UsersTable
->sortable()
->placeholder(__('admin.users.placeholders.not_verified'))
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('trial_ends_at')
->label(__('admin.users.fields.trial_ends_at'))
->dateTime()
->sortable()
->placeholder(__('admin.users.placeholders.empty'))
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('suspended_at')
->label(__('admin.users.fields.suspended_at'))
->dateTime()