Merge pull request #5727 from turbo124/v5-develop

Email invoice paid receipts
This commit is contained in:
David Bomba 2021-05-17 20:16:58 +10:00 committed by GitHub
commit 7f21696cca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 554 additions and 29 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Jobs\Util\ImportStripeCustomers;
use App\Jobs\Util\StripeUpdatePaymentMethods;
class StripeController extends BaseController
{
public function update()
{
if(auth()->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);
}
}

View File

@ -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');
}
}

View File

@ -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.');
}
}

View File

@ -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'];

View File

@ -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']);

View File

@ -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);

View File

@ -0,0 +1,68 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Util;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ImportStripeCustomers implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $company;
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
/**
* Create a new job instance.
*
* @param $event_id
* @param $entity
*/
public function __construct($company)
{
$this->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());
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Util;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StripeUpdatePaymentMethods implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $company;
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
/**
* Create a new job instance.
*
* @param $event_id
* @param $entity
*/
public function __construct($company)
{
$this->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());
}
}

View File

@ -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;
}

View File

@ -0,0 +1,115 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Factory\ClientContactFactory;
use App\Factory\ClientGatewayTokenFactory;
use App\Models\Client;
use App\Models\ClientGatewayToken;
use App\Models\Country;
use App\Models\Currency;
use App\Models\GatewayType;
use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Traits\MakesHash;
use Stripe\Customer;
use Stripe\PaymentMethod;
class ImportCustomers
{
use MakesHash;
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->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();
}
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Factory\ClientGatewayTokenFactory;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Traits\MakesHash;
use Stripe\Customer;
use Stripe\PaymentMethod;
class UpdatePaymentMethods
{
use MakesHash;
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->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;
}
}
}

View File

@ -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
}
}

View File

@ -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':

View File

@ -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');