Refactor checkout health resource
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\Pages;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\TenantCheckoutHealthResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTenantCheckoutHealths extends ListRecords
|
||||
{
|
||||
protected static string $resource = TenantCheckoutHealthResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\Tables;
|
||||
namespace App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\Tables;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource;
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\TenantCheckoutHealthResource;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Tenant;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@@ -13,12 +13,9 @@ use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class TenantLemonSqueezyHealthTable
|
||||
class TenantCheckoutHealthTable
|
||||
{
|
||||
private const FAILED_SYNC_STATUSES = ['failed', 'pull-failed'];
|
||||
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@@ -35,11 +32,6 @@ class TenantLemonSqueezyHealthTable
|
||||
->label(__('admin.tenants.fields.contact_email'))
|
||||
->searchable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_customer_id')
|
||||
->label('Lemon Squeezy customer')
|
||||
->toggleable(isToggledHiddenByDefault: true)
|
||||
->copyable()
|
||||
->formatStateUsing(fn (?string $state) => $state ?: '—'),
|
||||
TextColumn::make('subscription_status')
|
||||
->label('Subscription')
|
||||
->badge()
|
||||
@@ -56,134 +48,77 @@ class TenantLemonSqueezyHealthTable
|
||||
->badge()
|
||||
->color(fn (string $state) => $state === '—' ? 'gray' : 'success')
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_subscription_id')
|
||||
->label('Lemon Squeezy subscription')
|
||||
->toggleable(isToggledHiddenByDefault: true)
|
||||
->copyable()
|
||||
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->lemonsqueezy_subscription_id)
|
||||
->formatStateUsing(fn (?string $state) => $state ?: '—'),
|
||||
IconColumn::make('missing_lemonsqueezy_subscription')
|
||||
->label('Missing Lemon Squeezy subscription')
|
||||
->boolean()
|
||||
->getStateUsing(fn (Tenant $record) => self::missingLemonSqueezySubscription($record)),
|
||||
IconColumn::make('status_mismatch')
|
||||
->label('Status mismatch')
|
||||
->boolean()
|
||||
->getStateUsing(fn (Tenant $record) => self::hasStatusMismatch($record)),
|
||||
TextColumn::make('lemonsqueezy_customer_duplicates')
|
||||
->label('Lemon Squeezy duplicates')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true)
|
||||
->formatStateUsing(fn (?int $state) => $state && $state > 1 ? (string) $state : '—'),
|
||||
TextColumn::make('lemonsqueezy_sync_status')
|
||||
->label('Lemon Squeezy sync')
|
||||
->badge()
|
||||
->color(fn (?string $state) => match ($state) {
|
||||
'synced' => 'success',
|
||||
'syncing' => 'warning',
|
||||
'pulled' => 'info',
|
||||
'dry-run' => 'gray',
|
||||
'failed', 'pull-failed' => 'danger',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (?string $state) => $state ? Str::headline($state) : '—')
|
||||
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->package?->lemonsqueezy_sync_status)
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_synced_at')
|
||||
->label('Lemon Squeezy synced')
|
||||
->badge()
|
||||
->color(fn ($state) => self::syncAgeColor($state))
|
||||
->formatStateUsing(fn ($state) => $state?->diffForHumans() ?? '—')
|
||||
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->package?->lemonsqueezy_synced_at),
|
||||
TextColumn::make('last_lemonsqueezy_transaction_at')
|
||||
->label('Last Lemon Squeezy tx')
|
||||
TextColumn::make('last_checkout_transaction_at')
|
||||
->label('Last transaction')
|
||||
->badge()
|
||||
->color(fn (?Carbon $state) => self::transactionAgeColor($state))
|
||||
->getStateUsing(fn (Tenant $record) => $record->last_lemonsqueezy_transaction_at
|
||||
? Carbon::parse($record->last_lemonsqueezy_transaction_at)
|
||||
->getStateUsing(fn (Tenant $record) => $record->last_checkout_transaction_at
|
||||
? Carbon::parse($record->last_checkout_transaction_at)
|
||||
: null)
|
||||
->formatStateUsing(fn (?Carbon $state) => $state?->diffForHumans() ?? '—')
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_transaction_count_window')
|
||||
->label('Lemon Squeezy tx (30d)')
|
||||
TextColumn::make('checkout_transaction_count_window')
|
||||
->label('Transactions (30d)')
|
||||
->default('0')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
TextColumn::make('lemonsqueezy_transaction_total_window')
|
||||
->label('Lemon Squeezy total (30d)')
|
||||
TextColumn::make('checkout_transaction_total_window')
|
||||
->label('Total (30d)')
|
||||
->default(0)
|
||||
->money('EUR')
|
||||
->sortable()
|
||||
->toggleable(),
|
||||
TextColumn::make('lemonsqueezy_refund_count_window')
|
||||
TextColumn::make('checkout_refund_count_window')
|
||||
->label('Refunds (30d)')
|
||||
->badge()
|
||||
->color(fn (?int $state) => $state && $state > 0 ? 'danger' : 'gray')
|
||||
->default('0')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_refund_total_window')
|
||||
TextColumn::make('checkout_refund_total_window')
|
||||
->label('Refund total (30d)')
|
||||
->default(0)
|
||||
->money('EUR')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_checkout_requires_action_count')
|
||||
TextColumn::make('checkout_requires_action_count')
|
||||
->label('Checkout action required')
|
||||
->badge()
|
||||
->color(fn (?int $state) => $state && $state > 0 ? 'warning' : 'gray')
|
||||
->default('0')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_checkout_processing_count')
|
||||
TextColumn::make('checkout_processing_count')
|
||||
->label('Checkout processing')
|
||||
->badge()
|
||||
->color(fn (?int $state) => $state && $state > 0 ? 'warning' : 'gray')
|
||||
->default('0')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_checkout_expired_count')
|
||||
TextColumn::make('checkout_expired_count')
|
||||
->label('Checkout expired')
|
||||
->badge()
|
||||
->color(fn (?int $state) => $state && $state > 0 ? 'danger' : 'gray')
|
||||
->default('0')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_transaction_count')
|
||||
->label('Lemon Squeezy tx (all)')
|
||||
TextColumn::make('checkout_transaction_count')
|
||||
->label('Transactions (all)')
|
||||
->default('0')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('lemonsqueezy_transaction_total')
|
||||
->label('Lemon Squeezy total (all)')
|
||||
TextColumn::make('checkout_transaction_total')
|
||||
->label('Total (all)')
|
||||
->default(0)
|
||||
->money('EUR')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
Filter::make('missing_lemonsqueezy_customer')
|
||||
->label('Missing Lemon Squeezy customer')
|
||||
->indicator('Missing Lemon Squeezy customer')
|
||||
->query(fn (Builder $query) => $query->whereNull('lemonsqueezy_customer_id')),
|
||||
Filter::make('missing_lemonsqueezy_subscription')
|
||||
->label('Missing Lemon Squeezy subscription')
|
||||
->indicator('Missing Lemon Squeezy subscription')
|
||||
->query(fn (Builder $query) => $query->whereHas('activeResellerPackage', fn (Builder $query) => $query
|
||||
->where('active', true)
|
||||
->whereNull('lemonsqueezy_subscription_id'))),
|
||||
Filter::make('duplicate_lemonsqueezy_customer')
|
||||
->label('Duplicate Lemon Squeezy customer')
|
||||
->indicator('Duplicate Lemon Squeezy customer')
|
||||
->query(fn (Builder $query) => $query
|
||||
->whereNotNull('lemonsqueezy_customer_id')
|
||||
->whereIn('lemonsqueezy_customer_id', function ($subquery) {
|
||||
$subquery->select('lemonsqueezy_customer_id')
|
||||
->from('tenants')
|
||||
->whereNotNull('lemonsqueezy_customer_id')
|
||||
->groupBy('lemonsqueezy_customer_id')
|
||||
->havingRaw('count(*) > 1');
|
||||
})),
|
||||
Filter::make('status_mismatch')
|
||||
->label('Status mismatch')
|
||||
->indicator('Status mismatch')
|
||||
@@ -205,39 +140,24 @@ class TenantLemonSqueezyHealthTable
|
||||
->where('is_suspended', false)
|
||||
->whereNull('pending_deletion_at')
|
||||
->whereNull('anonymized_at')),
|
||||
Filter::make('lemonsqueezy_sync_failed')
|
||||
->label('Lemon Squeezy sync failed')
|
||||
->indicator('Lemon Squeezy sync failed')
|
||||
->query(fn (Builder $query) => $query->whereHas('activeResellerPackage.package', fn (Builder $query) => $query
|
||||
->whereIn('lemonsqueezy_sync_status', self::FAILED_SYNC_STATUSES))),
|
||||
Filter::make('lemonsqueezy_sync_stale')
|
||||
->label('Lemon Squeezy sync stale')
|
||||
->indicator('Lemon Squeezy sync stale')
|
||||
->query(fn (Builder $query) => $query->whereHas('activeResellerPackage.package', fn (Builder $query) => $query
|
||||
->whereNotNull('lemonsqueezy_synced_at')
|
||||
->where('lemonsqueezy_synced_at', '<', now()->subDays(TenantLemonSqueezyHealthResource::STALE_SYNC_DAYS)))),
|
||||
Filter::make('lemonsqueezy_sync_missing')
|
||||
->label('Missing Lemon Squeezy sync timestamp')
|
||||
->indicator('Missing Lemon Squeezy sync timestamp')
|
||||
->query(fn (Builder $query) => $query->whereHas('activeResellerPackage.package', fn (Builder $query) => $query
|
||||
->whereNull('lemonsqueezy_synced_at'))),
|
||||
Filter::make('lemonsqueezy_transaction_stale')
|
||||
->label('Stale Lemon Squeezy transactions')
|
||||
->indicator('Stale Lemon Squeezy transactions')
|
||||
Filter::make('checkout_transaction_stale')
|
||||
->label('Stale transactions')
|
||||
->indicator('Stale transactions')
|
||||
->query(function (Builder $query): Builder {
|
||||
$cutoff = now()->subDays(TenantLemonSqueezyHealthResource::TRANSACTION_WINDOW_DAYS);
|
||||
$cutoff = now()->subDays(TenantCheckoutHealthResource::TRANSACTION_WINDOW_DAYS);
|
||||
$provider = TenantCheckoutHealthResource::provider();
|
||||
|
||||
return $query
|
||||
->whereHas('purchases', fn (Builder $query) => $query->where('provider', 'lemonsqueezy'))
|
||||
->whereHas('purchases', fn (Builder $query) => $query->where('provider', $provider))
|
||||
->whereDoesntHave('purchases', fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('provider', $provider)
|
||||
->where('purchased_at', '>=', $cutoff));
|
||||
}),
|
||||
Filter::make('checkout_attention')
|
||||
->label('Checkout attention')
|
||||
->indicator('Checkout attention')
|
||||
->query(fn (Builder $query) => $query->whereHas('checkoutSessions', function (Builder $query) {
|
||||
$query->where('provider', 'lemonsqueezy')
|
||||
$query->where('provider', TenantCheckoutHealthResource::provider())
|
||||
->where(function (Builder $query) {
|
||||
$query->whereIn('status', [
|
||||
CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION,
|
||||
@@ -274,10 +194,11 @@ class TenantLemonSqueezyHealthTable
|
||||
return $query;
|
||||
}
|
||||
|
||||
$cutoff = now()->subDays(TenantLemonSqueezyHealthResource::TRANSACTION_WINDOW_DAYS);
|
||||
$cutoff = now()->subDays(TenantCheckoutHealthResource::TRANSACTION_WINDOW_DAYS);
|
||||
$provider = TenantCheckoutHealthResource::provider();
|
||||
|
||||
return $query->whereHas('purchases', fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('provider', $provider)
|
||||
->where('refunded', true)
|
||||
->where('purchased_at', '>=', $cutoff), '>=', $min);
|
||||
}),
|
||||
@@ -314,13 +235,6 @@ class TenantLemonSqueezyHealthTable
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function missingLemonSqueezySubscription(Tenant $record): bool
|
||||
{
|
||||
$package = $record->activeResellerPackage;
|
||||
|
||||
return $package && $package->active && ! $package->lemonsqueezy_subscription_id;
|
||||
}
|
||||
|
||||
private static function applyStatusMismatchFilter(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) {
|
||||
@@ -338,26 +252,13 @@ class TenantLemonSqueezyHealthTable
|
||||
});
|
||||
}
|
||||
|
||||
private static function syncAgeColor($state): string
|
||||
{
|
||||
if (! $state) {
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
if ($state->lt(now()->subDays(TenantLemonSqueezyHealthResource::STALE_SYNC_DAYS))) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private static function transactionAgeColor(?Carbon $state): string
|
||||
{
|
||||
if (! $state) {
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
if ($state->lt(now()->subDays(TenantLemonSqueezyHealthResource::TRANSACTION_WINDOW_DAYS))) {
|
||||
if ($state->lt(now()->subDays(TenantCheckoutHealthResource::TRANSACTION_WINDOW_DAYS))) {
|
||||
return 'danger';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\DailyOpsCluster;
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\Pages\ListTenantCheckoutHealths;
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\Tables\TenantCheckoutHealthTable;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Tenant;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use UnitEnum;
|
||||
|
||||
class TenantCheckoutHealthResource extends Resource
|
||||
{
|
||||
public const TRANSACTION_WINDOW_DAYS = 30;
|
||||
|
||||
public const DEFAULT_PROVIDER = CheckoutSession::PROVIDER_PAYPAL;
|
||||
|
||||
protected static ?string $model = Tenant::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-credit-card';
|
||||
|
||||
protected static ?string $cluster = DailyOpsCluster::class;
|
||||
|
||||
protected static ?string $slug = 'checkout-health';
|
||||
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return TenantCheckoutHealthTable::configure($table);
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('admin.checkout_health.navigation.label');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.billing');
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$provider = static::provider();
|
||||
$windowStart = now()->subDays(self::TRANSACTION_WINDOW_DAYS);
|
||||
|
||||
return parent::getEloquentQuery()
|
||||
->with(['activeResellerPackage.package'])
|
||||
->withExists('activeResellerPackage as has_active_reseller_package')
|
||||
->withCount([
|
||||
'purchases as checkout_transaction_count' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('refunded', false),
|
||||
'purchases as checkout_transaction_count_window' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('refunded', false)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
'purchases as checkout_refund_count_window' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('refunded', true)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
'checkoutSessions as checkout_requires_action_count' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('status', CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION),
|
||||
'checkoutSessions as checkout_processing_count' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('status', CheckoutSession::STATUS_PROCESSING),
|
||||
'checkoutSessions as checkout_expired_count' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->whereNotIn('status', [
|
||||
CheckoutSession::STATUS_COMPLETED,
|
||||
CheckoutSession::STATUS_CANCELLED,
|
||||
])
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', now()),
|
||||
])
|
||||
->withSum([
|
||||
'purchases as checkout_transaction_total' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('refunded', false),
|
||||
], 'price')
|
||||
->withSum([
|
||||
'purchases as checkout_transaction_total_window' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('refunded', false)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
], 'price')
|
||||
->withSum([
|
||||
'purchases as checkout_refund_total_window' => fn (Builder $query) => $query
|
||||
->where('provider', $provider)
|
||||
->where('refunded', true)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
], 'price')
|
||||
->withMax([
|
||||
'purchases as last_checkout_transaction_at' => fn (Builder $query) => $query
|
||||
->where('provider', $provider),
|
||||
], 'purchased_at');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListTenantCheckoutHealths::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function provider(): string
|
||||
{
|
||||
return (string) config('checkout.default_provider', self::DEFAULT_PROVIDER);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\Pages;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListTenantLemonSqueezyHealths extends ListRecords
|
||||
{
|
||||
protected static string $resource = TenantLemonSqueezyHealthResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\DailyOpsCluster;
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\Pages\ListTenantLemonSqueezyHealths;
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\Tables\TenantLemonSqueezyHealthTable;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Tenant;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use UnitEnum;
|
||||
|
||||
class TenantLemonSqueezyHealthResource extends Resource
|
||||
{
|
||||
public const STALE_SYNC_DAYS = 30;
|
||||
|
||||
public const TRANSACTION_WINDOW_DAYS = 30;
|
||||
|
||||
protected static ?string $model = Tenant::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-credit-card';
|
||||
|
||||
protected static ?string $cluster = DailyOpsCluster::class;
|
||||
|
||||
protected static ?string $slug = 'lemonsqueezy-health';
|
||||
|
||||
protected static ?int $navigationSort = 20;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return TenantLemonSqueezyHealthTable::configure($table);
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('admin.lemonsqueezy_health.navigation.label');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.billing');
|
||||
}
|
||||
|
||||
public static function getEloquentQuery(): Builder
|
||||
{
|
||||
$windowStart = now()->subDays(self::TRANSACTION_WINDOW_DAYS);
|
||||
|
||||
return parent::getEloquentQuery()
|
||||
->with(['activeResellerPackage.package'])
|
||||
->withExists('activeResellerPackage as has_active_reseller_package')
|
||||
->addSelect([
|
||||
'lemonsqueezy_customer_duplicates' => Tenant::query()
|
||||
->selectRaw('count(*)')
|
||||
->whereColumn('lemonsqueezy_customer_id', 'tenants.lemonsqueezy_customer_id')
|
||||
->whereNotNull('lemonsqueezy_customer_id'),
|
||||
])
|
||||
->withCount([
|
||||
'purchases as lemonsqueezy_transaction_count' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('refunded', false),
|
||||
'purchases as lemonsqueezy_transaction_count_window' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('refunded', false)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
'purchases as lemonsqueezy_refund_count_window' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('refunded', true)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
'checkoutSessions as lemonsqueezy_checkout_requires_action_count' => fn (Builder $query) => $query
|
||||
->where('provider', CheckoutSession::PROVIDER_LEMONSQUEEZY)
|
||||
->where('status', CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION),
|
||||
'checkoutSessions as lemonsqueezy_checkout_processing_count' => fn (Builder $query) => $query
|
||||
->where('provider', CheckoutSession::PROVIDER_LEMONSQUEEZY)
|
||||
->where('status', CheckoutSession::STATUS_PROCESSING),
|
||||
'checkoutSessions as lemonsqueezy_checkout_expired_count' => fn (Builder $query) => $query
|
||||
->where('provider', CheckoutSession::PROVIDER_LEMONSQUEEZY)
|
||||
->whereNotIn('status', [
|
||||
CheckoutSession::STATUS_COMPLETED,
|
||||
CheckoutSession::STATUS_CANCELLED,
|
||||
])
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', now()),
|
||||
])
|
||||
->withSum([
|
||||
'purchases as lemonsqueezy_transaction_total' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('refunded', false),
|
||||
], 'price')
|
||||
->withSum([
|
||||
'purchases as lemonsqueezy_transaction_total_window' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('refunded', false)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
], 'price')
|
||||
->withSum([
|
||||
'purchases as lemonsqueezy_refund_total_window' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy')
|
||||
->where('refunded', true)
|
||||
->where('purchased_at', '>=', $windowStart),
|
||||
], 'price')
|
||||
->withMax([
|
||||
'purchases as last_lemonsqueezy_transaction_at' => fn (Builder $query) => $query
|
||||
->where('provider', 'lemonsqueezy'),
|
||||
], 'purchased_at');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListTenantLemonSqueezyHealths::route('/'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -186,9 +186,9 @@ return [
|
||||
'deleted' => 'Gelöscht',
|
||||
],
|
||||
],
|
||||
'lemonsqueezy_health' => [
|
||||
'checkout_health' => [
|
||||
'navigation' => [
|
||||
'label' => 'PayPal-Status',
|
||||
'label' => 'Checkout-Status',
|
||||
],
|
||||
],
|
||||
'integrations_health' => [
|
||||
|
||||
@@ -186,9 +186,9 @@ return [
|
||||
'deleted' => 'Deleted',
|
||||
],
|
||||
],
|
||||
'lemonsqueezy_health' => [
|
||||
'checkout_health' => [
|
||||
'navigation' => [
|
||||
'label' => 'PayPal health',
|
||||
'label' => 'Checkout health',
|
||||
],
|
||||
],
|
||||
'integrations_health' => [
|
||||
|
||||
@@ -33,7 +33,7 @@ class SuperAdminNavigationGroupsTest extends TestCase
|
||||
\App\Filament\Resources\PackageResource::class => 'admin.nav.commercial',
|
||||
\App\Filament\Resources\PhotoboothSettings\PhotoboothSettingResource::class => 'admin.nav.storage',
|
||||
\App\Filament\Resources\PurchaseResource::class => 'admin.nav.billing',
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource::class => 'admin.nav.billing',
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\TenantCheckoutHealthResource::class => 'admin.nav.billing',
|
||||
\App\Filament\Resources\PurchaseHistoryResource::class => 'admin.nav.commercial',
|
||||
\App\Filament\Resources\EventPurchaseResource::class => 'admin.nav.commercial',
|
||||
\App\Filament\Resources\TenantPackageResource::class => 'admin.nav.commercial',
|
||||
@@ -57,7 +57,7 @@ class SuperAdminNavigationGroupsTest extends TestCase
|
||||
\App\Filament\Resources\PhotoResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\TenantResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\PurchaseResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\TenantCheckoutHealthResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\TenantFeedbackResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\SuperAdmin\Pages\OpsHealthDashboard::class => DailyOpsCluster::class,
|
||||
\App\Filament\SuperAdmin\Pages\IntegrationsHealthDashboard::class => DailyOpsCluster::class,
|
||||
|
||||
91
tests/Unit/TenantCheckoutHealthResourceTest.php
Normal file
91
tests/Unit/TenantCheckoutHealthResourceTest.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\Resources\TenantCheckoutHealths\TenantCheckoutHealthResource;
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Package;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantCheckoutHealthResourceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_counts_checkout_metrics_for_default_provider(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->endcustomer()->paid()->create();
|
||||
|
||||
PackagePurchase::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider' => CheckoutSession::PROVIDER_PAYPAL,
|
||||
'price' => 100.00,
|
||||
'refunded' => false,
|
||||
'purchased_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
PackagePurchase::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider' => CheckoutSession::PROVIDER_LEMONSQUEEZY,
|
||||
'price' => 50.00,
|
||||
'refunded' => false,
|
||||
'purchased_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
CheckoutSession::query()->create([
|
||||
'id' => (string) Str::uuid(),
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'package_snapshot' => [
|
||||
'id' => $package->id,
|
||||
'name' => $package->name,
|
||||
'price' => $package->price,
|
||||
'currency' => $package->currency ?? 'EUR',
|
||||
],
|
||||
'status' => CheckoutSession::STATUS_PROCESSING,
|
||||
'provider' => CheckoutSession::PROVIDER_PAYPAL,
|
||||
'status_history' => [],
|
||||
'currency' => $package->currency ?? 'EUR',
|
||||
'amount_subtotal' => 100.00,
|
||||
'amount_total' => 100.00,
|
||||
'amount_discount' => 0.00,
|
||||
]);
|
||||
|
||||
CheckoutSession::query()->create([
|
||||
'id' => (string) Str::uuid(),
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'package_snapshot' => [
|
||||
'id' => $package->id,
|
||||
'name' => $package->name,
|
||||
'price' => $package->price,
|
||||
'currency' => $package->currency ?? 'EUR',
|
||||
],
|
||||
'status' => CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION,
|
||||
'provider' => CheckoutSession::PROVIDER_LEMONSQUEEZY,
|
||||
'status_history' => [],
|
||||
'currency' => $package->currency ?? 'EUR',
|
||||
'amount_subtotal' => 50.00,
|
||||
'amount_total' => 50.00,
|
||||
'amount_discount' => 0.00,
|
||||
]);
|
||||
|
||||
$tenantWithCounts = TenantCheckoutHealthResource::getEloquentQuery()
|
||||
->whereKey($tenant->id)
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertSame(1, (int) $tenantWithCounts->checkout_transaction_count);
|
||||
$this->assertSame(1, (int) $tenantWithCounts->checkout_transaction_count_window);
|
||||
$this->assertSame(0, (int) $tenantWithCounts->checkout_refund_count_window);
|
||||
$this->assertSame(100.00, (float) $tenantWithCounts->checkout_transaction_total);
|
||||
$this->assertSame(100.00, (float) $tenantWithCounts->checkout_transaction_total_window);
|
||||
$this->assertSame(1, (int) $tenantWithCounts->checkout_processing_count);
|
||||
$this->assertSame(0, (int) $tenantWithCounts->checkout_requires_action_count);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user