diff --git a/.env.example b/.env.example index de742ede3b66..16c30973aae4 100644 --- a/.env.example +++ b/.env.example @@ -53,5 +53,8 @@ PHANTOMJS_SECRET=secret UPDATE_SECRET=secret +DELETE_PDF_DAYS=60 +DELETE_BACKUP_DAYS=60 + COMPOSER_AUTH='{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}' SENTRY_LARAVEL_DSN=https://39389664f3f14969b4c43dadda00a40b@sentry2.invoicing.co/5 \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index b472bf3a7442..4f14f22926d7 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -19,6 +19,7 @@ use App\Jobs\Ledger\LedgerBalanceUpdate; use App\Jobs\Ninja\AdjustEmailQuota; use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Ninja\QueueSize; +use App\Jobs\Ninja\SystemMaintenance; use App\Jobs\Util\DiskCleanup; use App\Jobs\Util\ReminderJob; use App\Jobs\Util\SchedulerCheck; @@ -56,7 +57,6 @@ class Kernel extends ConsoleKernel $schedule->job(new ReminderJob)->hourly()->withoutOverlapping(); - // $schedule->job(new LedgerBalanceUpdate)->everyFiveMinutes()->withoutOverlapping(); $schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping(); $schedule->job(new CompanySizeCheck)->daily()->withoutOverlapping(); @@ -73,6 +73,8 @@ class Kernel extends ConsoleKernel $schedule->job(new SchedulerCheck)->daily()->withoutOverlapping(); + $schedule->job(new SystemMaintenance)->weekly()->withoutOverlapping(); + if(Ninja::isSelfHost()) { diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 8b60aa001d2c..9a5d5bb00554 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -392,7 +392,7 @@ class LoginController extends BaseController if($cu->count() == 0) return $cu; - if(auth()->user()->company_users()->count() != auth()->user()->tokens()->count()) + if(auth()->user()->company_users()->count() != auth()->user()->tokens()->distinct('company_id')->count()) { auth()->user()->companies->each(function($company){ @@ -435,49 +435,11 @@ class LoginController extends BaseController Auth::login($existing_user, true); - // $cu = CompanyUser::query() - // ->where('user_id', auth()->user()->id) - // ->where('company_id', $existing_user->account->default_company_id); - - // if($cu->exists()) - // $set_company = $existing_user->account->default_company; - // else{ - // $cu = CompanyUser::query()->where('user_id', auth()->user()->id); - // $set_company = $cu->company; - // } - - // $existing_user->setCompany($set_company); - - // $this->setLoginCache($existing_user); - $cu = $this->hydrateCompanyUser(); if($cu->count() == 0) return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($cu->first()); - // $truth->setUser($existing_user); - // $truth->setCompany($set_company); - - // if($existing_user->company_users()->count() != $existing_user->tokens()->count()) - // { - - // $existing_user->companies->each(function($company) use($existing_user){ - - // if(!CompanyToken::where('user_id', $existing_user->id)->where('company_id', $company->id)->exists()){ - - // CreateCompanyToken::dispatchNow($company, $existing_user, "Google_O_Auth"); - - // } - - // }); - - // } - - // $truth->setCompanyToken(CompanyToken::where('user_id', $existing_user->id)->where('company_id', $set_company->id)->first()); - - if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_user->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); @@ -493,9 +455,6 @@ class LoginController extends BaseController Auth::login($existing_login_user, true); - // $existing_login_user->setCompany($existing_login_user->account->default_company); - // $this->setLoginCache($existing_login_user); - auth()->user()->update([ 'oauth_user_id' => $google->harvestSubField($user), 'oauth_provider_id'=> 'google', @@ -503,35 +462,9 @@ class LoginController extends BaseController $cu = $this->hydrateCompanyUser(); - // $cu = CompanyUser::query() - // ->where('user_id', auth()->user()->id); - if($cu->count() == 0) return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($cu->first()); - // $truth->setUser($existing_login_user); - // $truth->setCompany($existing_login_user->account->default_company); - - - // if($existing_login_user->company_users()->count() != $existing_login_user->tokens()->count()) - // { - - // $existing_login_user->companies->each(function($company) use($existing_login_user){ - - // if(!CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $company->id)->exists()){ - - // CreateCompanyToken::dispatchNow($company, $existing_login_user, "Google_O_Auth"); - - // } - - // }); - - // } - - // $truth->setCompanyToken(CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $existing_login_user->account->default_company->id)->first()); - if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); @@ -551,9 +484,6 @@ class LoginController extends BaseController Auth::login($existing_login_user, true); - // $existing_login_user->setCompany($existing_login_user->account->default_company); - // $this->setLoginCache($existing_login_user); - auth()->user()->update([ 'oauth_user_id' => $google->harvestSubField($user), 'oauth_provider_id'=> 'google', @@ -567,29 +497,6 @@ class LoginController extends BaseController if($cu->count() == 0) return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($cu->first()); - // $truth->setUser($existing_login_user); - // $truth->setCompany($existing_login_user->account->default_company); - - - // if($existing_login_user->company_users()->count() != $existing_login_user->tokens()->count()) - // { - - // $existing_login_user->companies->each(function($company) use($existing_login_user){ - - // if(!CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $company->id)->exists()){ - - // CreateCompanyToken::dispatchNow($company, $existing_login_user, "Google_O_Auth"); - - // } - - // }); - - // } - - // $truth->setCompanyToken(CompanyToken::where('user_id', $existing_login_user->id)->where('company_id', $existing_login_user->account->default_company->id)->first()); - if(Ninja::isHosted() && !$cu->first()->is_owner && !$existing_login_user->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); @@ -617,38 +524,11 @@ class LoginController extends BaseController auth()->user()->email_verified_at = now(); auth()->user()->save(); - // auth()->user()->setCompany(auth()->user()->account->default_company); - // $this->setLoginCache(auth()->user()); - // $cu = CompanyUser::whereUserId(auth()->user()->id); - $cu = $this->hydrateCompanyUser(); if($cu->count() == 0) return response()->json(['message' => 'User found, but not attached to any companies, please see your administrator'], 400); - // $truth = app()->make(TruthSource::class); - // $truth->setCompanyUser($cu->first()); - // $truth->setUser(auth()->user()); - // $truth->setCompany(auth()->user()->account->default_company); - - // if(auth()->user()->company_users()->count() != auth()->user()->tokens()->count()) - // { - - // auth()->user()->companies->each(function($company) { - - // if(!CompanyToken::where('user_id', auth()->user()->id)->where('company_id', $company->id)->exists()){ - - // CreateCompanyToken::dispatchNow($company, auth()->user(), "Google_O_Auth"); - - // } - - // }); - - // } - - // $truth->setCompanyToken(CompanyToken::where('user_id', auth()->user()->id)->where('company_id', auth()->user()->account->default_company->id)->first()); - - if(Ninja::isHosted() && !$cu->first()->is_owner && !auth()->user()->account->isEnterpriseClient()) return response()->json(['message' => 'Pro / Free accounts only the owner can log in. Please upgrade'], 403); diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index 93b85bf68027..1c8abd67003a 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -59,13 +59,6 @@ class NinjaPlanController extends Controller Auth::guard('contact')->loginUsingId($client_contact->id,true); - // /* Current paid users get pushed straight to subscription overview page*/ - // if($account->isPaidHostedClient()) - // return redirect('/client/dashboard'); - - // /* Users that are not paid get pushed to a custom purchase page */ - // return $this->render('subscriptions.ninja_plan', ['settings' => $client_contact->company->settings]); - return $this->plan(); } diff --git a/app/Http/Livewire/PaymentsTable.php b/app/Http/Livewire/PaymentsTable.php index e9b2cd3dbbfc..7c2cb8970840 100644 --- a/app/Http/Livewire/PaymentsTable.php +++ b/app/Http/Livewire/PaymentsTable.php @@ -41,7 +41,7 @@ class PaymentsTable extends Component { $query = Payment::query() ->with('type', 'client') - ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED]) + ->whereIn('status_id', [Payment::STATUS_FAILED, Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED]) ->where('company_id', $this->company->id) ->where('client_id', auth()->guard('contact')->user()->client->id) ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 6b17e0e0b3d7..d8e05af55f3a 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -79,7 +79,9 @@ class StoreInvoiceRequest extends Request $input = $this->decodePrimaryKeys($input); - $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + if (isset($input['line_items']) && is_array($input['line_items'])) + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + $input['amount'] = 0; $input['balance'] = 0; diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index e818842fe834..afbe0999a6f7 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -76,7 +76,7 @@ class UpdateInvoiceRequest extends Request $input['id'] = $this->invoice->id; - if (isset($input['line_items'])) { + if (isset($input['line_items']) && is_array($input['line_items'])) { $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } diff --git a/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php b/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php index 36068bdb7fea..b9a80815faeb 100644 --- a/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php +++ b/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php @@ -47,7 +47,7 @@ class ClientLedgerBalanceUpdate implements ShouldQueue */ public function handle() :void { - nlog("Updating company ledgers"); + nlog("Updating company ledger for client ". $this->client->id); MultiDB::setDb($this->company->db); @@ -62,8 +62,14 @@ class ClientLedgerBalanceUpdate implements ShouldQueue ->orderBy('id', 'DESC') ->first(); - if(!$last_record) - return; + if(!$last_record){ + + $last_record = CompanyLedger::where('client_id', $company_ledger->client_id) + ->where('company_id', $company_ledger->company_id) + ->orderBy('id', 'DESC') + ->first(); + + } nlog("Updating Balance NOW"); @@ -72,13 +78,7 @@ class ClientLedgerBalanceUpdate implements ShouldQueue }); - nlog("Finished checking company ledgers"); - - } - - public function checkLedger() - { - + nlog("Updating company ledger for client ". $this->client->id); } diff --git a/app/Jobs/Mail/PaymentFailedMailer.php b/app/Jobs/Mail/PaymentFailedMailer.php index 77f7585bdac1..4188a3d035f3 100644 --- a/app/Jobs/Mail/PaymentFailedMailer.php +++ b/app/Jobs/Mail/PaymentFailedMailer.php @@ -72,7 +72,6 @@ class PaymentFailedMailer implements ShouldQueue */ public function handle() { - //Set DB MultiDB::setDb($this->company->db); App::setLocale($this->client->locale()); diff --git a/app/Jobs/Ninja/SystemMaintenance.php b/app/Jobs/Ninja/SystemMaintenance.php new file mode 100644 index 000000000000..b2221aad93a9 --- /dev/null +++ b/app/Jobs/Ninja/SystemMaintenance.php @@ -0,0 +1,132 @@ +maintainPdfs($delete_pdf_days); + + $this->maintainBackups($delete_backup_days); + + } + + private function maintainPdfs(int $delete_pdf_days) + { + if($delete_pdf_days == 0) + return; + + Invoice::with('invitations') + ->whereBetween('created_at', [now()->subYear(), now()->subDays($delete_pdf_days)]) + ->withTrashed() + ->cursor() + ->each(function ($invoice){ + + nlog("deleting invoice {$invoice->number}"); + + $invoice->service()->deletePdf(); + + }); + + Quote::with('invitations') + ->whereBetween('created_at', [now()->subYear(), now()->subDays($delete_pdf_days)]) + ->withTrashed() + ->cursor() + ->each(function ($quote){ + + nlog("deleting quote {$quote->number}"); + + $quote->service()->deletePdf(); + + }); + + Credit::with('invitations') + ->whereBetween('created_at', [now()->subYear(), now()->subDays($delete_pdf_days)]) + ->withTrashed() + ->cursor() + ->each(function ($credit){ + + nlog("deleting credit {$credit->number}"); + + $credit->service()->deletePdf(); + + }); + + + } + + private function maintainBackups(int $delete_backup_days) + { + if($delete_backup_days == 0) + return; + + Backup::where('created_at', '<', now()->subDays($delete_backup_days)) + ->cursor() + ->each(function ($backup){ + + nlog("deleting {$backup->filename}"); + + if($backup->filename) + $backup->deleteFile(); + + $backup->delete(); + + }); + + } +} diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index d1bb41682e00..ca9fefb2d823 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -237,7 +237,6 @@ class Import implements ShouldQueue //company size check if ($this->company->invoices()->count() > 500 || $this->company->products()->count() > 500 || $this->company->clients()->count() > 500) { - // $this->company->is_large = true; $this->company->account->companies()->update(['is_large' => true]); } diff --git a/app/Models/Backup.php b/app/Models/Backup.php index 7b90743e2257..78e91bd11294 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -47,4 +47,22 @@ class Backup extends BaseModel } } + + public function deleteFile() + { + + nlog("deleting => ". $this->filename); + + try{ + + Storage::disk(config('filesystems.default'))->delete($this->filename); + + } + catch(\Exception $e){ + + nlog("BACKUPEXCEPTION deleting backup file with error ". $e->getMessage()); + + } + } + } diff --git a/app/Models/CompanyUser.php b/app/Models/CompanyUser.php index 4c940588cd55..8e8fe5e7ac8f 100644 --- a/app/Models/CompanyUser.php +++ b/app/Models/CompanyUser.php @@ -98,21 +98,6 @@ class CompanyUser extends Pivot public function token() { return $this->hasMany(CompanyToken::class, 'user_id', 'user_id'); - - //return $this->hasMany(CompanyToken::class); - //return $this->hasOne(CompanyToken::class, 'user_id', 'user_id','company_id', 'company_id'); - - - //return $this->belongsTo(CompanyToken::class, 'user_id', 'user_id'); - - // return $this->hasOneThrough( - // CompanyToken::class, - // CompanyUser::class, - // 'user_id', // Foreign key on CompanyUser table... - // 'company_id', // Foreign key on CompanyToken table... - // 'user_id', // Local key on CompanyToken table... - // 'company_id' // Local key on CompanyUser table... - // ); } public function tokens() diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 6df3448565e2..feddf40625de 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -103,7 +103,7 @@ class Gateway extends StaticModel case 20: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded','payment_intent.succeeded']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded','payment_intent.succeeded','charge.failed','payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], //Stripe diff --git a/app/PaymentDrivers/PayPalExpressPaymentDriver.php b/app/PaymentDrivers/PayPalExpressPaymentDriver.php index 940f1617ea21..d4904e7b71dd 100644 --- a/app/PaymentDrivers/PayPalExpressPaymentDriver.php +++ b/app/PaymentDrivers/PayPalExpressPaymentDriver.php @@ -179,7 +179,7 @@ class PayPalExpressPaymentDriver extends BaseDriver $_invoice = collect($this->payment_hash->data->invoices)->first(); $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id)); - $this->fee = $this->feeCalc($invoice, $data['total']['amount_with_fee']); + // $this->fee = $this->feeCalc($invoice, $data['total']['amount_with_fee']); return [ 'currency' => $this->client->getCurrencyCode(), @@ -218,18 +218,6 @@ class PayPalExpressPaymentDriver extends BaseDriver 'quantity' => 1, ]); - - // if($this->fee > 0.1){ - - // $items[] = new Item([ - // 'name' => " ", - // 'description' => ctrans('texts.gateway_fee_description'), - // 'price' => $this->fee, - // 'quantity' => 1, - // ]); - - // } - return $items; } diff --git a/app/PaymentDrivers/Stripe/ACH.php b/app/PaymentDrivers/Stripe/ACH.php index d151ed0d8ae7..f3b57eb03978 100644 --- a/app/PaymentDrivers/Stripe/ACH.php +++ b/app/PaymentDrivers/Stripe/ACH.php @@ -30,8 +30,12 @@ use App\PaymentDrivers\StripePaymentDriver; use App\Utils\Traits\MakesHash; use Exception; use Stripe\Customer; +use Stripe\Exception\ApiErrorException; +use Stripe\Exception\AuthenticationException; use Stripe\Exception\CardException; use Stripe\Exception\InvalidRequestException; +use Stripe\Exception\RateLimitException; +use Stripe\PaymentIntent; class ACH { @@ -45,6 +49,9 @@ class ACH $this->stripe = $stripe; } + /** + * Authorize a bank account - requires microdeposit verification + */ public function authorizeView(array $data) { $data['gateway'] = $this->stripe; @@ -134,7 +141,11 @@ class ACH return back()->with('error', $e->getMessage()); } } - + + /** + * Make a payment WITH instant verification. + */ + public function paymentView(array $data) { $data['gateway'] = $this->stripe; @@ -143,6 +154,23 @@ class ACH $data['customer'] = $this->stripe->findOrCreateCustomer(); $data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()); + $intent = false; + + if(count($data['tokens']) == 0) + { + $intent = + $this->stripe->createPaymentIntent([ + 'amount' => $data['amount'], + 'currency' => $data['currency'], + 'setup_future_usage' => 'off_session', + 'customer' => $data['customer']->id, + 'payment_method_types' => ['us_bank_account'], + ] + ); + } + + $data['client_secret'] = $intent ? $intent->client_secret : false; + return render('gateways.stripe.ach.pay', $data); } @@ -160,6 +188,9 @@ class ACH $description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}"; } + if(substr($cgt->token, 0, 2) === "pm") + return $this->paymentIntentTokenBilling($amount, $invoice, $description, $cgt, false); + $this->stripe->init(); $response = null; @@ -203,11 +234,179 @@ class ACH } + public function paymentIntentTokenBilling($amount, $invoice, $description, $cgt, $client_present = true) + { + $this->stripe->init(); + + try { + $data = [ + 'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()), + 'currency' => $this->stripe->client->getCurrencyCode(), + 'payment_method' => $cgt->token, + 'customer' => $cgt->gateway_customer_reference, + 'confirm' => true, + 'description' => $description, + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => $cgt->gateway_type_id, + ], + ]; + + if($cgt->gateway_type_id == GatewayType::BANK_TRANSFER) + $data['payment_method_types'] = ['us_bank_account']; + + $response = $this->stripe->createPaymentIntent($data, $this->stripe->stripe_connect_auth); + + SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company); + + }catch(\Exception $e) { + + $data =[ + 'status' => '', + 'error_type' => '', + 'error_code' => '', + 'param' => '', + 'message' => '', + ]; + + switch ($e) { + case ($e instanceof CardException): + $data['status'] = $e->getHttpStatus(); + $data['error_type'] = $e->getError()->type; + $data['error_code'] = $e->getError()->code; + $data['param'] = $e->getError()->param; + $data['message'] = $e->getError()->message; + break; + case ($e instanceof RateLimitException): + $data['message'] = 'Too many requests made to the API too quickly'; + break; + case ($e instanceof InvalidRequestException): + $data['message'] = 'Invalid parameters were supplied to Stripe\'s API'; + break; + case ($e instanceof AuthenticationException): + $data['message'] = 'Authentication with Stripe\'s API failed'; + break; + case ($e instanceof ApiErrorException): + $data['message'] = 'Network communication with Stripe failed'; + break; + + default: + $data['message'] = $e->getMessage(); + break; + } + + $this->stripe->processInternallyFailedPayment($this->stripe, $e); + + SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company); + } + + if (! $response) { + return false; + } + + $payment_method_type = PaymentType::ACH; + + $data = [ + 'gateway_type_id' => $cgt->gateway_type_id, + 'payment_type' => PaymentType::ACH, + 'transaction_reference' => $response->charges->data[0]->id, + 'amount' => $amount, + ]; + + $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING); + $payment->meta = $cgt->meta; + $payment->save(); + + $this->stripe->payment_hash->payment_id = $payment->id; + $this->stripe->payment_hash->save(); + + if($client_present){ + return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + } + + return $payment; + } + + public function handlePaymentIntentResponse($request) + { + + $response = json_decode($request->gateway_response); + $bank_account_response = json_decode($request->bank_account_response); + + $method = $bank_account_response->payment_method->us_bank_account; + $method->id = $response->payment_method; + $method->state = 'authorized'; + + $this->stripe->payment_hash = PaymentHash::where("hash", $request->input("payment_hash"))->first(); + + if($response->id && $response->status === "processing") { + $payment_intent = PaymentIntent::retrieve($response->id, $this->stripe->stripe_connect_auth); + + $state = [ + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + 'amount' => $response->amount, + 'currency' => $response->currency, + 'customer' => $request->customer, + 'source' => $response->payment_method, + 'charge' => $response + ]; + + $this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state); + $this->stripe->payment_hash->save(); + + $customer = $this->stripe->getCustomer($request->customer); + + $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer); + + return $this->processPendingPayment($state, true); + } + + if($response->next_action){ + + } + + } + + public function processPendingPaymentIntent($state, $client_present = true) + { + $this->stripe->init(); + + $data = [ + 'payment_method' => $state['source'], + 'payment_type' => PaymentType::ACH, + 'amount' => $state['amount'], + 'transaction_reference' => $state['charge'], + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING); + + SystemLogger::dispatch( + ['response' => $state, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + if(!$client_present) + return $payment; + + return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + } + + + public function paymentResponse($request) { $this->stripe->init(); + //it may be a payment intent here. + if($request->input('client_secret') != '') + return $this->handlePaymentIntentResponse($request); + $source = ClientGatewayToken::query() ->where('id', $this->decodePrimaryKey($request->source)) ->where('company_id', auth()->guard('contact')->user()->client->company->id) @@ -242,6 +441,9 @@ class ACH $description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}"; } + if(substr($source->token, 0, 2) === "pm") + return $this->paymentIntentTokenBilling($amount, $invoice, $description, $source); + try { $state['charge'] = \Stripe\Charge::create([ 'amount' => $state['amount'], @@ -270,6 +472,7 @@ class ACH } } + public function processPendingPayment($state, $client_present = true) { $this->stripe->init(); @@ -321,12 +524,14 @@ class ACH private function storePaymentMethod($method, $payment_method_id, $customer) { + $state = property_exists($method, 'state') ? $method->state : 'unauthorized'; + try { $payment_meta = new \stdClass; $payment_meta->brand = (string) \sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach')); $payment_meta->last4 = (string) $method->last4; $payment_meta->type = GatewayType::BANK_TRANSFER; - $payment_meta->state = 'unauthorized'; + $payment_meta->state = $state; $data = [ 'payment_meta' => $payment_meta, diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php new file mode 100644 index 000000000000..dd2cd68c125f --- /dev/null +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php @@ -0,0 +1,128 @@ +stripe_request = $stripe_request; + $this->company_key = $company_key; + $this->company_gateway_id = $company_gateway_id; + } + + public function handle() + { + + MultiDB::findAndSetDbByCompanyKey($this->company_key); + + $company = Company::where('company_key', $this->company_key)->first(); + + foreach ($this->stripe_request as $transaction) { + + if(array_key_exists('payment_intent', $transaction)) + { + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where(function ($query) use ($transaction) { + $query->where('transaction_reference', $transaction['payment_intent']) + ->orWhere('transaction_reference', $transaction['id']); + }) + ->first(); + + } + else + { + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where('transaction_reference', $transaction['id']) + ->first(); + + } + + if ($payment) { + + $client = $payment->client; + + if($payment->status_id == Payment::STATUS_PENDING) + $payment->service()->deletePayment(); + + $payment->status_id = Payment::STATUS_FAILED; + $payment->save(); + + $payment_hash = PaymentHash::where('payment_id', $payment->id)->first(); + + if($payment_hash) + { + + $error = ctrans('texts.client_payment_failure_body', [ + 'invoice' => implode(",", $payment->invoices->pluck('number')->toArray()), + 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total]); + + } + else + $error = "Payment for " . $payment->client->present()->name(). " for {$payment->amount} failed"; + + if(array_key_exists('failure_message', $transaction)){ + + $error .= "\n\n" .$transaction['failure_message']; + } + + PaymentFailedMailer::dispatch( + $payment_hash, + $client->company, + $client, + $error + ); + + + } + + } + + } + +} \ No newline at end of file diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php index a7b11bda0377..dfbe20efcf49 100644 --- a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php @@ -53,11 +53,7 @@ class PaymentIntentWebhook implements ShouldQueue public function handle() { - // nlog($this->stripe_request); - // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['gateway_type_id']); - // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash']); - // nlog(optional($this->stripe_request['object']['charges']['data'][0]['payment_method_details']['card'])['brand']); - + MultiDB::findAndSetDbByCompanyKey($this->company_key); $company = Company::where('company_key', $this->company_key)->first(); diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index cf47f1bd8e0b..ddcc5e274eb3 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -38,6 +38,7 @@ use App\PaymentDrivers\Stripe\EPS; use App\PaymentDrivers\Stripe\FPX; use App\PaymentDrivers\Stripe\GIROPAY; use App\PaymentDrivers\Stripe\ImportCustomers; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; use App\PaymentDrivers\Stripe\PRZELEWY24; use App\PaymentDrivers\Stripe\SEPA; @@ -47,6 +48,7 @@ use App\PaymentDrivers\Stripe\Utilities; use App\PaymentDrivers\Stripe\iDeal; use App\Utils\Traits\MakesHash; use Exception; +use Google\Service\ServiceConsumerManagement\CustomError; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Carbon; use Laracasts\Presenter\Exceptions\PresenterException; @@ -409,6 +411,16 @@ class StripePaymentDriver extends BaseDriver return $this->company_gateway->getPublishableKey(); } + public function getCustomer($customer_id) :?Customer + { + $customer = Customer::retrieve($customer_id, $this->stripe_connect_auth); + + if($customer) + return $customer; + + return false; + } + /** * Finds or creates a Stripe Customer object. * @@ -568,15 +580,21 @@ class StripePaymentDriver extends BaseDriver //payment_intent.succeeded - this will confirm or cancel the payment if($request->type === 'payment_intent.succeeded'){ - PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(10)); + PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2,10))); return response()->json([], 200); } + if(in_array($request->type, ['payment_intent.payment_failed','charge.failed'])){ + PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2,10))); + return response()->json([], 200); + } + + if ($request->type === 'charge.succeeded') { foreach ($request->data as $transaction) { - if(array_key_exists('payment_intent', $transaction)) + if(array_key_exists('payment_intent', $transaction) && $transaction['payment_intent']) { $payment = Payment::query() // ->where('company_id', $request->getCompany()->id) diff --git a/app/Services/Chart/ChartQueries.php b/app/Services/Chart/ChartQueries.php index 8771bdc2afbe..6224f98d3c53 100644 --- a/app/Services/Chart/ChartQueries.php +++ b/app/Services/Chart/ChartQueries.php @@ -117,7 +117,7 @@ trait ChartQueries GROUP BY invoices.date HAVING currency_id = :currency_id "), [ - 'company_currency' => $this->company->settings->currency_id, + 'company_currency' => (int)$this->company->settings->currency_id, 'currency_id' => $currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 6ae012e3282f..4a5c8cafca99 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -123,13 +123,14 @@ class AutoBillInvoice extends AbstractService ->setPaymentHash($payment_hash) ->tokenBilling($gateway_token, $payment_hash); } catch (\Exception $e) { - $this->invoice->auto_bill_tries = $number_of_retries + 1; + $this->invoice->auto_bill_tries += 1; if ($this->invoice->auto_bill_tries == 3) { $this->invoice->auto_bill_enabled = false; $this->invoice->auto_bill_tries = 0; //reset the counter here in case auto billing is turned on again in the future. $this->invoice->save(); } + nlog("payment NOT captured for " . $this->invoice->number . " with error " . $e->getMessage()); } diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index 896e8baead67..86e304182b6e 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -46,14 +46,6 @@ class DeletePayment ->save(); } - //reverse paymentables->invoices - - //reverse paymentables->credits - - //set refunded to amount - - //set applied amount to 0 - private function cleanupPayment() { $this->payment->is_deleted = true; diff --git a/config/app.php b/config/app.php index 8e4e58375b7a..34a1ce6dc6fb 100644 --- a/config/app.php +++ b/config/app.php @@ -66,7 +66,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => env('SERVER_TIMEZONE', 'UTC'), /* |-------------------------------------------------------------------------- diff --git a/config/ninja.php b/config/ninja.php index 08f79e716c7f..26295c816beb 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -155,6 +155,10 @@ return [ 'designs' => [ 'base_path' => resource_path('views/pdf-designs/'), ], + 'maintenance' => [ + 'delete_pdfs' => env('DELETE_PDF_DAYS', 0), + 'delete_backups' => env('DELETE_BACKUP_DAYS', 0), + ], 'log_pdf_html' => env('LOG_PDF_HTML', false), 'expanded_logging' => env('EXPANDED_LOGGING', false), 'snappdf_chromium_path' => env('SNAPPDF_CHROMIUM_PATH', false), diff --git a/database/migrations/2022_05_18_055442_update_custom_value_four_columns.php b/database/migrations/2022_05_18_055442_update_custom_value_four_columns.php new file mode 100644 index 000000000000..257cf3556cea --- /dev/null +++ b/database/migrations/2022_05_18_055442_update_custom_value_four_columns.php @@ -0,0 +1,142 @@ +text('custom_value4')->change(); + + }); + + Schema::table('client_contacts', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('clients', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('clients', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('documents', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('expenses', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('invoices', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('payments', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('products', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('projects', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('quotes', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('recurring_invoices', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('recurring_quotes', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('recurring_expenses', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('tasks', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('users', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('vendors', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + Schema::table('vendor_contacts', function (Blueprint $table) { + + $table->text('custom_value4')->change(); + + }); + + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/public/images/checkmark-round.svg b/public/images/checkmark-round.svg new file mode 100644 index 000000000000..cdd8105662da --- /dev/null +++ b/public/images/checkmark-round.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/checkmark.svg b/public/images/checkmark.svg new file mode 100644 index 000000000000..d44ffbf0ad6e --- /dev/null +++ b/public/images/checkmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/test.svg b/public/images/test.svg new file mode 100644 index 000000000000..82e4f790d8c1 --- /dev/null +++ b/public/images/test.svg @@ -0,0 +1,11 @@ + + Vector 1580-svg + + + + + + + \ No newline at end of file diff --git a/public/js/clients/payments/stripe-ach.js b/public/js/clients/payments/stripe-ach.js index a0cd224a2119..4cad4ced3f04 100755 --- a/public/js/clients/payments/stripe-ach.js +++ b/public/js/clients/payments/stripe-ach.js @@ -1,2 +1,2 @@ /*! For license information please see stripe-ach.js.LICENSE.txt */ -(()=>{function e(e,t){for(var n=0;n svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),r.errors.textContent="",r.errors.textContent=e,r.errors.hidden=!1})),t(this,"handleSuccess",(function(e){document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()})),t(this,"handleSubmit",(function(e){document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),r.errors.textContent="",r.errors.hidden=!0,r.stripe.createToken("bank_account",r.getFormData()).then((function(e){return e.hasOwnProperty("error")?r.handleError(e.error.message):r.handleSuccess(e)}))})),this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=null===(e=document.querySelector('meta[name="stripe-account-id"]'))||void 0===e?void 0:e.content}var r,o,u;return r=n,(o=[{key:"handle",value:function(){var e=this;document.getElementById("save-button").addEventListener("click",(function(t){return e.handleSubmit(t)}))}}])&&e(r.prototype,o),u&&e(r,u),n}())).setupStripe().handle()})(); \ No newline at end of file +(()=>{function e(e,t){for(var n=0;n svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),r.errors.textContent="",r.errors.textContent=e,r.errors.hidden=!1})),t(this,"handleSuccess",(function(e){document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()})),t(this,"handleSubmit",(function(e){if(!document.getElementById("accept-terms").checked)return errors.textContent="You must accept the mandate terms prior to making payment.",void(errors.hidden=!1);document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),r.errors.textContent="",r.errors.hidden=!0,r.stripe.createToken("bank_account",r.getFormData()).then((function(e){return e.hasOwnProperty("error")?r.handleError(e.error.message):r.handleSuccess(e)}))})),this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=null===(e=document.querySelector('meta[name="stripe-account-id"]'))||void 0===e?void 0:e.content}var r,o,u;return r=n,(o=[{key:"handle",value:function(){var e=this;document.getElementById("save-button").addEventListener("click",(function(t){return e.handleSubmit(t)}))}}])&&e(r.prototype,o),u&&e(r,u),n}())).setupStripe().handle()})(); \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json index b38b596ecded..9068d75070a0 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -2,7 +2,7 @@ "/js/app.js": "/js/app.js?id=0e3959ab851d3350364d", "/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=de4468c682d6861847de", "/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=cfe5de1cf87a0b01568d", - "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=a5f14c885c3aeef6c744", + "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=50d964c4a3ffa7f2f99f", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=6b79265cbb8c963eef19", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=2cccf9e51b60a0ab17b8", "/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=22fc06e698dea2c3bdf3", diff --git a/resources/js/clients/payments/stripe-ach.js b/resources/js/clients/payments/stripe-ach.js index e8f84827f78b..7002c83fecdb 100644 --- a/resources/js/clients/payments/stripe-ach.js +++ b/resources/js/clients/payments/stripe-ach.js @@ -70,6 +70,13 @@ class AuthorizeACH { }; handleSubmit = (e) => { + + if (!document.getElementById('accept-terms').checked) { + errors.textContent = "You must accept the mandate terms prior to making payment."; + errors.hidden = false; + return; + } + document.getElementById('save-button').disabled = true; document.querySelector('#save-button > svg').classList.remove('hidden'); document.querySelector('#save-button > span').classList.add('hidden'); diff --git a/resources/views/pdf-designs/business.html b/resources/views/pdf-designs/business.html index 2f6a42656729..84a1cd981512 100644 --- a/resources/views/pdf-designs/business.html +++ b/resources/views/pdf-designs/business.html @@ -105,7 +105,7 @@ [data-ref="table"] { margin-top: 0.5rem; - /* margin-bottom: 200px; */ + margin-bottom: 50px; min-width: 100%; table-layout: fixed; overflow-wrap: break-word; diff --git a/resources/views/pdf-designs/clean.html b/resources/views/pdf-designs/clean.html index fe5109228a29..f0da1c52d4b7 100644 --- a/resources/views/pdf-designs/clean.html +++ b/resources/views/pdf-designs/clean.html @@ -101,7 +101,7 @@ [data-ref="table"] { margin-top: 1rem; - /* margin-bottom: 200px; */ + margin-bottom: 50px; min-width: 100%; table-layout: fixed; overflow-wrap: break-word; diff --git a/resources/views/pdf-designs/creative.html b/resources/views/pdf-designs/creative.html index 49808d33f6e3..c68b769f1fab 100644 --- a/resources/views/pdf-designs/creative.html +++ b/resources/views/pdf-designs/creative.html @@ -95,7 +95,7 @@ } [data-ref="table"] { - /* margin-bottom: 200px; */ + margin-bottom: 50px; min-width: 100%; table-layout: fixed; overflow-wrap: break-word; diff --git a/resources/views/pdf-designs/elegant.html b/resources/views/pdf-designs/elegant.html index 7795518238a1..3843b50c50f7 100644 --- a/resources/views/pdf-designs/elegant.html +++ b/resources/views/pdf-designs/elegant.html @@ -95,7 +95,7 @@ [data-ref="table"] { margin-top: 3rem; - /* margin-bottom: 200px; */ + margin-bottom: 50px; min-width: 100%; table-layout: fixed; overflow-wrap: break-word; diff --git a/resources/views/pdf-designs/hipster.html b/resources/views/pdf-designs/hipster.html index a6a54310cc1c..63de4dd2a15d 100644 --- a/resources/views/pdf-designs/hipster.html +++ b/resources/views/pdf-designs/hipster.html @@ -107,7 +107,7 @@ [data-ref="table"] { margin-top: 1rem; - /* margin-bottom: 200px; */ + margin-bottom: 50px; min-width: 100%; table-layout: fixed; overflow-wrap: break-word; diff --git a/resources/views/pdf-designs/plain.html b/resources/views/pdf-designs/plain.html index 2561aac89d7a..c1a5ab55dade 100644 --- a/resources/views/pdf-designs/plain.html +++ b/resources/views/pdf-designs/plain.html @@ -84,7 +84,7 @@ min-width: 100%; table-layout: fixed; overflow-wrap: break-word; - /* margin-bottom: 200px; */ + margin-bottom: 50px; } .task-time-details { diff --git a/resources/views/pdf-designs/playful.html b/resources/views/pdf-designs/playful.html index 6b3be643a5e0..43d0d0a46842 100644 --- a/resources/views/pdf-designs/playful.html +++ b/resources/views/pdf-designs/playful.html @@ -110,7 +110,7 @@ padding-left: 2rem; padding-right: 0rem; margin-top: 1rem; - /* margin-bottom: 200px; */ + margin-bottom: 50px; min-width: 100%; table-layout: fixed; overflow-wrap: break-word; diff --git a/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php b/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php index 8044253e0108..b25bb252b53a 100644 --- a/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php +++ b/resources/views/portal/ninja2020/components/general/sidebar/main.blade.php @@ -1,5 +1,5 @@
diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php index c5e03cec9f8d..d88aeb847fa8 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php @@ -27,6 +27,10 @@ +
+

