diff --git a/app/Factory/CreditFactory.php b/app/Factory/CreditFactory.php index 4a34ece9820e..154352c46389 100644 --- a/app/Factory/CreditFactory.php +++ b/app/Factory/CreditFactory.php @@ -37,10 +37,10 @@ class CreditFactory $credit->tax_rate1 = 0; $credit->tax_name2 = ''; $credit->tax_rate2 = 0; - $credit->custom_value1 = 0; - $credit->custom_value2 = 0; - $credit->custom_value3 = 0; - $credit->custom_value4 = 0; + $credit->custom_value1 = ''; + $credit->custom_value2 = ''; + $credit->custom_value3 = ''; + $credit->custom_value4 = ''; $credit->amount = 0; $credit->balance = 0; $credit->partial = 0; diff --git a/app/Factory/InvoiceFactory.php b/app/Factory/InvoiceFactory.php index c69ae41e794d..fec4e05e3a62 100644 --- a/app/Factory/InvoiceFactory.php +++ b/app/Factory/InvoiceFactory.php @@ -38,10 +38,10 @@ class InvoiceFactory $invoice->tax_rate2 = 0; $invoice->tax_name3 = ''; $invoice->tax_rate3 = 0; - $invoice->custom_value1 = 0; - $invoice->custom_value2 = 0; - $invoice->custom_value3 = 0; - $invoice->custom_value4 = 0; + $invoice->custom_value1 = ''; + $invoice->custom_value2 = ''; + $invoice->custom_value3 = ''; + $invoice->custom_value4 = ''; $invoice->amount = 0; $invoice->balance = 0; $invoice->paid_to_date = 0; diff --git a/app/Factory/RecurringInvoiceFactory.php b/app/Factory/RecurringInvoiceFactory.php index 159fdce4da2a..8603c30812f9 100644 --- a/app/Factory/RecurringInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceFactory.php @@ -36,10 +36,10 @@ class RecurringInvoiceFactory $invoice->tax_rate1 = 0; $invoice->tax_name2 = ''; $invoice->tax_rate2 = 0; - $invoice->custom_value1 = 0; - $invoice->custom_value2 = 0; - $invoice->custom_value3 = 0; - $invoice->custom_value4 = 0; + $invoice->custom_value1 = ''; + $invoice->custom_value2 = ''; + $invoice->custom_value3 = ''; + $invoice->custom_value4 = ''; $invoice->amount = 0; $invoice->balance = 0; $invoice->partial = 0; diff --git a/app/Factory/RecurringQuoteFactory.php b/app/Factory/RecurringQuoteFactory.php index 18a9991eb800..28aba86ea583 100644 --- a/app/Factory/RecurringQuoteFactory.php +++ b/app/Factory/RecurringQuoteFactory.php @@ -35,10 +35,10 @@ class RecurringQuoteFactory $quote->tax_rate1 = 0; $quote->tax_name2 = ''; $quote->tax_rate2 = 0; - $quote->custom_value1 = 0; - $quote->custom_value2 = 0; - $quote->custom_value3 = 0; - $quote->custom_value4 = 0; + $quote->custom_value1 = ''; + $quote->custom_value2 = ''; + $quote->custom_value3 = ''; + $quote->custom_value4 = ''; $quote->amount = 0; $quote->balance = 0; $quote->partial = 0; diff --git a/app/Http/Controllers/StripeController.php b/app/Http/Controllers/StripeController.php new file mode 100644 index 000000000000..d1d3d3f54715 --- /dev/null +++ b/app/Http/Controllers/StripeController.php @@ -0,0 +1,52 @@ +user()->isAdmin()) + { + + StripeUpdatePaymentMethods::dispatch(auth()->user()->getCompany()); + + return response()->json(['message' => 'Processing'], 403); + + } + + + return response()->json(['message' => 'Unauthorized'], 403); + } + + public function import() + { + + if(auth()->user()->isAdmin()) + { + + ImportStripeCustomers::dispatch(auth()->user()->getCompany()); + + return response()->json(['message' => 'Processing'], 403); + + } + + + return response()->json(['message' => 'Unauthorized'], 403); + } + +} \ No newline at end of file diff --git a/app/Http/Middleware/SetDomainNameDb.php b/app/Http/Middleware/SetDomainNameDb.php index 260de03f8a7d..8bfde19417ce 100644 --- a/app/Http/Middleware/SetDomainNameDb.php +++ b/app/Http/Middleware/SetDomainNameDb.php @@ -52,7 +52,9 @@ class SetDomainNameDb if ($request->json) { return response()->json($error, 403); } else { - abort(400, 'Domain not found'); + MultiDB::setDb('db-ninja-01'); + nlog("I could not set the DB - defaulting to DB1"); + //abort(400, 'Domain not found'); } } @@ -68,7 +70,9 @@ class SetDomainNameDb if ($request->json) { return response()->json($error, 403); } else { - abort(400, 'Domain not found'); + MultiDB::setDb('db-ninja-01'); + nlog("I could not set the DB - defaulting to DB1"); + //abort(400, 'Domain not found'); } } diff --git a/app/Http/Middleware/SetInviteDb.php b/app/Http/Middleware/SetInviteDb.php index fd66829c62d1..b9067b4cf3df 100644 --- a/app/Http/Middleware/SetInviteDb.php +++ b/app/Http/Middleware/SetInviteDb.php @@ -28,7 +28,7 @@ class SetInviteDb public function handle($request, Closure $next) { $error = [ - 'message' => 'Invalid URL', + 'message' => 'I could not find the database for this object.', 'errors' => new stdClass, ]; /* @@ -46,7 +46,7 @@ class SetInviteDb if (request()->json) { return response()->json($error, 403); } else { - abort(404); + abort(404,'I could not find the database for this object.'); } } diff --git a/app/Http/Requests/Company/StoreCompanyRequest.php b/app/Http/Requests/Company/StoreCompanyRequest.php index 6cd74a3c3fb8..ac18ed1fc854 100644 --- a/app/Http/Requests/Company/StoreCompanyRequest.php +++ b/app/Http/Requests/Company/StoreCompanyRequest.php @@ -55,8 +55,9 @@ class StoreCompanyRequest extends Request { $input = $this->all(); - if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) - $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']); + //https not sure i should be forcing this. + // if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) + // $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']); if (array_key_exists('google_analytics_url', $input)) { $input['google_analytics_key'] = $input['google_analytics_url']; diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index 2c36c9bbdac9..0c7532160d9b 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -60,8 +60,8 @@ class UpdateCompanyRequest extends Request { $input = $this->all(); - if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) - $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']); + // if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1) + // $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']); if (array_key_exists('settings', $input)) { $input['settings'] = $this->filterSaveableSettings($input['settings']); diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index e3a2a267fcd3..de18ff547e77 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -1264,7 +1264,7 @@ class Import implements ShouldQueue $modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']); } - if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){ + else if(Ninja::isHosted() && !Ninja::isPaidHostedClient() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){ $modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34'; $modified['fees_and_limits'] = []; } @@ -1296,6 +1296,8 @@ class Import implements ShouldQueue $modified['company_id'] = $this->company->id; $modified['client_id'] = $this->transformId('clients', $resource['client_id']); + $modified['company_gateway_id'] = $this->transformId('company_gateways', $resource['company_gateway_id']); + //$modified['user_id'] = $this->processUserId($resource); $cgt = ClientGatewayToken::Create($modified); diff --git a/app/Jobs/Util/ImportStripeCustomers.php b/app/Jobs/Util/ImportStripeCustomers.php new file mode 100644 index 000000000000..ed1592955990 --- /dev/null +++ b/app/Jobs/Util/ImportStripeCustomers.php @@ -0,0 +1,68 @@ +company = $company; + } + + /** + * Execute the job. + * + * @return bool + */ + public function handle() + { + + MultiDB::setDb($this->company->db); + + $cgs = CompanyGateway::where('company_id', $this->company->id) + ->whereIn('gateway_key', $this->stripe_keys) + ->get(); + + $cgs->each(function ($company_gateway){ + + $company_gateway->driver(new Client)->importCustomers(); + + }); + + } + + public function failed($exception) + { + nlog("Stripe import customer methods exception"); + nlog($exception->getMessage()); + } +} diff --git a/app/Jobs/Util/StripeUpdatePaymentMethods.php b/app/Jobs/Util/StripeUpdatePaymentMethods.php new file mode 100644 index 000000000000..e2183daa9a43 --- /dev/null +++ b/app/Jobs/Util/StripeUpdatePaymentMethods.php @@ -0,0 +1,68 @@ +company = $company; + } + + /** + * Execute the job. + * + * @return bool + */ + public function handle() + { + + MultiDB::setDb($this->company->db); + + $cgs = CompanyGateway::where('company_id', $this->company->id) + ->whereIn('gateway_key', $this->stripe_keys) + ->get(); + + $cgs->each(function ($company_gateway){ + + $company_gateway->driver(new Client)->updateAllPaymentMethods(); + + }); + + } + + public function failed($exception) + { + nlog("Stripe update payment methods exception"); + nlog($exception->getMessage()); + } +} diff --git a/app/Mail/Engine/PaymentEmailEngine.php b/app/Mail/Engine/PaymentEmailEngine.php index 25a8ae6cb430..cc32c7e26f80 100644 --- a/app/Mail/Engine/PaymentEmailEngine.php +++ b/app/Mail/Engine/PaymentEmailEngine.php @@ -12,6 +12,7 @@ namespace App\Mail\Engine; use App\DataMapper\EmailTemplateDefaults; +use App\Models\Account; use App\Utils\Helpers; use App\Utils\Number; use App\Utils\Traits\MakesDates; @@ -72,6 +73,16 @@ class PaymentEmailEngine extends BaseEmailEngine ->setViewLink('') ->setViewText(''); + if ($this->client->getSetting('pdf_email_attachment') !== false && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + + $this->payment->invoices->each(function ($invoice){ + + $this->setAttachments([$invoice->pdf_file_path()]); + + }); + + } + return $this; } diff --git a/app/PaymentDrivers/Stripe/ImportCustomers.php b/app/PaymentDrivers/Stripe/ImportCustomers.php new file mode 100644 index 000000000000..720b4a5cc65d --- /dev/null +++ b/app/PaymentDrivers/Stripe/ImportCustomers.php @@ -0,0 +1,115 @@ +stripe = $stripe; + } + + public function run() + { + + $this->stripe->init(); + + $customers = Customer::all(); + + foreach($customers as $customer) + { + $this->addCustomer($customer); + } + + /* Now call the update payment methods handler*/ + $this->stripe->updateAllPaymentMethods(); + + } + + private function addCustomer(Customer $customer) + { + + $account = $this->company_gateway->company->account; + + $existing_customer = $this->company_gateway + ->client_gateway_tokens() + ->where('gateway_customer_reference', $customer->id) + ->exists(); + + + if($existing_customer) + return + + $client = ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id); + $client->address1 = $customer->address->line1 ?: ''; + $client->address2 = $customer->address->line2 ?: ''; + $client->city = $customer->address->city ?: ''; + $client->state = $customer->address->state ?: ''; + + if($customer->address->country){ + + $country = Country::where('iso_3166_2', $customer->address->country)->first() + + if($country) + $client->country_id = $country->id; + + } + + if($customer->currency) { + + $currency = Currency::where('code', $customer->currency)->first(); + + if($currency){ + + $settings = $client->settings; + $settings->currency_id = (string)$currency->id; + $client->settings = $settings; + + } + + } + + $client->phone = $customer->phone ?: ''; + $client->name = $customer->name ?: ''; + + if(!$account->isPaidHostedClient() && Client::where('company_id', $this->company_gateway->company_id)->count() <= config('ninja.quotas.free.clients')){ + + $client->save() + + $contact = ClientContactFactory::create($client->company_id, $client->user_id); + $contact->client_id = $client->id; + $contact->first_name = $client->name ?: ''; + $contact->phone = $client->phone ?: ''; + $contact->email = $client->email ?: ''; + $contact->save(); + + } + } +} diff --git a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php new file mode 100644 index 000000000000..360b5f451441 --- /dev/null +++ b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php @@ -0,0 +1,168 @@ +stripe = $stripe; + } + + public function run() + { + $this->stripe->init(); + + $this->stripe + ->company_gateway + ->client_gateway_tokens + ->each(function ($token){ + + $card_methods = PaymentMethod::all([ + 'customer' => $token->gateway_customer_reference, + 'type' => 'card', + ], + $this->stripe->stripe_connect_auth); + + foreach($card_methods as $method) + { + $this->addOrUpdateCard($method, $token, GatewayType::CREDIT_CARD); + } + + $alipay_methods = PaymentMethod::all([ + 'customer' => $token->gateway_customer_reference, + 'type' => 'alipay', + ], + $this->stripe->stripe_connect_auth); + + foreach($alipay_methods as $method) + { + $this->addOrUpdateCard($method, $token, GatewayType::ALIPAY); + } + + $sofort_methods = PaymentMethod::all([ + 'customer' => $token->gateway_customer_reference, + 'type' => 'sofort', + ], + $this->stripe->stripe_connect_auth); + + foreach($alipay_methods as $method) + { + $this->addOrUpdateCard($method, $token, GatewayType::SOFORT); + } + + $bank_accounts = Customer::allSources( + $token->gateway_customer_reference, + ['object' => 'bank_account', 'limit' => 300] + ); + + foreach($bank_accounts as $bank_account) + { + $this->addOrUpdateBankAccount($bank_account, $token); + } + + }); + + } + + private function addOrUpdateBankAccount($bank_account, ClientGatewayToken $token) + { + $token_exists = ClientGatewayToken::where([ + 'gateway_customer_reference' => $token->gateway_customer_reference, + 'token' => $bank_account->id, + ])->exists(); + + /* Already exists return */ + if($token_exists) + return; + + $cgt = ClientGatewayTokenFactory::create($token->company_id); + $cgt->client_id = $token->client_id; + $cgt->token = $bank_account->id; + $cgt->gateway_customer_reference = $token->gateway_customer_reference; + $cgt->company_gateway_id = $token->company_gateway_id; + $cgt->gateway_type_id = GatewayType::BANK_TRANSFER + $cgt->meta = new \stdClass; + $cgt->routing_number = $bank_account->routing_number; + $cgt->save(); + + } + + private function addOrUpdateCard(PaymentMethod $method, ClientGatewayToken $token, GatewayType $type_id) + { + + $token_exists = ClientGatewayToken::where([ + 'gateway_customer_reference' => $token->gateway_customer_reference, + 'token' => $method->id, + ])->exists(); + + /* Already exists return */ + if($token_exists) + return; + + /* Ignore Expired cards */ + if($method->card->exp_year <= date('Y') && $method->card->exp_month < date('m')) + return; + + $cgt = ClientGatewayTokenFactory::create($token->company_id); + $cgt->client_id = $token->client_id; + $cgt->token = $method->id; + $cgt->gateway_customer_reference = $token->gateway_customer_reference; + $cgt->company_gateway_id = $token->company_gateway_id; + $cgt->gateway_type_id = $type_id; + $cgt->meta = $this->buildPaymentMethodMeta($method, $type_id); + $cgt->save(); + + } + + private function buildPaymentMethodMeta(PaymentMethod $method, GatewayType $type_id) + { + + switch ($type_id) { + case GatewayType::CREDIT_CARD: + + $payment_meta = new \stdClass; + $payment_meta->exp_month = (string) $method->card->exp_month; + $payment_meta->exp_year = (string) $method->card->exp_year; + $payment_meta->brand = (string) $method->card->brand; + $payment_meta->last4 = (string) $method->card->last4; + $payment_meta->type = GatewayType::CREDIT_CARD; + return $payment_meta; + + break; + + case GatewayType::ALIPAY: + case GatewayType::SOFORT: + + return new \stdClass; + + default: + + + break; + } + } +} diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 2fdb5f27e5a3..bfbd5df65618 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -25,7 +25,9 @@ use App\PaymentDrivers\Stripe\ACH; use App\PaymentDrivers\Stripe\Alipay; use App\PaymentDrivers\Stripe\Charge; use App\PaymentDrivers\Stripe\CreditCard; +use App\PaymentDrivers\Stripe\ImportCustomers; use App\PaymentDrivers\Stripe\SOFORT; +use App\PaymentDrivers\Stripe\UpdatePaymentMethods; use App\PaymentDrivers\Stripe\Utilities; use App\Utils\Traits\MakesHash; use Exception; @@ -493,4 +495,30 @@ class StripePaymentDriver extends BaseDriver return Account::all(); } + + /** + * Pull all client payment methods and update + * the respective tokens in the system. + * + */ + public function updateAllPaymentMethods() + { + return (new UpdatePaymentMethods($this))->run(); + } + + /** + * Imports stripe customers and their payment methods + * Matches users in the system based on the $match_on_record + * ie. email + * + * Phone + * Email + */ + public function importCustomers() + { + + return (new ImportCustomers($this))->run(); + //match clients based on the gateway_customer_reference column + + } } diff --git a/app/Utils/Traits/Inviteable.php b/app/Utils/Traits/Inviteable.php index 02e04cde8d6a..ec5b0b64b9c3 100644 --- a/app/Utils/Traits/Inviteable.php +++ b/app/Utils/Traits/Inviteable.php @@ -11,6 +11,7 @@ namespace App\Utils\Traits; +use App\Utils\Ninja; use Illuminate\Support\Str; /** @@ -46,7 +47,10 @@ trait Inviteable { $entity_type = Str::snake(class_basename($this->entityType())); - $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain(); + if(Ninja::isHosted()) + $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain(); + else + $domain = config('ninja.app_url'); switch ($this->company->portal_mode) { case 'subdomain': @@ -69,7 +73,10 @@ trait Inviteable public function getPortalLink() :string { - $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain(); + if(Ninja::isHosted()) + $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain(); + else + $domain = config('ninja.app_url'); switch ($this->company->portal_mode) { case 'subdomain': diff --git a/routes/api.php b/routes/api.php index 74b994c693c3..ea1477edd5a8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -183,6 +183,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a // Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe'); // Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe'); + Route::post('stripe/update_payment_methods', 'StripeController@update')->middleware('password_protected')->name('stripe.update'); + Route::post('stripe/import_customers', 'StripeController@import')->middleware('password_protected')->name('stripe.import'); + Route::resource('subscriptions', 'SubscriptionController'); Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk'); @@ -193,9 +196,7 @@ Route::match(['get', 'post'], 'payment_webhook/{company_key}/{company_gateway_id ->name('payment_webhook'); Route::post('api/v1/postmark_webhook', 'PostMarkController@webhook'); - Route::get('token_hash_router', 'OneTimeTokenController@router'); - Route::get('webcron', 'WebCronController@index'); Route::fallback('BaseController@notFound');