From ae0ff13e92f35a04c7a928f92fd294c51632afde Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 5 Jan 2022 10:02:46 +1100 Subject: [PATCH 01/24] Handle negative payments - update client.paid_to_date --- app/Repositories/PaymentRepository.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 4ea1e65340e3..a4a70c834cfe 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -47,9 +47,13 @@ class PaymentRepository extends BaseRepository { */ public function save(array $data, Payment $payment): ?Payment { - if ($payment->amount >= 0) { + // if ($payment->amount >= 0) { + // return $this->applyPayment($data, $payment); + // } + + return $this->applyPayment($data, $payment); - } + return $payment; } @@ -80,8 +84,8 @@ class PaymentRepository extends BaseRepository { $client->service()->updatePaidToDate($data['amount'])->save(); } - elseif($data['amount'] >0){ - + // elseif($data['amount'] >0){ + else{ //this fixes an edge case with unapplied payments $client->service()->updatePaidToDate($data['amount'])->save(); } From 1efdd262fe764d69ce1b72a7f712c76705d418db Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 5 Jan 2022 14:41:57 +1100 Subject: [PATCH 02/24] Slack notifications for bounce/spam emails --- app/Http/Controllers/PostMarkController.php | 10 +++ app/Jobs/Company/CompanyImport.php | 3 +- app/Jobs/RecurringInvoice/SendRecurring.php | 2 +- .../Ninja/EmailBounceNotification.php | 88 +++++++++++++++++++ .../Ninja/EmailSpamNotification.php | 88 +++++++++++++++++++ app/Services/Invoice/AutoBillInvoice.php | 3 +- 6 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 app/Notifications/Ninja/EmailBounceNotification.php create mode 100644 app/Notifications/Ninja/EmailSpamNotification.php diff --git a/app/Http/Controllers/PostMarkController.php b/app/Http/Controllers/PostMarkController.php index 60bb918e688a..5401808119a0 100644 --- a/app/Http/Controllers/PostMarkController.php +++ b/app/Http/Controllers/PostMarkController.php @@ -20,6 +20,8 @@ use App\Models\InvoiceInvitation; use App\Models\QuoteInvitation; use App\Models\RecurringInvoiceInvitation; use App\Models\SystemLog; +use App\Notifications\Ninja\EmailBounceNotification; +use App\Notifications\Ninja\EmailSpamNotification; use Illuminate\Http\Request; use Turbo124\Beacon\Facades\LightLogs; @@ -173,6 +175,10 @@ class PostMarkController extends BaseController LightLogs::create($bounce)->queue(); SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if(config('ninja.notification.slack')) + $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja(); + } // { @@ -215,6 +221,10 @@ class PostMarkController extends BaseController LightLogs::create($spam)->queue(); SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if(config('ninja.notification.slack')) + $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); + } private function discoverInvitation($message_id) diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 52fbdb5b949d..88ee380f3cfc 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -335,11 +335,12 @@ class CompanyImport implements ShouldQueue } } - if($this->company->account->isFreeHostedClient() && $client_count = count($this->getObject('clients', true)) > config('ninja.quotas.free.clients')){ + if($this->company->account->isFreeHostedClient() && (count($this->getObject('clients', true)) > config('ninja.quotas.free.clients')) ){ nlog("client quota busted"); $client_limit = config('ninja.quotas.free.clients'); + $client_count = count($this->getObject('clients', true)); $this->message = "You are attempting to import ({$client_count}) clients, your current plan allows a total of ({$client_limit})"; diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 4d992607a054..8db0d968c122 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -135,7 +135,7 @@ class SendRecurring implements ShouldQueue if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) { try{ - EmailEntity::dispatch($invitation, $invoice->company)->delay(now()->addSeconds(1)); + EmailEntity::dispatch($invitation, $invoice->company); } catch(\Exception $e) { nlog($e->getMessage()); diff --git a/app/Notifications/Ninja/EmailBounceNotification.php b/app/Notifications/Ninja/EmailBounceNotification.php new file mode 100644 index 000000000000..91ba5576150c --- /dev/null +++ b/app/Notifications/Ninja/EmailBounceNotification.php @@ -0,0 +1,88 @@ +account = $account; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return MailMessage + */ + public function toMail($notifiable) + { + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } + + public function toSlack($notifiable) + { + + $content = "Email bounce notification for Account {$this->account->key} \n"; + + $owner = $this->account->companies()->first()->owner(); + + $content .= "Owner {$owner->present()->name() } | {$owner->email}"; + + return (new SlackMessage) + ->success() + ->from(ctrans('texts.notification_bot')) + ->image('https://app.invoiceninja.com/favicon.png') + ->content($content); + } +} diff --git a/app/Notifications/Ninja/EmailSpamNotification.php b/app/Notifications/Ninja/EmailSpamNotification.php new file mode 100644 index 000000000000..2044a870a1bd --- /dev/null +++ b/app/Notifications/Ninja/EmailSpamNotification.php @@ -0,0 +1,88 @@ +account = $account; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['slack']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return MailMessage + */ + public function toMail($notifiable) + { + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } + + public function toSlack($notifiable) + { + + $content = "Email SPAM notification for Account {$this->account->key} \n"; + + $owner = $this->account->companies()->first()->owner(); + + $content .= "Owner {$owner->present()->name() } | {$owner->email}"; + + return (new SlackMessage) + ->success() + ->from(ctrans('texts.notification_bot')) + ->image('https://app.invoiceninja.com/favicon.png') + ->content($content); + } +} diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 2771c3409a2a..43878f421c92 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -123,7 +123,8 @@ class AutoBillInvoice extends AbstractService ->tokenBilling($gateway_token, $payment_hash); } catch(\Exception $e){ - nlog($e->getMessage()); + nlog("payment NOT captured for ". $this->invoice->number . " with error " . $e->getMessage()); + // nlog($e->getMessage()); } if($payment){ From 2362ecb137b2040b923f6a920aa6fed0be515899 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 5 Jan 2022 15:32:07 +1100 Subject: [PATCH 03/24] Minor Fixes --- app/Models/Invoice.php | 1 + app/Services/Invoice/AutoBillInvoice.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index a6297b7034d0..7d2257c6b19a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -101,6 +101,7 @@ class Invoice extends BaseModel 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', + 'is_deleted' => 'bool', ]; protected $with = []; diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 43878f421c92..025e9dcec957 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -105,6 +105,7 @@ class AutoBillInvoice extends AbstractService $fee = 0; /* Build payment hash */ + $payment_hash = PaymentHash::create([ 'hash' => Str::random(64), 'data' => ['invoices' => [['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount, 'invoice_number' => $this->invoice->number]]], From 66843e6397ab2fe1e8cb49bed347c05d6b01a676 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 5 Jan 2022 17:12:10 +1100 Subject: [PATCH 04/24] Set locale for migrated companies --- app/Http/Controllers/MigrationController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/MigrationController.php b/app/Http/Controllers/MigrationController.php index 57abd6fb4f45..0b04cc504ea5 100644 --- a/app/Http/Controllers/MigrationController.php +++ b/app/Http/Controllers/MigrationController.php @@ -304,6 +304,7 @@ class MigrationController extends BaseController App::forgetInstance('translator'); $t = app('translator'); $t->replace(Ninja::transformTranslations($user->account->companies()->first()->settings)); + App::setLocale($user->account->companies()->first()->getLocale()); if(!$existing_company && $company_count >=10) { From 8bb0445725bcbea59da9b6ca455e21f8f9dee014 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 5 Jan 2022 17:26:23 +1100 Subject: [PATCH 05/24] Obfuscate tokens --- app/Http/Controllers/TokenController.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/Http/Controllers/TokenController.php b/app/Http/Controllers/TokenController.php index f095ca2070fe..aacdc145b84c 100644 --- a/app/Http/Controllers/TokenController.php +++ b/app/Http/Controllers/TokenController.php @@ -21,6 +21,7 @@ use App\Http\Requests\Token\StoreTokenRequest; use App\Http\Requests\Token\UpdateTokenRequest; use App\Models\CompanyToken; use App\Repositories\TokenRepository; +use App\Transformers\CompanyTokenHashedTransformer; use App\Transformers\CompanyTokenTransformer; use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\MakesHash; @@ -93,6 +94,8 @@ class TokenController extends BaseController */ public function index(TokenFilters $filters) { + $this->entity_transformer = CompanyTokenHashedTransformer::class; + $tokens = CompanyToken::filter($filters); return $this->listResponse($tokens); @@ -205,6 +208,8 @@ class TokenController extends BaseController */ public function edit(EditTokenRequest $request, CompanyToken $token) { + $this->entity_transformer = CompanyTokenHashedTransformer::class; + return $this->itemResponse($token); } @@ -265,6 +270,8 @@ class TokenController extends BaseController return $request->disallowUpdate(); } + $this->entity_transformer = CompanyTokenHashedTransformer::class; + $token = $this->token_repo->save($request->all(), $token); return $this->itemResponse($token->fresh()); @@ -419,6 +426,8 @@ class TokenController extends BaseController //may not need these destroy routes as we are using actions to 'archive/delete' $token->delete(); + $this->entity_transformer = CompanyTokenHashedTransformer::class; + return $this->itemResponse($token); } @@ -475,6 +484,9 @@ class TokenController extends BaseController */ public function bulk() { + + $this->entity_transformer = CompanyTokenHashedTransformer::class; + $action = request()->input('action'); $ids = request()->input('ids'); From 11d12b26663d996535097b987def6674d08ae344 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 5 Jan 2022 17:30:18 +1100 Subject: [PATCH 06/24] Update lang for bank transfer --- resources/lang/en/texts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 762ca47142bb..aeefce8f7cd6 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4342,7 +4342,7 @@ $LANG = array( 'payment_type_instant_bank_pay' => 'Instant Bank Pay', 'payment_type_iDEAL' => 'iDEAL', 'payment_type_Przelewy24' => 'Przelewy24', - 'payment_type_Mollie Bank Transfer' => 'Bank Transfer', + 'payment_type_Mollie Bank Transfer' => 'Mollie Bank Transfer', 'payment_type_KBC/CBC' => 'KBC/CBC', 'payment_type_Instant Bank Pay' => 'Instant Bank Pay', 'payment_type_Hosted Page' => 'Hosted Page', From e9d9b8a1379f5c5dde54a0fd30432e6fa6f54ec8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 10:19:31 +1100 Subject: [PATCH 07/24] Tests for apple pay --- .../ClientPortal/ApplePayDomainController.php | 105 ++++++++++++++++++ .../ClientPortal/CreditController.php | 9 ++ app/PaymentDrivers/StripePaymentDriver.php | 9 ++ app/Utils/Traits/Uploadable.php | 1 + routes/api.php | 1 + routes/web.php | 1 + .../Feature/ApplePayDomainMerchantUrlTest.php | 64 +++++++++++ 7 files changed, 190 insertions(+) create mode 100644 app/Http/Controllers/ClientPortal/ApplePayDomainController.php create mode 100644 tests/Feature/ApplePayDomainMerchantUrlTest.php diff --git a/app/Http/Controllers/ClientPortal/ApplePayDomainController.php b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php new file mode 100644 index 000000000000..8901102fec0c --- /dev/null +++ b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php @@ -0,0 +1,105 @@ +stripe_keys) + ->where('is_deleted', false) + ->get(); + + foreach($cgs as $cg) + { + + if($cg->getConfigField('appleMerchantId')){ + return response($cg->getConfigField('appleMerchantId'),200); + + } + + } + + return response('', 400); + } + + /* Hosted */ + + $domain_name = $request->getHost(); + + if (strpos($domain_name, 'invoicing.co') !== false) + { + $subdomain = explode('.', $domain_name)[0]; + + $query = [ + 'subdomain' => $subdomain, + 'portal_mode' => 'subdomain', + ]; + + if($company = MultiDB::findAndSetDbByDomain($query)){ + return $this->resolveAppleMerchantId($company); + } + } + + $query = [ + 'portal_domain' => $request->getSchemeAndHttpHost(), + 'portal_mode' => 'domain', + ]; + + if($company = MultiDB::findAndSetDbByDomain($query)){ + return $this->resolveAppleMerchantId($company); + } + + return response('', 400); + + } + + private function resolveAppleMerchantId($company) + { + + $cgs = $company->company_gateways() + ->whereIn('gateway_key', $this->stripe_keys) + ->where('is_deleted', false) + ->get(); + + foreach($cgs as $cg) + { + + if($cg->getConfigField('appleMerchantId')){ + return response($cg->getConfigField('appleMerchantId'),200); + } + + } + + return response('', 400); + + } + +} diff --git a/app/Http/Controllers/ClientPortal/CreditController.php b/app/Http/Controllers/ClientPortal/CreditController.php index 2d5cafaa012d..a72b8b70bd47 100644 --- a/app/Http/Controllers/ClientPortal/CreditController.php +++ b/app/Http/Controllers/ClientPortal/CreditController.php @@ -1,4 +1,13 @@ run(); } + public function setDomain() + { + // \Stripe\ApplePayDomain::create([ + // 'domain_name' => 'example.com', + // ],[ + // 'stripe_account' => '{{CONNECTED_ACCOUNT_ID}}', + // ]); + } + public function disconnect() { if(!$this->stripe_connect) diff --git a/app/Utils/Traits/Uploadable.php b/app/Utils/Traits/Uploadable.php index 604bab5f5b1b..911e11053c96 100644 --- a/app/Utils/Traits/Uploadable.php +++ b/app/Utils/Traits/Uploadable.php @@ -42,4 +42,5 @@ trait Uploadable } } + } diff --git a/routes/api.php b/routes/api.php index ac07a0c107fd..796b50cc43bf 100644 --- a/routes/api.php +++ b/routes/api.php @@ -208,6 +208,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('subscriptions', 'SubscriptionController'); Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk'); Route::get('statics', 'StaticController'); + Route::post('apple_pay/upload_file','ApplyPayController@upload'); }); diff --git a/routes/web.php b/routes/web.php index b8180d7f1583..f29bf383ab55 100644 --- a/routes/web.php +++ b/routes/web.php @@ -44,3 +44,4 @@ Route::get('stripe/completed', 'StripeConnectController@completed')->name('strip Route::get('checkout/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\Checkout3dsController@index')->name('checkout.3ds_redirect'); Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\Mollie3dsController@index')->name('mollie.3ds_redirect'); Route::get('gocardless/ibp_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\GoCardlessController@ibpRedirect')->name('gocardless.ibp_redirect'); +Route::get('.well-known/apple-developer-merchantid-domain-association', 'ClientPortal\ApplePayDomainController@showAppleMerchantId'); diff --git a/tests/Feature/ApplePayDomainMerchantUrlTest.php b/tests/Feature/ApplePayDomainMerchantUrlTest.php new file mode 100644 index 000000000000..869652de1b5a --- /dev/null +++ b/tests/Feature/ApplePayDomainMerchantUrlTest.php @@ -0,0 +1,64 @@ +makeTestData(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testMerchantFieldGet() + { + $config = new \stdClass; + $config->publishableKey = "pk_test"; + $config->apiKey = "sk_test"; + $config->appleMerchantId = "merchant_id"; + + $cg = new CompanyGateway; + $cg->company_id = $this->company->id; + $cg->user_id = $this->user->id; + $cg->gateway_key = 'd14dd26a37cecc30fdd65700bfb55b23'; + $cg->require_cvv = true; + $cg->require_billing_address = true; + $cg->require_shipping_address = true; + $cg->update_details = true; + $cg->config = encrypt(json_encode($config)); + $cg->fees_and_limits = ''; + $cg->save(); + + $response = $this->withHeaders([])->get('.well-known/apple-developer-merchantid-domain-association'); + + $arr = $response->getContent(); + $response->assertStatus(200); + $this->assertEquals("merchant_id", $arr); + } +} From a30941bdb8e49709376c2068cbf6a23efceeda7f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 11:19:29 +1100 Subject: [PATCH 08/24] Fixes for tests --- .../ClientPortal/ApplePayDomainController.php | 2 - .../Controllers/CompanyGatewayController.php | 6 ++ app/Jobs/Util/ApplePayDomain.php | 93 +++++++++++++++++++ app/PaymentDrivers/StripePaymentDriver.php | 14 +-- .../Feature/ApplePayDomainMerchantUrlTest.php | 28 ++++++ 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 app/Jobs/Util/ApplePayDomain.php diff --git a/app/Http/Controllers/ClientPortal/ApplePayDomainController.php b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php index 8901102fec0c..a62249313a69 100644 --- a/app/Http/Controllers/ClientPortal/ApplePayDomainController.php +++ b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php @@ -24,8 +24,6 @@ class ApplePayDomainController extends Controller private array $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23']; - private ?Company $company = null; - public function showAppleMerchantId(Request $request) { diff --git a/app/Http/Controllers/CompanyGatewayController.php b/app/Http/Controllers/CompanyGatewayController.php index f810c6e44ca0..4f254dff9562 100644 --- a/app/Http/Controllers/CompanyGatewayController.php +++ b/app/Http/Controllers/CompanyGatewayController.php @@ -20,6 +20,7 @@ use App\Http\Requests\CompanyGateway\EditCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\ShowCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\StoreCompanyGatewayRequest; use App\Http\Requests\CompanyGateway\UpdateCompanyGatewayRequest; +use App\Jobs\Util\ApplePayDomain; use App\Models\Client; use App\Models\CompanyGateway; use App\Repositories\CompanyRepository; @@ -45,6 +46,9 @@ class CompanyGatewayController extends BaseController public $forced_includes = []; + private array $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23']; + + /** * CompanyGatewayController constructor. * @param CompanyRepository $company_repo @@ -379,6 +383,8 @@ class CompanyGatewayController extends BaseController $company_gateway->save(); + ApplePayDomain::dispatch($company_gateway, $company_gateway->company->db); + return $this->itemResponse($company_gateway); } diff --git a/app/Jobs/Util/ApplePayDomain.php b/app/Jobs/Util/ApplePayDomain.php new file mode 100644 index 000000000000..305abe8e554a --- /dev/null +++ b/app/Jobs/Util/ApplePayDomain.php @@ -0,0 +1,93 @@ +db = $db; + + $this->company_gateway = $company_gateway; + + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + MultiDB::setDB($this->db); + + if(in_array($this->company_gateway->gateway_key, $this->stripe_keys)) + { + $domain = $this->getDomain(); + + $this->company_gateway->driver()->setApplePayDomain($domain); + + } + + } + + private function getDomain() + { + + $domain = ''; + + if(Ninja::isHosted()) + { + + if($this->company->portal_mode == 'domain'){ + $domain = $this->company->portal_domain; + } + else{ + $domain = $this->company->subdomain . '.' . config('ninja.app_domain'); + } + + } + else { + + $domain = config('ninja.app_url'); + } + + $parsed_url = parse_url($domain); + + return $parsed_url['host']; + + } + +} diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 48db7ff7c40e..8e5b960b268c 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -725,13 +725,15 @@ class StripePaymentDriver extends BaseDriver return (new Verify($this))->run(); } - public function setDomain() + public function setApplePayDomain($domain) { - // \Stripe\ApplePayDomain::create([ - // 'domain_name' => 'example.com', - // ],[ - // 'stripe_account' => '{{CONNECTED_ACCOUNT_ID}}', - // ]); + + $this->init(); + + \Stripe\ApplePayDomain::create([ + 'domain_name' => $domain, + ],$this->stripe_connect_auth); + } public function disconnect() diff --git a/tests/Feature/ApplePayDomainMerchantUrlTest.php b/tests/Feature/ApplePayDomainMerchantUrlTest.php index 869652de1b5a..b670970db5bd 100644 --- a/tests/Feature/ApplePayDomainMerchantUrlTest.php +++ b/tests/Feature/ApplePayDomainMerchantUrlTest.php @@ -38,6 +38,11 @@ class ApplePayDomainMerchantUrlTest extends TestCase public function testMerchantFieldGet() { + + if (! config('ninja.testvars.stripe')) { + $this->markTestSkipped('Skip test no company gateways installed'); + } + $config = new \stdClass; $config->publishableKey = "pk_test"; $config->apiKey = "sk_test"; @@ -61,4 +66,27 @@ class ApplePayDomainMerchantUrlTest extends TestCase $response->assertStatus(200); $this->assertEquals("merchant_id", $arr); } + + public function testDomainParsing() + { + $domain = 'http://ninja.test:8000'; + + $parsed = parse_url($domain); + + $this->assertEquals('ninja.test', $parsed['host']); + + $domain = 'ninja.test:8000'; + + $parsed = parse_url($domain); + + $this->assertEquals('ninja.test', $parsed['host']); + + $domain = 'http://ninja.test:8000/afadf/dfdfdf/dfdfasf'; + + $parsed = parse_url($domain); + + $this->assertEquals('ninja.test', $parsed['host']); + + + } } From 2f34411b5fe66b16c384bbc39695cde4e26ebe70 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 11:30:45 +1100 Subject: [PATCH 09/24] Try/catch for apple pay domain --- app/Jobs/Util/ApplePayDomain.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Util/ApplePayDomain.php b/app/Jobs/Util/ApplePayDomain.php index 305abe8e554a..add6041ba478 100644 --- a/app/Jobs/Util/ApplePayDomain.php +++ b/app/Jobs/Util/ApplePayDomain.php @@ -55,9 +55,15 @@ class ApplePayDomain implements ShouldQueue if(in_array($this->company_gateway->gateway_key, $this->stripe_keys)) { + $domain = $this->getDomain(); - $this->company_gateway->driver()->setApplePayDomain($domain); + try{ + $this->company_gateway->driver()->setApplePayDomain($domain); + } + catch(\Exception $e){ + nlog("failed to set Apple Domain with Stripe " . $e->getMessage()); + } } From 37a4c4810d4953b0fe25c0d4170d1e8167e1aa28 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 11:52:24 +1100 Subject: [PATCH 10/24] Stripe Apple Pay --- app/Http/Controllers/CompanyGatewayController.php | 2 ++ app/Jobs/Util/ApplePayDomain.php | 1 + app/Services/Invoice/InvoiceService.php | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/CompanyGatewayController.php b/app/Http/Controllers/CompanyGatewayController.php index 4f254dff9562..9c0a4f4eff4a 100644 --- a/app/Http/Controllers/CompanyGatewayController.php +++ b/app/Http/Controllers/CompanyGatewayController.php @@ -210,6 +210,8 @@ class CompanyGatewayController extends BaseController $company_gateway->save(); } + ApplePayDomain::dispatch($company_gateway, $company_gateway->company->db); + return $this->itemResponse($company_gateway); } diff --git a/app/Jobs/Util/ApplePayDomain.php b/app/Jobs/Util/ApplePayDomain.php index add6041ba478..8dc6230b557f 100644 --- a/app/Jobs/Util/ApplePayDomain.php +++ b/app/Jobs/Util/ApplePayDomain.php @@ -51,6 +51,7 @@ class ApplePayDomain implements ShouldQueue */ public function handle() { + MultiDB::setDB($this->db); if(in_array($this->company_gateway->gateway_key, $this->stripe_keys)) diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index f551b8d1972a..23c50125c89d 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -333,9 +333,10 @@ class InvoiceService try{ - Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); + if(Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) + Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); - if(Ninja::isHosted()) { + if(Ninja::isHosted() && Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) { Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'); } From 0634698c5b550575d2e0ee5e9cc7d93c51fe1092 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 13:08:19 +1100 Subject: [PATCH 11/24] Enfore payment_terms to '0' if none is set --- app/Jobs/Util/Import.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 347ed8998dd7..773ef8326243 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -471,8 +471,9 @@ class Import implements ShouldQueue } - if ($key == 'payment_terms' && $key = '') { - $value = -1; + /* changes $key = '' to $value == '' and changed the return value from -1 to "0" 06/01/2022 */ + if ($key == 'payment_terms' && $value == '') { + $value = "0"; } $company_settings->{$key} = $value; From abbca58b4d9260b7a4991931376801d3cd5fdc03 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 13:31:44 +1100 Subject: [PATCH 12/24] Do not allow an invoice to be created for a deleted client --- .../Requests/Invoice/StoreInvoiceRequest.php | 2 +- .../ClientDeletedInvoiceCreationTest.php | 78 +++++++++++++++++++ tests/Feature/InvoiceTest.php | 15 ++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/ClientDeletedInvoiceCreationTest.php diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index b0fe7266fd8a..527c108cdbdd 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -47,7 +47,7 @@ class StoreInvoiceRequest extends Request $rules['documents'] = 'file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; } - $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id; + $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; // $rules['client_id'] = ['required', Rule::exists('clients')->where('company_id', auth()->user()->company()->id)]; $rules['invitations.*.client_contact_id'] = 'distinct'; diff --git a/tests/Feature/ClientDeletedInvoiceCreationTest.php b/tests/Feature/ClientDeletedInvoiceCreationTest.php new file mode 100644 index 000000000000..09429203658b --- /dev/null +++ b/tests/Feature/ClientDeletedInvoiceCreationTest.php @@ -0,0 +1,78 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->makeTestData(); + } + + public function testClientedDeletedAttemptingToCreateInvoice() + { + /* Test fire new invoice */ + $data = [ + 'client_id' => $this->client->hashed_id, + 'number' => 'dude', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/', $data) + ->assertStatus(200); + + $this->client->is_deleted = true; + $this->client->save(); + + + $data = [ + 'client_id' => $this->client->hashed_id, + 'number' => 'dude2', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/', $data) + ->assertStatus(302); + + + } + +} diff --git a/tests/Feature/InvoiceTest.php b/tests/Feature/InvoiceTest.php index 68341e10b631..87d37188f2f1 100644 --- a/tests/Feature/InvoiceTest.php +++ b/tests/Feature/InvoiceTest.php @@ -220,4 +220,19 @@ class InvoiceTest extends TestCase ])->put('/api/v1/invoices/'.$arr['data']['id'], $data) ->assertStatus(200); } + + public function testClientedDeletedAttemptingToCreateInvoice() + { + /* Test fire new invoice */ + $data = [ + 'client_id' => $this->client->hashed_id, + 'number' => 'dude', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/', $data) + ->assertStatus(200); + } } From b42ae9023b29d0cd5b8ffb9ddfac33868b233065 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 14:53:03 +1100 Subject: [PATCH 13/24] Minor fixes for tests --- tests/Feature/ClientDeletedInvoiceCreationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/ClientDeletedInvoiceCreationTest.php b/tests/Feature/ClientDeletedInvoiceCreationTest.php index 09429203658b..3ec20cb30f10 100644 --- a/tests/Feature/ClientDeletedInvoiceCreationTest.php +++ b/tests/Feature/ClientDeletedInvoiceCreationTest.php @@ -51,7 +51,7 @@ class ClientDeletedInvoiceCreationTest extends TestCase 'number' => 'dude', ]; - $response = $this->withHeaders([ + $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->post('/api/v1/invoices/', $data) @@ -66,7 +66,7 @@ class ClientDeletedInvoiceCreationTest extends TestCase 'number' => 'dude2', ]; - $response = $this->withHeaders([ + $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, ])->post('/api/v1/invoices/', $data) From 240df693a51d59a5de6e74e1a8e622c9163c8417 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 15:58:55 +1100 Subject: [PATCH 14/24] Fixes for FlySystem exceptions for corrupt paths --- app/Models/Invoice.php | 25 +++++++++++++++++-- .../Authorize/AuthorizePaymentMethod.php | 3 ++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 7d2257c6b19a..93ada7f481c5 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -409,7 +409,19 @@ class Invoice extends BaseModel $file_path = $this->client->invoice_filepath($invitation).$this->numberFormatter().'.pdf'; - if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){ + $file_exists = false; + + /* Flysystem throws an exception if the path is "corrupted" so lets wrap it in a try catch and return a bool 06/01/2022*/ + try{ + $file_exists = Storage::disk(config('filesystems.default'))->exists($file_path); + } + catch(\Exception $e){ + + nlog($e->getMessage()); + + } + + if(Ninja::isHosted() && $portal && $file_exists){ return Storage::disk(config('filesystems.default'))->{$type}($file_path); } elseif(Ninja::isHosted()){ @@ -417,7 +429,16 @@ class Invoice extends BaseModel return Storage::disk(config('filesystems.default'))->{$type}($file_path); } - if(Storage::disk('public')->exists($file_path)) + try{ + $file_exists = Storage::disk('public')->exists($file_path); + } + catch(\Exception $e){ + + nlog($e->getMessage()); + + } + + if($file_exists) return Storage::disk('public')->{$type}($file_path); $file_path = CreateEntityPdf::dispatchNow($invitation); diff --git a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php index e2cb1e6613d1..ba359b259443 100644 --- a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php +++ b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php @@ -157,7 +157,8 @@ class AuthorizePaymentMethod $paymentOne->setOpaqueData($op); $contact = $this->authorize->client->primary_contact()->first(); - + $billto = false; + if ($contact) { // Create the Bill To info for new payment type $billto = new CustomerAddressType(); From 996b3f246bb9980136271115a355f326837c3da3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 17:21:11 +1100 Subject: [PATCH 15/24] Add Apple Domain Verification to Stripe Gateways --- ...31_add_app_domain_id_to_gateways_table.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php diff --git a/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php b/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php new file mode 100644 index 000000000000..23c8757e6bff --- /dev/null +++ b/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php @@ -0,0 +1,39 @@ +fields = '{"account_id":"", "appleDomainVerification":""}'; + $stripe_connect->save(); + + } + + $stripe_connect = Gateway::find(20); + $stripe_connect->fields = '{"account_id":"", "appleDomainVerification":""}'; + $stripe_connect->save(); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + +} From 057cf9cd4248bc10aee64a9a901754067deb8f00 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 17:30:43 +1100 Subject: [PATCH 16/24] Minor fixes for Additional Stripe Field --- .../2021_04_12_095424_stripe_connect_gateway.php | 2 +- ...061231_add_app_domain_id_to_gateways_table.php | 15 ++++++++++----- database/seeders/PaymentLibrariesSeeder.php | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/database/migrations/2021_04_12_095424_stripe_connect_gateway.php b/database/migrations/2021_04_12_095424_stripe_connect_gateway.php index 4a6959c13a30..ef85ec207b3d 100644 --- a/database/migrations/2021_04_12_095424_stripe_connect_gateway.php +++ b/database/migrations/2021_04_12_095424_stripe_connect_gateway.php @@ -24,7 +24,7 @@ class StripeConnectGateway extends Migration 'provider' => 'StripeConnect', 'sort_order' => 1, 'key' => 'd14dd26a47cecc30fdd65700bfb67b34', - 'fields' => '{"account_id":""}' + 'fields' => '{"account_id":"","appleDomainVerification":""}' ]; Gateway::create($gateway); diff --git a/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php b/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php index 23c8757e6bff..35193d605575 100644 --- a/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php +++ b/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php @@ -19,14 +19,19 @@ class AddAppDomainIdToGatewaysTable extends Migration if(Ninja::isHosted()){ $stripe_connect = Gateway::find(56); - $stripe_connect->fields = '{"account_id":"", "appleDomainVerification":""}'; - $stripe_connect->save(); - + + if($stripe_connect){ + $stripe_connect->fields = '{"account_id":"", "appleDomainVerification":""}'; + $stripe_connect->save(); + } } $stripe_connect = Gateway::find(20); - $stripe_connect->fields = '{"account_id":"", "appleDomainVerification":""}'; - $stripe_connect->save(); + + if($stripe_connect){ + $stripe_connect->fields = '{"account_id":"", "appleDomainVerification":""}'; + $stripe_connect->save(); + } } diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index c4d50c57d099..fac849bf7398 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -44,7 +44,7 @@ class PaymentLibrariesSeeder extends Seeder ['id' => 17, 'name' => 'Pin', 'provider' => 'Pin', 'key' => '0749cb92a6b36c88bd9ff8aabd2efcab', 'fields' => '{"secretKey":"","testMode":false}'], ['id' => 18, 'name' => 'SagePay Direct', 'provider' => 'SagePay_Direct', 'key' => '4c8f4e5d0f353a122045eb9a60cc0f2d', 'fields' => '{"vendor":"","testMode":false,"referrerId":""}'], ['id' => 19, 'name' => 'SecurePay DirectPost', 'provider' => 'SecurePay_DirectPost', 'key' => '8036a5aadb2bdaafb23502da8790b6a2', 'fields' => '{"merchantId":"","transactionPassword":"","testMode":false,"enable_ach":"","enable_sofort":"","enable_apple_pay":"","enable_alipay":""}'], - ['id' => 20, 'name' => 'Stripe', 'provider' => 'Stripe', 'sort_order' => 1, 'key' => 'd14dd26a37cecc30fdd65700bfb55b23', 'fields' => '{"publishableKey":"","apiKey":""}'], + ['id' => 20, 'name' => 'Stripe', 'provider' => 'Stripe', 'sort_order' => 1, 'key' => 'd14dd26a37cecc30fdd65700bfb55b23', 'fields' => '{"publishableKey":"","apiKey":"","appleDomainVerification":""}'], ['id' => 21, 'name' => 'TargetPay Direct eBanking', 'provider' => 'TargetPay_Directebanking', 'key' => 'd14dd26a37cdcc30fdd65700bfb55b23', 'fields' => '{"subAccountId":""}'], ['id' => 22, 'name' => 'TargetPay Ideal', 'provider' => 'TargetPay_Ideal', 'key' => 'ea3b328bd72d381387281c3bd83bd97c', 'fields' => '{"subAccountId":""}'], ['id' => 23, 'name' => 'TargetPay Mr Cash', 'provider' => 'TargetPay_Mrcash', 'key' => 'a0035fc0d87c4950fb82c73e2fcb825a', 'fields' => '{"subAccountId":""}'], From f5f2395d7eb34928d213239cae807c709f924997 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 20:03:30 +1100 Subject: [PATCH 17/24] Fixes for clean design --- .../Controllers/ClientPortal/ApplePayDomainController.php | 8 ++++---- resources/views/pdf-designs/clean.html | 7 ++++++- tests/Feature/ApplePayDomainMerchantUrlTest.php | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/ApplePayDomainController.php b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php index a62249313a69..ab89cc869035 100644 --- a/app/Http/Controllers/ClientPortal/ApplePayDomainController.php +++ b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php @@ -38,8 +38,8 @@ class ApplePayDomainController extends Controller foreach($cgs as $cg) { - if($cg->getConfigField('appleMerchantId')){ - return response($cg->getConfigField('appleMerchantId'),200); + if($cg->getConfigField('appleDomainVerification')){ + return response($cg->getConfigField('appleDomainVerification'),200); } @@ -90,8 +90,8 @@ class ApplePayDomainController extends Controller foreach($cgs as $cg) { - if($cg->getConfigField('appleMerchantId')){ - return response($cg->getConfigField('appleMerchantId'),200); + if($cg->getConfigField('appleDomainVerification')){ + return response($cg->getConfigField('appleDomainVerification'),200); } } diff --git a/resources/views/pdf-designs/clean.html b/resources/views/pdf-designs/clean.html index e23a1a111b12..aac1b3969f96 100644 --- a/resources/views/pdf-designs/clean.html +++ b/resources/views/pdf-designs/clean.html @@ -117,17 +117,22 @@ [data-ref="table"] > tbody > tr > td { border-top: 1px solid #d8d8d8; border-bottom: 1px solid #d8d8d8; - padding: 1.5rem; + padding: 1.5rem 1rem; } [data-ref="table"] > tbody > tr > td:first-child { color: var(--primary-color); } + [data-ref="table"] > thead > tr > th:last-child, [data-ref="table"] > tbody > tr > td:last-child { text-align: right; } + [data-ref="table"] > thead > tr > th:last-child { + padding-right: 1rem; + } + [data-ref="table"] > tbody > tr:nth-child(odd) { background-color: #f5f5f5; } diff --git a/tests/Feature/ApplePayDomainMerchantUrlTest.php b/tests/Feature/ApplePayDomainMerchantUrlTest.php index b670970db5bd..a2387f5f3536 100644 --- a/tests/Feature/ApplePayDomainMerchantUrlTest.php +++ b/tests/Feature/ApplePayDomainMerchantUrlTest.php @@ -46,7 +46,7 @@ class ApplePayDomainMerchantUrlTest extends TestCase $config = new \stdClass; $config->publishableKey = "pk_test"; $config->apiKey = "sk_test"; - $config->appleMerchantId = "merchant_id"; + $config->appleDomainVerification = "merchant_id"; $cg = new CompanyGateway; $cg->company_id = $this->company->id; From 89ce2b443eb8b4d30319d7b873a8e93e5a4a9b20 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 6 Jan 2022 21:06:46 +1100 Subject: [PATCH 18/24] Fixes for mollie payment driver --- app/PaymentDrivers/BaseDriver.php | 4 ++-- app/Services/Invoice/TriggeredActions.php | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index c9031daaeae0..a85229956014 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -256,13 +256,13 @@ class BaseDriver extends AbstractPaymentDriver $this->payment_hash->payment_id = $payment->id; $this->payment_hash->save(); + $this->attachInvoices($payment, $this->payment_hash); + if($this->payment_hash->credits_total() > 0) $payment = $payment->service()->applyCredits($this->payment_hash)->save(); $payment->service()->updateInvoicePayment($this->payment_hash); - $this->attachInvoices($payment, $this->payment_hash); - event('eloquent.created: App\Models\Payment', $payment); if ($this->client->getSetting('client_online_payment_notification') && in_array($status, [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING])) diff --git a/app/Services/Invoice/TriggeredActions.php b/app/Services/Invoice/TriggeredActions.php index b2f2ee395b53..f9b19f4b1b5c 100644 --- a/app/Services/Invoice/TriggeredActions.php +++ b/app/Services/Invoice/TriggeredActions.php @@ -49,6 +49,7 @@ class TriggeredActions extends AbstractService } if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') { + $this->invoice->service()->touchPdf()->save(); $this->sendEmail(); } From 3417b1ff0f5cc0aa2840e752f130243efda424af Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 7 Jan 2022 11:54:12 +1100 Subject: [PATCH 19/24] Fixes for send_email triggered action not generating PDFs in time. --- app/Services/Credit/TriggeredActions.php | 1 + app/Services/Invoice/TriggeredActions.php | 2 +- app/Services/Quote/TriggeredActions.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Services/Credit/TriggeredActions.php b/app/Services/Credit/TriggeredActions.php index 28134972af8d..cfc10e2d286a 100644 --- a/app/Services/Credit/TriggeredActions.php +++ b/app/Services/Credit/TriggeredActions.php @@ -38,6 +38,7 @@ class TriggeredActions extends AbstractService { if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') { + $this->credit = $this->credit->service()->markSent()->save(); $this->sendEmail(); } diff --git a/app/Services/Invoice/TriggeredActions.php b/app/Services/Invoice/TriggeredActions.php index f9b19f4b1b5c..2636a5376f34 100644 --- a/app/Services/Invoice/TriggeredActions.php +++ b/app/Services/Invoice/TriggeredActions.php @@ -49,7 +49,7 @@ class TriggeredActions extends AbstractService } if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') { - $this->invoice->service()->touchPdf()->save(); + $this->invoice->service()->markSent()->touchPdf()->save(); $this->sendEmail(); } diff --git a/app/Services/Quote/TriggeredActions.php b/app/Services/Quote/TriggeredActions.php index fa630d753fe6..ea38a696c509 100644 --- a/app/Services/Quote/TriggeredActions.php +++ b/app/Services/Quote/TriggeredActions.php @@ -38,6 +38,7 @@ class TriggeredActions extends AbstractService { if ($this->request->has('send_email') && $this->request->input('send_email') == 'true') { + $this->quote = $this->quote->service()->markSent()->save(); $this->sendEmail(); } From 8f9febe5f84a1a066af738b4d8852b5721548ac5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 7 Jan 2022 13:39:31 +1100 Subject: [PATCH 20/24] Force ZAR for PayFast --- app/PaymentDrivers/PayFastPaymentDriver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/PaymentDrivers/PayFastPaymentDriver.php b/app/PaymentDrivers/PayFastPaymentDriver.php index b67026eaf518..8d3ab1e6d062 100644 --- a/app/PaymentDrivers/PayFastPaymentDriver.php +++ b/app/PaymentDrivers/PayFastPaymentDriver.php @@ -49,7 +49,8 @@ class PayFastPaymentDriver extends BaseDriver { $types = []; - if($this->client->currency()->code == 'ZAR' || $this->client->currency()->code == 'USD') + // if($this->client->currency()->code == 'ZAR' || $this->client->currency()->code == 'USD') + if($this->client->currency()->code == 'ZAR') $types[] = GatewayType::CREDIT_CARD; return $types; From addc10ef4e7ea451cef160434b6462dcbff99aff Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 7 Jan 2022 13:56:02 +1100 Subject: [PATCH 21/24] Hide expired quotes from client portal --- app/Http/Livewire/QuotesTable.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Livewire/QuotesTable.php b/app/Http/Livewire/QuotesTable.php index f52ee96fd945..e93d24ec6d06 100644 --- a/app/Http/Livewire/QuotesTable.php +++ b/app/Http/Livewire/QuotesTable.php @@ -49,6 +49,10 @@ class QuotesTable extends Component ->where('company_id', $this->company->id) ->where('client_id', auth('contact')->user()->client->id) ->where('status_id', '<>', Quote::STATUS_DRAFT) + ->where(function ($query){ + $query->whereDate('due_date', '>=', now()) + ->orWhereNull('due_date'); + }) ->where('is_deleted', 0) ->withTrashed() ->paginate($this->per_page); From 959d45f0818614ca46b6e950f6595e3b01f093cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 7 Jan 2022 15:03:42 +1100 Subject: [PATCH 22/24] Fixes for client portal quote tables --- .../ClientPortal/QuoteController.php | 5 ++- app/Http/Livewire/QuotesTable.php | 38 ++++++++++++++++--- app/Models/Quote.php | 11 ++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php index c646d41efee9..1b0fa8214428 100644 --- a/app/Http/Controllers/ClientPortal/QuoteController.php +++ b/app/Http/Controllers/ClientPortal/QuoteController.php @@ -31,6 +31,7 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse; use ZipStream\Option\Archive; use ZipStream\ZipStream; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; class QuoteController extends Controller { @@ -54,7 +55,9 @@ class QuoteController extends Controller * @return Factory|View|BinaryFileResponse */ public function show(ShowQuoteRequest $request, Quote $quote) - { + { + /* If the quote is expired, convert the status here */ + $data = [ 'quote' => $quote, ]; diff --git a/app/Http/Livewire/QuotesTable.php b/app/Http/Livewire/QuotesTable.php index e93d24ec6d06..1aa996f089d9 100644 --- a/app/Http/Livewire/QuotesTable.php +++ b/app/Http/Livewire/QuotesTable.php @@ -42,17 +42,45 @@ class QuotesTable extends Component ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc'); if (count($this->status) > 0) { - $query = $query->whereIn('status_id', $this->status); + + /* Special filter for expired*/ + if(in_array("-1", $this->status)){ + // $query->whereDate('due_date', '<=', now()->startOfDay()); + + $query->where(function ($query){ + $query->whereDate('due_date', '<=', now()->startOfDay()) + ->whereNotNull('due_date') + ->where('status_id', '<>', Quote::STATUS_CONVERTED); + }); + + } + + if(in_array("2", $this->status)){ + + $query->where(function ($query){ + $query->whereDate('due_date', '>=', now()->startOfDay()) + ->orWhereNull('due_date'); + })->where('status_id', Quote::STATUS_SENT); + + } + + if(in_array("3", $this->status)){ + $query->whereIn('status_id', [Quote::STATUS_APPROVED, Quote::STATUS_CONVERTED]); + } + + } + + $query = $query ->where('company_id', $this->company->id) ->where('client_id', auth('contact')->user()->client->id) ->where('status_id', '<>', Quote::STATUS_DRAFT) - ->where(function ($query){ - $query->whereDate('due_date', '>=', now()) - ->orWhereNull('due_date'); - }) + // ->where(function ($query){ + // $query->whereDate('due_date', '>=', now()) + // ->orWhereNull('due_date'); + // }) ->where('is_deleted', 0) ->withTrashed() ->paginate($this->per_page); diff --git a/app/Models/Quote.php b/app/Models/Quote.php index b5d105313913..451c73f69d5a 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -87,6 +87,7 @@ class Quote extends BaseModel 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', + 'is_deleted' => 'boolean', ]; protected $dates = []; @@ -117,6 +118,16 @@ class Quote extends BaseModel return $this->dateMutator($value); } + public function getStatusIdAttribute($value) + { + if($this->due_date && !$this->is_deleted && $value == Quote::STATUS_SENT && Carbon::parse($this->due_date)->lte(now()->startOfDay())){ + return Quote::STATUS_EXPIRED; + } + + return $value; + + } + public function company() { return $this->belongsTo(Company::class); From e0204a19d5778eae03a0274d5d6256a10bfdb4ba Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 8 Jan 2022 12:28:41 +1100 Subject: [PATCH 23/24] touch pdf on sent --- app/Services/Invoice/MarkSent.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Services/Invoice/MarkSent.php b/app/Services/Invoice/MarkSent.php index 7527ab633b1f..40a851f6dd09 100644 --- a/app/Services/Invoice/MarkSent.php +++ b/app/Services/Invoice/MarkSent.php @@ -63,7 +63,8 @@ class MarkSent extends AbstractService ->service() ->applyNumber() ->setDueDate() - ->deletePdf() + // ->deletePdf() //08-01-2022 + ->touchPdf() //08-01-2022 ->setReminder() ->save(); From 92b490043c8fcf5b5153cddd4994f325bc2f3bad Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 8 Jan 2022 14:54:02 +1100 Subject: [PATCH 24/24] Handle 100% gateway fees --- VERSION.txt | 2 +- app/Models/CompanyGateway.php | 41 ++-------------- config/ninja.php | 4 +- tests/Feature/CompanyGatewayApiTest.php | 65 +++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 40 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 6ce7f3e4b421..93a75a086973 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.41 \ No newline at end of file +5.3.42 \ No newline at end of file diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index a31823a5541b..7c6451253f1e 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -343,7 +343,10 @@ class CompanyGateway extends BaseModel } if ($fees_and_limits->fee_percent) { - if ($fees_and_limits->adjust_fee_percent) { + if($fees_and_limits->fee_percent == 100){ //unusual edge case if the user wishes to charge a fee of 100% 09/01/2022 + $fee += $amount; + } + elseif ($fees_and_limits->adjust_fee_percent) { $fee += round(($amount / (1 - $fees_and_limits->fee_percent / 100) - $amount), 2); } else { $fee += round(($amount * $fees_and_limits->fee_percent / 100), 2); @@ -383,42 +386,6 @@ class CompanyGateway extends BaseModel return route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $this->hashed_id]); } - /** - * we need to average out the gateway fees across all the invoices - * so lets iterate. - * - * we MAY need to adjust the final fee to ensure our rounding makes sense! - * @param $amount - * @param $invoice_count - * @return stdClass - */ - // public function calcGatewayFeeObject($amount, $invoice_count) - // { - // $total_gateway_fee = $this->calcGatewayFee($amount); - - // $fee_object = new stdClass; - - // $fees_and_limits = $this->getFeesAndLimits(); - - // if (! $fees_and_limits) { - // return $fee_object; - // } - - // $fee_component_amount = $fees_and_limits->fee_amount ?: 0; - // $fee_component_percent = $fees_and_limits->fee_percent ? ($amount * $fees_and_limits->fee_percent / 100) : 0; - - // $combined_fee_component = $fee_component_amount + $fee_component_percent; - - // $fee_component_tax_name1 = $fees_and_limits->fee_tax_name1 ?: ''; - // $fee_component_tax_rate1 = $fees_and_limits->fee_tax_rate1 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate1 / 100) : 0; - - // $fee_component_tax_name2 = $fees_and_limits->fee_tax_name2 ?: ''; - // $fee_component_tax_rate2 = $fees_and_limits->fee_tax_rate2 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate2 / 100) : 0; - - // $fee_component_tax_name3 = $fees_and_limits->fee_tax_name3 ?: ''; - // $fee_component_tax_rate3 = $fees_and_limits->fee_tax_rate3 ? ($combined_fee_component * $fees_and_limits->fee_tax_rate3 / 100) : 0; - // } - public function resolveRouteBinding($value, $field = null) { return $this diff --git a/config/ninja.php b/config/ninja.php index 10e0fa553f36..c3656dce7023 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.3.41', - 'app_tag' => '5.3.41', + 'app_version' => '5.3.42', + 'app_tag' => '5.3.42', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/tests/Feature/CompanyGatewayApiTest.php b/tests/Feature/CompanyGatewayApiTest.php index 373196c8e92b..f1b3e5fec774 100644 --- a/tests/Feature/CompanyGatewayApiTest.php +++ b/tests/Feature/CompanyGatewayApiTest.php @@ -319,6 +319,71 @@ class CompanyGatewayApiTest extends TestCase $this->assertEquals(10.2, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD)); } + + public function testFeesAndLimitsFeePercentAndAmountCalcuationOneHundredPercent() + { + //{"1":{"min_limit":1,"max_limit":1000000,"fee_amount":10,"fee_percent":2,"fee_tax_name1":"","fee_tax_name2":"","fee_tax_name3":"","fee_tax_rate1":0,"fee_tax_rate2":0,"fee_tax_rate3":0,"fee_cap":10,"adjust_fee_percent":true}} + $fee = new FeesAndLimits; + $fee->fee_amount = 0; + $fee->fee_percent = 100; + // $fee->fee_tax_name1 = 'GST'; + // $fee->fee_tax_rate1 = '10.0'; + + $fee_arr[1] = (array) $fee; + + $data = [ + 'config' => 'random config', + 'gateway_key' => '3b6621f970ab18887c4f6dca78d3f8bb', + 'fees_and_limits' => $fee_arr, + ]; + + /* POST */ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/company_gateways', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + $id = $this->decodePrimaryKey($arr['data']['id']); + + $company_gateway = CompanyGateway::find($id); + + $this->assertEquals(10, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD)); + } + + public function testFeesAndLimitsFeePercentAndAmountCalcuationOneHundredPercentVariationOne() + { + $fee = new FeesAndLimits; + $fee->fee_amount = 0; + $fee->fee_percent = 10; + + $fee_arr[1] = (array) $fee; + + $data = [ + 'config' => 'random config', + 'gateway_key' => '3b6621f970ab18887c4f6dca78d3f8bb', + 'fees_and_limits' => $fee_arr, + ]; + + /* POST */ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/company_gateways', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + $id = $this->decodePrimaryKey($arr['data']['id']); + + $company_gateway = CompanyGateway::find($id); + + $this->assertEquals(1, $company_gateway->calcGatewayFee(10, GatewayType::CREDIT_CARD)); + } + + public function testFeesAndLimitsFeePercentAndAmountAndTaxCalcuation() { //{"1":{"min_limit":1,"max_limit":1000000,"fee_amount":10,"fee_percent":2,"fee_tax_name1":"","fee_tax_name2":"","fee_tax_name3":"","fee_tax_rate1":0,"fee_tax_rate2":0,"fee_tax_rate3":0,"fee_cap":10,"adjust_fee_percent":true}}