Adding a bank account here requires verification, which may take several days. In order to use Instant Verification please pay an invoice first, this process will automatically verify your bank account.

+
+ @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')]) @@ -39,14 +43,18 @@ @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')]) - + @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')]) @endcomponent @@ -55,7 +63,11 @@ @endcomponent diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php index 684287124574..31e56758f9d0 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php @@ -1,64 +1,227 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) +@section('gateway_head') + @if($gateway->company_gateway->getConfigField('account_id')) + + + @else + + @endif + + + + +@endsection + @section('gateway_content') + + +
+ @csrf + + + + + + + + + + +
+ @if(count($tokens) > 0) - @include('portal.ninja2020.gateways.includes.payment_details') -
- @csrf - - - - - - - -
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) @if(count($tokens) > 0) +
    @foreach($tokens as $token) - +
  • + +
  • @endforeach - @endisset +
+ @endif @endcomponent - @else - @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false]) - {{ ctrans('texts.bank_account_not_linked') }} - {{ ctrans('texts.add_payment_method') }} - @endcomponent - @endif - @include('portal.ninja2020.gateways.includes.pay_now') + + @else + + @component('portal.ninja2020.components.general.card-element-single') + + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')]) + + @endcomponent + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.email')]) + + @endcomponent +
+
+ Connect a bank account +
+
+ +
+
+ @endif + @endsection @push('footer') - + + -@endpush + + } + + document.getElementById('new-bank').addEventListener('click', (ev) => { + + if (!document.getElementById('accept-terms').checked) { + errors.textContent = "You must accept the mandate terms prior to making payment."; + errors.hidden = false; + return; + } + + errors.hidden = true; + + let stripe; + + let publishableKey = document.querySelector('meta[name="stripe-publishable-key"]').content + + let stripeConnect = document.querySelector('meta[name="stripe-account-id"]')?.content + + if(stripeConnect){ + stripe = Stripe(publishableKey, { stripeAccount: stripeConnect}); + } + else { + stripe = Stripe(publishableKey); + } + + let newBankButton = document.getElementById('new-bank'); + newBankButton.disabled = true; + newBankButton.querySelector('svg').classList.remove('hidden'); + newBankButton.querySelector('span').classList.add('hidden'); + + ev.preventDefault(); + const accountHolderNameField = document.getElementById('account-holder-name-field'); + const emailField = document.getElementById('email-field'); + const clientSecret = document.querySelector('meta[name="client_secret"]')?.content; + // Calling this method will open the instant verification dialog. + stripe.collectBankAccountForPayment({ + clientSecret: clientSecret, + params: { + payment_method_type: 'us_bank_account', + payment_method_data: { + billing_details: { + name: accountHolderNameField.value, + email: emailField.value, + }, + }, + }, + expand: ['payment_method'], + }) + .then(({paymentIntent, error}) => { + if (error) { + + console.error(error.message); + errors.textContent = error.message; + errors.hidden = false; + resetButtons(); + + // PaymentMethod collection failed for some reason. + } else if (paymentIntent.status === 'requires_payment_method') { + // Customer canceled the hosted verification modal. Present them with other + // payment method type options. + + errors.textContent = "We were unable to process the payment with this account, please try another one."; + errors.hidden = false; + resetButtons(); + return; + + } else if (paymentIntent.status === 'requires_confirmation') { + + let bank_account_response = document.getElementById('bank_account_response'); + bank_account_response.value = JSON.stringify(paymentIntent); + + confirmPayment(stripe, clientSecret); + } + + }); + }); + + function confirmPayment(stripe, clientSecret){ + stripe.confirmUsBankAccountPayment(clientSecret) + .then(({paymentIntent, error}) => { + console.log(paymentIntent); + if (error) { + console.error(error.message); + // The payment failed for some reason. + } else if (paymentIntent.status === "requires_payment_method") { + // Confirmation failed. Attempt again with a different payment method. + + errors.textContent = "We were unable to process the payment with this account, please try another one."; + errors.hidden = false; + resetButtons(); + + } else if (paymentIntent.status === "processing") { + // Confirmation succeeded! The account will be debited. + + let gateway_response = document.getElementById('gateway_response'); + gateway_response.value = JSON.stringify(paymentIntent); + document.getElementById('server-response').submit(); + + } else if (paymentIntent.next_action?.type === "verify_with_microdeposits") { + + } + }); + + } + + function resetButtons() + { + + let newBankButton = document.getElementById('new-bank'); + newBankButton.disabled = false; + newBankButton.querySelector('svg').classList.add('hidden'); + newBankButton.querySelector('span').classList.remove('hidden'); + + } + +@endpush \ No newline at end of file diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/pay_instant_verification.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/pay_instant_verification.blade.php new file mode 100644 index 000000000000..007b204e0ffc --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/ach/pay_instant_verification.blade.php @@ -0,0 +1,68 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) + +@section('gateway_content') + @if(count($tokens) > 0) + + + @include('portal.ninja2020.gateways.includes.payment_details') + +
+ @csrf + + + + + + + + +
+ + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if(count($tokens) > 0) + @foreach($tokens as $token) + + @endforeach + @endisset + @endcomponent + + @include('portal.ninja2020.gateways.includes.pay_now') + + @else + + @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false]) + Pay with a new bank account. + + + @endcomponent + + @endif + +@endsection + +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php index 8a91de072bb8..8508977d1f48 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php @@ -11,7 +11,6 @@ @endphp - @section('gateway_head') @if($gateway->company_gateway->getConfigField('account_id')) @@ -48,32 +47,40 @@ @include('portal.ninja2020.gateways.includes.payment_details') @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) +
    @if(count($tokens) > 0) @foreach($tokens as $token) +
  • +
  • @endforeach @endisset - +
  • + +
  • +
