Migrate billing from Paddle to Lemon Squeezy

This commit is contained in:
Codex Agent
2026-02-03 10:59:54 +01:00
parent 2f4ebfefd4
commit a0ef90e13a
228 changed files with 4369 additions and 4067 deletions

View File

@@ -0,0 +1,16 @@
<?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 [];
}
}

View File

@@ -1,8 +1,8 @@
<?php
namespace App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\Tables;
namespace App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\Tables;
use App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\TenantPaddleHealthResource;
use App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource;
use App\Models\CheckoutSession;
use App\Models\Tenant;
use Filament\Forms\Components\TextInput;
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
class TenantPaddleHealthTable
class TenantLemonSqueezyHealthTable
{
private const FAILED_SYNC_STATUSES = ['failed', 'pull-failed'];
@@ -35,8 +35,8 @@ class TenantPaddleHealthTable
->label(__('admin.tenants.fields.contact_email'))
->searchable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_customer_id')
->label('Paddle customer')
TextColumn::make('lemonsqueezy_customer_id')
->label('Lemon Squeezy customer')
->toggleable(isToggledHiddenByDefault: true)
->copyable()
->formatStateUsing(fn (?string $state) => $state ?: '—'),
@@ -56,27 +56,27 @@ class TenantPaddleHealthTable
->badge()
->color(fn (string $state) => $state === '—' ? 'gray' : 'success')
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_subscription_id')
->label('Paddle subscription')
TextColumn::make('lemonsqueezy_subscription_id')
->label('Lemon Squeezy subscription')
->toggleable(isToggledHiddenByDefault: true)
->copyable()
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->paddle_subscription_id)
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->lemonsqueezy_subscription_id)
->formatStateUsing(fn (?string $state) => $state ?: '—'),
IconColumn::make('missing_paddle_subscription')
->label('Missing Paddle subscription')
IconColumn::make('missing_lemonsqueezy_subscription')
->label('Missing Lemon Squeezy subscription')
->boolean()
->getStateUsing(fn (Tenant $record) => self::missingPaddleSubscription($record)),
->getStateUsing(fn (Tenant $record) => self::missingLemonSqueezySubscription($record)),
IconColumn::make('status_mismatch')
->label('Status mismatch')
->boolean()
->getStateUsing(fn (Tenant $record) => self::hasStatusMismatch($record)),
TextColumn::make('paddle_customer_duplicates')
->label('Paddle duplicates')
TextColumn::make('lemonsqueezy_customer_duplicates')
->label('Lemon Squeezy duplicates')
->sortable()
->toggleable(isToggledHiddenByDefault: true)
->formatStateUsing(fn (?int $state) => $state && $state > 1 ? (string) $state : '—'),
TextColumn::make('paddle_sync_status')
->label('Paddle sync')
TextColumn::make('lemonsqueezy_sync_status')
->label('Lemon Squeezy sync')
->badge()
->color(fn (?string $state) => match ($state) {
'synced' => 'success',
@@ -87,101 +87,101 @@ class TenantPaddleHealthTable
default => 'gray',
})
->formatStateUsing(fn (?string $state) => $state ? Str::headline($state) : '—')
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->package?->paddle_sync_status)
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->package?->lemonsqueezy_sync_status)
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_synced_at')
->label('Paddle synced')
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?->paddle_synced_at),
TextColumn::make('last_paddle_transaction_at')
->label('Last Paddle tx')
->getStateUsing(fn (Tenant $record) => $record->activeResellerPackage?->package?->lemonsqueezy_synced_at),
TextColumn::make('last_lemonsqueezy_transaction_at')
->label('Last Lemon Squeezy tx')
->badge()
->color(fn (?Carbon $state) => self::transactionAgeColor($state))
->getStateUsing(fn (Tenant $record) => $record->last_paddle_transaction_at
? Carbon::parse($record->last_paddle_transaction_at)
->getStateUsing(fn (Tenant $record) => $record->last_lemonsqueezy_transaction_at
? Carbon::parse($record->last_lemonsqueezy_transaction_at)
: null)
->formatStateUsing(fn (?Carbon $state) => $state?->diffForHumans() ?? '—')
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_transaction_count_window')
->label('Paddle tx (30d)')
TextColumn::make('lemonsqueezy_transaction_count_window')
->label('Lemon Squeezy tx (30d)')
->default('0')
->sortable()
->toggleable(),
TextColumn::make('paddle_transaction_total_window')
->label('Paddle total (30d)')
TextColumn::make('lemonsqueezy_transaction_total_window')
->label('Lemon Squeezy total (30d)')
->default(0)
->money('EUR')
->sortable()
->toggleable(),
TextColumn::make('paddle_refund_count_window')
TextColumn::make('lemonsqueezy_refund_count_window')
->label('Refunds (30d)')
->badge()
->color(fn (?int $state) => $state && $state > 0 ? 'danger' : 'gray')
->default('0')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_refund_total_window')
TextColumn::make('lemonsqueezy_refund_total_window')
->label('Refund total (30d)')
->default(0)
->money('EUR')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_checkout_requires_action_count')
TextColumn::make('lemonsqueezy_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('paddle_checkout_processing_count')
TextColumn::make('lemonsqueezy_checkout_processing_count')
->label('Checkout processing')
->badge()
->color(fn (?int $state) => $state && $state > 0 ? 'warning' : 'gray')
->default('0')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_checkout_expired_count')
TextColumn::make('lemonsqueezy_checkout_expired_count')
->label('Checkout expired')
->badge()
->color(fn (?int $state) => $state && $state > 0 ? 'danger' : 'gray')
->default('0')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_transaction_count')
->label('Paddle tx (all)')
TextColumn::make('lemonsqueezy_transaction_count')
->label('Lemon Squeezy tx (all)')
->default('0')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_transaction_total')
->label('Paddle total (all)')
TextColumn::make('lemonsqueezy_transaction_total')
->label('Lemon Squeezy total (all)')
->default(0)
->money('EUR')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Filter::make('missing_paddle_customer')
->label('Missing Paddle customer')
->indicator('Missing Paddle customer')
->query(fn (Builder $query) => $query->whereNull('paddle_customer_id')),
Filter::make('missing_paddle_subscription')
->label('Missing Paddle subscription')
->indicator('Missing Paddle subscription')
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('paddle_subscription_id'))),
Filter::make('duplicate_paddle_customer')
->label('Duplicate Paddle customer')
->indicator('Duplicate Paddle customer')
->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('paddle_customer_id')
->whereIn('paddle_customer_id', function ($subquery) {
$subquery->select('paddle_customer_id')
->whereNotNull('lemonsqueezy_customer_id')
->whereIn('lemonsqueezy_customer_id', function ($subquery) {
$subquery->select('lemonsqueezy_customer_id')
->from('tenants')
->whereNotNull('paddle_customer_id')
->groupBy('paddle_customer_id')
->whereNotNull('lemonsqueezy_customer_id')
->groupBy('lemonsqueezy_customer_id')
->havingRaw('count(*) > 1');
})),
Filter::make('status_mismatch')
@@ -205,39 +205,39 @@ class TenantPaddleHealthTable
->where('is_suspended', false)
->whereNull('pending_deletion_at')
->whereNull('anonymized_at')),
Filter::make('paddle_sync_failed')
->label('Paddle sync failed')
->indicator('Paddle sync failed')
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('paddle_sync_status', self::FAILED_SYNC_STATUSES))),
Filter::make('paddle_sync_stale')
->label('Paddle sync stale')
->indicator('Paddle sync stale')
->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('paddle_synced_at')
->where('paddle_synced_at', '<', now()->subDays(TenantPaddleHealthResource::STALE_SYNC_DAYS)))),
Filter::make('paddle_sync_missing')
->label('Missing Paddle sync timestamp')
->indicator('Missing Paddle sync timestamp')
->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('paddle_synced_at'))),
Filter::make('paddle_transaction_stale')
->label('Stale Paddle transactions')
->indicator('Stale Paddle transactions')
->whereNull('lemonsqueezy_synced_at'))),
Filter::make('lemonsqueezy_transaction_stale')
->label('Stale Lemon Squeezy transactions')
->indicator('Stale Lemon Squeezy transactions')
->query(function (Builder $query): Builder {
$cutoff = now()->subDays(TenantPaddleHealthResource::TRANSACTION_WINDOW_DAYS);
$cutoff = now()->subDays(TenantLemonSqueezyHealthResource::TRANSACTION_WINDOW_DAYS);
return $query
->whereHas('purchases', fn (Builder $query) => $query->where('provider', 'paddle'))
->whereHas('purchases', fn (Builder $query) => $query->where('provider', 'lemonsqueezy'))
->whereDoesntHave('purchases', fn (Builder $query) => $query
->where('provider', 'paddle')
->where('provider', 'lemonsqueezy')
->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', 'paddle')
$query->where('provider', 'lemonsqueezy')
->where(function (Builder $query) {
$query->whereIn('status', [
CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION,
@@ -274,10 +274,10 @@ class TenantPaddleHealthTable
return $query;
}
$cutoff = now()->subDays(TenantPaddleHealthResource::TRANSACTION_WINDOW_DAYS);
$cutoff = now()->subDays(TenantLemonSqueezyHealthResource::TRANSACTION_WINDOW_DAYS);
return $query->whereHas('purchases', fn (Builder $query) => $query
->where('provider', 'paddle')
->where('provider', 'lemonsqueezy')
->where('refunded', true)
->where('purchased_at', '>=', $cutoff), '>=', $min);
}),
@@ -314,11 +314,11 @@ class TenantPaddleHealthTable
return false;
}
private static function missingPaddleSubscription(Tenant $record): bool
private static function missingLemonSqueezySubscription(Tenant $record): bool
{
$package = $record->activeResellerPackage;
return $package && $package->active && ! $package->paddle_subscription_id;
return $package && $package->active && ! $package->lemonsqueezy_subscription_id;
}
private static function applyStatusMismatchFilter(Builder $query): Builder
@@ -344,7 +344,7 @@ class TenantPaddleHealthTable
return 'gray';
}
if ($state->lt(now()->subDays(TenantPaddleHealthResource::STALE_SYNC_DAYS))) {
if ($state->lt(now()->subDays(TenantLemonSqueezyHealthResource::STALE_SYNC_DAYS))) {
return 'danger';
}
@@ -357,7 +357,7 @@ class TenantPaddleHealthTable
return 'gray';
}
if ($state->lt(now()->subDays(TenantPaddleHealthResource::TRANSACTION_WINDOW_DAYS))) {
if ($state->lt(now()->subDays(TenantLemonSqueezyHealthResource::TRANSACTION_WINDOW_DAYS))) {
return 'danger';
}

View File

@@ -1,10 +1,10 @@
<?php
namespace App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths;
namespace App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths;
use App\Filament\Clusters\DailyOps\DailyOpsCluster;
use App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\Pages\ListTenantPaddleHealths;
use App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\Tables\TenantPaddleHealthTable;
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;
@@ -13,7 +13,7 @@ use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use UnitEnum;
class TenantPaddleHealthResource extends Resource
class TenantLemonSqueezyHealthResource extends Resource
{
public const STALE_SYNC_DAYS = 30;
@@ -25,13 +25,13 @@ class TenantPaddleHealthResource extends Resource
protected static ?string $cluster = DailyOpsCluster::class;
protected static ?string $slug = 'paddle-health';
protected static ?string $slug = 'lemonsqueezy-health';
protected static ?int $navigationSort = 20;
public static function table(Table $table): Table
{
return TenantPaddleHealthTable::configure($table);
return TenantLemonSqueezyHealthTable::configure($table);
}
public static function canCreate(): bool
@@ -41,7 +41,7 @@ class TenantPaddleHealthResource extends Resource
public static function getNavigationLabel(): string
{
return __('admin.paddle_health.navigation.label');
return __('admin.lemonsqueezy_health.navigation.label');
}
public static function getNavigationGroup(): UnitEnum|string|null
@@ -57,31 +57,31 @@ class TenantPaddleHealthResource extends Resource
->with(['activeResellerPackage.package'])
->withExists('activeResellerPackage as has_active_reseller_package')
->addSelect([
'paddle_customer_duplicates' => Tenant::query()
'lemonsqueezy_customer_duplicates' => Tenant::query()
->selectRaw('count(*)')
->whereColumn('paddle_customer_id', 'tenants.paddle_customer_id')
->whereNotNull('paddle_customer_id'),
->whereColumn('lemonsqueezy_customer_id', 'tenants.lemonsqueezy_customer_id')
->whereNotNull('lemonsqueezy_customer_id'),
])
->withCount([
'purchases as paddle_transaction_count' => fn (Builder $query) => $query
->where('provider', 'paddle')
'purchases as lemonsqueezy_transaction_count' => fn (Builder $query) => $query
->where('provider', 'lemonsqueezy')
->where('refunded', false),
'purchases as paddle_transaction_count_window' => fn (Builder $query) => $query
->where('provider', 'paddle')
'purchases as lemonsqueezy_transaction_count_window' => fn (Builder $query) => $query
->where('provider', 'lemonsqueezy')
->where('refunded', false)
->where('purchased_at', '>=', $windowStart),
'purchases as paddle_refund_count_window' => fn (Builder $query) => $query
->where('provider', 'paddle')
'purchases as lemonsqueezy_refund_count_window' => fn (Builder $query) => $query
->where('provider', 'lemonsqueezy')
->where('refunded', true)
->where('purchased_at', '>=', $windowStart),
'checkoutSessions as paddle_checkout_requires_action_count' => fn (Builder $query) => $query
->where('provider', CheckoutSession::PROVIDER_PADDLE)
'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 paddle_checkout_processing_count' => fn (Builder $query) => $query
->where('provider', CheckoutSession::PROVIDER_PADDLE)
'checkoutSessions as lemonsqueezy_checkout_processing_count' => fn (Builder $query) => $query
->where('provider', CheckoutSession::PROVIDER_LEMONSQUEEZY)
->where('status', CheckoutSession::STATUS_PROCESSING),
'checkoutSessions as paddle_checkout_expired_count' => fn (Builder $query) => $query
->where('provider', CheckoutSession::PROVIDER_PADDLE)
'checkoutSessions as lemonsqueezy_checkout_expired_count' => fn (Builder $query) => $query
->where('provider', CheckoutSession::PROVIDER_LEMONSQUEEZY)
->whereNotIn('status', [
CheckoutSession::STATUS_COMPLETED,
CheckoutSession::STATUS_CANCELLED,
@@ -90,32 +90,32 @@ class TenantPaddleHealthResource extends Resource
->where('expires_at', '<', now()),
])
->withSum([
'purchases as paddle_transaction_total' => fn (Builder $query) => $query
->where('provider', 'paddle')
'purchases as lemonsqueezy_transaction_total' => fn (Builder $query) => $query
->where('provider', 'lemonsqueezy')
->where('refunded', false),
], 'price')
->withSum([
'purchases as paddle_transaction_total_window' => fn (Builder $query) => $query
->where('provider', 'paddle')
'purchases as lemonsqueezy_transaction_total_window' => fn (Builder $query) => $query
->where('provider', 'lemonsqueezy')
->where('refunded', false)
->where('purchased_at', '>=', $windowStart),
], 'price')
->withSum([
'purchases as paddle_refund_total_window' => fn (Builder $query) => $query
->where('provider', 'paddle')
'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_paddle_transaction_at' => fn (Builder $query) => $query
->where('provider', 'paddle'),
'purchases as last_lemonsqueezy_transaction_at' => fn (Builder $query) => $query
->where('provider', 'lemonsqueezy'),
], 'purchased_at');
}
public static function getPages(): array
{
return [
'index' => ListTenantPaddleHealths::route('/'),
'index' => ListTenantLemonSqueezyHealths::route('/'),
];
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\Pages;
use App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\TenantPaddleHealthResource;
use Filament\Resources\Pages\ListRecords;
class ListTenantPaddleHealths extends ListRecords
{
protected static string $resource = TenantPaddleHealthResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Filament\Resources\Coupons\Pages;
use App\Filament\Resources\Coupons\CouponResource;
use App\Filament\Resources\Pages\AuditedCreateRecord;
use App\Jobs\SyncCouponToPaddle;
use App\Jobs\SyncCouponToLemonSqueezy;
class CreateCoupon extends AuditedCreateRecord
{
@@ -14,6 +14,6 @@ class CreateCoupon extends AuditedCreateRecord
{
parent::afterCreate();
SyncCouponToPaddle::dispatch($this->record);
SyncCouponToLemonSqueezy::dispatch($this->record);
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Filament\Resources\Coupons\Pages;
use App\Filament\Resources\Coupons\CouponResource;
use App\Filament\Resources\Pages\AuditedEditRecord;
use App\Jobs\SyncCouponToPaddle;
use App\Jobs\SyncCouponToLemonSqueezy;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
@@ -27,7 +27,7 @@ class EditCoupon extends AuditedEditRecord
source: static::class
);
SyncCouponToPaddle::dispatch($record, true);
SyncCouponToLemonSqueezy::dispatch($record, true);
}),
ForceDeleteAction::make()
->after(fn ($record) => app(SuperAdminAuditLogger::class)->recordModelMutation(
@@ -48,6 +48,6 @@ class EditCoupon extends AuditedEditRecord
{
parent::afterSave();
SyncCouponToPaddle::dispatch($this->record);
SyncCouponToLemonSqueezy::dispatch($this->record);
}
}

View File

@@ -21,7 +21,7 @@ class RedemptionsRelationManager extends RelationManager
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('paddle_transaction_id')
->recordTitleAttribute('lemonsqueezy_order_id')
->columns([
TextColumn::make('tenant.name')
->label(__('Tenant'))
@@ -65,7 +65,7 @@ class RedemptionsRelationManager extends RelationManager
'failed' => 'danger',
default => 'warning',
}),
TextColumn::make('paddle_transaction_id')
TextColumn::make('lemonsqueezy_order_id')
->label(__('Transaction'))
->copyable()
->toggleable(isToggledHiddenByDefault: true),

View File

@@ -123,22 +123,22 @@ class CouponForm
->nullable()
->columnSpanFull(),
]),
Section::make(__('Paddle sync'))
Section::make(__('Lemon Squeezy sync'))
->columns(2)
->schema([
Select::make('paddle_mode')
->label(__('Paddle mode'))
Select::make('lemonsqueezy_mode')
->label(__('Lemon Squeezy mode'))
->options([
'standard' => __('Standard'),
'custom' => __('Custom (one-off)'),
])
->default('standard'),
Placeholder::make('paddle_discount_id')
->label(__('Paddle Discount ID'))
->content(fn ($record) => $record?->paddle_discount_id ?? '—'),
Placeholder::make('paddle_last_synced_at')
Placeholder::make('lemonsqueezy_discount_id')
->label(__('Lemon Squeezy Discount ID'))
->content(fn ($record) => $record?->lemonsqueezy_discount_id ?? '—'),
Placeholder::make('lemonsqueezy_last_synced_at')
->label(__('Last synced'))
->content(fn ($record) => $record?->paddle_last_synced_at?->diffForHumans() ?? '—'),
->content(fn ($record) => $record?->lemonsqueezy_last_synced_at?->diffForHumans() ?? '—'),
Placeholder::make('redemptions_count')
->label(__('Total redemptions'))
->content(fn ($record) => number_format($record?->redemptions_count ?? 0)),

View File

@@ -63,17 +63,17 @@ class CouponInfolist
TextEntry::make('description')->label(__('Description'))->columnSpanFull(),
KeyValueEntry::make('metadata')->label(__('Metadata'))->columnSpanFull(),
]),
Section::make(__('Paddle'))
Section::make(__('Lemon Squeezy'))
->columns(3)
->schema([
TextEntry::make('paddle_discount_id')
TextEntry::make('lemonsqueezy_discount_id')
->label(__('Discount ID'))
->copyable()
->placeholder('—'),
TextEntry::make('paddle_last_synced_at')
TextEntry::make('lemonsqueezy_last_synced_at')
->label(__('Last synced'))
->state(fn ($record) => $record?->paddle_last_synced_at?->diffForHumans() ?? '—'),
TextEntry::make('paddle_mode')
->state(fn ($record) => $record?->lemonsqueezy_last_synced_at?->diffForHumans() ?? '—'),
TextEntry::make('lemonsqueezy_mode')
->label(__('Mode'))
->badge()
->placeholder('standard'),

View File

@@ -4,7 +4,7 @@ namespace App\Filament\Resources\Coupons\Tables;
use App\Enums\CouponStatus;
use App\Enums\CouponType;
use App\Jobs\SyncCouponToPaddle;
use App\Jobs\SyncCouponToLemonSqueezy;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
@@ -105,9 +105,9 @@ class CouponsTable
static::class
)),
Action::make('sync')
->label(__('Sync to Paddle'))
->label(__('Sync to Lemon Squeezy'))
->icon('heroicon-m-arrow-path')
->action(fn ($record) => SyncCouponToPaddle::dispatch($record))
->action(fn ($record) => SyncCouponToLemonSqueezy::dispatch($record))
->requiresConfirmation(),
])
->toolbarActions([

View File

@@ -63,8 +63,8 @@ class GiftVoucherResource extends Resource
->label('Empfänger')
->toggleable()
->searchable(),
TextColumn::make('paddle_transaction_id')
->label('Paddle Tx')
TextColumn::make('lemonsqueezy_order_id')
->label('Lemon Squeezy Order')
->toggleable()
->copyable()
->wrap(),

View File

@@ -46,24 +46,27 @@ class ListGiftVouchers extends ListRecords
])
->action(function (array $data, GiftVoucherService $service): void {
$payload = [
'id' => null,
'metadata' => [
'type' => 'gift_voucher',
'purchaser_email' => $data['purchaser_email'],
'recipient_email' => $data['recipient_email'] ?? null,
'recipient_name' => $data['recipient_name'] ?? null,
'message' => $data['message'] ?? null,
'gift_code' => $data['code'] ?? null,
'meta' => [
'custom_data' => [
'type' => 'gift_voucher',
'purchaser_email' => $data['purchaser_email'],
'recipient_email' => $data['recipient_email'] ?? null,
'recipient_name' => $data['recipient_name'] ?? null,
'message' => $data['message'] ?? null,
'gift_code' => $data['code'] ?? null,
],
],
'currency_code' => $data['currency'] ?? 'EUR',
'totals' => [
'grand_total' => [
'amount' => (float) $data['amount'],
'data' => [
'id' => 'manual_'.Str::uuid(),
'attributes' => [
'currency' => $data['currency'] ?? 'EUR',
'total' => (float) $data['amount'] * 100,
'user_email' => $data['purchaser_email'],
],
],
];
$voucher = $service->issueFromPaddle($payload);
$voucher = $service->issueFromLemonSqueezy($payload);
app(SuperAdminAuditLogger::class)->recordModelMutation(
'issued',

View File

@@ -4,7 +4,7 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\PackageAddonResource\Pages;
use App\Jobs\SyncPackageAddonToPaddle;
use App\Jobs\SyncPackageAddonToLemonSqueezy;
use App\Models\PackageAddon;
use App\Services\Audit\SuperAdminAuditLogger;
use Filament\Actions;
@@ -50,9 +50,9 @@ class PackageAddonResource extends Resource
->required()
->unique(ignoreRecord: true)
->maxLength(191),
TextInput::make('price_id')
->label('Paddle Preis-ID')
->helperText('Paddle Billing Preis-ID für dieses Add-on')
TextInput::make('variant_id')
->label('Lemon Squeezy Variant-ID')
->helperText('Variant-ID aus Lemon Squeezy für dieses Add-on')
->maxLength(191),
TextInput::make('sort')
->label('Sortierung')
@@ -96,8 +96,8 @@ class PackageAddonResource extends Resource
->label('Schlüssel')
->copyable()
->sortable(),
TextColumn::make('price_id')
->label('Paddle Preis-ID')
TextColumn::make('variant_id')
->label('Lemon Squeezy Variant-ID')
->toggleable()
->copyable(),
TextColumn::make('extra_photos')->label('Fotos +'),
@@ -120,16 +120,16 @@ class PackageAddonResource extends Resource
->label('Aktiv'),
])
->actions([
Actions\Action::make('syncPaddle')
->label('Mit Paddle synchronisieren')
Actions\Action::make('syncLemonSqueezy')
->label('Mit Lemon Squeezy synchronisieren')
->icon('heroicon-o-cloud-arrow-up')
->action(function (PackageAddon $record) {
SyncPackageAddonToPaddle::dispatch($record->id);
SyncPackageAddonToLemonSqueezy::dispatch($record->id);
Notification::make()
->success()
->title('Paddle-Sync gestartet')
->body('Das Add-on wird im Hintergrund mit Paddle abgeglichen.')
->title('Lemon Squeezy-Sync gestartet')
->body('Das Add-on wird im Hintergrund mit Lemon Squeezy abgeglichen.')
->send();
}),
Actions\EditAction::make()

View File

@@ -4,8 +4,8 @@ namespace App\Filament\Resources;
use App\Filament\Clusters\WeeklyOps\WeeklyOpsCluster;
use App\Filament\Resources\PackageResource\Pages;
use App\Jobs\PullPackageFromPaddle;
use App\Jobs\SyncPackageToPaddle;
use App\Jobs\PullPackageFromLemonSqueezy;
use App\Jobs\SyncPackageToLemonSqueezy;
use App\Models\Package;
use App\Services\Audit\SuperAdminAuditLogger;
use BackedEnum;
@@ -172,31 +172,31 @@ class PackageResource extends Resource
->columnSpanFull()
->default([]),
]),
Section::make('Paddle Billing')
Section::make('Lemon Squeezy Billing')
->columns(2)
->schema([
TextInput::make('paddle_product_id')
->label('Paddle Produkt-ID')
TextInput::make('lemonsqueezy_product_id')
->label('Lemon Squeezy Produkt-ID')
->maxLength(191)
->helperText('Produkt aus Paddle Billing. Leer lassen, wenn noch nicht synchronisiert.')
->helperText('Produkt aus Lemon Squeezy. Leer lassen, wenn noch nicht synchronisiert.')
->placeholder('nicht verknüpft'),
TextInput::make('paddle_price_id')
->label('Paddle Preis-ID')
TextInput::make('lemonsqueezy_variant_id')
->label('Lemon Squeezy Variant-ID')
->maxLength(191)
->helperText('Preis-ID aus Paddle Billing, verknüpft mit diesem Paket.')
->helperText('Variant-ID aus Lemon Squeezy, verknüpft mit diesem Paket.')
->placeholder('nicht verknüpft'),
Placeholder::make('paddle_sync_status')
Placeholder::make('lemonsqueezy_sync_status')
->label('Sync-Status')
->content(fn (?Package $record) => $record?->paddle_sync_status ? Str::headline($record->paddle_sync_status) : '')
->content(fn (?Package $record) => $record?->lemonsqueezy_sync_status ? Str::headline($record->lemonsqueezy_sync_status) : '')
->columnSpanFull(),
Placeholder::make('paddle_synced_at')
Placeholder::make('lemonsqueezy_synced_at')
->label('Zuletzt synchronisiert')
->content(fn (?Package $record) => $record?->paddle_synced_at ? $record->paddle_synced_at->diffForHumans() : '')
->content(fn (?Package $record) => $record?->lemonsqueezy_synced_at ? $record->lemonsqueezy_synced_at->diffForHumans() : '')
->columnSpanFull(),
Placeholder::make('paddle_sync_error')
Placeholder::make('lemonsqueezy_sync_error')
->label('Letzter Fehler')
->content(fn (?Package $record) => $record?->paddle_sync_error_message ?? '')
->visible(fn (?Package $record) => filled($record?->paddle_sync_error_message))
->content(fn (?Package $record) => $record?->lemonsqueezy_sync_error_message ?? '')
->visible(fn (?Package $record) => filled($record?->lemonsqueezy_sync_error_message))
->columnSpanFull(),
]),
]);
@@ -263,15 +263,15 @@ class PackageResource extends Resource
->label('Features')
->wrap()
->formatStateUsing(fn ($state) => static::formatFeaturesForDisplay($state)),
TextColumn::make('paddle_product_id')
->label('Paddle Produkt')
TextColumn::make('lemonsqueezy_product_id')
->label('Lemon Squeezy Produkt')
->toggleable(isToggledHiddenByDefault: true)
->formatStateUsing(fn ($state) => $state ?: '-'),
TextColumn::make('paddle_price_id')
->label('Paddle Preis')
TextColumn::make('lemonsqueezy_variant_id')
->label('Lemon Squeezy Variant')
->toggleable(isToggledHiddenByDefault: true)
->formatStateUsing(fn ($state) => $state ?: '-'),
BadgeColumn::make('paddle_sync_status')
BadgeColumn::make('lemonsqueezy_sync_status')
->label('Sync-Status')
->colors([
'success' => 'synced',
@@ -281,13 +281,13 @@ class PackageResource extends Resource
])
->formatStateUsing(fn ($state) => $state ? Str::headline($state) : null)
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_synced_at')
TextColumn::make('lemonsqueezy_synced_at')
->label('Sync am')
->dateTime()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('paddle_sync_error_message')
TextColumn::make('lemonsqueezy_sync_error_message')
->label('Sync-Fehler')
->getStateUsing(fn (Package $record) => $record->paddle_sync_error_message)
->getStateUsing(fn (Package $record) => $record->lemonsqueezy_sync_error_message)
->wrap()
->toggleable(isToggledHiddenByDefault: true),
])
@@ -301,43 +301,43 @@ class PackageResource extends Resource
TrashedFilter::make(),
])
->actions([
Actions\Action::make('syncPaddle')
->label('Mit Paddle abgleichen')
Actions\Action::make('syncLemonSqueezy')
->label('Mit Lemon Squeezy abgleichen')
->icon('heroicon-o-cloud-arrow-up')
->color('success')
->requiresConfirmation()
->disabled(fn (Package $record) => $record->paddle_sync_status === 'syncing')
->disabled(fn (Package $record) => $record->lemonsqueezy_sync_status === 'syncing')
->action(function (Package $record) {
SyncPackageToPaddle::dispatch($record->id);
SyncPackageToLemonSqueezy::dispatch($record->id);
Notification::make()
->success()
->title('Paddle-Sync gestartet')
->body('Das Paket wird im Hintergrund mit Paddle abgeglichen.')
->title('Lemon Squeezy-Sync gestartet')
->body('Das Paket wird im Hintergrund mit Lemon Squeezy abgeglichen.')
->send();
}),
Actions\Action::make('linkPaddle')
->label('Paddle verknüpfen')
Actions\Action::make('linkLemonSqueezy')
->label('Lemon Squeezy verknüpfen')
->icon('heroicon-o-link')
->color('info')
->form([
TextInput::make('paddle_product_id')
->label('Paddle Produkt-ID')
TextInput::make('lemonsqueezy_product_id')
->label('Lemon Squeezy Produkt-ID')
->required()
->maxLength(191),
TextInput::make('paddle_price_id')
->label('Paddle Preis-ID')
TextInput::make('lemonsqueezy_variant_id')
->label('Lemon Squeezy Variant-ID')
->required()
->maxLength(191),
])
->fillForm(fn (Package $record) => [
'paddle_product_id' => $record->paddle_product_id,
'paddle_price_id' => $record->paddle_price_id,
'lemonsqueezy_product_id' => $record->lemonsqueezy_product_id,
'lemonsqueezy_variant_id' => $record->lemonsqueezy_variant_id,
])
->action(function (Package $record, array $data): void {
$record->linkPaddleIds($data['paddle_product_id'], $data['paddle_price_id']);
$record->linkLemonSqueezyIds($data['lemonsqueezy_product_id'], $data['lemonsqueezy_variant_id']);
PullPackageFromPaddle::dispatch($record->id);
PullPackageFromLemonSqueezy::dispatch($record->id);
app(SuperAdminAuditLogger::class)->recordModelMutation(
'linked',
@@ -348,22 +348,22 @@ class PackageResource extends Resource
Notification::make()
->success()
->title('Paddle-Verknüpfung gespeichert')
->title('Lemon Squeezy-Verknüpfung gespeichert')
->body('Die IDs wurden gespeichert und ein Pull wurde angestoßen.')
->send();
}),
Actions\Action::make('pullPaddle')
->label('Status von Paddle holen')
Actions\Action::make('pullLemonSqueezy')
->label('Status von Lemon Squeezy holen')
->icon('heroicon-o-cloud-arrow-down')
->disabled(fn (Package $record) => ! $record->paddle_product_id && ! $record->paddle_price_id)
->disabled(fn (Package $record) => ! $record->lemonsqueezy_product_id && ! $record->lemonsqueezy_variant_id)
->requiresConfirmation()
->action(function (Package $record) {
PullPackageFromPaddle::dispatch($record->id);
PullPackageFromLemonSqueezy::dispatch($record->id);
Notification::make()
->info()
->title('Paddle-Abgleich angefordert')
->body('Der aktuelle Stand aus Paddle wird geladen und hier hinterlegt.')
->title('Lemon Squeezy-Abgleich angefordert')
->body('Der aktuelle Stand aus Lemon Squeezy wird geladen und hier hinterlegt.')
->send();
}),
ViewAction::make(),

View File

@@ -8,7 +8,7 @@ use App\Models\PackagePurchase;
use App\Notifications\Customer\RefundReceipt;
use App\Notifications\Ops\RefundProcessed;
use App\Services\Audit\SuperAdminAuditLogger;
use App\Services\Paddle\PaddleTransactionService;
use App\Services\LemonSqueezy\LemonSqueezyOrderService;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
@@ -203,15 +203,15 @@ class PurchaseResource extends Resource
$refundSuccess = true;
$errorMessage = null;
if ($record->provider === 'paddle' && $record->provider_id) {
if ($record->provider === 'lemonsqueezy' && $record->provider_id) {
try {
/** @var PaddleTransactionService $paddle */
$paddle = App::make(PaddleTransactionService::class);
$paddle->refund($record->provider_id, ['reason' => $reason]);
/** @var LemonSqueezyOrderService $lemonsqueezy */
$lemonsqueezy = App::make(LemonSqueezyOrderService::class);
$lemonsqueezy->refund($record->provider_id, ['reason' => $reason]);
} catch (\Throwable $exception) {
$refundSuccess = false;
$errorMessage = $exception->getMessage();
Log::warning('Paddle refund failed', [
Log::warning('Lemon Squeezy refund failed', [
'purchase_id' => $record->id,
'provider_id' => $record->provider_id,
'error' => $exception->getMessage(),

View File

@@ -35,7 +35,7 @@ class ViewPurchase extends ViewRecord
->visible(fn ($record): bool => ! $record->refunded)
->action(function ($record) {
$record->update(['refunded' => true]);
// TODO: Call Paddle API for actual refund
// TODO: Call Lemon Squeezy API for actual refund
app(SuperAdminAuditLogger::class)->record(
'purchase.refunded',

View File

@@ -73,10 +73,10 @@ class TenantResource extends Resource
->email()
->required()
->maxLength(255),
TextInput::make('paddle_customer_id')
->label('Paddle Customer ID')
TextInput::make('lemonsqueezy_customer_id')
->label('Lemon Squeezy Customer ID')
->maxLength(191)
->helperText('Verknuepfung mit Paddle Billing Kundenkonto.')
->helperText('Verknüpfung mit Lemon Squeezy Kundenkonto.')
->nullable(),
TextInput::make('total_revenue')
->label(__('admin.tenants.fields.total_revenue'))
@@ -135,8 +135,8 @@ class TenantResource extends Resource
->getStateUsing(fn (Tenant $record) => $record->user?->full_name ?? 'Unbekannt'),
Tables\Columns\TextColumn::make('slug')->searchable(),
Tables\Columns\TextColumn::make('contact_email'),
Tables\Columns\TextColumn::make('paddle_customer_id')
->label('Paddle Customer')
Tables\Columns\TextColumn::make('lemonsqueezy_customer_id')
->label('Lemon Squeezy Customer')
->toggleable(isToggledHiddenByDefault: true)
->formatStateUsing(fn ($state) => $state ?: '-'),
Tables\Columns\TextColumn::make('active_reseller_package_id')

View File

@@ -44,7 +44,7 @@ class PackagePurchasesRelationManager extends RelationManager
Select::make('provider')
->label('Anbieter')
->options([
'paddle' => 'Paddle',
'lemonsqueezy' => 'Lemon Squeezy',
'manual' => 'Manuell',
'free' => 'Kostenlos',
])
@@ -89,7 +89,7 @@ class PackagePurchasesRelationManager extends RelationManager
TextColumn::make('provider')
->badge()
->color(fn (string $state): string => match ($state) {
'paddle' => 'success',
'lemonsqueezy' => 'success',
'manual' => 'gray',
'free' => 'success',
default => 'gray',
@@ -116,7 +116,7 @@ class PackagePurchasesRelationManager extends RelationManager
]),
SelectFilter::make('provider')
->options([
'paddle' => 'Paddle',
'lemonsqueezy' => 'Lemon Squeezy',
'manual' => 'Manuell',
'free' => 'Kostenlos',
]),

View File

@@ -40,10 +40,10 @@ class TenantPackagesRelationManager extends RelationManager
DateTimePicker::make('expires_at')
->label('Ablaufdatum')
->required(),
TextInput::make('paddle_subscription_id')
->label('Paddle Subscription ID')
TextInput::make('lemonsqueezy_subscription_id')
->label('Lemon Squeezy Subscription ID')
->maxLength(191)
->helperText('Abonnement-ID aus Paddle Billing.')
->helperText('Abonnement-ID aus Lemon Squeezy.')
->nullable(),
Toggle::make('active')
->label('Aktiv'),
@@ -75,8 +75,8 @@ class TenantPackagesRelationManager extends RelationManager
TextColumn::make('expires_at')
->dateTime()
->sortable(),
TextColumn::make('paddle_subscription_id')
->label('Paddle Subscription')
TextColumn::make('lemonsqueezy_subscription_id')
->label('Lemon Squeezy Subscription')
->toggleable(isToggledHiddenByDefault: true)
->formatStateUsing(fn ($state) => $state ?: '-'),
IconColumn::make('active')

View File

@@ -22,8 +22,8 @@ class TenantInfolist
TextEntry::make('user.full_name')
->label(__('admin.tenants.fields.owner'))
->state(fn (Tenant $record) => $record->user?->full_name ?? '—'),
TextEntry::make('paddle_customer_id')
->label('Paddle Customer ID')
TextEntry::make('lemonsqueezy_customer_id')
->label('Lemon Squeezy Customer ID')
->placeholder('—'),
TextEntry::make('total_revenue')
->label(__('admin.tenants.fields.total_revenue'))