feat(wallets): implement wallet and transaction management with associated models, policies, and resources
This commit is contained in:
17
app/Enums/WalletTransactionType.php
Normal file
17
app/Enums/WalletTransactionType.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum WalletTransactionType: string
|
||||
{
|
||||
case Credit = 'credit';
|
||||
case Debit = 'debit';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Credit => 'واریز',
|
||||
self::Debit => 'برداشت',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions\Pages;
|
||||
|
||||
use App\Filament\Resources\WalletTransactions\WalletTransactionResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListWalletTransactions extends ListRecords
|
||||
{
|
||||
protected static string $resource = WalletTransactionResource::class;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions\Pages;
|
||||
|
||||
use App\Filament\Resources\WalletTransactions\WalletTransactionResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewWalletTransaction extends ViewRecord
|
||||
{
|
||||
protected static string $resource = WalletTransactionResource::class;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions\Tables;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class WalletTransactionsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('id')
|
||||
->label('#')
|
||||
->sortable(),
|
||||
TextColumn::make('wallet.user.name')
|
||||
->label('کاربر')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('wallet.user.mobile')
|
||||
->label('موبایل')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('type')
|
||||
->label('نوع')
|
||||
->badge()
|
||||
->formatStateUsing(fn (WalletTransactionType $state): string => $state->label())
|
||||
->color(fn (WalletTransactionType $state): string => $state === WalletTransactionType::Credit ? 'success' : 'danger'),
|
||||
TextColumn::make('amount')
|
||||
->label('مبلغ')
|
||||
->sortable()
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
TextColumn::make('balance_after')
|
||||
->label('موجودی بعد')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
TextColumn::make('description')
|
||||
->label('توضیحات')
|
||||
->limit(50)
|
||||
->searchable(),
|
||||
TextColumn::make('creator.name')
|
||||
->label('ثبتکننده')
|
||||
->placeholder('سیستم'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->label('نوع')
|
||||
->options([
|
||||
WalletTransactionType::Credit->value => WalletTransactionType::Credit->label(),
|
||||
WalletTransactionType::Debit->value => WalletTransactionType::Debit->label(),
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\WalletTransactions;
|
||||
|
||||
use App\Filament\Resources\WalletTransactions\Pages\ListWalletTransactions;
|
||||
use App\Filament\Resources\WalletTransactions\Pages\ViewWalletTransaction;
|
||||
use App\Filament\Resources\WalletTransactions\Tables\WalletTransactionsTable;
|
||||
use App\Models\WalletTransaction;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
|
||||
class WalletTransactionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = WalletTransaction::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedArrowsRightLeft;
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'مالی';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'id';
|
||||
|
||||
protected static ?string $modelLabel = 'تراکنش';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'تراکنشها';
|
||||
|
||||
protected static ?string $navigationLabel = 'تراکنشها';
|
||||
|
||||
protected static ?int $navigationSort = 11;
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return WalletTransactionsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListWalletTransactions::route('/'),
|
||||
'view' => ViewWalletTransaction::route('/{record}'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canDelete($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Wallets/Pages/EditWallet.php
Normal file
19
app/Filament/Resources/Wallets/Pages/EditWallet.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Pages;
|
||||
|
||||
use App\Filament\Resources\Wallets\WalletResource;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditWallet extends EditRecord
|
||||
{
|
||||
protected static string $resource = WalletResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
11
app/Filament/Resources/Wallets/Pages/ListWallets.php
Normal file
11
app/Filament/Resources/Wallets/Pages/ListWallets.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Pages;
|
||||
|
||||
use App\Filament\Resources\Wallets\WalletResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListWallets extends ListRecords
|
||||
{
|
||||
protected static string $resource = WalletResource::class;
|
||||
}
|
||||
19
app/Filament/Resources/Wallets/Pages/ViewWallet.php
Normal file
19
app/Filament/Resources/Wallets/Pages/ViewWallet.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Pages;
|
||||
|
||||
use App\Filament\Resources\Wallets\WalletResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewWallet extends ViewRecord
|
||||
{
|
||||
protected static string $resource = WalletResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\RelationManagers;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class TransactionsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'transactions';
|
||||
|
||||
protected static ?string $title = 'تراکنشها';
|
||||
|
||||
protected static ?string $modelLabel = 'تراکنش';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'تراکنشها';
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('type')
|
||||
->label('نوع')
|
||||
->badge()
|
||||
->formatStateUsing(fn (WalletTransactionType $state): string => $state->label())
|
||||
->color(fn (WalletTransactionType $state): string => $state === WalletTransactionType::Credit ? 'success' : 'danger'),
|
||||
TextColumn::make('amount')
|
||||
->label('مبلغ')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
TextColumn::make('balance_before')
|
||||
->label('قبل')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state)),
|
||||
TextColumn::make('balance_after')
|
||||
->label('بعد')
|
||||
->formatStateUsing(fn (int $state): string => number_format($state)),
|
||||
TextColumn::make('description')
|
||||
->label('توضیحات')
|
||||
->limit(40),
|
||||
TextColumn::make('creator.name')
|
||||
->label('ثبتکننده')
|
||||
->placeholder('سیستم'),
|
||||
TextColumn::make('created_at')
|
||||
->label('تاریخ')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->filters([
|
||||
SelectFilter::make('type')
|
||||
->label('نوع')
|
||||
->options([
|
||||
WalletTransactionType::Credit->value => WalletTransactionType::Credit->label(),
|
||||
WalletTransactionType::Debit->value => WalletTransactionType::Debit->label(),
|
||||
]),
|
||||
])
|
||||
->recordActions([])
|
||||
->toolbarActions([]);
|
||||
}
|
||||
}
|
||||
19
app/Filament/Resources/Wallets/Schemas/WalletForm.php
Normal file
19
app/Filament/Resources/Wallets/Schemas/WalletForm.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class WalletForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Toggle::make('is_active')
|
||||
->label('فعال')
|
||||
->default(true),
|
||||
]);
|
||||
}
|
||||
}
|
||||
106
app/Filament/Resources/Wallets/Tables/WalletsTable.php
Normal file
106
app/Filament/Resources/Wallets/Tables/WalletsTable.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets\Tables;
|
||||
|
||||
use App\Models\Wallet;
|
||||
use App\Services\WalletService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use RuntimeException;
|
||||
|
||||
class WalletsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.name')
|
||||
->label('کاربر')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('user.email')
|
||||
->label('ایمیل')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('user.mobile')
|
||||
->label('موبایل')
|
||||
->searchable()
|
||||
->toggleable(),
|
||||
TextColumn::make('balance')
|
||||
->label('موجودی (تومان)')
|
||||
->numeric()
|
||||
->sortable()
|
||||
->formatStateUsing(fn (int $state): string => number_format($state).' تومان'),
|
||||
IconColumn::make('is_active')
|
||||
->label('فعال')
|
||||
->boolean(),
|
||||
TextColumn::make('updated_at')
|
||||
->label('آخرین بروزرسانی')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->defaultSort('balance', 'desc')
|
||||
->filters([
|
||||
TernaryFilter::make('is_active')
|
||||
->label('وضعیت')
|
||||
->placeholder('همه')
|
||||
->trueLabel('فعال')
|
||||
->falseLabel('غیرفعال'),
|
||||
])
|
||||
->recordActions([
|
||||
ViewAction::make(),
|
||||
EditAction::make(),
|
||||
self::adjustAction('credit', 'واریز', Heroicon::OutlinedPlusCircle, 'success'),
|
||||
self::adjustAction('debit', 'برداشت', Heroicon::OutlinedMinusCircle, 'danger'),
|
||||
]);
|
||||
}
|
||||
|
||||
private static function adjustAction(string $type, string $label, Heroicon $icon, string $color): Action
|
||||
{
|
||||
return Action::make($type)
|
||||
->label($label)
|
||||
->icon($icon)
|
||||
->color($color)
|
||||
->schema([
|
||||
TextInput::make('amount')
|
||||
->label('مبلغ (تومان)')
|
||||
->numeric()
|
||||
->required()
|
||||
->minValue(1)
|
||||
->integer(),
|
||||
Textarea::make('description')
|
||||
->label('توضیحات')
|
||||
->maxLength(500)
|
||||
->rows(2),
|
||||
])
|
||||
->action(function (Wallet $record, array $data, WalletService $walletService): void {
|
||||
try {
|
||||
$transaction = $type === 'credit'
|
||||
? $walletService->credit($record, (int) $data['amount'], $data['description'] ?? null, auth()->user())
|
||||
: $walletService->debit($record, (int) $data['amount'], $data['description'] ?? null, auth()->user());
|
||||
|
||||
Notification::make()
|
||||
->title($type === 'credit' ? 'واریز انجام شد' : 'برداشت انجام شد')
|
||||
->body('موجودی جدید: '.number_format($transaction->balance_after).' تومان')
|
||||
->success()
|
||||
->send();
|
||||
} catch (RuntimeException $exception) {
|
||||
Notification::make()
|
||||
->title('خطا')
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
67
app/Filament/Resources/Wallets/WalletResource.php
Normal file
67
app/Filament/Resources/Wallets/WalletResource.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Wallets;
|
||||
|
||||
use App\Filament\Resources\Wallets\Pages\EditWallet;
|
||||
use App\Filament\Resources\Wallets\Pages\ListWallets;
|
||||
use App\Filament\Resources\Wallets\Pages\ViewWallet;
|
||||
use App\Filament\Resources\Wallets\RelationManagers\TransactionsRelationManager;
|
||||
use App\Filament\Resources\Wallets\Schemas\WalletForm;
|
||||
use App\Filament\Resources\Wallets\Tables\WalletsTable;
|
||||
use App\Models\Wallet;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use UnitEnum;
|
||||
|
||||
class WalletResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Wallet::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedWallet;
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'مالی';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'id';
|
||||
|
||||
protected static ?string $modelLabel = 'کیف پول';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'کیف پولها';
|
||||
|
||||
protected static ?string $navigationLabel = 'کیف پولها';
|
||||
|
||||
protected static ?int $navigationSort = 10;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return WalletForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return WalletsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
TransactionsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListWallets::route('/'),
|
||||
'view' => ViewWallet::route('/{record}'),
|
||||
'edit' => EditWallet::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
@@ -29,6 +31,22 @@ class User extends Authenticatable implements FilamentUser
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasOne<Wallet, $this>
|
||||
*/
|
||||
public function wallet(): HasOne
|
||||
{
|
||||
return $this->hasOne(Wallet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasManyThrough<WalletTransaction, Wallet, $this>
|
||||
*/
|
||||
public function walletTransactions(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(WalletTransaction::class, Wallet::class);
|
||||
}
|
||||
|
||||
public function hasRole(string|array $roles): bool
|
||||
{
|
||||
$roles = (array) $roles;
|
||||
|
||||
39
app/Models/Wallet.php
Normal file
39
app/Models/Wallet.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable(['user_id', 'balance', 'is_active'])]
|
||||
class Wallet extends Model
|
||||
{
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<WalletTransaction, $this>
|
||||
*/
|
||||
public function transactions(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletTransaction::class)->latest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'balance' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Models/WalletTransaction.php
Normal file
49
app/Models/WalletTransaction.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable([
|
||||
'wallet_id',
|
||||
'type',
|
||||
'amount',
|
||||
'balance_before',
|
||||
'balance_after',
|
||||
'description',
|
||||
'created_by',
|
||||
])]
|
||||
class WalletTransaction extends Model
|
||||
{
|
||||
/**
|
||||
* @return BelongsTo<Wallet, $this>
|
||||
*/
|
||||
public function wallet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => WalletTransactionType::class,
|
||||
'amount' => 'integer',
|
||||
'balance_before' => 'integer',
|
||||
'balance_after' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
16
app/Observers/UserObserver.php
Normal file
16
app/Observers/UserObserver.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\WalletService;
|
||||
|
||||
class UserObserver
|
||||
{
|
||||
public function __construct(private WalletService $walletService) {}
|
||||
|
||||
public function created(User $user): void
|
||||
{
|
||||
$this->walletService->createForUser($user);
|
||||
}
|
||||
}
|
||||
29
app/Policies/WalletPolicy.php
Normal file
29
app/Policies/WalletPolicy.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
|
||||
class WalletPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function view(User $user, Wallet $wallet): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function update(User $user, Wallet $wallet): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
private function canManage(User $user): bool
|
||||
{
|
||||
return $user->hasRole('admin') || $user->hasPermission('wallets.manage');
|
||||
}
|
||||
}
|
||||
24
app/Policies/WalletTransactionPolicy.php
Normal file
24
app/Policies/WalletTransactionPolicy.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WalletTransaction;
|
||||
|
||||
class WalletTransactionPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
public function view(User $user, WalletTransaction $transaction): bool
|
||||
{
|
||||
return $this->canManage($user);
|
||||
}
|
||||
|
||||
private function canManage(User $user): bool
|
||||
{
|
||||
return $user->hasRole('admin') || $user->hasPermission('wallets.manage');
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,14 @@ namespace App\Providers;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use App\Observers\UserObserver;
|
||||
use App\Policies\PermissionPolicy;
|
||||
use App\Policies\RolePolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use App\Policies\WalletPolicy;
|
||||
use App\Policies\WalletTransactionPolicy;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@@ -26,8 +31,12 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
User::observe(UserObserver::class);
|
||||
|
||||
Gate::policy(User::class, UserPolicy::class);
|
||||
Gate::policy(Role::class, RolePolicy::class);
|
||||
Gate::policy(Permission::class, PermissionPolicy::class);
|
||||
Gate::policy(Wallet::class, WalletPolicy::class);
|
||||
Gate::policy(WalletTransaction::class, WalletTransactionPolicy::class);
|
||||
}
|
||||
}
|
||||
|
||||
81
app/Services/WalletService.php
Normal file
81
app/Services/WalletService.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\WalletTransactionType;
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
public function createForUser(User $user): Wallet
|
||||
{
|
||||
return Wallet::query()->firstOrCreate(
|
||||
['user_id' => $user->id],
|
||||
['balance' => 0, 'is_active' => true],
|
||||
);
|
||||
}
|
||||
|
||||
public function credit(
|
||||
Wallet $wallet,
|
||||
int $amount,
|
||||
?string $description = null,
|
||||
?User $performedBy = null,
|
||||
): WalletTransaction {
|
||||
return $this->apply($wallet, WalletTransactionType::Credit, $amount, $description, $performedBy);
|
||||
}
|
||||
|
||||
public function debit(
|
||||
Wallet $wallet,
|
||||
int $amount,
|
||||
?string $description = null,
|
||||
?User $performedBy = null,
|
||||
): WalletTransaction {
|
||||
return $this->apply($wallet, WalletTransactionType::Debit, $amount, $description, $performedBy);
|
||||
}
|
||||
|
||||
private function apply(
|
||||
Wallet $wallet,
|
||||
WalletTransactionType $type,
|
||||
int $amount,
|
||||
?string $description,
|
||||
?User $performedBy,
|
||||
): WalletTransaction {
|
||||
if ($amount <= 0) {
|
||||
throw new InvalidArgumentException('مبلغ باید بزرگتر از صفر باشد.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($wallet, $type, $amount, $description, $performedBy): WalletTransaction {
|
||||
$wallet = Wallet::query()->lockForUpdate()->findOrFail($wallet->id);
|
||||
|
||||
if (! $wallet->is_active) {
|
||||
throw new RuntimeException('کیف پول غیرفعال است.');
|
||||
}
|
||||
|
||||
$balanceBefore = $wallet->balance;
|
||||
$balanceAfter = $type === WalletTransactionType::Credit
|
||||
? $balanceBefore + $amount
|
||||
: $balanceBefore - $amount;
|
||||
|
||||
if ($balanceAfter < 0) {
|
||||
throw new RuntimeException('موجودی کیف پول کافی نیست.');
|
||||
}
|
||||
|
||||
$wallet->update(['balance' => $balanceAfter]);
|
||||
|
||||
return WalletTransaction::query()->create([
|
||||
'wallet_id' => $wallet->id,
|
||||
'type' => $type,
|
||||
'amount' => $amount,
|
||||
'balance_before' => $balanceBefore,
|
||||
'balance_after' => $balanceAfter,
|
||||
'description' => $description,
|
||||
'created_by' => $performedBy?->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user