diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 4ee1a351dda1..84801bd79ce6 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -17,6 +17,7 @@ use App\Jobs\Cron\RecurringInvoicesCron; use App\Jobs\Cron\SubscriptionCron; use App\Jobs\Ledger\LedgerBalanceUpdate; use App\Jobs\Ninja\AdjustEmailQuota; +use App\Jobs\Ninja\BankTransactionSync; use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Ninja\QueueSize; use App\Jobs\Ninja\SystemMaintenance; @@ -68,6 +69,8 @@ class Kernel extends ConsoleKernel $schedule->job(new SystemMaintenance)->weekly()->withoutOverlapping(); + $schedule->job(new BankTransactionSync)->dailyAt('04:00')->withoutOverlapping(); + if (Ninja::isSelfHost()) { $schedule->call(function () { Account::whereNotNull('id')->update(['is_scheduler_running' => true]); diff --git a/app/Factory/BankTransactionFactory.php b/app/Factory/BankTransactionFactory.php index 120bbae5fd3d..f5bbb92d31de 100644 --- a/app/Factory/BankTransactionFactory.php +++ b/app/Factory/BankTransactionFactory.php @@ -30,6 +30,7 @@ class BankTransactionFactory $bank_transaction->date = now()->format('Y-m-d'); $bank_transaction->description = ''; $bank_transaction->is_matched = 0; + $bank_transaction->base_type = 'CREDIT'; return $bank_transaction; } diff --git a/app/Helpers/Bank/Yodlee/Transformer/IncomeTransformer.php b/app/Helpers/Bank/Yodlee/Transformer/IncomeTransformer.php index fd9f207a4888..505f96eba1b2 100644 --- a/app/Helpers/Bank/Yodlee/Transformer/IncomeTransformer.php +++ b/app/Helpers/Bank/Yodlee/Transformer/IncomeTransformer.php @@ -134,18 +134,15 @@ class IncomeTransformer implements BankRevenueInterface { return [ - 'id' => $transaction->id, + 'transaction_id' => $transaction->id, 'amount' => $transaction->amount->amount, - 'currency' => $transaction->amount->currency, + 'currency_code' => $transaction->amount->currency, 'account_type' => $transaction->CONTAINER, 'category_id' => $transaction->categoryId, 'category_type' => $transaction->categoryType, 'date' => $transaction->date, - 'account_id' => $transaction->accountId, + 'bank_account_id' => $transaction->accountId, 'description' => $transaction->description->original, - 'invoice_id' => '', - 'expense_id' => '', - 'payment_id' => '', ]; } diff --git a/app/Http/Controllers/BankIntegrationController.php b/app/Http/Controllers/BankIntegrationController.php index 0df2848f0b19..4bcb7bde27e8 100644 --- a/app/Http/Controllers/BankIntegrationController.php +++ b/app/Http/Controllers/BankIntegrationController.php @@ -574,7 +574,8 @@ class BankIntegrationController extends BaseController } /** - * Return the remote list of accounts stored on the third part provider. + * Return the remote list of accounts stored on the third party provider + * and update our local cache. * * @return Response * @@ -631,12 +632,49 @@ class BankIntegrationController extends BaseController } + /** + * Return the remote list of accounts stored on the third party provider + * and update our local cache. + * + * @return Response + * + * @OA\Post( + * path="/api/v1/bank_integrations/get_transactions/account_id", + * operationId="getAccountTransactions", + * tags={"bank_integrations"}, + * summary="Retrieve transactions for a account", + * description="Retrieve transactions for a account", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="Retrieve transactions for a account", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/BankIntegration"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ public function getTransactions(AdminBankIntegrationRequest $request) { $bank_account_id = auth()->user()->account->bank_integration_account_id; - $bank_account_id = 'sbMem62e1e69547bfb1'; + // $bank_account_id = 'sbMem62e1e69547bfb1'; if(!$bank_account_id) return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400); @@ -645,17 +683,15 @@ class BankIntegrationController extends BaseController $yodlee->setTestMode(); $data = [ - 'CONTAINER' => 'bank', - 'categoryType' => 'INCOME, UNCATEGORIZE', 'top' => 500, 'fromDate' => '2000-10-10', /// YYYY-MM-DD ]; $transactions = $yodlee->getTransactions($data); - $transactions = (new BankService(auth()->user()->company()))->match(); + BankService::dispatch(auth()->user()->company()->id, auth()->user()->company()->db)); - return response()->json($transactions, 200, [], JSON_PRETTY_PRINT); + return response()->json(['message' => 'Fetching transactions....'], 200); } } \ No newline at end of file diff --git a/app/Http/Controllers/OpenAPI/BankTransaction.php b/app/Http/Controllers/OpenAPI/BankTransaction.php index d5861b693414..5e9185c13fea 100644 --- a/app/Http/Controllers/OpenAPI/BankTransaction.php +++ b/app/Http/Controllers/OpenAPI/BankTransaction.php @@ -13,6 +13,7 @@ * @OA\Property(property="description", type="string", example="Potato purchases for kevin", description="The description of the transaction"), * @OA\Property(property="category_id", type="integer", example=1, description="The category id"), * @OA\Property(property="category_type", type="string", example="Expenses", description="The category description"), + * @OA\Property(property="base_type", type="string", example="CREDIT", description="Either CREDIT or DEBIT"), * @OA\Property(property="date", type="string", example="2022-09-01", description="The date of the transaction"), * @OA\Property(property="bank_account_id", type="integer", example="1", description="The ID number of the bank account"), * ) diff --git a/app/Jobs/Bank/ProcessBankTransactions.php b/app/Jobs/Bank/ProcessBankTransactions.php new file mode 100644 index 000000000000..e2b4880758ef --- /dev/null +++ b/app/Jobs/Bank/ProcessBankTransactions.php @@ -0,0 +1,117 @@ +bank_integration_account_id = $bank_integration_account_id; + $this->bank_integration = $bank_integration; + $this->from_date = $from_date; + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + + $yodlee = new Yodlee($this->bank_integration_account_id); + $yodlee->setTestMode(); + + $data = [ + 'baseType' => 'DEBIT', //CREDIT + 'top' => 500, + 'fromDate' => $this->from_date ?: $this->default_date, /// YYYY-MM-DD + 'accountId' => $this->bank_integration->bank_account_id, + ]; + + //expense transactions + $transactions = $yodlee->getTransactions($data); + + $company = $this->bank_integration->company; + $user_id = $company->owner()->id; + + foreach($transactions as $transaction) + { + + if(BankTransaction::where('transaction_id', $transaction['transaction_id'])->where('company_id', $company->id)->withTrashed()->exists()) + continue; + + $bt = BankTransaction::create( + $transaction + ); + + $bt->company_id = $company->id; + $bt->user_id = $user_id; + $bt->base_type = 'DEBIT'; + $bt->save(); + } + + $data = [ + 'baseType' => 'CREDIT', //CREDIT + 'top' => 500, + 'fromDate' => $this->from_date ?: $this->default_date, /// YYYY-MM-DD + 'accountId' => $this->bank_integration->bank_account_id, + ]; + + //income transactions + $transactions = $yodlee->getTransactions($data); + + foreach($transactions as $transaction) + { + + if(BankTransaction::where('transaction_id', $transaction['transaction_id'])->where('company_id', $company->id)->withTrashed()->exists()) + continue; + + $bt = BankTransaction::create( + $transaction + ); + + $bt->company_id = $company->id; + $bt->user_id = $user_id; + $bt->base_type = 'CREDIT'; + $bt->save(); + } + + BankService::dispatch($company->id, $company->db); + + } + +} \ No newline at end of file diff --git a/app/Jobs/Ninja/BankTransactionSync.php b/app/Jobs/Ninja/BankTransactionSync.php new file mode 100644 index 000000000000..ec08af0581aa --- /dev/null +++ b/app/Jobs/Ninja/BankTransactionSync.php @@ -0,0 +1,79 @@ +syncTransactions(); + } + } + + public function syncTransactions() + { + $a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){ + + $queue = Ninja::isHosted() ? 'bank' : 'default'; + + if($account->isPaid()) + { + + $account->bank_integrations->each(function ($bank_integration) use ($queue, $account){ + + ProcessBankTransactions::dispatch($account->bank_integration_account_id, $bank_integration)->onQueue($queue); + + }); + + } + + }); + } + +} diff --git a/app/Models/Account.php b/app/Models/Account.php index b75a7ce327df..cb275e5e1980 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -138,6 +138,11 @@ class Account extends BaseModel return $this->hasMany(Company::class); } + public function bank_integrations() + { + return $this->hasMany(BankIntegration::class); + } + public function company_users() { return $this->hasMany(CompanyUser::class); diff --git a/app/Services/Bank/BankService.php b/app/Services/Bank/BankService.php index 9514a0c14c85..8eea9138408e 100644 --- a/app/Services/Bank/BankService.php +++ b/app/Services/Bank/BankService.php @@ -11,18 +11,38 @@ namespace App\Services\Bank; +use App\Libraries\MultiDB; use App\Models\Company; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; -class BankService +class BankService implements ShouldQueue { + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public Company $company; + private $company_id; + + private Company $company; + + private $db; private $invoices; - public function __construct(Company $company) + public function __construct($company_id, $db) { - $this->company = $company; + $this->company_id = $company_id; + $this->db = $db; + } + + public function handle() + { + + MultiDB::setDb($this->db); + + $this->company = Company::find($this->company_id); $this->invoices = $this->company->invoices()->whereIn('status_id', [1,2,3]) ->where('is_deleted', 0) diff --git a/app/Transformers/BankTransactionTransformer.php b/app/Transformers/BankTransactionTransformer.php index d6975469619e..7d72343e05fc 100644 --- a/app/Transformers/BankTransactionTransformer.php +++ b/app/Transformers/BankTransactionTransformer.php @@ -60,6 +60,7 @@ class BankTransactionTransformer extends EntityTransformer 'date' => (string) $bank_transaction->date ?: '', 'bank_account_id' => (int) $bank_transaction->bank_account_id, 'description' => (string) $bank_transaction->description ?: '', + 'base_type' => (string) $bank_transaction->base_type ?: '', 'invoice_id' => (string) $this->encodePrimaryKey($bank_transaction->invoice_id) ?: '', 'expense_id'=> (string) $this->encodePrimaryKey($bank_transaction->expense_id) ?: '', 'is_matched'=> (bool) $bank_transaction->is_matched ?: '', diff --git a/database/migrations/2022_08_05_023357_bank_integration.php b/database/migrations/2022_08_05_023357_bank_integration.php index 0dcc056c29f9..83cdf19f4b22 100644 --- a/database/migrations/2022_08_05_023357_bank_integration.php +++ b/database/migrations/2022_08_05_023357_bank_integration.php @@ -55,10 +55,11 @@ return new class extends Migration $table->unsignedInteger('company_id'); $table->unsignedInteger('user_id'); $table->unsignedBigInteger('bank_integration_id'); - $table->unsignedBigInteger('transaction_id')->nullable(); + $table->unsignedBigInteger('transaction_id')->index(); $table->decimal('amount', 20, 6); $table->string('currency_code'); $table->string('account_type'); + $table->string('base_type')->index(); $table->unsignedInteger('category_id'); $table->string('category_type'); $table->date('date'); diff --git a/routes/api.php b/routes/api.php index 4ed5327aa56d..7cdfaa271ba6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -109,8 +109,9 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::put('accounts/{account}', [AccountController::class, 'update'])->name('account.update'); Route::resource('bank_integrations', BankIntegrationController::class); // name = (clients. index / create / show / update / destroy / edit Route::post('bank_integrations/refresh_accounts', [BankIntegrationController::class, 'refreshAccounts'])->name('bank_integrations.refresh_accounts'); - Route::post('bank_integrations/transactions', [BankIntegrationController::class, 'getTransactions'])->name('bank_integrations.transactions'); Route::post('bank_integrations/remove_account/{acc_id}', [BankIntegrationController::class, 'removeAccount'])->name('bank_integrations.remove_account'); + Route::post('bank_integrations/get_transactions/{acc_id}', [BankIntegrationController::class, 'getTransactions'])->name('bank_integrations.transactions'); + Route::post('bank_integrations/bulk', [BankIntegrationController::class, 'bulk'])->name('bank_integrations.bulk'); Route::resource('bank_transactions', BankTransactionController::class); // name = (clients. index / create / show / update / destroy / edit