+ @endcomponent @include('portal.ninja2020.gateways.stripe.includes.card_widget') @include('portal.ninja2020.gateways.includes.pay_now') + @endsection @section('gateway_footer') diff --git a/resources/views/portal/ninja2020/gateways/stripe/plaid/auth.blade.php b/resources/views/portal/ninja2020/gateways/stripe/plaid/auth.blade.php new file mode 100644 index 000000000000..da23ba1ab8d5 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/plaid/auth.blade.php @@ -0,0 +1,57 @@ + + + + \ No newline at end of file diff --git a/resources/views/portal/ninja2020/plan/trial.blade.php b/resources/views/portal/ninja2020/plan/trial.blade.php index 95c6ed3e9b24..cfdb3635245b 100644 --- a/resources/views/portal/ninja2020/plan/trial.blade.php +++ b/resources/views/portal/ninja2020/plan/trial.blade.php @@ -2,261 +2,1318 @@ @section('meta_title', ctrans('texts.account_management')) @section('body') + - - + + -
- -
-

Start your 14 day Pro Trial!

-

Enjoy 14 days of our Pro Plan

- -
- -
    -
  • Unlimited Clients & Invoices & Quotes
  • -
  • Remove "Created by Invoice Ninja"
  • -
  • 10 Professional Invoice & Quote Templates
  • -
  • Send Invoice Emails Sent via Your Gmail
  • -
  • Attach Invoice PDF's to Client Emails
  • -
  • Customize Auto-Reminder Emails
  • -
  • Display Client E-Signatures on Invoices
  • -
  • Enable a Client "Approve Terms' Checkbox
  • -
  • Interlink Multiple Companies (x10) with 1 Login
  • -
  • Customize Invoice Designs & Email Templates
  • -
  • Custom Settings for Different Client "Groups"
  • -
  • Client Subscriptions: Recurring & Auto-billing
  • -
  • Password Protected Client-Side Portal
  • -
  • API Integration with 3rd Party Apps
  • -
  • & Much More!
  • -
