columns([ TextColumn::make('position') ->label('Order') ->sortable(), TextColumn::make('name') ->label('Name') ->searchable(), TextColumn::make('type') ->label('Type'), TextColumn::make('driver') ->label('Driver') ->formatStateUsing(fn (?string $state) => static::driverLabel($state)), IconColumn::make('driver_missing_at') ->label('Driver status') ->boolean() ->trueIcon('heroicon-o-exclamation-triangle') ->falseIcon('heroicon-o-check-circle') ->trueColor('danger') ->falseColor('success') ->tooltip(fn (PaymentMethod $record) => $record->driver_missing_at ? 'Driver not found as of '.$record->driver_missing_at->diffForHumans() : 'Driver resolves correctly'), ToggleColumn::make('enabled') ->label('Enabled') ->updateStateUsing(fn (PaymentMethod $record, $state) => app(PaymentMethodService::class) ->update($record, ['enabled' => $state])), TextColumn::make('data.fee') ->label('Fee') ->formatStateUsing(fn (?int $state) => $state ? number_format($state / 100, 2) : '—'), TextColumn::make('updated_at') ->label('Last updated') ->dateTime(), ]) ->reorderable('position') ->afterReordering(function (array $order) { app(PaymentMethodCache::class)->forget(); Event::dispatch(new PaymentMethodsReordered(array_map('intval', array_values($order)))); }) ->recordActions([ static::editAction(), static::editFeeAction(), static::deleteAction(), ]) ->defaultSort('position'); } /** * @return array */ public static function getFormComponents(): array { return [ TextInput::make('name') ->label('Name') ->required() ->maxLength(255), TextInput::make('type') ->label('Type') ->helperText('Machine-facing slug — stored on the cart/order, used by other code to identify this method. Cannot be changed once orders reference it.') ->required() ->unique(ignoreRecord: true) ->maxLength(255), static::getDriverFormComponent(), Select::make('capture_mode') ->label('Capture mode') ->helperText('Whether checkout charges immediately, or places a hold to settle later.') ->options([ 'pay' => 'Charge immediately', 'authorize' => 'Hold now, charge later', ]) ->default('pay') ->live() ->required(), static::getOrderStatusSelect('captured_status', 'Order status once paid') ->helperText('Applied the moment a payment is fully charged.'), static::getOrderStatusSelect('authorized_status', 'Order status once held') ->helperText('Applied the moment a hold is placed, before it\'s charged.') ->visible(fn (Get $get) => $get('capture_mode') === 'authorize'), static::getOrderStatusSelect('refunded_status', 'Order status once refunded') ->helperText('Applied when a payment taken through this method is refunded — even if the refund itself is processed through a different method.'), ]; } public static function getDriverFormComponent(): Component { return Select::make('driver') ->label('Driver') ->options(fn () => app(PaymentDriverRegistry::class)->labels()) ->required(); } /** * Lunar's own Order::status is a plain, admin-extensible string * (config('lunar.orders.statuses')) rather than a fixed enum — * deliberately so a store can add its own custom status without a * code change (see docs/payments.md). This Select still reads from * that same open-ended list, just so an admin picks a real status * instead of typing a slug from memory. */ private static function getOrderStatusSelect(string $name, string $label): Select { return Select::make($name) ->label($label) ->options(collect(config('lunar.orders.statuses', [])) ->map(fn (array $status) => $status['label'] ?? $status) ->all()) ->native(false); } public static function getPages(): array { return [ 'index' => ListPaymentMethods::route('/'), ]; } public static function canCreate(): bool { return true; } public static function canDelete($record = null): bool { return true; } private static function editAction(): Action { return Action::make('edit') ->label('Edit') ->icon('heroicon-o-pencil-square') ->schema(static::getFormComponents()) ->fillForm(fn (PaymentMethod $record) => $record->only([ 'name', 'type', 'driver', 'capture_mode', 'captured_status', 'authorized_status', 'refunded_status', ])) ->action(fn (PaymentMethod $record, array $data) => app(PaymentMethodService::class)->update($record, $data)); } /** * $data['fee'] is stored as an integer minor unit (cents), matching * Lunar's own Price convention everywhere else in this codebase — the * form collects/displays a decimal and converts at the boundary. */ private static function editFeeAction(): Action { return Action::make('edit_fee') ->label('Edit fee') ->icon('heroicon-o-pencil') ->schema([ TextInput::make('fee') ->label('Fee') ->numeric() ->minValue(0) ->step(0.01) ->helperText('Flat surcharge added when this payment method is selected.'), ]) ->fillForm(fn (PaymentMethod $record) => [ 'fee' => filled($record->data['fee'] ?? null) ? $record->data['fee'] / 100 : null, ]) ->action(function (PaymentMethod $record, array $data) { app(PaymentMethodService::class)->update($record, [ 'data' => [ ...$record->data->toArray(), 'fee' => filled($data['fee']) ? (int) round($data['fee'] * 100) : null, ], ]); }); } private static function deleteAction(): Action { return Action::make('delete') ->label('Delete') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->action(fn (PaymentMethod $record) => app(PaymentMethodService::class)->delete($record)); } private static function driverLabel(?string $key): string { if ($key === null) { return '—'; } return app(PaymentDriverRegistry::class)->label($key) ?? $key; } }