plaidService = $plaidService; } public function mount(): void { $this->user = Auth::user(); } #[Computed] public function connectedInstitutions(): Collection | array { return Institution::withWhereHas('connectedBankAccounts') ->get(); } public function startImportingTransactions(): Action { return Action::make('startImportingTransactions') ->link() ->icon('heroicon-o-cloud-arrow-down') ->label('Start importing transactions') ->modalWidth(fn () => $this->modalWidth) ->modalFooterActionsAlignment(fn () => $this->modalWidth === 'screen' ? Alignment::Center : Alignment::Start) ->stickyModalHeader() ->stickyModalFooter() ->record(fn (array $arguments) => ConnectedBankAccount::find($arguments['connectedBankAccount'])) ->form([ Placeholder::make('import_from') ->label('Import transactions from') ->content(static fn (ConnectedBankAccount $connectedBankAccount): View => view( 'components.actions.transaction-import-modal', compact('connectedBankAccount') )), Placeholder::make('info') ->hiddenLabel() ->visible(static fn (ConnectedBankAccount $connectedBankAccount) => ! $connectedBankAccount->bank_account_id) ->content(static fn (ConnectedBankAccount $connectedBankAccount) => 'If ' . $connectedBankAccount->name . ' already has transactions for an existing account, select the account to import transactions into.'), Select::make('bank_account_id') ->label('Select account') ->visible(static fn (ConnectedBankAccount $connectedBankAccount) => ! $connectedBankAccount->bank_account_id) ->options(fn (ConnectedBankAccount $connectedBankAccount) => $this->getBankAccountOptions($connectedBankAccount)) ->required(), DatePicker::make('start_date') ->label('Start date') ->required() ->placeholder('Select a start date for importing transactions.') ->minDate(company_today()->subDays(PlaidService::TRANSACTION_DAYS_REQUESTED)->toDateString()) ->maxDate(company_today()->toDateString()), ]) ->action(function (array $data, ConnectedBankAccount $connectedBankAccount) { $selectedBankAccountId = $data['bank_account_id'] ?? $connectedBankAccount->bank_account_id; $startDate = $data['start_date']; $company = $this->user->currentCompany; StartTransactionImport::dispatch($company, $connectedBankAccount, $selectedBankAccountId, $startDate); unset($this->connectedInstitutions); }); } public function getBankAccountOptions(ConnectedBankAccount $connectedBankAccount): array { $institutionId = $connectedBankAccount->institution_id ?? null; $options = ['new' => 'New Account']; if ($institutionId) { $accountOptions = BankAccount::query() ->join('accounts', 'bank_accounts.account_id', '=', 'accounts.id') ->where('bank_accounts.institution_id', $institutionId) ->whereDoesntHave('connectedBankAccount') ->select(['bank_accounts.id', 'accounts.name']) ->pluck('accounts.name', 'bank_accounts.id') ->toArray(); $options += $accountOptions; } return $options; } public function stopImportingTransactions(): Action { return Action::make('stopImportingTransactions') ->link() ->icon('heroicon-o-stop-circle') ->label('Stop importing transactions') ->color('danger') ->requiresConfirmation() ->modalHeading('Stop Importing Transactions') ->modalDescription('Importing transactions automatically helps keep your bookkeeping up to date. Are you sure you want to turn this off?') ->modalSubmitActionLabel('Turn Off') ->modalCancelActionLabel('Keep On') ->action(function (array $arguments) { $connectedBankAccount = ConnectedBankAccount::find($arguments['connectedBankAccount']); if ($connectedBankAccount) { $connectedBankAccount->update([ 'import_transactions' => false, ]); } unset($this->connectedInstitutions); }); } public function refreshTransactions(): Action { return Action::make('refreshTransactions') ->iconButton() ->icon('heroicon-o-arrow-path') ->color('primary') ->record(fn (array $arguments) => Institution::find($arguments['institution'])) ->modalWidth(fn () => $this->modalWidth) ->modalFooterActionsAlignment(fn () => $this->modalWidth === 'screen' ? Alignment::Center : Alignment::Start) ->stickyModalHeader() ->stickyModalFooter() ->modalHeading('Refresh Transactions') ->modalSubmitActionLabel('Refresh') ->form([ Placeholder::make('modalDetails') ->hiddenLabel() ->content('Refreshing transactions will update the selected account with the latest transactions from the bank if there are any new transactions available. This may take a few moments.'), Select::make('connected_bank_account_id') ->label('Select account') ->softRequired() ->selectablePlaceholder(false) ->hint( fn (Institution $institution) => $institution->getEnabledConnectedBankAccounts()->count() . ' ' . Str::plural('account', $institution->getEnabledConnectedBankAccounts()->count()) . ' available' ) ->hintColor('primary') ->options(fn (Institution $institution) => $institution->getEnabledConnectedBankAccounts()->pluck('name', 'id')->toArray()) ->default(fn (Institution $institution) => $institution->getEnabledConnectedBankAccounts()->first()?->id), ]) ->action(function (array $data) { $connectedBankAccountId = $data['connected_bank_account_id']; $connectedBankAccount = ConnectedBankAccount::find($connectedBankAccountId); if ($connectedBankAccount) { $access_token = $connectedBankAccount->access_token; $this->plaidService->refreshTransactions($access_token); } unset($this->connectedInstitutions); }); } public function deleteBankConnection(): Action { return Action::make('deleteBankConnection') ->iconButton() ->icon('heroicon-o-trash') ->color('danger') ->modalHeading('Delete Bank Connection') ->modalWidth(fn () => $this->modalWidth) ->modalFooterActionsAlignment(fn () => $this->modalWidth === 'screen' ? Alignment::Center : Alignment::Start) ->stickyModalHeader() ->stickyModalFooter() ->record(fn (array $arguments) => Institution::find($arguments['institution'])) ->form([ Placeholder::make('modalDetails') ->hiddenLabel() ->content(static fn (Institution $institution): View => view( 'components.actions.delete-bank-connection-modal', compact('institution') )), Checkbox::make('confirm') ->label('Yes, I want to delete this bank connection.') ->markAsRequired(false) ->required(), ]) ->action(function (array $arguments, Institution $institution) { try { $this->processBankConnectionDeletion($institution); } catch (RuntimeException $e) { Log::error('Error deleting bank connection ' . $e->getMessage()); $this->sendErrorNotification("We're currently experiencing issues deleting your bank connection. Please try again in a few moments."); } finally { unset($this->connectedInstitutions); } }); } private function processBankConnectionDeletion(Institution $institution): void { DB::transaction(function () use ($institution) { $accessTokens = $institution->connectedBankAccounts->pluck('access_token')->unique()->toArray(); foreach ($accessTokens as $accessToken) { $this->plaidService->removeItem($accessToken); } $institution->connectedBankAccounts()->each(fn (ConnectedBankAccount $connectedBankAccount) => $connectedBankAccount->delete()); }); } #[On('createToken')] public function createLinkToken(): void { try { $company = $this->user->currentCompany; $companyLanguage = $company->locale->language ?? 'en'; $companyCountry = $company->profile?->address?->country_code ?? 'US'; $plaidUser = $this->plaidService->createPlaidUser($company); $response = $this->plaidService->createToken($companyLanguage, $companyCountry, $plaidUser, ['transactions']); $plaidLinkToken = $response->link_token; $this->dispatch('initializeLink', $plaidLinkToken)->self(); } catch (RuntimeException) { Log::error('Error creating Plaid token.'); $this->sendErrorNotification("We're currently experiencing issues connecting your account. Please try again in a few moments."); } } #[On('linkSuccess')] public function handleLinkSuccess($publicToken, $metadata): void { $response = $this->plaidService->exchangePublicToken($publicToken); $accessToken = $response->access_token; $company = $this->user->currentCompany; PlaidSuccess::dispatch($publicToken, $accessToken, $company); unset($this->connectedInstitutions); } public function sendErrorNotification(string $message): void { Notification::make() ->title('Hold on...') ->danger() ->body($message) ->persistent() ->send(); } public function render(): View { return view('livewire.company.service.connected-account.list-institutions'); } }