- -
- -
-
- -
- @csrf - - - -
- -
- -
- -
-
- -
- -
- -
-
- +
+
+
+

+ Enjoy 14 days of our Pro Plan +

+
    +
  • Unlimited Clients & Invoices & Quotes
  • +
  • Remove "Created by Invoice Ninja"
  • +
  • 10 Professional Invoice & Quote Templates
  • +
  • Send Invoice Emails Sent via Your Gmail
  • +
  • Attach Invoice PDF's to Client Emails
  • +
  • Customize Auto-Reminder Emails
  • +
  • Display Client E-Signatures on Invoices
  • +
  • Enable a Client "Approve Terms' Checkbox
  • +
+

+ & Much More! +

+
+
+
+
+

+ Start your 14 day Pro Trial! +

+ + + @csrf + + +
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+ * At the end of your 14 day trial your card will be charged + $10/month. Cancel anytime. +
+ +
+
-
+
+

+ Skip the 14-day trial and get a discounted annual upgrade! +

+
+
+
+
+

PRO Plan

+

+ $100/year +

+

+ Buy 10 months, get 2 free! +

+
+
+
Whats included
+
    +
  • Unlimited Clients & Invoicing
  • +
  • Remove "Created by Invoice Ninja"
  • +
  • API Integration with 3rd Party Apps
  • +
+

+ & Much More! +

+ Buy Now! +
+
+
+
+
+
+

Enterprise Plan

+

+ $140/year +

+

+ Buy 10 months, get 2 free! +

+
+
+
Whats included
+
    +
  • Additional Account Users
  • +
  • Fully Branded Client Portal
  • +
  • Attach 3rd Party Documents
  • +
-
-
- +

+ & Much More! +

+ Buy Now! +
+
+
+
-
- -
-
- -
-
-
- -
- -
- -
-
-
- -
- -
-
- * At the end of your 14 day trial your card will be charged $10/month. Cancel anytime. -
- - - -
- - -
-
- -
- Discounted Plans -
-
-
- - -
-
-
-

- Enterprise Plan (1-2 Users) Annual - Buy 10 months, get 2 free! $140 -

-
- -
-
-
-
- -
-
-
-

- Pro Plan Annual - Buy 10 months, get 2 free! $100 -

-
- -
-
-
-
@endsection