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/Http/Controllers/ClientPortal/ApplePayDomainController.php b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php new file mode 100644 index 000000000000..ab89cc869035 --- /dev/null +++ b/app/Http/Controllers/ClientPortal/ApplePayDomainController.php @@ -0,0 +1,103 @@ +stripe_keys) + ->where('is_deleted', false) + ->get(); + + foreach($cgs as $cg) + { + + if($cg->getConfigField('appleDomainVerification')){ + return response($cg->getConfigField('appleDomainVerification'),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('appleDomainVerification')){ + return response($cg->getConfigField('appleDomainVerification'),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 @@ $quote, ]; diff --git a/app/Http/Controllers/CompanyGatewayController.php b/app/Http/Controllers/CompanyGatewayController.php index f810c6e44ca0..9c0a4f4eff4a 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 @@ -206,6 +210,8 @@ class CompanyGatewayController extends BaseController $company_gateway->save(); } + ApplePayDomain::dispatch($company_gateway, $company_gateway->company->db); + return $this->itemResponse($company_gateway); } @@ -379,6 +385,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/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) { 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/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'); diff --git a/app/Http/Livewire/QuotesTable.php b/app/Http/Livewire/QuotesTable.php index f52ee96fd945..1aa996f089d9 100644 --- a/app/Http/Livewire/QuotesTable.php +++ b/app/Http/Livewire/QuotesTable.php @@ -42,13 +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('is_deleted', 0) ->withTrashed() ->paginate($this->per_page); 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/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/Jobs/Util/ApplePayDomain.php b/app/Jobs/Util/ApplePayDomain.php new file mode 100644 index 000000000000..8dc6230b557f --- /dev/null +++ b/app/Jobs/Util/ApplePayDomain.php @@ -0,0 +1,100 @@ +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(); + + try{ + $this->company_gateway->driver()->setApplePayDomain($domain); + } + catch(\Exception $e){ + nlog("failed to set Apple Domain with Stripe " . $e->getMessage()); + } + + } + + } + + 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/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; 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/app/Models/Invoice.php b/app/Models/Invoice.php index a6297b7034d0..93ada7f481c5 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 = []; @@ -408,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()){ @@ -416,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/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); 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/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(); 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/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; diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 5f4f60776a48..8e5b960b268c 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -725,6 +725,17 @@ class StripePaymentDriver extends BaseDriver return (new Verify($this))->run(); } + public function setApplePayDomain($domain) + { + + $this->init(); + + \Stripe\ApplePayDomain::create([ + 'domain_name' => $domain, + ],$this->stripe_connect_auth); + + } + public function disconnect() { if(!$this->stripe_connect) 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(); } 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/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 2771c3409a2a..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]]], @@ -123,7 +124,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){ 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'); } 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(); diff --git a/app/Services/Invoice/TriggeredActions.php b/app/Services/Invoice/TriggeredActions.php index b2f2ee395b53..2636a5376f34 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()->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(); } 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/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/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 new file mode 100644 index 000000000000..35193d605575 --- /dev/null +++ b/database/migrations/2022_01_06_061231_add_app_domain_id_to_gateways_table.php @@ -0,0 +1,44 @@ +fields = '{"account_id":"", "appleDomainVerification":""}'; + $stripe_connect->save(); + } + } + + $stripe_connect = Gateway::find(20); + + if($stripe_connect){ + $stripe_connect->fields = '{"account_id":"", "appleDomainVerification":""}'; + $stripe_connect->save(); + } + + } + + /** + * Reverse the migrations. + * + * @return void + */ + +} 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":""}'], 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', 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/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..a2387f5f3536 --- /dev/null +++ b/tests/Feature/ApplePayDomainMerchantUrlTest.php @@ -0,0 +1,92 @@ +makeTestData(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + 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"; + $config->appleDomainVerification = "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); + } + + 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']); + + + } +} diff --git a/tests/Feature/ClientDeletedInvoiceCreationTest.php b/tests/Feature/ClientDeletedInvoiceCreationTest.php new file mode 100644 index 000000000000..3ec20cb30f10 --- /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', + ]; + + $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', + ]; + + $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/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}} 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); + } }