diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index c9df6ee84990..39e807126eb5 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -733,7 +733,7 @@ class InvoiceController extends BaseController } break; case 'mark_sent': - $invoice->service()->markSent()->save(); + $invoice->service()->markSent(true)->save(); if (! $bulk) { return $this->itemResponse($invoice); diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index 2a8d881bca6d..79934b23820c 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -54,6 +54,8 @@ class UpdateCompanyRequest extends Request $rules['size_id'] = 'integer|nullable'; $rules['country_id'] = 'integer|nullable'; $rules['work_email'] = 'email|nullable'; + $rules['matomo_id'] = 'nullable|integer'; + // $rules['client_registration_fields'] = 'array'; if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { diff --git a/app/Http/Requests/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php index aa0d9ad62abd..a513096774bd 100644 --- a/app/Http/Requests/Vendor/UpdateVendorRequest.php +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -35,17 +35,13 @@ class UpdateVendorRequest extends Request { /* Ensure we have a client name, and that all emails are unique*/ - $rules['country_id'] = 'integer|nullable'; + $rules['country_id'] = 'integer'; if ($this->number) { $rules['number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id)->ignore($this->vendor->id); } - - // if($this->id_number) - // $rules['id_number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id)->ignore($this->vendor->id); - + $rules['contacts.*.email'] = 'nullable|distinct'; - // $rules['id_number'] = 'unique:vendors,id_number,'.$this->id.',id,company_id,'.auth()->user()->company()->id; return $rules; } @@ -67,6 +63,9 @@ class UpdateVendorRequest extends Request $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } + if(array_key_exists('country_id', $input) && is_null($input['country_id'])) + unset($input['country_id']); + $input = $this->decodePrimaryKeys($input); $this->replace($input); diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 40305dc6eb20..43e9e67f0ff2 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -167,7 +167,7 @@ class Import implements ShouldQueue public $tries = 1; - public $timeout = 0; + public $timeout = 10000000; // public $backoff = 86430; @@ -188,10 +188,10 @@ class Import implements ShouldQueue $this->resources = $resources; } - public function middleware() - { - return [new WithoutOverlapping("only_one_migration_at_a_time_ever")]; - } + // public function middleware() + // { + // return [new WithoutOverlapping("only_one_migration_at_a_time_ever")]; + // } /** * Execute the job. diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 47f35f9c9863..24d53a510c2e 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -115,6 +115,7 @@ class Gateway extends StaticModel GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], + GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'payment_intent.succeeded']], GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], ]; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index bd6b2054fff2..8268f80f638d 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -70,6 +70,7 @@ class CreditCard 'sequenceType' => 'recurring', 'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash), 'webhookUrl' => $this->mollie->company_gateway->webhookUrl(), + 'idempotencyKey' => uniqid("st",true), 'metadata' => [ 'client_id' => $this->mollie->client->hashed_id, 'hash' => $this->mollie->payment_hash->hash, diff --git a/app/PaymentDrivers/Stripe/Klarna.php b/app/PaymentDrivers/Stripe/Klarna.php index d3e2678d215c..718d3f8cfa1a 100644 --- a/app/PaymentDrivers/Stripe/Klarna.php +++ b/app/PaymentDrivers/Stripe/Klarna.php @@ -19,6 +19,8 @@ use App\Models\Payment; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\StripePaymentDriver; +use App\Utils\Number; +use Illuminate\Support\Facades\Cache; class Klarna { @@ -50,10 +52,10 @@ class Klarna $invoice_numbers = collect($data['invoices'])->pluck('invoice_number'); - if ($invoice_numbers > 0) { - $description = ctrans('texts.payment_provider_paymenttext', ['invoicenumber' => $invoice_numbers->implode(', '), 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]); + if ($invoice_numbers->count() > 0) { + $description = ctrans('texts.stripe_paymenttext', ['invoicenumber' => $invoice_numbers->implode(', '), 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]); } else { - $description = ctrans('texts.payment_prvoder_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]); + $description = ctrans('texts.stripe_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]); } $intent = \Stripe\PaymentIntent::create([ @@ -149,6 +151,6 @@ class Klarna $this->stripe->client->company, ); - throw new PaymentFailed(ctrans('texts.payment_provider_failed_process_payment'), 500); + throw new PaymentFailed(ctrans('texts.gateway_error'), 500); } } diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index 49f6918c7c4c..fb3a138e4938 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -59,8 +59,10 @@ class UserRepository extends BaseRepository // if(array_key_exists('oauth_provider_id', $details)) // unset($details['oauth_provider_id']); - if (request()->has('validated_phone')) + if (request()->has('validated_phone')){ $details['phone'] = request()->input('validated_phone'); + $user->verified_phone_number = false; + } $user->fill($details); diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 6905bd03e635..bdc770c7826b 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -175,9 +175,9 @@ class InvoiceService return $this; } - public function markSent() + public function markSent($fire_event = false) { - $this->invoice = (new MarkSent($this->invoice->client, $this->invoice))->run(); + $this->invoice = (new MarkSent($this->invoice->client, $this->invoice))->run($fire_event); $this->setExchangeRate(); diff --git a/app/Services/Invoice/MarkSent.php b/app/Services/Invoice/MarkSent.php index 07bae6f98659..1fc7db1773ae 100644 --- a/app/Services/Invoice/MarkSent.php +++ b/app/Services/Invoice/MarkSent.php @@ -30,7 +30,7 @@ class MarkSent extends AbstractService $this->invoice = $invoice; } - public function run() + public function run($fire_webhook = false) { /* Return immediately if status is not draft or invoice has been deleted */ @@ -68,6 +68,10 @@ class MarkSent extends AbstractService event(new InvoiceWasUpdated($this->invoice, $this->invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + if($fire_webhook) + event('eloquent.updated: App\Models\Invoice', $this->invoice); + + return $this->invoice->fresh(); } } diff --git a/app/Services/Invoice/TriggeredActions.php b/app/Services/Invoice/TriggeredActions.php index 5f52483ad026..d31c35b9bb2d 100644 --- a/app/Services/Invoice/TriggeredActions.php +++ b/app/Services/Invoice/TriggeredActions.php @@ -48,7 +48,7 @@ class TriggeredActions extends AbstractService if ($this->request->has('mark_sent') && $this->request->input('mark_sent') == 'true' && $this->invoice->status_id == Invoice::STATUS_DRAFT) { $this->invoice = $this->invoice->service()->markSent()->save(); //update notification NOT sent - $this->updated = true; + $this->updated = false; } if ($this->request->has('amount_paid') && is_numeric($this->request->input('amount_paid'))) { diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 7e874532bf0f..ccaa1805b644 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -150,7 +150,7 @@ class CompanyTransformer extends EntityTransformer 'google_analytics_url' => (string) $company->google_analytics_key, //@deprecate 1-2-2021 'google_analytics_key' => (string) $company->google_analytics_key, 'matomo_url' => (string) $company->matomo_url, - 'matomo_id' => (int) $company->matomo_id, + 'matomo_id' => (string) $company->matomo_id ?: '', 'enabled_item_tax_rates' => (int) $company->enabled_item_tax_rates, 'client_can_register' => (bool) $company->client_can_register, 'is_large' => (bool) $company->is_large, diff --git a/lang/en/texts.php b/lang/en/texts.php index e9ff188f2082..9fc9764b2a61 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4851,6 +4851,47 @@ $LANG = array( 'cash_vs_accrual_help' => 'Turn on for accrual reporting, turn off for cash basis reporting.', 'expense_paid_report' => 'Expensed reporting', 'expense_paid_report_help' => 'Turn on for reporting all expenses, turn off for reporting only paid expenses', + 'payment_type_Klarna' => 'Klarna', + 'online_payment_email_help' => 'Send an email when an online payment is made', + 'manual_payment_email_help' => 'Send an email when manually entering a payment', + 'mark_paid_payment_email_help' => 'Send an email when marking an invoice as pad', + 'linked_transaction' => 'Successfully linked transaction', + 'link_payment' => 'Link Payment', + 'link_expense' => 'Link Expense', + 'lock_invoiced_tasks' => 'Lock Invoiced Tasks', + 'lock_invoiced_tasks_help' => 'Prevent tasks from being edited once invoiced', + 'registration_required_help' => 'Require clients to register', + 'use_inventory_management' => 'Use Inventory Management', + 'use_inventory_management_help' => 'Require products to be in stock', + 'optional_products' => 'Optional Products', + 'optional_recurring_products' => 'Optional Recurring Products', + 'convert_matched' => 'Convert', + 'auto_billed_invoice' => 'Successfully queued invoice to be auto-billed', + 'auto_billed_invoices' => 'Successfully queued invoices to be auto-billed', + 'operator' => 'Operator', + 'value' => 'Value', + 'is' => 'Is', + 'contains' => 'Contains', + 'starts_with' => 'Starts with', + 'is_empty' => 'Is empty', + 'add_rule' => 'Add Rule', + 'match_all_rules' => 'Match All Rules', + 'match_all_rules_help' => 'All criteria needs to match for the rule to be applied', + 'auto_convert_help' => 'Automatically convert matched transactions to expenses', + 'rules' => 'Rules', + 'transaction_rule' => 'Transaction Rule', + 'transaction_rules' => 'Transaction Rules', + 'new_transaction_rule' => 'New Transaction Rule', + 'edit_transaction_rule' => 'Edit Transaction Rule', + 'created_transaction_rule' => 'Successfully created rule', + 'updated_transaction_rule' => 'Successfully updated transaction rule', + 'archived_transaction_rule' => 'Successfully archived transaction rule', + 'deleted_transaction_rule' => 'Successfully deleted transaction rule', + 'removed_transaction_rule' => 'Successfully removed transaction rule', + 'restored_transaction_rule' => 'Successfully restored transaction rule', + 'search_transaction_rule' => 'Search Transaction Rule', + 'search_transaction_rules' => 'Search Transaction Rules', + ); return $LANG; diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 0c428ad069cb..659ea6979811 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -5,6 +5,7 @@ "/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js?id=f42dd0caddb3603e71db061924c4b172", "/js/clients/payments/forte-ach-payment.js": "/js/clients/payments/forte-ach-payment.js?id=b8173c7c0dee76bf9ae6312a963ae0e4", "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=207f218c44553470287f35f33a7eb154", + "/js/clients/payments/stripe-klarna.js": "/js/clients/payments/stripe-klarna.js?id=1c248bc1f4f45310cd585a95a5055375", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=404b7ee18e420de0e73f5402b7e39122", "/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=2f0c4e3bab30a98e33ac768255113174", "/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js?id=9bb483a89a887f753e49c0b635d6276a", diff --git a/resources/js/clients/payments/stripe-klarna.js b/resources/js/clients/payments/stripe-klarna.js index dfaa63092919..67c379abd99d 100644 --- a/resources/js/clients/payments/stripe-klarna.js +++ b/resources/js/clients/payments/stripe-klarna.js @@ -33,6 +33,16 @@ class ProcessKlarna { return this; }; + handleError = (message) => { + document.getElementById('pay-now').disabled = false; + document.querySelector('#pay-now > svg').classList.add('hidden'); + document.querySelector('#pay-now > span').classList.remove('hidden'); + + this.errors.textContent = ''; + this.errors.textContent = message; + this.errors.hidden = false; + }; + handle = () => { document.getElementById('pay-now').addEventListener('click', (e) => { let errors = document.getElementById('errors'); @@ -46,14 +56,28 @@ class ProcessKlarna { { payment_method: { billing_details: { - name: document.getElementById("giropay-name").value, + name: document.getElementById("klarna-name").value, + email: document.querySelector('meta[name=email').content, + address: { + line1: document.querySelector('input[name=address1]').value, + line2: document.querySelector('input[name=address2]').value, + city: document.querySelector('input[name=city]').value, + postal_code: document.querySelector('input[name=postal_code]').value, + state: document.querySelector('input[name=state]').value, + country: document.querySelector('meta[name=country').content, + } }, }, return_url: document.querySelector( 'meta[name="return-url"]' ).content, } - ); + ).then((result) => { + if (result.hasOwnProperty('error')) { + return this.handleError(result.error.message); + } + + });; }); }; } diff --git a/resources/views/portal/ninja2020/gateways/stripe/klarna/klarna.blade.php b/resources/views/portal/ninja2020/gateways/stripe/klarna/klarna.blade.php new file mode 100644 index 000000000000..69bda5a6c29c --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/klarna/klarna.blade.php @@ -0,0 +1,69 @@ +