From 4f362f17ab1b21210de2fb6f89cf1fee5dab041d Mon Sep 17 00:00:00 2001 From: hillelcoren Date: Wed, 25 Aug 2021 09:50:56 +0000 Subject: [PATCH 01/66] Admin Portal - Hosted --- public/flutter_service_worker.js | 4 ++-- public/main.dart.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/flutter_service_worker.js b/public/flutter_service_worker.js index 64679534a1..249a706e35 100755 --- a/public/flutter_service_worker.js +++ b/public/flutter_service_worker.js @@ -33,8 +33,8 @@ const RESOURCES = { "manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40", "version.json": "46d4015fc9abcefe5371cafcf2084173", "favicon.ico": "51636d3a390451561744c42188ccd628", -"main.dart.js": "6c0d755c0f7fe5211d33ac59d499dafe", -"/": "76a5fa48cfed240c8326ae7736d8044d" +"main.dart.js": "ad09f3d4a2f418fe67aa5e04dfde7fc7", +"/": "557b6af2ed285b00c0499fe5e06805cd" }; // The application shell files that are downloaded before a service worker can diff --git a/public/main.dart.js b/public/main.dart.js index e1293306dd..14a53e6ac8 100755 --- a/public/main.dart.js +++ b/public/main.dart.js @@ -38997,7 +38997,7 @@ while(true)switch(s){case 0:p=a.a a.a=p==null?null:p if(a.db==null){q=a.r1.aVA(a.k4) a.db=q}p=a.cy -a.cy=p==null?"ebe27c5902e4f81eb389257cb979a4091da52207":p +a.cy=p==null?"576eabd27a218caae56535d3be029e6e855c5331":p p=a.go a.go=p==null?null:p return P.S(null,r)}}) @@ -154235,7 +154235,7 @@ return s.a=J.f_(s.a,"\n \u2022 "+H.i(a))}, $S:9} F.ddo.prototype={ $1:function(a){a.a="https://634363c8dd6048b8ae89ab6c66dd9c24@sentry.invoicing.co/7" -a.cy="ebe27c5902e4f81eb389257cb979a4091da52207" +a.cy="576eabd27a218caae56535d3be029e6e855c5331" a.go="5.0.58" a.ch=new F.ddn(this.a)}, $S:741} From 34fe703c78caf6bfca21ceb7b6db8ee5234b4ddb Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 25 Aug 2021 20:25:33 +1000 Subject: [PATCH 02/66] Schema Dump --- database/schema/db-ninja-01-schema.dump | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/database/schema/db-ninja-01-schema.dump b/database/schema/db-ninja-01-schema.dump index 7e27ead707..87ee5eaa83 100644 --- a/database/schema/db-ninja-01-schema.dump +++ b/database/schema/db-ninja-01-schema.dump @@ -372,9 +372,12 @@ CREATE TABLE `companies` ( `expense_inclusive_taxes` tinyint(1) NOT NULL DEFAULT '0', `session_timeout` int(11) NOT NULL DEFAULT '0', `oauth_password_required` tinyint(1) NOT NULL DEFAULT '0', - `invoice_task_datelog` tinyint(1) NOT NULL DEFAULT '0', + `invoice_task_datelog` tinyint(1) NOT NULL DEFAULT '1', `default_password_timeout` int(11) NOT NULL DEFAULT '30', `show_task_end_date` tinyint(1) NOT NULL DEFAULT '0', + `markdown_enabled` tinyint(1) NOT NULL DEFAULT '1', + `use_comma_as_decimal_place` tinyint(1) NOT NULL DEFAULT '0', + `report_include_drafts` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `companies_company_key_unique` (`company_key`), KEY `companies_industry_id_foreign` (`industry_id`), @@ -1959,3 +1962,14 @@ INSERT INTO `migrations` VALUES (80,'2021_06_24_095942_payments_table_currency_n INSERT INTO `migrations` VALUES (81,'2021_06_24_115919_update_designs',2); INSERT INTO `migrations` VALUES (82,'2021_07_08_115919_update_designs',3); INSERT INTO `migrations` VALUES (83,'2021_07_10_085821_activate_payfast_payment_driver',3); +INSERT INTO `migrations` VALUES (84,'2021_07_19_074503_set_invoice_task_datelog_true_in_companies_table',4); +INSERT INTO `migrations` VALUES (85,'2021_07_20_095537_activate_paytrace_payment_driver',4); +INSERT INTO `migrations` VALUES (86,'2021_07_21_213344_change_english_languages_tables',4); +INSERT INTO `migrations` VALUES (87,'2021_07_21_234227_activate_eway_payment_driver',4); +INSERT INTO `migrations` VALUES (88,'2021_08_03_115024_activate_mollie_payment_driver',4); +INSERT INTO `migrations` VALUES (89,'2021_08_05_235942_add_zelle_payment_type',4); +INSERT INTO `migrations` VALUES (90,'2021_08_07_222435_add_markdown_enabled_column_to_companies_table',4); +INSERT INTO `migrations` VALUES (91,'2021_08_10_034407_add_more_languages',4); +INSERT INTO `migrations` VALUES (92,'2021_08_18_220124_use_comma_as_decimal_place_companies_table',4); +INSERT INTO `migrations` VALUES (93,'2021_08_24_115919_update_designs',4); +INSERT INTO `migrations` VALUES (94,'2021_08_25_093105_report_include_drafts_in_companies_table',5); From 74b4ef5b13705eb6ea4bb29a5d7d4af0f80ce8d2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 25 Aug 2021 20:53:13 +1000 Subject: [PATCH 03/66] Fixes for translations in emails --- app/Http/Controllers/MigrationController.php | 5 +++++ app/Jobs/Util/Import.php | 2 -- app/Jobs/Util/StartMigration.php | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/MigrationController.php b/app/Http/Controllers/MigrationController.php index 94ab88c512..dad349fde6 100644 --- a/app/Http/Controllers/MigrationController.php +++ b/app/Http/Controllers/MigrationController.php @@ -25,6 +25,7 @@ use App\Utils\Ninja; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Request; use Illuminate\Support\Str; +use Illuminate\Support\Facades\App; class MigrationController extends BaseController { @@ -263,6 +264,10 @@ class MigrationController extends BaseController // Look for possible existing company (based on company keys). $existing_company = Company::whereRaw('BINARY `company_key` = ?', [$company->company_key])->first(); + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($user->account->companies()->first()->settings)); + if(!$existing_company && $company_count >=10) { $nmo = new NinjaMailerObject; diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 664c09afa9..4ee2ab0d4e 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -262,8 +262,6 @@ class Import implements ShouldQueue /*After a migration first some basic jobs to ensure the system is up to date*/ VersionCheck::dispatch(); - - // CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78); // CreateCompanyTaskStatuses::dispatchNow($this->company, $this->user); diff --git a/app/Jobs/Util/StartMigration.php b/app/Jobs/Util/StartMigration.php index b4e54a07dc..b7d8654b09 100644 --- a/app/Jobs/Util/StartMigration.php +++ b/app/Jobs/Util/StartMigration.php @@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Storage; use ZipArchive; +use Illuminate\Support\Facades\App; class StartMigration implements ShouldQueue { @@ -122,6 +123,10 @@ class StartMigration implements ShouldQueue $this->company->update_products = $update_product_flag; $this->company->save(); + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->company->settings)); + } catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing | \Exception $e) { $this->company->update_products = $update_product_flag; From 2fa360df193f66755b1c0ec36e05b7592b94d64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 25 Aug 2021 14:31:08 +0200 Subject: [PATCH 04/66] Fixes for `Paytrace` --- .../clients/payments/paytrace-credit-card.js | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/resources/js/clients/payments/paytrace-credit-card.js b/resources/js/clients/payments/paytrace-credit-card.js index 838c2333e5..ba49c834e9 100644 --- a/resources/js/clients/payments/paytrace-credit-card.js +++ b/resources/js/clients/payments/paytrace-credit-card.js @@ -137,49 +137,49 @@ class PayTraceCreditCard { } handle() { - this.setupPayTrace().then((instance) => { - this.ptInstance = instance; - this.updatePayTraceLabels(); + Array.from( + document.getElementsByClassName('toggle-payment-with-token') + ).forEach((element) => + element.addEventListener('click', (element) => { + document + .getElementById('paytrace--credit-card-container') + .classList.add('hidden'); + document.getElementById( + 'save-card--container' + ).style.display = 'none'; + document.querySelector('input[name=token]').value = + element.target.dataset.token; + }) + ); - Array.from( - document.getElementsByClassName('toggle-payment-with-token') - ).forEach((element) => - element.addEventListener('click', (element) => { - document - .getElementById('paytrace--credit-card-container') - .classList.add('hidden'); - document.getElementById( - 'save-card--container' - ).style.display = 'none'; - document.querySelector('input[name=token]').value = - element.target.dataset.token; - }) - ); + document + .getElementById('toggle-payment-with-credit-card') + ?.addEventListener('click', (element) => { + document + .getElementById('paytrace--credit-card-container') + .classList.remove('hidden'); + document.getElementById( + 'save-card--container' + ).style.display = 'grid'; + document.querySelector('input[name=token]').value = ''; - document - .getElementById('toggle-payment-with-credit-card') - ?.addEventListener('click', (element) => { - document - .getElementById('paytrace--credit-card-container') - .classList.remove('hidden'); - document.getElementById( - 'save-card--container' - ).style.display = 'grid'; - document.querySelector('input[name=token]').value = ''; + this.setupPayTrace().then((instance) => { + this.ptInstance = instance; + this.updatePayTraceLabels(); }); + }); - document - .getElementById('pay-now') - .addEventListener('click', (e) => { - if ( - document.querySelector('input[name=token]').value === '' - ) { - return this.handlePaymentWithCreditCard(e); - } + document + .getElementById('pay-now') + .addEventListener('click', (e) => { + if ( + document.querySelector('input[name=token]').value === '' + ) { + return this.handlePaymentWithCreditCard(e); + } - return this.handlePaymentWithToken(e); - }); - }); + return this.handlePaymentWithToken(e); + }); } } From 73ecbe0d398e62aa217d00c1d06f80c7733330d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 25 Aug 2021 14:55:28 +0200 Subject: [PATCH 05/66] Production builds --- public/js/clients/payments/paytrace-credit-card.js | 2 +- public/mix-manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/clients/payments/paytrace-credit-card.js b/public/js/clients/payments/paytrace-credit-card.js index b9e91af4e4..e2f9f0f53e 100644 --- a/public/js/clients/payments/paytrace-credit-card.js +++ b/public/js/clients/payments/paytrace-credit-card.js @@ -1,2 +1,2 @@ /*! For license information please see paytrace-credit-card.js.LICENSE.txt */ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=21)}({"0Swb":function(e,t){function n(e,t){for(var n=0;n=1){var r=document.getElementById("errors");return r.textContent=n[0].description,r.hidden=!1,e.target.parentElement.disabled=!1}t.ptInstance.process().then((function(e){document.getElementById("HPF_Token").value=e.message.hpf_token,document.getElementById("enc_key").value=e.message.enc_key;var t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value),document.getElementById("server_response").submit()})).catch((function(e){document.getElementById("errors").textContent=JSON.stringify(e),document.getElementById("errors").hidden=!1,console.log(e)}))}))}},{key:"handlePaymentWithToken",value:function(e){e.target.parentElement.disabled=!0,document.getElementById("server_response").submit()}},{key:"handle",value:function(){var e=this;this.setupPayTrace().then((function(t){var n;e.ptInstance=t,e.updatePayTraceLabels(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(e){return e.addEventListener("click",(function(e){document.getElementById("paytrace--credit-card-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token}))})),null===(n=document.getElementById("toggle-payment-with-credit-card"))||void 0===n||n.addEventListener("click",(function(e){document.getElementById("paytrace--credit-card-container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value=""})),document.getElementById("pay-now").addEventListener("click",(function(t){return""===document.querySelector("input[name=token]").value?e.handlePaymentWithCreditCard(t):e.handlePaymentWithToken(t)}))}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()},21:function(e,t,n){e.exports=n("0Swb")}}); \ No newline at end of file +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=21)}({"0Swb":function(e,t){function n(e,t){for(var n=0;n=1){var r=document.getElementById("errors");return r.textContent=n[0].description,r.hidden=!1,e.target.parentElement.disabled=!1}t.ptInstance.process().then((function(e){document.getElementById("HPF_Token").value=e.message.hpf_token,document.getElementById("enc_key").value=e.message.enc_key;var t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value),document.getElementById("server_response").submit()})).catch((function(e){document.getElementById("errors").textContent=JSON.stringify(e),document.getElementById("errors").hidden=!1,console.log(e)}))}))}},{key:"handlePaymentWithToken",value:function(e){e.target.parentElement.disabled=!0,document.getElementById("server_response").submit()}},{key:"handle",value:function(){var e,t=this;Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(e){return e.addEventListener("click",(function(e){document.getElementById("paytrace--credit-card-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token}))})),null===(e=document.getElementById("toggle-payment-with-credit-card"))||void 0===e||e.addEventListener("click",(function(e){document.getElementById("paytrace--credit-card-container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value="",t.setupPayTrace().then((function(e){t.ptInstance=e,t.updatePayTraceLabels()}))})),document.getElementById("pay-now").addEventListener("click",(function(e){return""===document.querySelector("input[name=token]").value?t.handlePaymentWithCreditCard(e):t.handlePaymentWithToken(e)}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()},21:function(e,t,n){e.exports=n("0Swb")}}); \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 2f4ce14c06..af47692a14 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -13,7 +13,7 @@ "/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=065e5450233cc5b47020", "/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js?id=08ea84e9451abd434cff", "/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js?id=73b66e88e2daabcd6549", - "/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=c8d3808a4c02d1392e96", + "/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=c2b5f7831e1a46dd5fb2", "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7", "/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=665ddf663500767f1a17", "/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=a30464874dee84678344", From 86e069d8231de8a05cc921b044aba1d617d5a285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 25 Aug 2021 16:28:10 +0200 Subject: [PATCH 06/66] `error` layout --- .../portal/ninja2020/layout/error.blade.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 resources/views/portal/ninja2020/layout/error.blade.php diff --git a/resources/views/portal/ninja2020/layout/error.blade.php b/resources/views/portal/ninja2020/layout/error.blade.php new file mode 100644 index 0000000000..920eb59334 --- /dev/null +++ b/resources/views/portal/ninja2020/layout/error.blade.php @@ -0,0 +1,26 @@ +@extends('portal.ninja2020.layout.clean') +@section('meta_title', $__env->yieldContent('title')) + +@section('body') + +@endsection + + From d3c265abc79dc4f99df95347df82a4e591904521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 25 Aug 2021 16:28:18 +0200 Subject: [PATCH 07/66] Error pages --- resources/views/errors/401.blade.php | 2 +- resources/views/errors/403.blade.php | 2 +- resources/views/errors/404.blade.php | 2 +- resources/views/errors/419.blade.php | 2 +- resources/views/errors/429.blade.php | 2 +- resources/views/errors/500.blade.php | 2 +- resources/views/errors/503.blade.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/views/errors/401.blade.php b/resources/views/errors/401.blade.php index 5c586db96b..8eb9a8a7db 100644 --- a/resources/views/errors/401.blade.php +++ b/resources/views/errors/401.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Unauthorized')) @section('code', '401') diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php index a5506f01f2..c5cb0b442c 100644 --- a/resources/views/errors/403.blade.php +++ b/resources/views/errors/403.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Forbidden')) @section('code', '403') diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php index 7549540d8d..2e32946703 100644 --- a/resources/views/errors/404.blade.php +++ b/resources/views/errors/404.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Not Found')) @section('code', '404') diff --git a/resources/views/errors/419.blade.php b/resources/views/errors/419.blade.php index c09216e212..6c955c53bc 100644 --- a/resources/views/errors/419.blade.php +++ b/resources/views/errors/419.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Page Expired')) @section('code', '419') diff --git a/resources/views/errors/429.blade.php b/resources/views/errors/429.blade.php index f01b07b8ed..9d86f733b8 100644 --- a/resources/views/errors/429.blade.php +++ b/resources/views/errors/429.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Too Many Requests')) @section('code', '429') diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php index d9e95d9b99..7d212a1c42 100644 --- a/resources/views/errors/500.blade.php +++ b/resources/views/errors/500.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Server Error')) @section('code', '500') diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php index acd38100a7..d72c1f1ad6 100644 --- a/resources/views/errors/503.blade.php +++ b/resources/views/errors/503.blade.php @@ -1,4 +1,4 @@ -@extends('errors::minimal') +@extends('portal.ninja2020.layout.error') @section('title', __('Service Unavailable')) @section('code', '503') From 8593b0c35736263824ac66f90ae4c2ec4e516b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 25 Aug 2021 16:28:30 +0200 Subject: [PATCH 08/66] Update `button-link` --- resources/sass/components/buttons.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/sass/components/buttons.scss b/resources/sass/components/buttons.scss index 8aeca8c757..0dfea5b567 100644 --- a/resources/sass/components/buttons.scss +++ b/resources/sass/components/buttons.scss @@ -35,8 +35,10 @@ button:disabled { } .button-link { + @apply text-gray-700; + &:hover { - @apply font-semibold underline; + @apply text-gray-900 underline; } &:focus { From 9123f88570d05f15f2a22b628931358bfd094f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 25 Aug 2021 16:28:39 +0200 Subject: [PATCH 09/66] Production build of assets --- public/css/app.css | 2 +- public/mix-manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/css/app.css b/public/css/app.css index a7643b05cf..dbbedf1cd9 100755 --- a/public/css/app.css +++ b/public/css/app.css @@ -1 +1 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}details{display:block}summary{display:list-item}[hidden],template{display:none}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:Open Sans,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #d2d6dc}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#a0aec0}input::placeholder,textarea::placeholder{color:#a0aec0}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}.form-select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M7 7l3-3 3 3m0 6l-3 3-3-3' stroke='%239fa6b2' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;background-repeat:no-repeat;background-color:#fff;border-color:#d2d6dc;border-width:1px;border-radius:.375rem;padding:.5rem 2.5rem .5rem .75rem;font-size:1rem;line-height:1.5;background-position:right .5rem center;background-size:1.5em 1.5em}.form-select::-ms-expand{color:#9fa6b2;border:none}@media not print{.form-select::-ms-expand{display:none}}@media print and (-ms-high-contrast:active),print and (-ms-high-contrast:none){.form-select{padding-right:.75rem}}.form-select:focus{outline:none;box-shadow:0 0 0 3px rgba(164,202,254,.45);border-color:#a4cafe}.form-checkbox:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4a1 1 0 00-1.414-1.414L7 8.586 5.707 7.293z'/%3E%3C/svg%3E");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media not print{.form-checkbox::-ms-check{border-width:1px;color:transparent;background:inherit;border-color:inherit;border-radius:inherit}}.form-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#3f83f8;background-color:#fff;border-color:#d2d6dc;border-width:1px;border-radius:.25rem}.form-checkbox:focus{outline:none;box-shadow:0 0 0 3px rgba(164,202,254,.45);border-color:#a4cafe}.form-checkbox:checked:focus,.form-radio:checked{border-color:transparent}.form-radio:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E");background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media not print{.form-radio::-ms-check{border-width:1px;color:transparent;background:inherit;border-color:inherit;border-radius:inherit}}.form-radio{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;border-radius:100%;height:1rem;width:1rem;color:#3f83f8;background-color:#fff;border-color:#d2d6dc;border-width:1px}.form-radio:focus{outline:none;box-shadow:0 0 0 3px rgba(164,202,254,.45);border-color:#a4cafe}.form-radio:checked:focus{border-color:transparent}.button{border-radius:.25rem;padding:.75rem 1rem;font-size:.875rem;line-height:1rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}button:disabled{opacity:.5;cursor:not-allowed}.button-primary{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.button-primary:hover{font-weight:600}.button-block{display:block;width:100%}.button-danger{--bg-opacity:1;background-color:#f05252;background-color:rgba(240,82,82,var(--bg-opacity));--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.button-danger:hover{--bg-opacity:1;background-color:#e02424;background-color:rgba(224,36,36,var(--bg-opacity))}.button-secondary{--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity))}.button-secondary:hover{--bg-opacity:1;background-color:#e5e7eb;background-color:rgba(229,231,235,var(--bg-opacity))}.button-link:hover{font-weight:600;text-decoration:underline}.button-link:focus{outline:2px solid transparent;outline-offset:2px;text-decoration:underline}.validation{border-left-width:2px;margin-top:.5rem;margin-bottom:.25rem;--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity));padding:.25rem .75rem}.validation-fail{border-color:#f05252;border-color:rgba(240,82,82,var(--border-opacity))}.validation-fail,.validation-pass{--border-opacity:1;--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity));font-size:.875rem}.validation-pass{border-color:#0e9f6e;border-color:rgba(14,159,110,var(--border-opacity))}.input{align-items:center;border-width:1px;--border-opacity:1;border-color:#d2d6dc;border-color:rgba(210,214,220,var(--border-opacity));border-radius:.25rem;margin-top:.5rem;padding:.5rem 1rem;font-size:.875rem}.input:focus{outline:2px solid transparent;outline-offset:2px;--bg-opacity:1;background-color:#f9fafb;background-color:rgba(249,250,251,var(--bg-opacity))}.input-label{font-size:.875rem;--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.input-slim{padding-top:.5rem;padding-bottom:.5rem}.alert{padding:.75rem 1rem;font-size:.875rem;border-left-width:2px;margin-top:.5rem;margin-bottom:.25rem;--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity));--border-opacity:1;border-color:#9fa6b2;border-color:rgba(159,166,178,var(--border-opacity))}.alert-success{--border-opacity:1;border-color:#0e9f6e;border-color:rgba(14,159,110,var(--border-opacity))}.alert-failure{--border-opacity:1;border-color:#f05252;border-color:rgba(240,82,82,var(--border-opacity))}.badge{display:inline-flex;align-items:center;padding:.125rem .625rem;border-radius:9999px;font-size:.75rem;font-weight:500;line-height:1rem}.badge-light{background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity));color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.badge-light,.badge-primary{--bg-opacity:1;--text-opacity:1}.badge-primary{background-color:#c3ddfd;background-color:rgba(195,221,253,var(--bg-opacity));color:#3f83f8;color:rgba(63,131,248,var(--text-opacity))}.badge-danger{background-color:#fde8e8;background-color:rgba(253,232,232,var(--bg-opacity));color:#f05252;color:rgba(240,82,82,var(--text-opacity))}.badge-danger,.badge-success{--bg-opacity:1;--text-opacity:1}.badge-success{background-color:#def7ec;background-color:rgba(222,247,236,var(--bg-opacity));color:#0e9f6e;color:rgba(14,159,110,var(--text-opacity))}.badge-secondary{--bg-opacity:1;background-color:#252f3f;background-color:rgba(37,47,63,var(--bg-opacity));--text-opacity:1;color:#e5e7eb;color:rgba(229,231,235,var(--text-opacity))}.badge-warning{background-color:#feecdc;background-color:rgba(254,236,220,var(--bg-opacity));color:#ff5a1f;color:rgba(255,90,31,var(--text-opacity))}.badge-info,.badge-warning{--bg-opacity:1;--text-opacity:1}.badge-info{background-color:#e1effe;background-color:rgba(225,239,254,var(--bg-opacity));color:#3f83f8;color:rgba(63,131,248,var(--text-opacity))}@media (min-width:640px){.dataTables_length{margin-top:1.25rem!important;margin-bottom:1.25rem!important}}@media (min-width:1024px){.dataTables_length{margin-top:1rem!important;margin-bottom:1rem!important}}.dataTables_length select{--bg-opacity:1!important;background-color:#fff!important;background-color:rgba(255,255,255,var(--bg-opacity))!important;align-items:center!important;border-width:1px!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important;border-radius:.25rem!important;margin-top:.5rem!important;font-size:.875rem!important;margin-left:.5rem!important;margin-right:.5rem!important;padding:.5rem!important}.dataTables_filter{margin-bottom:1rem}.dataTables_filter input{-webkit-appearance:none!important;-moz-appearance:none!important;appearance:none!important;background-color:#fff!important;border-radius:.375rem!important;font-size:1rem!important;line-height:1.5!important;align-items:center!important;border-width:1px!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important;border-radius:.25rem!important;margin-top:.5rem!important;padding:.5rem 1rem!important;font-size:.875rem!important}@media (min-width:1024px){.dataTables_filter{margin-top:-3rem!important}}.dataTables_paginate{padding-bottom:1.5rem!important;padding-top:.5rem!important}.dataTables_paginate .paginate_button{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform!important;transition-duration:.15s!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;--bg-opacity:1!important;background-color:#fff!important;background-color:rgba(255,255,255,var(--bg-opacity))!important;border-width:1px!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important;font-size:.875rem!important;line-height:1rem!important;font-weight:500!important;border-radius:.25rem!important;--text-opacity:1!important;color:#374151!important;color:rgba(55,65,81,var(--text-opacity))!important;margin-right:.25rem!important;padding:.5rem 1rem!important;cursor:pointer!important}.dataTables_paginate .current{--bg-opacity:1!important;background-color:#1c64f2!important;background-color:rgba(28,100,242,var(--bg-opacity))!important;--text-opacity:1!important;color:#fff!important;color:rgba(255,255,255,var(--text-opacity))!important}.dataTables_info{font-size:.875rem!important}.dataTables_empty{padding-top:1rem!important;padding-bottom:1rem!important}.pagination{display:flex!important;align-items:center!important}.pagination .page-link{margin-top:-1px!important;border-top-width:2px!important;border-color:transparent!important;padding-top:1rem!important;padding-left:1rem!important;padding-right:1rem!important;display:inline-flex!important;align-items:center!important;font-size:.875rem!important;line-height:1.25rem!important;font-weight:500!important;--text-opacity:1!important;color:#6b7280!important;color:rgba(107,114,128,var(--text-opacity))!important;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;transition-duration:.15s!important;cursor:pointer!important}.pagination .page-link:hover{--text-opacity:1!important;color:#374151!important;color:rgba(55,65,81,var(--text-opacity))!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important}.pagination .page-link:focus{outline:2px solid transparent;outline-offset:2px;--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity));--border-opacity:1;border-color:#9fa6b2;border-color:rgba(159,166,178,var(--border-opacity))}.pagination .active>span{--text-opacity:1!important;color:#1c64f2!important;color:rgba(28,100,242,var(--text-opacity))!important;--border-opacity:1!important;border-color:#1c64f2!important;border-color:rgba(28,100,242,var(--border-opacity))!important}.space-x-1>:not(template)~:not(template){--space-x-reverse:0;margin-right:calc(0.25rem*var(--space-x-reverse));margin-left:calc(0.25rem*(1 - var(--space-x-reverse)))}.space-x-2>:not(template)~:not(template){--space-x-reverse:0;margin-right:calc(0.5rem*var(--space-x-reverse));margin-left:calc(0.5rem*(1 - var(--space-x-reverse)))}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-50{--bg-opacity:1;background-color:#f9fafb;background-color:rgba(249,250,251,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity))}.bg-gray-200{--bg-opacity:1;background-color:#e5e7eb;background-color:rgba(229,231,235,var(--bg-opacity))}.bg-gray-500{--bg-opacity:1;background-color:#6b7280;background-color:rgba(107,114,128,var(--bg-opacity))}.bg-gray-600{--bg-opacity:1;background-color:#4b5563;background-color:rgba(75,85,99,var(--bg-opacity))}.bg-red-100{--bg-opacity:1;background-color:#fde8e8;background-color:rgba(253,232,232,var(--bg-opacity))}.bg-blue-50{--bg-opacity:1;background-color:#ebf5ff;background-color:rgba(235,245,255,var(--bg-opacity))}.bg-blue-600{--bg-opacity:1;background-color:#1c64f2;background-color:rgba(28,100,242,var(--bg-opacity))}.focus\:bg-gray-100:focus,.hover\:bg-gray-100:hover{--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity))}.focus\:bg-gray-600:focus{--bg-opacity:1;background-color:#4b5563;background-color:rgba(75,85,99,var(--bg-opacity))}.active\:bg-gray-50:active{--bg-opacity:1;background-color:#f9fafb;background-color:rgba(249,250,251,var(--bg-opacity))}.border-collapse{border-collapse:collapse}.border-gray-100{--border-opacity:1;border-color:#f4f5f7;border-color:rgba(244,245,247,var(--border-opacity))}.border-gray-200{--border-opacity:1;border-color:#e5e7eb;border-color:rgba(229,231,235,var(--border-opacity))}.border-gray-300{--border-opacity:1;border-color:#d2d6dc;border-color:rgba(210,214,220,var(--border-opacity))}.border-red-300{--border-opacity:1;border-color:#f8b4b4;border-color:rgba(248,180,180,var(--border-opacity))}.border-red-400{--border-opacity:1;border-color:#f98080;border-color:rgba(249,128,128,var(--border-opacity))}.border-green-500{--border-opacity:1;border-color:#0e9f6e;border-color:rgba(14,159,110,var(--border-opacity))}.border-blue-500{--border-opacity:1;border-color:#3f83f8;border-color:rgba(63,131,248,var(--border-opacity))}.group:hover .group-hover\:border-transparent{border-color:transparent}.hover\:border-gray-800:hover{--border-opacity:1;border-color:#252f3f;border-color:rgba(37,47,63,var(--border-opacity))}.hover\:border-blue-600:hover{--border-opacity:1;border-color:#1c64f2;border-color:rgba(28,100,242,var(--border-opacity))}.focus\:border-blue-300:focus{--border-opacity:1;border-color:#a4cafe;border-color:rgba(164,202,254,var(--border-opacity))}.rounded-sm{border-radius:.125rem}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-0{border-width:0}.border-4{border-width:4px}.border{border-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-pointer{cursor:pointer}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.focus\:font-semibold:focus,.hover\:font-semibold:hover{font-weight:600}.h-0{height:0}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-32{height:8rem}.h-64{height:16rem}.h-auto{height:auto}.h-screen{height:100vh}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem}.text-3xl{font-size:1.875rem}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.m-0{margin:0}.m-auto{margin:auto}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.mx-auto{margin-left:auto;margin-right:auto}.-my-2{margin-top:-.5rem;margin-bottom:-.5rem}.mt-0{margin-top:0}.mb-0{margin-bottom:0}.ml-0{margin-left:0}.mt-1{margin-top:.25rem}.mr-1{margin-right:.25rem}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mb-6{margin-bottom:1.5rem}.mt-8{margin-top:2rem}.mt-10{margin-top:2.5rem}.mb-10{margin-bottom:2.5rem}.mt-16{margin-top:4rem}.-mr-1{margin-right:-.25rem}.-ml-1{margin-left:-.25rem}.-mt-4{margin-top:-1rem}.-ml-4{margin-left:-1rem}.-mr-14{margin-right:-3.5rem}.max-w-xs{max-width:20rem}.max-w-xl{max-width:36rem}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.object-cover{-o-object-fit:cover;object-fit:cover}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.opacity-100{opacity:1}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.p-10{padding:2.5rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.pt-0{padding-top:0}.pl-0{padding-left:0}.pt-4{padding-top:1rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pb-20{padding-bottom:5rem}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{right:0;left:0}.inset-0,.inset-y-0{top:0;bottom:0}.inset-x-0{right:0;left:0}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.shadow-xs{box-shadow:0 0 0 1px rgba(0,0,0,.05)}.shadow-sm{box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.shadow-lg{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)}.shadow-xl{box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04)}.hover\:shadow-lg:hover{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)}.focus\:shadow-outline:focus{box-shadow:0 0 0 3px rgba(118,169,250,.45)}.focus\:shadow-outline-blue:focus{box-shadow:0 0 0 3px rgba(164,202,254,.45)}.fill-current{fill:currentColor}.table-auto{table-layout:auto}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.text-black{--text-opacity:1;color:#000;color:rgba(0,0,0,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#d2d6dc;color:rgba(210,214,220,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#9fa6b2;color:rgba(159,166,178,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity))}.text-gray-800{--text-opacity:1;color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity))}.text-red-400{--text-opacity:1;color:#f98080;color:rgba(249,128,128,var(--text-opacity))}.text-red-500{--text-opacity:1;color:#f05252;color:rgba(240,82,82,var(--text-opacity))}.text-red-600{--text-opacity:1;color:#e02424;color:rgba(224,36,36,var(--text-opacity))}.text-green-600{--text-opacity:1;color:#057a55;color:rgba(5,122,85,var(--text-opacity))}.text-blue-600{--text-opacity:1;color:#1c64f2;color:rgba(28,100,242,var(--text-opacity))}.hover\:text-gray-300:hover{--text-opacity:1;color:#d2d6dc;color:rgba(210,214,220,var(--text-opacity))}.hover\:text-gray-500:hover{--text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--text-opacity))}.hover\:text-gray-600:hover{--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.hover\:text-gray-700:hover{--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity))}.hover\:text-gray-800:hover{--text-opacity:1;color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.hover\:text-gray-900:hover{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity))}.hover\:text-blue-600:hover{--text-opacity:1;color:#1c64f2;color:rgba(28,100,242,var(--text-opacity))}.hover\:text-indigo-900:hover{--text-opacity:1;color:#362f78;color:rgba(54,47,120,var(--text-opacity))}.focus\:text-gray-500:focus{--text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--text-opacity))}.focus\:text-gray-600:focus{--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.focus\:text-gray-900:focus{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity))}.active\:text-gray-800:active{--text-opacity:1;color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.underline{text-decoration:underline}.line-through{text-decoration:line-through}.focus\:underline:focus,.hover\:underline:hover{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.whitespace-no-wrap{white-space:nowrap}.break-words{word-wrap:break-word;overflow-wrap:break-word}.break-all{word-break:break-all}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.w-0{width:0}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-48{width:12rem}.w-56{width:14rem}.w-64{width:16rem}.w-auto{width:auto}.w-1\/2{width:50%}.w-full{width:100%}.w-screen{width:100vw}.z-0{z-index:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.gap-4{grid-gap:1rem;gap:1rem}.gap-5{grid-gap:1.25rem;gap:1.25rem}.gap-6{grid-gap:1.5rem;gap:1.5rem}.gap-8{grid-gap:2rem;gap:2rem}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-6{grid-column:span 6/span 6}.col-span-8{grid-column:span 8/span 8}.col-span-12{grid-column:span 12/span 12}.transform{--transform-translate-x:0;--transform-translate-y:0;--transform-rotate:0;--transform-skew-x:0;--transform-skew-y:0;--transform-scale-x:1;--transform-scale-y:1;transform:translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y))}.origin-top-right{transform-origin:top right}.scale-95{--transform-scale-x:.95;--transform-scale-y:.95}.scale-100{--transform-scale-x:1;--transform-scale-y:1}.translate-x-0{--transform-translate-x:0}.-translate-x-full{--transform-translate-x:-100%}.translate-y-0{--transform-translate-y:0}.translate-y-4{--transform-translate-y:1rem}.transition-all{transition-property:all}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform}.transition-opacity{transition-property:opacity}.ease-linear{transition-timing-function:linear}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-75{transition-duration:75ms}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:flex-no-wrap{flex-wrap:nowrap}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:flex-shrink-0{flex-shrink:0}.sm\:h-10{height:2.5rem}.sm\:h-screen{height:100vh}.sm\:mx-0{margin-left:0;margin-right:0}.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mt-0{margin-top:0}.sm\:ml-3{margin-left:.75rem}.sm\:mt-4{margin-top:1rem}.sm\:ml-4{margin-left:1rem}.sm\:mt-6{margin-top:1.5rem}.sm\:ml-6{margin-left:1.5rem}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-lg{max-width:32rem}.sm\:p-0{padding:0}.sm\:p-6{padding:1.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:inset-0{top:0;right:0;bottom:0;left:0}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:w-10{width:2.5rem}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:gap-4{grid-gap:1rem;gap:1rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:scale-95{--transform-scale-x:.95;--transform-scale-y:.95}.sm\:scale-100{--transform-scale-x:1;--transform-scale-y:1}.sm\:translate-y-0{--transform-translate-y:0}}@media (min-width:768px){.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:justify-between{justify-content:space-between}.md\:flex-shrink-0{flex-shrink:0}.md\:text-sm{font-size:.875rem}.md\:mt-0{margin-top:0}.md\:mr-2{margin-right:.5rem}.md\:ml-2{margin-left:.5rem}.md\:ml-6{margin-left:1.5rem}.md\:mt-10{margin-top:2.5rem}.md\:-mr-1{margin-right:-.25rem}.md\:max-w-3xl{max-width:48rem}.md\:p-24{padding:6rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.333333%}.md\:gap-6{grid-gap:1.5rem;gap:1.5rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-start-2{grid-column-start:2}.md\:col-start-4{grid-column-start:4}}@media (min-width:1024px){.lg\:rounded-lg{border-radius:.5rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:items-center{align-items:center}.lg\:h-screen{height:100vh}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:mt-24{margin-top:6rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:gap-4{grid-gap:1rem;gap:1rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-start-3{grid-column-start:3}.lg\:col-start-4{grid-column-start:4}}@media (min-width:1280px){.xl\:mt-32{margin-top:8rem}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-6{grid-column:span 6/span 6}.xl\:col-span-8{grid-column:span 8/span 8}.xl\:col-span-9{grid-column:span 9/span 9}.xl\:col-start-4{grid-column-start:4}} \ No newline at end of file +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}details{display:block}summary{display:list-item}[hidden],template{display:none}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:Open Sans,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #d2d6dc}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#a0aec0}input::placeholder,textarea::placeholder{color:#a0aec0}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}.form-select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M7 7l3-3 3 3m0 6l-3 3-3-3' stroke='%239fa6b2' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;background-repeat:no-repeat;background-color:#fff;border-color:#d2d6dc;border-width:1px;border-radius:.375rem;padding:.5rem 2.5rem .5rem .75rem;font-size:1rem;line-height:1.5;background-position:right .5rem center;background-size:1.5em 1.5em}.form-select::-ms-expand{color:#9fa6b2;border:none}@media not print{.form-select::-ms-expand{display:none}}@media print and (-ms-high-contrast:active),print and (-ms-high-contrast:none){.form-select{padding-right:.75rem}}.form-select:focus{outline:none;box-shadow:0 0 0 3px rgba(164,202,254,.45);border-color:#a4cafe}.form-checkbox:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4a1 1 0 00-1.414-1.414L7 8.586 5.707 7.293z'/%3E%3C/svg%3E");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media not print{.form-checkbox::-ms-check{border-width:1px;color:transparent;background:inherit;border-color:inherit;border-radius:inherit}}.form-checkbox{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#3f83f8;background-color:#fff;border-color:#d2d6dc;border-width:1px;border-radius:.25rem}.form-checkbox:focus{outline:none;box-shadow:0 0 0 3px rgba(164,202,254,.45);border-color:#a4cafe}.form-checkbox:checked:focus,.form-radio:checked{border-color:transparent}.form-radio:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E");background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media not print{.form-radio::-ms-check{border-width:1px;color:transparent;background:inherit;border-color:inherit;border-radius:inherit}}.form-radio{-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;border-radius:100%;height:1rem;width:1rem;color:#3f83f8;background-color:#fff;border-color:#d2d6dc;border-width:1px}.form-radio:focus{outline:none;box-shadow:0 0 0 3px rgba(164,202,254,.45);border-color:#a4cafe}.form-radio:checked:focus{border-color:transparent}.button{border-radius:.25rem;padding:.75rem 1rem;font-size:.875rem;line-height:1rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}button:disabled{opacity:.5;cursor:not-allowed}.button-primary{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.button-primary:hover{font-weight:600}.button-block{display:block;width:100%}.button-danger{--bg-opacity:1;background-color:#f05252;background-color:rgba(240,82,82,var(--bg-opacity));--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.button-danger:hover{--bg-opacity:1;background-color:#e02424;background-color:rgba(224,36,36,var(--bg-opacity))}.button-secondary{--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity))}.button-secondary:hover{--bg-opacity:1;background-color:#e5e7eb;background-color:rgba(229,231,235,var(--bg-opacity))}.button-link{--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity))}.button-link:hover{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity));text-decoration:underline}.button-link:focus{outline:2px solid transparent;outline-offset:2px;text-decoration:underline}.validation{border-left-width:2px;margin-top:.5rem;margin-bottom:.25rem;--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity));padding:.25rem .75rem}.validation-fail{border-color:#f05252;border-color:rgba(240,82,82,var(--border-opacity))}.validation-fail,.validation-pass{--border-opacity:1;--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity));font-size:.875rem}.validation-pass{border-color:#0e9f6e;border-color:rgba(14,159,110,var(--border-opacity))}.input{align-items:center;border-width:1px;--border-opacity:1;border-color:#d2d6dc;border-color:rgba(210,214,220,var(--border-opacity));border-radius:.25rem;margin-top:.5rem;padding:.5rem 1rem;font-size:.875rem}.input:focus{outline:2px solid transparent;outline-offset:2px;--bg-opacity:1;background-color:#f9fafb;background-color:rgba(249,250,251,var(--bg-opacity))}.input-label{font-size:.875rem;--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.input-slim{padding-top:.5rem;padding-bottom:.5rem}.alert{padding:.75rem 1rem;font-size:.875rem;border-left-width:2px;margin-top:.5rem;margin-bottom:.25rem;--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity));--border-opacity:1;border-color:#9fa6b2;border-color:rgba(159,166,178,var(--border-opacity))}.alert-success{--border-opacity:1;border-color:#0e9f6e;border-color:rgba(14,159,110,var(--border-opacity))}.alert-failure{--border-opacity:1;border-color:#f05252;border-color:rgba(240,82,82,var(--border-opacity))}.badge{display:inline-flex;align-items:center;padding:.125rem .625rem;border-radius:9999px;font-size:.75rem;font-weight:500;line-height:1rem}.badge-light{background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity));color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.badge-light,.badge-primary{--bg-opacity:1;--text-opacity:1}.badge-primary{background-color:#c3ddfd;background-color:rgba(195,221,253,var(--bg-opacity));color:#3f83f8;color:rgba(63,131,248,var(--text-opacity))}.badge-danger{background-color:#fde8e8;background-color:rgba(253,232,232,var(--bg-opacity));color:#f05252;color:rgba(240,82,82,var(--text-opacity))}.badge-danger,.badge-success{--bg-opacity:1;--text-opacity:1}.badge-success{background-color:#def7ec;background-color:rgba(222,247,236,var(--bg-opacity));color:#0e9f6e;color:rgba(14,159,110,var(--text-opacity))}.badge-secondary{--bg-opacity:1;background-color:#252f3f;background-color:rgba(37,47,63,var(--bg-opacity));--text-opacity:1;color:#e5e7eb;color:rgba(229,231,235,var(--text-opacity))}.badge-warning{background-color:#feecdc;background-color:rgba(254,236,220,var(--bg-opacity));color:#ff5a1f;color:rgba(255,90,31,var(--text-opacity))}.badge-info,.badge-warning{--bg-opacity:1;--text-opacity:1}.badge-info{background-color:#e1effe;background-color:rgba(225,239,254,var(--bg-opacity));color:#3f83f8;color:rgba(63,131,248,var(--text-opacity))}@media (min-width:640px){.dataTables_length{margin-top:1.25rem!important;margin-bottom:1.25rem!important}}@media (min-width:1024px){.dataTables_length{margin-top:1rem!important;margin-bottom:1rem!important}}.dataTables_length select{--bg-opacity:1!important;background-color:#fff!important;background-color:rgba(255,255,255,var(--bg-opacity))!important;align-items:center!important;border-width:1px!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important;border-radius:.25rem!important;margin-top:.5rem!important;font-size:.875rem!important;margin-left:.5rem!important;margin-right:.5rem!important;padding:.5rem!important}.dataTables_filter{margin-bottom:1rem}.dataTables_filter input{-webkit-appearance:none!important;-moz-appearance:none!important;appearance:none!important;background-color:#fff!important;border-radius:.375rem!important;font-size:1rem!important;line-height:1.5!important;align-items:center!important;border-width:1px!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important;border-radius:.25rem!important;margin-top:.5rem!important;padding:.5rem 1rem!important;font-size:.875rem!important}@media (min-width:1024px){.dataTables_filter{margin-top:-3rem!important}}.dataTables_paginate{padding-bottom:1.5rem!important;padding-top:.5rem!important}.dataTables_paginate .paginate_button{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform!important;transition-duration:.15s!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;--bg-opacity:1!important;background-color:#fff!important;background-color:rgba(255,255,255,var(--bg-opacity))!important;border-width:1px!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important;font-size:.875rem!important;line-height:1rem!important;font-weight:500!important;border-radius:.25rem!important;--text-opacity:1!important;color:#374151!important;color:rgba(55,65,81,var(--text-opacity))!important;margin-right:.25rem!important;padding:.5rem 1rem!important;cursor:pointer!important}.dataTables_paginate .current{--bg-opacity:1!important;background-color:#1c64f2!important;background-color:rgba(28,100,242,var(--bg-opacity))!important;--text-opacity:1!important;color:#fff!important;color:rgba(255,255,255,var(--text-opacity))!important}.dataTables_info{font-size:.875rem!important}.dataTables_empty{padding-top:1rem!important;padding-bottom:1rem!important}.pagination{display:flex!important;align-items:center!important}.pagination .page-link{margin-top:-1px!important;border-top-width:2px!important;border-color:transparent!important;padding-top:1rem!important;padding-left:1rem!important;padding-right:1rem!important;display:inline-flex!important;align-items:center!important;font-size:.875rem!important;line-height:1.25rem!important;font-weight:500!important;--text-opacity:1!important;color:#6b7280!important;color:rgba(107,114,128,var(--text-opacity))!important;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform!important;transition-timing-function:cubic-bezier(.4,0,.2,1)!important;transition-duration:.15s!important;cursor:pointer!important}.pagination .page-link:hover{--text-opacity:1!important;color:#374151!important;color:rgba(55,65,81,var(--text-opacity))!important;--border-opacity:1!important;border-color:#d2d6dc!important;border-color:rgba(210,214,220,var(--border-opacity))!important}.pagination .page-link:focus{outline:2px solid transparent;outline-offset:2px;--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity));--border-opacity:1;border-color:#9fa6b2;border-color:rgba(159,166,178,var(--border-opacity))}.pagination .active>span{--text-opacity:1!important;color:#1c64f2!important;color:rgba(28,100,242,var(--text-opacity))!important;--border-opacity:1!important;border-color:#1c64f2!important;border-color:rgba(28,100,242,var(--border-opacity))!important}.space-x-1>:not(template)~:not(template){--space-x-reverse:0;margin-right:calc(0.25rem*var(--space-x-reverse));margin-left:calc(0.25rem*(1 - var(--space-x-reverse)))}.space-x-2>:not(template)~:not(template){--space-x-reverse:0;margin-right:calc(0.5rem*var(--space-x-reverse));margin-left:calc(0.5rem*(1 - var(--space-x-reverse)))}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-50{--bg-opacity:1;background-color:#f9fafb;background-color:rgba(249,250,251,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity))}.bg-gray-200{--bg-opacity:1;background-color:#e5e7eb;background-color:rgba(229,231,235,var(--bg-opacity))}.bg-gray-500{--bg-opacity:1;background-color:#6b7280;background-color:rgba(107,114,128,var(--bg-opacity))}.bg-gray-600{--bg-opacity:1;background-color:#4b5563;background-color:rgba(75,85,99,var(--bg-opacity))}.bg-red-100{--bg-opacity:1;background-color:#fde8e8;background-color:rgba(253,232,232,var(--bg-opacity))}.bg-blue-50{--bg-opacity:1;background-color:#ebf5ff;background-color:rgba(235,245,255,var(--bg-opacity))}.bg-blue-600{--bg-opacity:1;background-color:#1c64f2;background-color:rgba(28,100,242,var(--bg-opacity))}.focus\:bg-gray-100:focus,.hover\:bg-gray-100:hover{--bg-opacity:1;background-color:#f4f5f7;background-color:rgba(244,245,247,var(--bg-opacity))}.focus\:bg-gray-600:focus{--bg-opacity:1;background-color:#4b5563;background-color:rgba(75,85,99,var(--bg-opacity))}.active\:bg-gray-50:active{--bg-opacity:1;background-color:#f9fafb;background-color:rgba(249,250,251,var(--bg-opacity))}.border-collapse{border-collapse:collapse}.border-gray-100{--border-opacity:1;border-color:#f4f5f7;border-color:rgba(244,245,247,var(--border-opacity))}.border-gray-200{--border-opacity:1;border-color:#e5e7eb;border-color:rgba(229,231,235,var(--border-opacity))}.border-gray-300{--border-opacity:1;border-color:#d2d6dc;border-color:rgba(210,214,220,var(--border-opacity))}.border-red-300{--border-opacity:1;border-color:#f8b4b4;border-color:rgba(248,180,180,var(--border-opacity))}.border-red-400{--border-opacity:1;border-color:#f98080;border-color:rgba(249,128,128,var(--border-opacity))}.border-green-500{--border-opacity:1;border-color:#0e9f6e;border-color:rgba(14,159,110,var(--border-opacity))}.border-blue-500{--border-opacity:1;border-color:#3f83f8;border-color:rgba(63,131,248,var(--border-opacity))}.group:hover .group-hover\:border-transparent{border-color:transparent}.hover\:border-gray-800:hover{--border-opacity:1;border-color:#252f3f;border-color:rgba(37,47,63,var(--border-opacity))}.hover\:border-blue-600:hover{--border-opacity:1;border-color:#1c64f2;border-color:rgba(28,100,242,var(--border-opacity))}.focus\:border-blue-300:focus{--border-opacity:1;border-color:#a4cafe;border-color:rgba(164,202,254,var(--border-opacity))}.rounded-sm{border-radius:.125rem}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-0{border-width:0}.border-4{border-width:4px}.border{border-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-pointer{cursor:pointer}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.focus\:font-semibold:focus,.hover\:font-semibold:hover{font-weight:600}.h-0{height:0}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-32{height:8rem}.h-64{height:16rem}.h-auto{height:auto}.h-screen{height:100vh}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem}.text-3xl{font-size:1.875rem}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.m-0{margin:0}.m-auto{margin:auto}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.mx-auto{margin-left:auto;margin-right:auto}.-my-2{margin-top:-.5rem;margin-bottom:-.5rem}.mt-0{margin-top:0}.mb-0{margin-bottom:0}.ml-0{margin-left:0}.mt-1{margin-top:.25rem}.mr-1{margin-right:.25rem}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mb-6{margin-bottom:1.5rem}.mt-8{margin-top:2rem}.mt-10{margin-top:2.5rem}.mb-10{margin-bottom:2.5rem}.mt-16{margin-top:4rem}.-mr-1{margin-right:-.25rem}.-ml-1{margin-left:-.25rem}.-mt-4{margin-top:-1rem}.-ml-4{margin-left:-1rem}.-mr-14{margin-right:-3.5rem}.max-w-xs{max-width:20rem}.max-w-xl{max-width:36rem}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.min-h-screen{min-height:100vh}.min-w-full{min-width:100%}.object-cover{-o-object-fit:cover;object-fit:cover}.opacity-0{opacity:0}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.opacity-100{opacity:1}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-8{padding:2rem}.p-10{padding:2.5rem}.py-0{padding-top:0;padding-bottom:0}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.pt-0{padding-top:0}.pl-0{padding-left:0}.pt-4{padding-top:1rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pb-20{padding-bottom:5rem}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{right:0;left:0}.inset-0,.inset-y-0{top:0;bottom:0}.inset-x-0{right:0;left:0}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.shadow-xs{box-shadow:0 0 0 1px rgba(0,0,0,.05)}.shadow-sm{box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.shadow-lg{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)}.shadow-xl{box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04)}.hover\:shadow-lg:hover{box-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -2px rgba(0,0,0,.05)}.focus\:shadow-outline:focus{box-shadow:0 0 0 3px rgba(118,169,250,.45)}.focus\:shadow-outline-blue:focus{box-shadow:0 0 0 3px rgba(164,202,254,.45)}.fill-current{fill:currentColor}.table-auto{table-layout:auto}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.text-black{--text-opacity:1;color:#000;color:rgba(0,0,0,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#d2d6dc;color:rgba(210,214,220,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#9fa6b2;color:rgba(159,166,178,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity))}.text-gray-800{--text-opacity:1;color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity))}.text-red-400{--text-opacity:1;color:#f98080;color:rgba(249,128,128,var(--text-opacity))}.text-red-500{--text-opacity:1;color:#f05252;color:rgba(240,82,82,var(--text-opacity))}.text-red-600{--text-opacity:1;color:#e02424;color:rgba(224,36,36,var(--text-opacity))}.text-green-600{--text-opacity:1;color:#057a55;color:rgba(5,122,85,var(--text-opacity))}.text-blue-600{--text-opacity:1;color:#1c64f2;color:rgba(28,100,242,var(--text-opacity))}.hover\:text-gray-300:hover{--text-opacity:1;color:#d2d6dc;color:rgba(210,214,220,var(--text-opacity))}.hover\:text-gray-500:hover{--text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--text-opacity))}.hover\:text-gray-600:hover{--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.hover\:text-gray-700:hover{--text-opacity:1;color:#374151;color:rgba(55,65,81,var(--text-opacity))}.hover\:text-gray-800:hover{--text-opacity:1;color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.hover\:text-gray-900:hover{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity))}.hover\:text-blue-600:hover{--text-opacity:1;color:#1c64f2;color:rgba(28,100,242,var(--text-opacity))}.hover\:text-indigo-900:hover{--text-opacity:1;color:#362f78;color:rgba(54,47,120,var(--text-opacity))}.focus\:text-gray-500:focus{--text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--text-opacity))}.focus\:text-gray-600:focus{--text-opacity:1;color:#4b5563;color:rgba(75,85,99,var(--text-opacity))}.focus\:text-gray-900:focus{--text-opacity:1;color:#161e2e;color:rgba(22,30,46,var(--text-opacity))}.active\:text-gray-800:active{--text-opacity:1;color:#252f3f;color:rgba(37,47,63,var(--text-opacity))}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.underline{text-decoration:underline}.line-through{text-decoration:line-through}.focus\:underline:focus,.hover\:underline:hover{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.whitespace-no-wrap{white-space:nowrap}.break-words{word-wrap:break-word;overflow-wrap:break-word}.break-all{word-break:break-all}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.w-0{width:0}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-48{width:12rem}.w-56{width:14rem}.w-64{width:16rem}.w-auto{width:auto}.w-1\/2{width:50%}.w-full{width:100%}.w-screen{width:100vw}.z-0{z-index:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.gap-4{grid-gap:1rem;gap:1rem}.gap-5{grid-gap:1.25rem;gap:1.25rem}.gap-6{grid-gap:1.5rem;gap:1.5rem}.gap-8{grid-gap:2rem;gap:2rem}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-6{grid-column:span 6/span 6}.col-span-8{grid-column:span 8/span 8}.col-span-12{grid-column:span 12/span 12}.transform{--transform-translate-x:0;--transform-translate-y:0;--transform-rotate:0;--transform-skew-x:0;--transform-skew-y:0;--transform-scale-x:1;--transform-scale-y:1;transform:translateX(var(--transform-translate-x)) translateY(var(--transform-translate-y)) rotate(var(--transform-rotate)) skewX(var(--transform-skew-x)) skewY(var(--transform-skew-y)) scaleX(var(--transform-scale-x)) scaleY(var(--transform-scale-y))}.origin-top-right{transform-origin:top right}.scale-95{--transform-scale-x:.95;--transform-scale-y:.95}.scale-100{--transform-scale-x:1;--transform-scale-y:1}.translate-x-0{--transform-translate-x:0}.-translate-x-full{--transform-translate-x:-100%}.translate-y-0{--transform-translate-y:0}.translate-y-4{--transform-translate-y:1rem}.transition-all{transition-property:all}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform}.transition-opacity{transition-property:opacity}.ease-linear{transition-timing-function:linear}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-75{transition-duration:75ms}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:flex-no-wrap{flex-wrap:nowrap}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:flex-shrink-0{flex-shrink:0}.sm\:h-10{height:2.5rem}.sm\:h-screen{height:100vh}.sm\:mx-0{margin-left:0;margin-right:0}.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mt-0{margin-top:0}.sm\:ml-3{margin-left:.75rem}.sm\:mt-4{margin-top:1rem}.sm\:ml-4{margin-left:1rem}.sm\:mt-6{margin-top:1.5rem}.sm\:ml-6{margin-left:1.5rem}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-lg{max-width:32rem}.sm\:p-0{padding:0}.sm\:p-6{padding:1.5rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:inset-0{top:0;right:0;bottom:0;left:0}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:w-10{width:2.5rem}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:gap-4{grid-gap:1rem;gap:1rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:scale-95{--transform-scale-x:.95;--transform-scale-y:.95}.sm\:scale-100{--transform-scale-x:1;--transform-scale-y:1}.sm\:translate-y-0{--transform-translate-y:0}}@media (min-width:768px){.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:justify-between{justify-content:space-between}.md\:flex-shrink-0{flex-shrink:0}.md\:text-sm{font-size:.875rem}.md\:mt-0{margin-top:0}.md\:mr-2{margin-right:.5rem}.md\:ml-2{margin-left:.5rem}.md\:ml-6{margin-left:1.5rem}.md\:mt-10{margin-top:2.5rem}.md\:-mr-1{margin-right:-.25rem}.md\:max-w-3xl{max-width:48rem}.md\:p-24{padding:6rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.333333%}.md\:gap-6{grid-gap:1.5rem;gap:1.5rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-start-2{grid-column-start:2}.md\:col-start-4{grid-column-start:4}}@media (min-width:1024px){.lg\:rounded-lg{border-radius:.5rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:items-center{align-items:center}.lg\:h-screen{height:100vh}.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:mt-24{margin-top:6rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:gap-4{grid-gap:1rem;gap:1rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-start-3{grid-column-start:3}.lg\:col-start-4{grid-column-start:4}}@media (min-width:1280px){.xl\:mt-32{margin-top:8rem}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-6{grid-column:span 6/span 6}.xl\:col-span-8{grid-column:span 8/span 8}.xl\:col-span-9{grid-column:span 9/span 9}.xl\:col-start-4{grid-column-start:4}} \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json index af47692a14..9455fb18be 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,6 +1,6 @@ { "/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5", - "/css/app.css": "/css/app.css?id=56fdeb0a3b78b00b9a52", + "/css/app.css": "/css/app.css?id=27d1431e24a51260d3f1", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1", "/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7", From bf690e5cb8e8722e0d07c16c764ed0504d4f4b49 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 26 Aug 2021 13:02:54 +1000 Subject: [PATCH 10/66] Minor fix for postmark --- app/Http/Controllers/PostMarkController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/PostMarkController.php b/app/Http/Controllers/PostMarkController.php index 89620945c9..ceb9e1e4e0 100644 --- a/app/Http/Controllers/PostMarkController.php +++ b/app/Http/Controllers/PostMarkController.php @@ -213,7 +213,7 @@ class PostMarkController extends BaseController $request->input('MessageID') ); - LightLogs::create($bounce)->batch(); + LightLogs::create($spam)->batch(); SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); } From c68fc24e2c609003c7c53ff7f6f2e083ccb02952 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 26 Aug 2021 15:19:04 +1000 Subject: [PATCH 11/66] Remove local file --- app/Jobs/Company/CompanyExport.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Jobs/Company/CompanyExport.php b/app/Jobs/Company/CompanyExport.php index 239104c410..64529b4d10 100644 --- a/app/Jobs/Company/CompanyExport.php +++ b/app/Jobs/Company/CompanyExport.php @@ -498,6 +498,7 @@ class CompanyExport implements ShouldQueue if(Ninja::isHosted()) { Storage::disk(config('filesystems.default'))->put('backups/'.$file_name, file_get_contents($zip_path)); + unlink($zip_path); } App::forgetInstance('translator'); From 969a76003b1b59d9bd4cc8e09b576f093e26ceac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 26 Aug 2021 15:37:17 +0200 Subject: [PATCH 12/66] Add `BANK_TRANSFER` to `Gateway` model --- app/Models/Gateway.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 676112f979..dadb195e52 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -83,7 +83,7 @@ class Gateway extends StaticModel break; case 3: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]];//eWay - break; + break; case 11: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false]];//Payfast break; @@ -106,11 +106,12 @@ class Gateway extends StaticModel case 49: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true]]; //WePay - break; + break; case 50: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], //Braintree - GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true] + GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true], ]; break; case 7: From 5a0c6b07342abda54bd53d197e5d82896a265e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 26 Aug 2021 15:37:37 +0200 Subject: [PATCH 13/66] Scaffold `MethodInterface` for child classes --- app/PaymentDrivers/Common/MethodInterface.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 app/PaymentDrivers/Common/MethodInterface.php diff --git a/app/PaymentDrivers/Common/MethodInterface.php b/app/PaymentDrivers/Common/MethodInterface.php new file mode 100644 index 0000000000..41ce99ac7f --- /dev/null +++ b/app/PaymentDrivers/Common/MethodInterface.php @@ -0,0 +1,31 @@ + Date: Thu, 26 Aug 2021 15:37:56 +0200 Subject: [PATCH 14/66] Braintree ACH class --- app/PaymentDrivers/Braintree/ACH.php | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/PaymentDrivers/Braintree/ACH.php diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php new file mode 100644 index 0000000000..98ab379c3b --- /dev/null +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -0,0 +1,79 @@ +braintree = $braintree; + + $this->braintree->init(); + } + + public function authorizeView(array $data) + { + $data['gateway'] = $this->braintree; + $data['client_token'] = $this->braintree->gateway->clientToken()->generate(); + + return render('gateways.braintree.ach.authorize', $data); + } + + public function authorizeResponse(Request $request) + { + $request->validate([ + 'nonce' => ['required'], + 'gateway_type_id' => ['required'], + ]); + + $customer = $this->braintree->findOrCreateCustomer(); + + $result = $this->braintree->gateway->paymentMethod()->create([ + 'customerId' => $customer->id, + 'paymentMethodNonce' => $request->nonce, + 'options' => [ + 'usBankAccountVerificationMethod' => \Braintree\Result\UsBankAccountVerification::NETWORK_CHECK, + ], + ]); + + if ($result->success) { + $account = $result->paymentMethod; + + try { + $payment_meta = new \stdClass; + $payment_meta->brand = (string)$account->bankName; + $payment_meta->last4 = (string)$account->last4; + $payment_meta->type = GatewayType::BANK_TRANSFER; + $payment_meta->state = $account->verified ? 'authorized' : 'unauthorized'; + + $data = [ + 'payment_meta' => $payment_meta, + 'token' => $account->token, + 'payment_method_id' => $request->gateway_type_id, + ]; + + $this->braintree->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]); + + return redirect()->route('client.payment_methods.index'); + } catch (\Exception $e) { + // .. + } + } + } +} From 29d56f69d59650bee4f5f482d37bb8fbff58fcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 26 Aug 2021 15:38:28 +0200 Subject: [PATCH 15/66] Frontend for authorizing ACH --- app/PaymentDrivers/BraintreePaymentDriver.php | 15 +- .../braintree/ach/authorize.blade.php | 149 ++++++++++++++++++ 2 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php diff --git a/app/PaymentDrivers/BraintreePaymentDriver.php b/app/PaymentDrivers/BraintreePaymentDriver.php index 7c0cdd2786..ca22ca6f4f 100644 --- a/app/PaymentDrivers/BraintreePaymentDriver.php +++ b/app/PaymentDrivers/BraintreePaymentDriver.php @@ -23,6 +23,7 @@ use App\Models\Payment; use App\Models\PaymentHash; use App\Models\PaymentType; use App\Models\SystemLog; +use App\PaymentDrivers\Braintree\ACH; use App\PaymentDrivers\Braintree\CreditCard; use App\PaymentDrivers\Braintree\PayPal; use Braintree\Gateway; @@ -45,6 +46,7 @@ class BraintreePaymentDriver extends BaseDriver public static $methods = [ GatewayType::CREDIT_CARD => CreditCard::class, GatewayType::PAYPAL => PayPal::class, + GatewayType::BANK_TRANSFER => ACH::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_BRAINTREE; @@ -72,9 +74,10 @@ class BraintreePaymentDriver extends BaseDriver { $types = [ GatewayType::PAYPAL, - GatewayType::CREDIT_CARD + GatewayType::CREDIT_CARD, + GatewayType::BANK_TRANSFER, ]; - + return $types; } @@ -125,9 +128,9 @@ class BraintreePaymentDriver extends BaseDriver $this->init(); try{ - + $response = $this->gateway->transaction()->refund($payment->transaction_reference, $amount); - + } catch (Exception $e) { $data = [ @@ -137,12 +140,12 @@ class BraintreePaymentDriver extends BaseDriver 'description' => $e->getMessage(), 'code' => $e->getCode(), ]; - + SystemLogger::dispatch(['server_response' => null, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_BRAINTREE, $this->client, $this->client->company); return $data; } - + if($response->success) { diff --git a/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php new file mode 100644 index 0000000000..ba2e9357ba --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php @@ -0,0 +1,149 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) + +@section('gateway_head') + +@endsection + +@section('gateway_content') + @if(session()->has('ach_error')) +
+

{{ session('ach_error') }}

+
+ @endif + +
+ @csrf + + + + + + +
+ + + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_type')]) + + + {{ __('texts.checking') }} + + + + {{ __('texts.savings') }} + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')]) + + + {{ __('texts.individual_account') }} + + + + {{ __('texts.company_account') }} + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.address1')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.address2')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.locality')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.state')]) + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.postal_code')]) + + @endcomponent + + @component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-bank-account']) + {{ ctrans('texts.add_payment_method') }} + @endcomponent +@endsection + +@section('gateway_footer') + + + + +@endsection From 016e8033fb701757b79683e163ff435eac232461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 26 Aug 2021 15:46:17 +0200 Subject: [PATCH 16/66] Add `paymentView` to `MethodInterface` --- app/PaymentDrivers/Common/MethodInterface.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/PaymentDrivers/Common/MethodInterface.php b/app/PaymentDrivers/Common/MethodInterface.php index 41ce99ac7f..6e68b82dcf 100644 --- a/app/PaymentDrivers/Common/MethodInterface.php +++ b/app/PaymentDrivers/Common/MethodInterface.php @@ -28,4 +28,11 @@ interface MethodInterface * @param Request $request */ public function authorizeResponse(Request $request); + + /** + * Payment page for the gateway method. + * + * @param array $data + */ + public function paymentView(array $data); } From 760de008f84cca366dd75f731790620f82620080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 26 Aug 2021 15:46:31 +0200 Subject: [PATCH 17/66] Payments without token --- app/PaymentDrivers/Braintree/ACH.php | 7 +++ .../gateways/braintree/ach/pay.blade.php | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php index 98ab379c3b..00a3cc0371 100644 --- a/app/PaymentDrivers/Braintree/ACH.php +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -76,4 +76,11 @@ class ACH implements MethodInterface } } } + + public function paymentView(array $data) + { + $data['gateway'] = $this->braintree; + + return render('gateways.braintree.ach.pay', $data); + } } diff --git a/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php b/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php new file mode 100644 index 0000000000..d6ccfb9851 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php @@ -0,0 +1,58 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) + +@section('gateway_content') + @if(count($tokens) > 0) + + + @include('portal.ninja2020.gateways.includes.payment_details') + +
+ @csrf + + + + + + + +
+ + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if(count($tokens) > 0) + @foreach($tokens as $token) + + @endforeach + @endisset + @endcomponent + + @include('portal.ninja2020.gateways.includes.pay_now') + + @else + @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false]) + {{ ctrans('texts.bank_account_not_linked') }} + {{ ctrans('texts.add_payment_method') }} + @endcomponent + @endif +@endsection + +@push('footer') + +@endpush From ea38fb2b37248d401614ae8d042ba67cb0a8aca4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 27 Aug 2021 07:36:05 +1000 Subject: [PATCH 18/66] Fixes for Support Message Template Colors --- app/Mail/SupportMessageSent.php | 3 ++- resources/views/email/support/message.blade.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Mail/SupportMessageSent.php b/app/Mail/SupportMessageSent.php index 8192696600..337495ab8e 100644 --- a/app/Mail/SupportMessageSent.php +++ b/app/Mail/SupportMessageSent.php @@ -45,8 +45,8 @@ class SupportMessageSent extends Mailable $log_file->seek(PHP_INT_MAX); $last_line = $log_file->key(); + $lines = new LimitIterator($log_file, $last_line - 100, $last_line); - $log_lines = iterator_to_array($lines); } @@ -76,6 +76,7 @@ class SupportMessageSent extends Mailable 'system_info' => $system_info, 'laravel_log' => $log_lines, 'logo' => $company->present()->logo(), + 'settings' => $company->settings ]); } } diff --git a/resources/views/email/support/message.blade.php b/resources/views/email/support/message.blade.php index db71b1a42a..7ecc3d7b05 100644 --- a/resources/views/email/support/message.blade.php +++ b/resources/views/email/support/message.blade.php @@ -1,4 +1,4 @@ -@component('email.template.admin', ['logo' => $logo ?? 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png']) +@component('email.template.admin', ['settings' => $settings, 'logo' => $logo ?? 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png']) {{-- Body --}} {{ $support_message }} From 97c58bcf70086cc3038c76d30fb89038203421aa Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 27 Aug 2021 09:56:42 +1000 Subject: [PATCH 19/66] fixes for refundS --- app/Http/Controllers/CompanyController.php | 5 ++++- app/Services/Payment/RefundPayment.php | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index e77fa41e7e..af8bb9522e 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -72,6 +72,9 @@ class CompanyController extends BaseController parent::__construct(); $this->company_repo = $company_repo; + + // $this->middleware('password_protected')->only(['destroy']); + } /** @@ -477,7 +480,7 @@ class CompanyController extends BaseController */ public function destroy(DestroyCompanyRequest $request, Company $company) { - + if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id) return response()->json(['message' => 'Cannot purge this company'], 400); diff --git a/app/Services/Payment/RefundPayment.php b/app/Services/Payment/RefundPayment.php index a1d733311e..0d10b83fa7 100644 --- a/app/Services/Payment/RefundPayment.php +++ b/app/Services/Payment/RefundPayment.php @@ -81,8 +81,14 @@ class RefundPayment if ($response['success'] == false) { $this->payment->save(); - throw new PaymentRefundFailed($response['description']); + + if(array_key_exists('description', $response)) + throw new PaymentRefundFailed($response['description']); + else + throw new PaymentRefundFailed(); + } + } } else { $this->payment->refunded += $this->total_refund; From dd4dbf566b7a519aae0ed1c7d085b781d146cc98 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 27 Aug 2021 14:32:49 +1000 Subject: [PATCH 20/66] save payment earlier --- app/Services/Invoice/MarkPaid.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Services/Invoice/MarkPaid.php b/app/Services/Invoice/MarkPaid.php index 8b8d700c6b..a23e42a378 100644 --- a/app/Services/Invoice/MarkPaid.php +++ b/app/Services/Invoice/MarkPaid.php @@ -53,6 +53,7 @@ class MarkPaid extends AbstractService $payment->amount = $this->invoice->balance; $payment->applied = $this->invoice->balance; $payment->number = $this->getNextPaymentNumber($this->invoice->client); + $payment->save(); $payment->status_id = Payment::STATUS_COMPLETED; $payment->client_id = $this->invoice->client_id; $payment->transaction_reference = ctrans('texts.manual_entry'); From f2a8d94f3f0833960198373d0b4aaaff7d987d21 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 27 Aug 2021 21:27:13 +1000 Subject: [PATCH 21/66] Additional checks for SystemLogger --- app/Jobs/Util/SystemLogger.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Jobs/Util/SystemLogger.php b/app/Jobs/Util/SystemLogger.php index f4677cb2c7..3775eda3fa 100644 --- a/app/Jobs/Util/SystemLogger.php +++ b/app/Jobs/Util/SystemLogger.php @@ -55,6 +55,10 @@ class SystemLogger implements ShouldQueue MultiDB::setDb($this->company->db); $client_id = $this->client ? $this->client->id : null; + + if(!$this->client && !$this->company->owner()) + return; + $user_id = $this->client ? $this->client->user_id : $this->company->owner()->id; $sl = [ From ca9de2bd74e6e55794eaf1a613d1276efd637674 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 27 Aug 2021 21:46:24 +1000 Subject: [PATCH 22/66] Fixes for tests --- app/Services/Invoice/MarkPaid.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Services/Invoice/MarkPaid.php b/app/Services/Invoice/MarkPaid.php index a23e42a378..8b8d700c6b 100644 --- a/app/Services/Invoice/MarkPaid.php +++ b/app/Services/Invoice/MarkPaid.php @@ -53,7 +53,6 @@ class MarkPaid extends AbstractService $payment->amount = $this->invoice->balance; $payment->applied = $this->invoice->balance; $payment->number = $this->getNextPaymentNumber($this->invoice->client); - $payment->save(); $payment->status_id = Payment::STATUS_COMPLETED; $payment->client_id = $this->invoice->client_id; $payment->transaction_reference = ctrans('texts.manual_entry'); From 90326321049c78eb6a5766d5308357eb2d0feaea Mon Sep 17 00:00:00 2001 From: = Date: Fri, 27 Aug 2021 22:10:21 +1000 Subject: [PATCH 23/66] Fixes for quote workflows --- app/Jobs/Quote/QuoteWorkflowSettings.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Jobs/Quote/QuoteWorkflowSettings.php b/app/Jobs/Quote/QuoteWorkflowSettings.php index ffc5730ac0..54ec01ca03 100644 --- a/app/Jobs/Quote/QuoteWorkflowSettings.php +++ b/app/Jobs/Quote/QuoteWorkflowSettings.php @@ -55,8 +55,8 @@ class QuoteWorkflowSettings implements ShouldQueue }); } - if ($this->client->getSetting('auto_archive_quote')) { - $this->base_repository->archive($this->quote); - } + // if ($this->client->getSetting('auto_archive_quote')) { + // $this->base_repository->archive($this->quote); + // } } } From 38ceefc5369a2a558d513a214fc8b7472d39fe95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 16:19:10 +0200 Subject: [PATCH 24/66] Fixes for `processInternallyFailedPayment` --- app/PaymentDrivers/BaseDriver.php | 43 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 65705fc19c..b984b2b41e 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -360,16 +360,15 @@ class BaseDriver extends AbstractPaymentDriver public function processInternallyFailedPayment($gateway, $e) { - - $this->unWindGatewayFees($this->payment_hash); + if (!is_null($this->payment_hash)) { + $this->unWindGatewayFees($this->payment_hash); + } if ($e instanceof CheckoutHttpException) { $error = $e->getBody(); - } - else if ($e instanceof Exception) { + } else if ($e instanceof Exception) { $error = $e->getMessage(); - } - else + } else $error = $e->getMessage(); PaymentFailureMailer::dispatch( @@ -379,29 +378,29 @@ class BaseDriver extends AbstractPaymentDriver $this->payment_hash ); - $nmo = new NinjaMailerObject; - $nmo->mailable = new NinjaMailer( (new ClientPaymentFailureObject($gateway->client, $error, $gateway->client->company, $this->payment_hash))->build() ); - $nmo->company = $gateway->client->company; - $nmo->settings = $gateway->client->company->settings; + if (!is_null($this->payment_hash)) { - $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get(); + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new ClientPaymentFailureObject($gateway->client, $error, $gateway->client->company, $this->payment_hash))->build()); + $nmo->company = $gateway->client->company; + $nmo->settings = $gateway->client->company->settings; - $invoices->each(function ($invoice){ + $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get(); - $invoice->service()->deletePdf(); + $invoices->each(function ($invoice) { - }); + $invoice->service()->deletePdf(); + }); - $invoices->first()->invitations->each(function ($invitation) use ($nmo){ + $invoices->first()->invitations->each(function ($invitation) use ($nmo) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if ($invitation->contact->send_email && $invitation->contact->email) { - $nmo->to_user = $invitation->contact; - NinjaMailerJob::dispatch($nmo); - - } - - }); + $nmo->to_user = $invitation->contact; + NinjaMailerJob::dispatch($nmo); + } + }); + } SystemLogger::dispatch( From a1875e2e32c01821a5860d9882a8b24213243637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 16:21:11 +0200 Subject: [PATCH 25/66] Handle failed authorization method --- app/PaymentDrivers/Braintree/ACH.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php index 00a3cc0371..e50778176b 100644 --- a/app/PaymentDrivers/Braintree/ACH.php +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -72,7 +72,7 @@ class ACH implements MethodInterface return redirect()->route('client.payment_methods.index'); } catch (\Exception $e) { - // .. + return $this->braintree->processInternallyFailedPayment($this->braintree, $e); } } } From 2856f36a8677dd4fe937b304d7a3a5f362c789ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 17:00:32 +0200 Subject: [PATCH 26/66] Payment page with token --- app/PaymentDrivers/Braintree/ACH.php | 100 +++++++++++++++++- .../gateways/braintree/ach/pay.blade.php | 5 +- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php index e50778176b..1b400e61dd 100644 --- a/app/PaymentDrivers/Braintree/ACH.php +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -11,13 +11,24 @@ namespace App\PaymentDrivers\Braintree; +use App\Exceptions\PaymentFailed; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Http\Requests\Request; +use App\Jobs\Mail\PaymentFailureMailer; +use App\Jobs\Util\SystemLogger; +use App\Models\ClientGatewayToken; use App\Models\GatewayType; +use App\Models\Payment; +use App\Models\PaymentType; +use App\Models\SystemLog; use App\PaymentDrivers\BraintreePaymentDriver; use App\PaymentDrivers\Common\MethodInterface; +use App\Utils\Traits\MakesHash; class ACH implements MethodInterface { + use MakesHash; + protected BraintreePaymentDriver $braintree; public function __construct(BraintreePaymentDriver $braintree) @@ -70,7 +81,7 @@ class ACH implements MethodInterface $this->braintree->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]); - return redirect()->route('client.payment_methods.index'); + return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added')); } catch (\Exception $e) { return $this->braintree->processInternallyFailedPayment($this->braintree, $e); } @@ -80,7 +91,94 @@ class ACH implements MethodInterface public function paymentView(array $data) { $data['gateway'] = $this->braintree; + $data['currency'] = $this->braintree->client->getCurrencyCode(); + $data['payment_method_id'] = GatewayType::BANK_TRANSFER; + $data['amount'] = $this->braintree->payment_hash->data->amount_with_fee; return render('gateways.braintree.ach.pay', $data); } + + public function paymentResponse(PaymentResponseRequest $request) + { + $request->validate([ + 'source' => ['required'], + 'payment_hash' => ['required'], + ]); + + $customer = $this->braintree->findOrCreateCustomer(); + + $token = ClientGatewayToken::query() + ->where('client_id', auth('contact')->user()->client->id) + ->where('id', $this->decodePrimaryKey($request->source)) + ->firstOrFail(); + + $result = $this->braintree->gateway->transaction()->sale([ + 'amount' => $this->braintree->payment_hash->data->amount_with_fee, + 'paymentMethodToken' => $token->token, + 'options' => [ + 'submitForSettlement' => true + ], + ]); + + if ($result->success) { + $this->braintree->logSuccessfulGatewayResponse(['response' => $request->server_response, 'data' => $this->braintree->payment_hash], SystemLog::TYPE_BRAINTREE); + + return $this->processSuccessfulPayment($result); + } + + return $this->processUnsuccessfulPayment($result); + } + + private function processSuccessfulPayment($response) + { + $state = $this->braintree->payment_hash->data; + + $data = [ + 'payment_type' => PaymentType::ACH, + 'amount' => $this->braintree->payment_hash->data->amount_with_fee, + 'transaction_reference' => $response->transaction->id, + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment = $this->braintree->createPayment($data, Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $response, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_BRAINTREE, + $this->braintree->client, + $this->braintree->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->braintree->encodePrimaryKey($payment->id)]); + } + + private function processUnsuccessfulPayment($response) + { + PaymentFailureMailer::dispatch($this->braintree->client, $response->transaction->additionalProcessorResponse, $this->braintree->client->company, $this->braintree->payment_hash->data->amount_with_fee); + + PaymentFailureMailer::dispatch( + $this->braintree->client, + $response, + $this->braintree->client->company, + $this->braintree->payment_hash->data->amount_with_fee, + ); + + $message = [ + 'server_response' => $response, + 'data' => $this->braintree->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_BRAINTREE, + $this->braintree->client, + $this->braintree->client->company, + ); + + throw new PaymentFailed($response->transaction->additionalProcessorResponse, $response->transaction->processorResponseCode); + } } diff --git a/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php b/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php index d6ccfb9851..50f87edcd4 100644 --- a/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/braintree/ach/pay.blade.php @@ -13,7 +13,6 @@ - @@ -51,7 +50,9 @@ document.querySelector('input[name=source]').value = element.target.dataset.token; })); - document.getElementById('pay-now').addEventListener('click', function () { + document.getElementById('pay-now').addEventListener('click', function (e) { + e.target.parentElement.disabled = true; + document.getElementById('server-response').submit(); }); From cd15861158339344945ba5cd051c0f4bbf24f811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 17:00:43 +0200 Subject: [PATCH 27/66] Add `paymentResponse` to MethodInterface.php --- app/PaymentDrivers/Common/MethodInterface.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/PaymentDrivers/Common/MethodInterface.php b/app/PaymentDrivers/Common/MethodInterface.php index 6e68b82dcf..0c839d602c 100644 --- a/app/PaymentDrivers/Common/MethodInterface.php +++ b/app/PaymentDrivers/Common/MethodInterface.php @@ -11,6 +11,7 @@ namespace App\PaymentDrivers\Common; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Http\Requests\Request; interface MethodInterface @@ -35,4 +36,11 @@ interface MethodInterface * @param array $data */ public function paymentView(array $data); + + /** + * Process the response from the payments page. + * + * @param PaymentResponseRequest $request + */ + public function paymentResponse(PaymentResponseRequest $request); } From fef4665c5aaa4ba2b488ac5de3d449e93a7670f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 17:32:20 +0200 Subject: [PATCH 28/66] Extract Javascript into separate file --- public/mix-manifest.json | 1 + .../clients/payment_methods/braintree-ach.js | 66 +++++++++++++++++++ .../braintree/ach/authorize.blade.php | 60 +---------------- webpack.mix.js | 4 ++ 4 files changed, 72 insertions(+), 59 deletions(-) create mode 100644 resources/js/clients/payment_methods/braintree-ach.js diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 9455fb18be..bf6470cd60 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -5,6 +5,7 @@ "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1", "/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7", "/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=f7f4ecfb1771951b91e7", + "/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js?id=9fb7941baba1f9645ed9", "/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js?id=8fea0be371d430064a89", "/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=7c2cbef525868592f42e", "/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js?id=81957e7cb1cb49f23b90", diff --git a/resources/js/clients/payment_methods/braintree-ach.js b/resources/js/clients/payment_methods/braintree-ach.js new file mode 100644 index 0000000000..849145fa6a --- /dev/null +++ b/resources/js/clients/payment_methods/braintree-ach.js @@ -0,0 +1,66 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +window.braintree.client.create({ + authorization: document.querySelector('meta[name="client-token"]')?.content +}).then(function (clientInstance) { + return braintree.usBankAccount.create({ + client: clientInstance + }); +}).then(function (usBankAccountInstance) { + document + .getElementById('authorize-bank-account') + ?.addEventListener('click', (e) => { + e.target.parentElement.disabled = true; + + document.getElementById('errors').hidden = true; + document.getElementById('errors').textContent = ''; + + let bankDetails = { + accountNumber: document.getElementById('account-number').value, + routingNumber: document.getElementById('routing-number').value, + accountType: document.querySelector('input[name="account-type"]:checked').value, + ownershipType: document.querySelector('input[name="ownership-type"]:checked').value, + billingAddress: { + streetAddress: document.getElementById('billing-street-address').value, + extendedAddress: document.getElementById('billing-extended-address').value, + locality: document.getElementById('billing-locality').value, + region: document.getElementById('billing-region').value, + postalCode: document.getElementById('billing-postal-code').value + } + } + + if (bankDetails.ownershipType === 'personal') { + let name = document.getElementById('account-holder-name').value.split(' ', 2); + + bankDetails.firstName = name[0]; + bankDetails.lastName = name[1]; + } else { + bankDetails.businessName = document.getElementById('account-holder-name').value; + } + + usBankAccountInstance.tokenize({ + bankDetails, + mandateText: 'By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.' + }).then(function (payload) { + document.querySelector('input[name=nonce]').value = payload.nonce; + document.getElementById('server_response').submit(); + }) + .catch(function (error) { + e.target.parentElement.disabled = false; + + document.getElementById('errors').textContent = `${error.details.originalError.message} ${error.details.originalError.details.originalError[0].message}`; + document.getElementById('errors').hidden = false; + }); + }); +}).catch(function (err) { + document.getElementById('errors').textContent = `${error.details.originalError.message} ${error.details.originalError.details.originalError[0].message}`; + document.getElementById('errors').hidden = false; +}); diff --git a/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php index ba2e9357ba..e5b48683b3 100644 --- a/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php @@ -87,63 +87,5 @@ @section('gateway_footer') - - + @endsection diff --git a/webpack.mix.js b/webpack.mix.js index faec00d75f..dfc018aa86 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -93,6 +93,10 @@ mix.js("resources/js/app.js", "public/js") .js( "resources/js/clients/payments/eway-credit-card.js", "public/js/clients/payments/eway-credit-card.js" + ) + .js( + "resources/js/clients/payment_methods/braintree-ach.js", + "public/js/clients/payment_methods/braintree-ach.js" ); mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css'); From 0468363c8da87baa43507c5613601acdd30f0247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 17:32:28 +0200 Subject: [PATCH 29/66] Assets production build --- .../clients/payment_methods/braintree-ach.js | 2 ++ .../braintree-ach.js.LICENSE.txt | 9 ++++++++ .../Gateways/Braintree/ACHTest.php | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 public/js/clients/payment_methods/braintree-ach.js create mode 100644 public/js/clients/payment_methods/braintree-ach.js.LICENSE.txt create mode 100644 tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php diff --git a/public/js/clients/payment_methods/braintree-ach.js b/public/js/clients/payment_methods/braintree-ach.js new file mode 100644 index 0000000000..21a7acd168 --- /dev/null +++ b/public/js/clients/payment_methods/braintree-ach.js @@ -0,0 +1,2 @@ +/*! For license information please see braintree-ach.js.LICENSE.txt */ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=24)}({24:function(e,t,n){e.exports=n("cGea")},cGea:function(e,t){var n;window.braintree.client.create({authorization:null===(n=document.querySelector('meta[name="client-token"]'))||void 0===n?void 0:n.content}).then((function(e){return braintree.usBankAccount.create({client:e})})).then((function(e){var t;null===(t=document.getElementById("authorize-bank-account"))||void 0===t||t.addEventListener("click",(function(t){t.target.parentElement.disabled=!0,document.getElementById("errors").hidden=!0,document.getElementById("errors").textContent="";var n={accountNumber:document.getElementById("account-number").value,routingNumber:document.getElementById("routing-number").value,accountType:document.querySelector('input[name="account-type"]:checked').value,ownershipType:document.querySelector('input[name="ownership-type"]:checked').value,billingAddress:{streetAddress:document.getElementById("billing-street-address").value,extendedAddress:document.getElementById("billing-extended-address").value,locality:document.getElementById("billing-locality").value,region:document.getElementById("billing-region").value,postalCode:document.getElementById("billing-postal-code").value}};if("personal"===n.ownershipType){var r=document.getElementById("account-holder-name").value.split(" ",2);n.firstName=r[0],n.lastName=r[1]}else n.businessName=document.getElementById("account-holder-name").value;e.tokenize({bankDetails:n,mandateText:'By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.'}).then((function(e){document.querySelector("input[name=nonce]").value=e.nonce,document.getElementById("server_response").submit()})).catch((function(e){t.target.parentElement.disabled=!1,document.getElementById("errors").textContent="".concat(e.details.originalError.message," ").concat(e.details.originalError.details.originalError[0].message),document.getElementById("errors").hidden=!1}))}))})).catch((function(e){document.getElementById("errors").textContent="".concat(error.details.originalError.message," ").concat(error.details.originalError.details.originalError[0].message),document.getElementById("errors").hidden=!1}))}}); \ No newline at end of file diff --git a/public/js/clients/payment_methods/braintree-ach.js.LICENSE.txt b/public/js/clients/payment_methods/braintree-ach.js.LICENSE.txt new file mode 100644 index 0000000000..6b30888ddb --- /dev/null +++ b/public/js/clients/payment_methods/braintree-ach.js.LICENSE.txt @@ -0,0 +1,9 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ diff --git a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php new file mode 100644 index 0000000000..025301e4e0 --- /dev/null +++ b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php @@ -0,0 +1,23 @@ +browse(function (Browser $browser) { + $browser->visit('/') + ->assertSee('Laravel'); + }); + } +} From ab90db5295a11a161a2782f3cc1e69f6ba4f3bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 19:36:21 +0200 Subject: [PATCH 30/66] Scaffold ACHTest --- .../Gateways/Braintree/ACHTest.php | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php index 025301e4e0..0673dc6fc9 100644 --- a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php @@ -2,22 +2,48 @@ namespace Tests\Browser\ClientPortal\Gateways\Braintree; +use App\DataMapper\FeesAndLimits; +use App\Models\Company; +use App\Models\CompanyGateway; +use App\Models\GatewayType; use Illuminate\Foundation\Testing\DatabaseMigrations; use Laravel\Dusk\Browser; +use Tests\Browser\Pages\ClientPortal\Login; use Tests\DuskTestCase; class ACHTest extends DuskTestCase { - /** - * A Dusk test example. - * - * @return void - */ - public function testExample() + protected function setUp(): void { + parent::setUp(); + + foreach (static::$browsers as $browser) { + $browser->driver->manage()->deleteAllCookies(); + } + $this->browse(function (Browser $browser) { - $browser->visit('/') - ->assertSee('Laravel'); + $browser + ->visit(new Login()) + ->auth(); }); + + $this->disableCompanyGateways(); + + CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->restore(); + + $cg = CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->firstOrFail(); + $fees_and_limits = $cg->fees_and_limits; + $fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits(); + $cg->fees_and_limits = $fees_and_limits; + $cg->save(); + + $company = Company::first(); + $settings = $company->settings; + + $settings->client_portal_allow_under_payment = true; + $settings->client_portal_allow_over_payment = true; + + $company->settings = $settings; + $company->save(); } } From 3b22fc5e176403cada3eadb1c70b1beb5588821c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 19:45:45 +0200 Subject: [PATCH 31/66] Test adding bank account --- resources/lang/en/texts.php | 5 ++++- .../ClientPortal/Gateways/Braintree/ACHTest.php | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 18fd55344c..dc5ee29bd4 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4297,7 +4297,10 @@ $LANG = array( 'lang_Latvian' => 'Latvian', 'expiry_date' => 'Expiry date', 'cardholder_name' => 'Card holder name', - + 'account_type' => 'Account type', + 'locality' => 'Locality', + 'checking' => 'Checking', + 'savings' => 'Savings', ); return $LANG; diff --git a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php index 0673dc6fc9..9f0887dccc 100644 --- a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php @@ -46,4 +46,21 @@ class ACHTest extends DuskTestCase $company->settings = $settings; $company->save(); } + + public function testAddingBankAccount() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.payment_methods.index') + ->press('Add Payment Method') + ->clickLink('Bank Account') + ->type('#account-holder-name', 'John Doe') + ->type('#account-number', '1000000000') + ->type('#routing-number', '011000015') + ->type('#billing-region', 'AL') + ->type('#billing-postal-code', '12345') + ->press('Add Payment Method') + ->assertSee('Added payment method.'); + }); + } } From a89f5a8fc0eadef67e80d1af689746487cef34d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 19:45:59 +0200 Subject: [PATCH 32/66] Test paying with ACH --- .../ClientPortal/Gateways/Braintree/ACHTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php index 9f0887dccc..12d91dca9a 100644 --- a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php @@ -63,4 +63,18 @@ class ACHTest extends DuskTestCase ->assertSee('Added payment method.'); }); } + + public function testPayingWithExistingACH() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->press('Pay Now') + ->clickLink('Bank Transfer') + ->click('.toggle-payment-with-token') + ->press('Pay Now') + ->waitForText('Details of the payment', 60); + }); + } } From 8467c60a3bed93d46a7fdc16dd29406a01709238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 27 Aug 2021 19:46:20 +0200 Subject: [PATCH 33/66] Test removing ACH account --- .../ClientPortal/Gateways/Braintree/ACHTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php index 12d91dca9a..38caa94abb 100644 --- a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php @@ -77,4 +77,17 @@ class ACHTest extends DuskTestCase ->waitForText('Details of the payment', 60); }); } + + public function testRemoveACHAccount() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.payment_methods.index') + ->clickLink('View') + ->press('Remove Payment Method') + ->waitForText('Confirmation') + ->click('@confirm-payment-removal') + ->assertSee('Payment method has been successfully removed.'); + }); + } } From 712ef7852761ac407c0919353b6251e5e70c2d40 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 28 Aug 2021 08:01:14 +1000 Subject: [PATCH 34/66] Minor Fixes --- app/Models/Invoice.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index bff1699183..a6297b7034 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -84,10 +84,6 @@ class Invoice extends BaseModel 'custom_surcharge2', 'custom_surcharge3', 'custom_surcharge4', - // 'custom_surcharge_tax1', - // 'custom_surcharge_tax2', - // 'custom_surcharge_tax3', - // 'custom_surcharge_tax4', 'design_id', 'assigned_user_id', 'exchange_rate', From d8ceb2c68ae2941fc7dc678dd6fb2cef113c53cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 09:28:53 +1000 Subject: [PATCH 35/66] Additional check when sending mails - ensure the to_user exists! --- app/Jobs/Mail/NinjaMailerJob.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 0c3fd19287..057045531e 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -224,7 +224,7 @@ class NinjaMailerJob implements ShouldQueue return true; /* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */ - if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false) + if(Ninja::isHosted() && $this->nmo->to_user && strpos($this->nmo->to_user->email, '@example.com') !== false) return true; /* GMail users are uncapped */ From 9e4f8da211ff8f50dde1c45189798aa32615c8e0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 09:34:51 +1000 Subject: [PATCH 36/66] Fixes for expense import - category_id --- app/Import/Transformers/Csv/ExpenseTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Import/Transformers/Csv/ExpenseTransformer.php b/app/Import/Transformers/Csv/ExpenseTransformer.php index d7aea550b8..baf5321058 100644 --- a/app/Import/Transformers/Csv/ExpenseTransformer.php +++ b/app/Import/Transformers/Csv/ExpenseTransformer.php @@ -25,7 +25,7 @@ class ExpenseTransformer extends BaseTransformer { 'date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null, 'public_notes' => $this->getString( $data, 'expense.public_notes' ), 'private_notes' => $this->getString( $data, 'expense.private_notes' ), - 'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null, + 'category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null, 'project_id' => isset( $data['expense.project'] ) ? $this->getProjectId( $data['expense.project'] ) : null, 'payment_type_id' => isset( $data['expense.payment_type'] ) ? $this->getPaymentTypeId( $data['expense.payment_type'] ) : null, 'payment_date' => isset( $data['expense.payment_date'] ) ? date( 'Y-m-d', strtotime( $data['expense.payment_date'] ) ) : null, From 5c99656fe29ee19d0682cb13cb397fb9d1147084 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 20:54:26 +1000 Subject: [PATCH 37/66] Add UBL feature to invoices --- app/Mail/Engine/InvoiceEmailEngine.php | 1 - app/Mail/TemplateEmail.php | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Mail/Engine/InvoiceEmailEngine.php b/app/Mail/Engine/InvoiceEmailEngine.php index d6cc1c8245..9ede43715d 100644 --- a/app/Mail/Engine/InvoiceEmailEngine.php +++ b/app/Mail/Engine/InvoiceEmailEngine.php @@ -132,7 +132,6 @@ class InvoiceEmailEngine extends BaseEmailEngine } - } return $this; diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 268424b4fb..c14e6dcba2 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -11,6 +11,8 @@ namespace App\Mail; +use App\Jobs\Invoice\CreateUbl; +use App\Models\Account; use App\Models\Client; use App\Models\ClientContact; use App\Models\User; @@ -116,6 +118,13 @@ class TemplateEmail extends Mailable } + if($this->invitation->invoice && $settings->ubl_email_attachment && $this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){ + + $ubl_string = CreateUbl::dispatchNow($this->invitation->invoice); + $this->attachData($ubl_string, $this->invitation->invoice->getFileName('xml')); + + } + return $this; } } From 0a25df317ef26535738e3119d218be987a4fd672 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 21:03:00 +1000 Subject: [PATCH 38/66] Minor fixes for balance adjustments --- app/Services/Invoice/MarkInvoiceDeleted.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/Services/Invoice/MarkInvoiceDeleted.php b/app/Services/Invoice/MarkInvoiceDeleted.php index 95590695d8..51b7c3ccc1 100644 --- a/app/Services/Invoice/MarkInvoiceDeleted.php +++ b/app/Services/Invoice/MarkInvoiceDeleted.php @@ -26,6 +26,8 @@ class MarkInvoiceDeleted extends AbstractService private $total_payments = 0; + private $balance_adjustment = 0; + public function __construct(Invoice $invoice) { $this->invoice = $invoice; @@ -51,7 +53,7 @@ class MarkInvoiceDeleted extends AbstractService private function adjustLedger() { // $this->invoice->ledger()->updatePaymentBalance($this->adjustment_amount * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals - $this->invoice->ledger()->updatePaymentBalance($this->invoice->balance * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals + $this->invoice->ledger()->updatePaymentBalance($this->balance_adjustment * -1, 'Invoice Deleted - reducing ledger balance'); //reduces the payment balance by payment totals return $this; } @@ -65,7 +67,7 @@ class MarkInvoiceDeleted extends AbstractService private function adjustBalance() { - $this->invoice->client->service()->updateBalance($this->invoice->balance * -1)->save(); //reduces the client balance by the invoice amount. + $this->invoice->client->service()->updateBalance($this->balance_adjustment * -1)->save(); //reduces the client balance by the invoice amount. return $this; } @@ -122,11 +124,14 @@ class MarkInvoiceDeleted extends AbstractService } - $this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded');; + $this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded'); + + $this->balance_adjustment = $this->invoice->balance; + //$this->total_payments = $this->invoice->payments->sum('amount - refunded'); -nlog("adjustment amount = {$this->adjustment_amount}"); -nlog("total payments = {$this->total_payments}"); + // nlog("adjustment amount = {$this->adjustment_amount}"); + // nlog("total payments = {$this->total_payments}"); return $this; } From ac763b3a0a728eb11cd458fd58e3ece5f443d850 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 21:17:27 +1000 Subject: [PATCH 39/66] Small fix for edge case where invoice may appear to be paid --- app/Services/Invoice/InvoiceService.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index f6df31475c..7023afd562 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -192,6 +192,8 @@ class InvoiceService public function handleCancellation() { + $this->removeUnpaidGatewayFees(); + $this->invoice = (new HandleCancellation($this->invoice))->run(); return $this; @@ -199,6 +201,8 @@ class InvoiceService public function markDeleted() { + $this->removeUnpaidGatewayFees(); + $this->invoice = (new MarkInvoiceDeleted($this->invoice))->run(); return $this; @@ -213,6 +217,8 @@ class InvoiceService public function reverseCancellation() { + $this->removeUnpaidGatewayFees(); + $this->invoice = (new HandleCancellation($this->invoice))->reverse(); return $this; @@ -278,11 +284,14 @@ class InvoiceService public function updateStatus() { - if ((int)$this->invoice->balance == 0) { + if($this->invoice->status_id == Invoice::STATUS_DRAFT) + return $this; + + // if ((int)$this->invoice->balance == 0) { - $this->setStatus(Invoice::STATUS_PAID)->workFlow(); - // InvoiceWorkflowSettings::dispatchNow($this->invoice); - } + // $this->setStatus(Invoice::STATUS_PAID)->workFlow(); + + // } if ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) { $this->setStatus(Invoice::STATUS_PARTIAL); From 4ca034c9f615c595d2f5c44b769e480a2296ee07 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 30 Aug 2021 07:53:51 +1000 Subject: [PATCH 40/66] Fixes for import correcting amounts --- app/Import/Transformers/BaseTransformer.php | 2 +- tests/Unit/NumberTest.php | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/Import/Transformers/BaseTransformer.php b/app/Import/Transformers/BaseTransformer.php index 72a76edc09..f0b2721c3e 100644 --- a/app/Import/Transformers/BaseTransformer.php +++ b/app/Import/Transformers/BaseTransformer.php @@ -146,7 +146,7 @@ class BaseTransformer $number = 0; } - return Number::parseStringFloat($number); + return Number::parseFloat($number); } /** diff --git a/tests/Unit/NumberTest.php b/tests/Unit/NumberTest.php index 254ec23639..fa12911a87 100644 --- a/tests/Unit/NumberTest.php +++ b/tests/Unit/NumberTest.php @@ -41,6 +41,27 @@ class NumberTest extends TestCase $this->assertEquals(2.15, $rounded); } + //this method proved an error! removing this method from production + // public function testImportFloatConversion() + // { + + // $amount = '€7,99'; + + // $converted_amount = Number::parseStringFloat($amount); + + // $this->assertEquals(799, $converted_amount); + + // } + + public function testParsingStringCurrency() + { + $amount = '€7,99'; + + $converted_amount = Number::parseFloat($amount); + + $this->assertEquals(7.99, $converted_amount); + } + // public function testParsingFloats() // { // Currency::all()->each(function ($currency) { From 003f326f8d1243b5a9df7cc1fdfb2528bd17c3c0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 30 Aug 2021 13:20:29 +1000 Subject: [PATCH 41/66] WePay ACH / Credit card Token billing --- app/PaymentDrivers/WePay/ACH.php | 62 +++++++++++++++++++++++ app/PaymentDrivers/WePay/CreditCard.php | 55 +++++++++++++++++++- app/PaymentDrivers/WePay/WePayCommon.php | 5 +- app/PaymentDrivers/WePayPaymentDriver.php | 13 +++-- app/Utils/Traits/Inviteable.php | 3 +- 5 files changed, 130 insertions(+), 8 deletions(-) diff --git a/app/PaymentDrivers/WePay/ACH.php b/app/PaymentDrivers/WePay/ACH.php index fcc463f015..f6030a1655 100644 --- a/app/PaymentDrivers/WePay/ACH.php +++ b/app/PaymentDrivers/WePay/ACH.php @@ -271,4 +271,66 @@ class ACH $this->wepay_payment_driver->storeGatewayToken($data); } + + + public function tokenBilling($token, $payment_hash) + { + + $token_meta = $token->meta; + + if(!property_exists($token_meta, 'state') || $token_meta->state != "authorized") + return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]); + + $app_fee = (config('ninja.wepay.fee_ach_multiplier') * $this->wepay_payment_driver->payment_hash->data->amount_with_fee) + config('ninja.wepay.fee_fixed'); + + $response = $this->wepay_payment_driver->wepay->request('checkout/create', array( + 'unique_id' => Str::random(40), + 'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'), + 'amount' => $this->wepay_payment_driver->payment_hash->data->amount_with_fee, + 'currency' => $this->wepay_payment_driver->client->getCurrencyCode(), + 'short_description' => 'Goods and Services', + 'type' => 'goods', + 'fee' => [ + 'fee_payer' => config('ninja.wepay.fee_payer'), + 'app_fee' => $app_fee, + ], + 'payment_method' => array( + 'type' => 'payment_bank', + 'payment_bank' => array( + 'id' => $token->token + ) + ) + )); + + /* Merge all data and store in the payment hash*/ + $state = [ + 'server_response' => $response, + 'payment_hash' => $this->wepay_payment_driver->payment_hash, + ]; + + $this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state); + $this->wepay_payment_driver->payment_hash->save(); + + if(in_array($response->state, ['authorized', 'captured'])){ + //success + nlog("success"); + $payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING; + + return $this->processSuccessfulPayment($response, $payment_status, GatewayType::BANK_TRANSFER, true); + } + + if(in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])){ + //some type of failure + nlog("failure"); + + $payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED; + + $this->processUnSuccessfulPayment($response, $payment_status); + } + + } + + + + } diff --git a/app/PaymentDrivers/WePay/CreditCard.php b/app/PaymentDrivers/WePay/CreditCard.php index 8e6a155ddc..0b5ac2088e 100644 --- a/app/PaymentDrivers/WePay/CreditCard.php +++ b/app/PaymentDrivers/WePay/CreditCard.php @@ -261,7 +261,8 @@ https://developer.wepay.com/api/api-calls/checkout private function storePaymentMethod($response, $payment_method_id) { -nlog("storing card"); + nlog("storing card"); + $payment_meta = new \stdClass; $payment_meta->exp_month = (string) $response->expiration_month; $payment_meta->exp_year = (string) $response->expiration_year; @@ -281,5 +282,57 @@ nlog("storing card"); + public function tokenBilling($cgt, $payment_hash) + { + + $app_fee = (config('ninja.wepay.fee_cc_multiplier') * $this->wepay_payment_driver->payment_hash->data->amount_with_fee) + config('ninja.wepay.fee_fixed'); + // charge the credit card + $response = $this->wepay_payment_driver->wepay->request('checkout/create', array( + 'unique_id' => Str::random(40), + 'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'), + 'amount' => $this->wepay_payment_driver->payment_hash->data->amount_with_fee, + 'currency' => $this->wepay_payment_driver->client->getCurrencyCode(), + 'short_description' => 'Goods and services', + 'type' => 'goods', + 'fee' => [ + 'fee_payer' => config('ninja.wepay.fee_payer'), + 'app_fee' => $app_fee, + ], + 'payment_method' => array( + 'type' => 'credit_card', + 'credit_card' => array( + 'id' => $cgt->token + ) + ) + )); + + /* Merge all data and store in the payment hash*/ + $state = [ + 'server_response' => $response, + 'payment_hash' => $payment_hash, + ]; + + $this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state); + $this->wepay_payment_driver->payment_hash->save(); + + + if(in_array($response->state, ['authorized', 'captured'])){ + //success + nlog("success"); + $payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING; + + return $this->processSuccessfulPayment($response, $payment_status, GatewayType::CREDIT_CARD, true); + } + + if(in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])){ + //some type of failure + nlog("failure"); + + $payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED; + + $this->processUnSuccessfulPayment($response, $payment_status); + } + } + } diff --git a/app/PaymentDrivers/WePay/WePayCommon.php b/app/PaymentDrivers/WePay/WePayCommon.php index ec28874bd0..2cf843b438 100644 --- a/app/PaymentDrivers/WePay/WePayCommon.php +++ b/app/PaymentDrivers/WePay/WePayCommon.php @@ -22,7 +22,7 @@ trait WePayCommon { - private function processSuccessfulPayment($response, $payment_status, $gateway_type) + private function processSuccessfulPayment($response, $payment_status, $gateway_type, $return_payment = false) { if($gateway_type == GatewayType::BANK_TRANSFER) @@ -48,6 +48,9 @@ trait WePayCommon $this->wepay_payment_driver->client->company, ); + if($return_payment) + return $payment; + return redirect()->route('client.payments.show', ['payment' => $this->wepay_payment_driver->encodePrimaryKey($payment->id)]); } diff --git a/app/PaymentDrivers/WePayPaymentDriver.php b/app/PaymentDrivers/WePayPaymentDriver.php index 89f628654b..32d20cbbcd 100644 --- a/app/PaymentDrivers/WePayPaymentDriver.php +++ b/app/PaymentDrivers/WePayPaymentDriver.php @@ -119,14 +119,14 @@ class WePayPaymentDriver extends BaseDriver $contact = $client->primary_contact()->first() ? $client->primary_contact()->first() : $lient->contacts->first(); $data['contact'] = $contact; - return $this->payment_method->authorizeView($data); //this is your custom implementation from here + return $this->payment_method->authorizeView($data); } public function authorizeResponse($request) { $this->init(); - return $this->payment_method->authorizeResponse($request); //this is your custom implementation from here + return $this->payment_method->authorizeResponse($request); } public function verificationView(ClientGatewayToken $cgt) @@ -147,19 +147,22 @@ class WePayPaymentDriver extends BaseDriver { $this->init(); - return $this->payment_method->paymentView($data); //this is your custom implementation from here + return $this->payment_method->paymentView($data); } public function processPaymentResponse($request) { $this->init(); - return $this->payment_method->paymentResponse($request); //this is your custom implementation from here + return $this->payment_method->paymentResponse($request); } public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) { - return $this->payment_method->yourTokenBillingImplmentation(); //this is your custom implementation from here + $this->setPaymentMethod($cgt->gateway_type_id); + $this->setPaymentHash($payment_hash); + + return $this->payment_method->tokenBilling($cgt, $payment_hash); } public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null) diff --git a/app/Utils/Traits/Inviteable.php b/app/Utils/Traits/Inviteable.php index a65dbc7e73..91520a6380 100644 --- a/app/Utils/Traits/Inviteable.php +++ b/app/Utils/Traits/Inviteable.php @@ -47,8 +47,9 @@ trait Inviteable { $entity_type = Str::snake(class_basename($this->entityType())); - if(Ninja::isHosted()) + if(Ninja::isHosted()){ $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain(); + } else $domain = config('ninja.app_url'); From ae88b61e8d8670308d3c482b8813553e90f94329 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 30 Aug 2021 15:35:37 +1000 Subject: [PATCH 42/66] Fixes for uploading and downloading company imports --- app/Http/Controllers/ImportJsonController.php | 3 ++- app/Jobs/Company/CompanyImport.php | 8 ++++---- app/Models/GroupSetting.php | 9 +++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/ImportJsonController.php b/app/Http/Controllers/ImportJsonController.php index bfdf1cfe29..f509dbc53e 100644 --- a/app/Http/Controllers/ImportJsonController.php +++ b/app/Http/Controllers/ImportJsonController.php @@ -66,7 +66,8 @@ class ImportJsonController extends BaseController $file_location = $request->file('files') ->storeAs( 'migrations', - $request->file('files')->getClientOriginalName() + $request->file('files')->getClientOriginalName(), + config('filesystems.default'), ); if(Ninja::isHosted()) diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 591c03bf33..17017722a2 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -224,7 +224,7 @@ class CompanyImport implements ShouldQueue // if(mime_content_type(Storage::path($this->file_location)) == 'text/plain') // return Storage::path($this->file_location); - $path = TempFile::filePath(Storage::get($this->file_location), basename($this->file_location)); + $path = TempFile::filePath(Storage::disk(config('filesystems.default'))->get($this->file_location), basename($this->file_location)); $zip = new ZipArchive(); $archive = $zip->open($path); @@ -235,7 +235,7 @@ class CompanyImport implements ShouldQueue $zip->close(); $file_location = "{$file_path}/backup.json"; - if (! file_exists($file_location)) + if (! file_exists($file_path)) throw new NonExistingMigrationFile('Backup file does not exist, or is corrupted.'); return $file_location; @@ -568,7 +568,7 @@ class CompanyImport implements ShouldQueue { $this->genericImport(GroupSetting::class, - ['user_id', 'company_id', 'id', 'hashed_id',], + ['user_id', 'company_id', 'id', 'hashed_id'], [['users' => 'user_id']], 'group_settings', 'name'); @@ -580,7 +580,7 @@ class CompanyImport implements ShouldQueue { $this->genericImport(Subscription::class, - ['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id',], + ['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id'], [['group_settings' => 'group_id'], ['users' => 'user_id'], ['users' => 'assigned_user_id']], 'subscriptions', 'name'); diff --git a/app/Models/GroupSetting.php b/app/Models/GroupSetting.php index fd6bb52bf6..5fe53afdac 100644 --- a/app/Models/GroupSetting.php +++ b/app/Models/GroupSetting.php @@ -34,6 +34,15 @@ class GroupSetting extends StaticModel 'settings', ]; + protected $appends = [ + 'hashed_id', + ]; + + public function getHashedIdAttribute() + { + return $this->encodePrimaryKey($this->id); + } + protected $touches = []; public function company() From ba7ea8bbea28c34e1901c5de017033e9a5c9a3cc Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 30 Aug 2021 16:00:21 +1000 Subject: [PATCH 43/66] WePay token billing --- app/PaymentDrivers/WePay/ACH.php | 6 ++++-- app/PaymentDrivers/WePay/CreditCard.php | 7 +++++-- app/PaymentDrivers/WePayPaymentDriver.php | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/PaymentDrivers/WePay/ACH.php b/app/PaymentDrivers/WePay/ACH.php index f6030a1655..6de6498c2e 100644 --- a/app/PaymentDrivers/WePay/ACH.php +++ b/app/PaymentDrivers/WePay/ACH.php @@ -281,12 +281,14 @@ class ACH if(!property_exists($token_meta, 'state') || $token_meta->state != "authorized") return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]); - $app_fee = (config('ninja.wepay.fee_ach_multiplier') * $this->wepay_payment_driver->payment_hash->data->amount_with_fee) + config('ninja.wepay.fee_fixed'); + $amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total; + + $app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed'); $response = $this->wepay_payment_driver->wepay->request('checkout/create', array( 'unique_id' => Str::random(40), 'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'), - 'amount' => $this->wepay_payment_driver->payment_hash->data->amount_with_fee, + 'amount' => $amount, 'currency' => $this->wepay_payment_driver->client->getCurrencyCode(), 'short_description' => 'Goods and Services', 'type' => 'goods', diff --git a/app/PaymentDrivers/WePay/CreditCard.php b/app/PaymentDrivers/WePay/CreditCard.php index 0b5ac2088e..5cb3567976 100644 --- a/app/PaymentDrivers/WePay/CreditCard.php +++ b/app/PaymentDrivers/WePay/CreditCard.php @@ -285,12 +285,15 @@ https://developer.wepay.com/api/api-calls/checkout public function tokenBilling($cgt, $payment_hash) { - $app_fee = (config('ninja.wepay.fee_cc_multiplier') * $this->wepay_payment_driver->payment_hash->data->amount_with_fee) + config('ninja.wepay.fee_fixed'); + $amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total; + + + $app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed'); // charge the credit card $response = $this->wepay_payment_driver->wepay->request('checkout/create', array( 'unique_id' => Str::random(40), 'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'), - 'amount' => $this->wepay_payment_driver->payment_hash->data->amount_with_fee, + 'amount' => $amount, 'currency' => $this->wepay_payment_driver->client->getCurrencyCode(), 'short_description' => 'Goods and services', 'type' => 'goods', diff --git a/app/PaymentDrivers/WePayPaymentDriver.php b/app/PaymentDrivers/WePayPaymentDriver.php index 32d20cbbcd..27c60e570b 100644 --- a/app/PaymentDrivers/WePayPaymentDriver.php +++ b/app/PaymentDrivers/WePayPaymentDriver.php @@ -159,6 +159,7 @@ class WePayPaymentDriver extends BaseDriver public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) { + $this->init(); $this->setPaymentMethod($cgt->gateway_type_id); $this->setPaymentHash($payment_hash); From 185e47522d2e2d1ce49cf788ca282ab32133211b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 30 Aug 2021 19:30:25 +1000 Subject: [PATCH 44/66] Add correct translations for emails --- app/Listeners/SendVerificationNotification.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Listeners/SendVerificationNotification.php b/app/Listeners/SendVerificationNotification.php index 99e095b10d..cec4f916b0 100644 --- a/app/Listeners/SendVerificationNotification.php +++ b/app/Listeners/SendVerificationNotification.php @@ -24,6 +24,7 @@ use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\App; class SendVerificationNotification implements ShouldQueue { @@ -53,6 +54,10 @@ class SendVerificationNotification implements ShouldQueue $event->user->service()->invite($event->company); + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($event->company->settings)); + $nmo = new NinjaMailerObject; $nmo->mailable = new UserAdded($event->company, $event->creating_user, $event->user); $nmo->company = $event->company; From e67c668fd823d54c1097e0603c8026f3fe531797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 12:18:57 +0200 Subject: [PATCH 45/66] Show message on unsuccessful verification --- app/PaymentDrivers/Braintree/ACH.php | 6 ++++-- resources/lang/en/texts.php | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php index 1b400e61dd..1a91473d5b 100644 --- a/app/PaymentDrivers/Braintree/ACH.php +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -63,7 +63,7 @@ class ACH implements MethodInterface ], ]); - if ($result->success) { + if ($result->success && optional($result->paymentMethod)->verified) { $account = $result->paymentMethod; try { @@ -71,7 +71,7 @@ class ACH implements MethodInterface $payment_meta->brand = (string)$account->bankName; $payment_meta->last4 = (string)$account->last4; $payment_meta->type = GatewayType::BANK_TRANSFER; - $payment_meta->state = $account->verified ? 'authorized' : 'unauthorized'; + $payment_meta->state = 'authorized'; $data = [ 'payment_meta' => $payment_meta, @@ -86,6 +86,8 @@ class ACH implements MethodInterface return $this->braintree->processInternallyFailedPayment($this->braintree, $e); } } + + return back()->withMessage(ctrans('texts.unable_to_verify_payment_method')); } public function paymentView(array $data) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index dc5ee29bd4..7ed8c1b22e 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4301,6 +4301,7 @@ $LANG = array( 'locality' => 'Locality', 'checking' => 'Checking', 'savings' => 'Savings', + 'unable_to_verify_payment_method' => 'Unable to verify payment method.', ); return $LANG; From 7be6252233ade5b267145875d44f80a801b16cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 12:28:23 +0200 Subject: [PATCH 46/66] Provide US states for Braintree ACH --- app/DataProviders/USStates.php | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 app/DataProviders/USStates.php diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php new file mode 100644 index 0000000000..0551b5fbc3 --- /dev/null +++ b/app/DataProviders/USStates.php @@ -0,0 +1,75 @@ + 'Alabama', + 'AK' => 'Alaska', + 'AZ' => 'Arizona', + 'AR' => 'Arkansas', + 'CA' => 'California', + 'CO' => 'Colorado', + 'CT' => 'Connecticut', + 'DE' => 'Delaware', + 'DC' => 'District Of Columbia', + 'FL' => 'Florida', + 'GA' => 'Georgia', + 'HI' => 'Hawaii', + 'ID' => 'Idaho', + 'IL' => 'Illinois', + 'IN' => 'Indiana', + 'IA' => 'Iowa', + 'KS' => 'Kansas', + 'KY' => 'Kentucky', + 'LA' => 'Louisiana', + 'ME' => 'Maine', + 'MD' => 'Maryland', + 'MA' => 'Massachusetts', + 'MI' => 'Michigan', + 'MN' => 'Minnesota', + 'MS' => 'Mississippi', + 'MO' => 'Missouri', + 'MT' => 'Montana', + 'NE' => 'Nebraska', + 'NV' => 'Nevada', + 'NH' => 'New Hampshire', + 'NJ' => 'New Jersey', + 'NM' => 'New Mexico', + 'NY' => 'New York', + 'NC' => 'North Carolina', + 'ND' => 'North Dakota', + 'OH' => 'Ohio', + 'OK' => 'Oklahoma', + 'OR' => 'Oregon', + 'PA' => 'Pennsylvania', + 'RI' => 'Rhode Island', + 'SC' => 'South Carolina', + 'SD' => 'South Dakota', + 'TN' => 'Tennessee', + 'TX' => 'Texas', + 'UT' => 'Utah', + 'VT' => 'Vermont', + 'VA' => 'Virginia', + 'WA' => 'Washington', + 'WV' => 'West Virginia', + 'WI' => 'Wisconsin', + 'WY' => 'Wyoming', + ]; + + public static function get(): array + { + return self::$states; + } +} From 631814db2610a3872144d4e96c2a2e1d82d5d9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 12:28:34 +0200 Subject: [PATCH 47/66] Update form field to use states --- .../ninja2020/gateways/braintree/ach/authorize.blade.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php index e5b48683b3..91e313fc27 100644 --- a/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php @@ -72,7 +72,11 @@ @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.state')]) - + @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.postal_code')]) From 5b8347a18429b6dbff168f596b6037f9850aa5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 12:34:45 +0200 Subject: [PATCH 48/66] Fixes for tests --- tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php index 38caa94abb..e884e63ed6 100644 --- a/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php +++ b/tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php @@ -57,10 +57,9 @@ class ACHTest extends DuskTestCase ->type('#account-holder-name', 'John Doe') ->type('#account-number', '1000000000') ->type('#routing-number', '011000015') - ->type('#billing-region', 'AL') ->type('#billing-postal-code', '12345') ->press('Add Payment Method') - ->assertSee('Added payment method.'); + ->waitForText('Added payment method.'); }); } From 236a0f225aae1bc00a375fdce302efe8a118c1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 12:43:00 +0200 Subject: [PATCH 49/66] Fixes for auto billing --- app/PaymentDrivers/BraintreePaymentDriver.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/PaymentDrivers/BraintreePaymentDriver.php b/app/PaymentDrivers/BraintreePaymentDriver.php index ca22ca6f4f..f1d0858284 100644 --- a/app/PaymentDrivers/BraintreePaymentDriver.php +++ b/app/PaymentDrivers/BraintreePaymentDriver.php @@ -221,7 +221,8 @@ class BraintreePaymentDriver extends BaseDriver SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_BRAINTREE, - $this->client + $this->client, + $this->client->company, ); return $payment; @@ -242,7 +243,8 @@ class BraintreePaymentDriver extends BaseDriver SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_BRAINTREE, - $this->client + $this->client, + $this->client->company ); return false; From 9f6fdb74c4b80311700393c9eef456b3037f0574 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 30 Aug 2021 22:04:51 +1000 Subject: [PATCH 50/66] Fixes for Payment URL --- .../ClientPortal/InvitationController.php | 17 ++++++++++++++++ app/Models/Payment.php | 20 +++++++++++++++++-- routes/client.php | 3 +++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index b47ba96461..797af5a09e 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -16,6 +16,9 @@ use App\Events\Invoice\InvoiceWasViewed; use App\Events\Misc\InvitationWasViewed; use App\Events\Quote\QuoteWasViewed; use App\Http\Controllers\Controller; +use App\Models\Client; +use App\Models\ClientContact; +use App\Models\Payment; use App\Utils\Ninja; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; @@ -113,4 +116,18 @@ class InvitationController extends Controller public function routerForIframe(string $entity, string $client_hash, string $invitation_key) { } + + public function paymentRouter(string $contact_key, string $payment_id) + { + $contact = ClientContact::where('contact_key', $contact_key)->firstOrFail(); + $payment = Payment::find($this->decodePrimaryKey($payment_id)); + + if($payment->client_id != $contact->client_id) + abort(403, 'You are not authorized to view this resource'); + + auth()->guard('contact')->login($contact, true); + + return redirect()->route('client.payments.show', $payment->hashed_id); + + } } diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 8416c35e43..0043a284cb 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -287,8 +287,24 @@ class Payment extends BaseModel event(new PaymentWasVoided($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); } - public function getLink() + // public function getLink() + // { + // return route('client.payments.show', $this->hashed_id); + // } + + public function getLink() :string { - return route('client.payments.show', $this->hashed_id); + + if(Ninja::isHosted()){ + $domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain(); + } + else + $domain = config('ninja.app_url'); + + return $domain.'/client/payment/'. $this->client->contacts()->first()->contact_key .'/' .$this->hashed_id; + + + } + } diff --git a/routes/client.php b/routes/client.php index b95d5a95de..dc103e11fc 100644 --- a/routes/client.php +++ b/routes/client.php @@ -25,6 +25,8 @@ Route::get('client/key_login/{contact_key}', 'ClientPortal\ContactHashLoginContr Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginController@magicLink')->name('client.contact_magic_link')->middleware(['domain_db','contact_key_login']); Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download')->middleware(['document_db']); Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error'); +Route::get('client/payment/{contact_key}/{payment_id}', 'ClientPortal\InvitationController@paymentRouter')->middleware(['domain_db','contact_key_login']); + Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () { Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit @@ -95,6 +97,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie Route::get('credit/{invitation_key}/download_pdf', 'CreditController@downloadPdf')->name('credit.download_invitation_key'); Route::get('{entity}/{invitation_key}/download', 'ClientPortal\InvitationController@routerForDownload'); Route::get('{entity}/{client_hash}/{invitation_key}', 'ClientPortal\InvitationController@routerForIframe')->name('invoice.client_hash_and_invitation_key'); //should never need this + }); Route::get('phantom/{entity}/{invitation_key}', '\App\Utils\PhantomJS\Phantom@displayInvitation')->middleware(['invite_db', 'phantom_secret'])->name('phantom_view'); From 252c7a4bab42c2e4789db3fa87729eddbc499eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 15:47:49 +0200 Subject: [PATCH 51/66] Add cache-control properties component --- .../views/portal/ninja2020/components/no-cache.blade.php | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 resources/views/portal/ninja2020/components/no-cache.blade.php diff --git a/resources/views/portal/ninja2020/components/no-cache.blade.php b/resources/views/portal/ninja2020/components/no-cache.blade.php new file mode 100644 index 0000000000..4a7bc88647 --- /dev/null +++ b/resources/views/portal/ninja2020/components/no-cache.blade.php @@ -0,0 +1,5 @@ + + + + + From f5ca85ce3797a71671ff725ea1855a7e534aee80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 15:48:01 +0200 Subject: [PATCH 52/66] Don't push cache for invoices & credits --- resources/views/portal/ninja2020/credits/show.blade.php | 2 ++ resources/views/portal/ninja2020/invoices/show.blade.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/resources/views/portal/ninja2020/credits/show.blade.php b/resources/views/portal/ninja2020/credits/show.blade.php index 1b914210e8..6a23a51594 100644 --- a/resources/views/portal/ninja2020/credits/show.blade.php +++ b/resources/views/portal/ninja2020/credits/show.blade.php @@ -3,6 +3,8 @@ @push('head') + @include('portal.ninja2020.components.no-cache') + @endpush diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php index aeaee1553f..52cdc09ae2 100644 --- a/resources/views/portal/ninja2020/invoices/show.blade.php +++ b/resources/views/portal/ninja2020/invoices/show.blade.php @@ -4,6 +4,8 @@ @push('head') + @include('portal.ninja2020.components.no-cache') + @endpush From e5af3d291e9a6187ed5c2eb3013686d222597067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 30 Aug 2021 15:49:16 +0200 Subject: [PATCH 53/66] Don't push cache for quotes --- resources/views/portal/ninja2020/quotes/show.blade.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/views/portal/ninja2020/quotes/show.blade.php b/resources/views/portal/ninja2020/quotes/show.blade.php index da929a8656..905815a29a 100644 --- a/resources/views/portal/ninja2020/quotes/show.blade.php +++ b/resources/views/portal/ninja2020/quotes/show.blade.php @@ -8,6 +8,8 @@ + @include('portal.ninja2020.components.no-cache') + @endpush From 04f8a89d75f868c85afb6a12c72ee4770a159026 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 07:44:05 +1000 Subject: [PATCH 54/66] Fixes for auto bill --- app/Http/Livewire/RecurringInvoices/UpdateAutoBilling.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Livewire/RecurringInvoices/UpdateAutoBilling.php b/app/Http/Livewire/RecurringInvoices/UpdateAutoBilling.php index 8f2144ad05..dfcd3d7717 100644 --- a/app/Http/Livewire/RecurringInvoices/UpdateAutoBilling.php +++ b/app/Http/Livewire/RecurringInvoices/UpdateAutoBilling.php @@ -21,7 +21,7 @@ class UpdateAutoBilling extends Component public function updateAutoBilling(): void { - if ($this->invoice->auto_bill === 'optin' || $this->invoice->auto_bill === 'optout') { + if ($this->invoice->auto_bill == 'optin' || $this->invoice->auto_bill == 'optout') { $this->invoice->auto_bill_enabled = !$this->invoice->auto_bill_enabled; $this->invoice->save(); } From b22831f42ec35f54b96f7e8b8a334a94f2f77eb0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 08:20:45 +1000 Subject: [PATCH 55/66] Do no send emails to trashed contacts --- app/Http/Controllers/EmailController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 14866f1e47..3e2806c7dd 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -127,12 +127,11 @@ class EmailController extends BaseController $entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { $entity_obj->service()->markSent()->save(); EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data); - // ->delay(now()->addSeconds(45)); } From fd0a162197d89896ae19cd19796ab7903554d7df Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 10:16:26 +1000 Subject: [PATCH 56/66] Fixes for tests --- app/Jobs/RecurringInvoice/SendRecurring.php | 2 +- app/Jobs/Util/SendFailedEmails.php | 2 +- app/PaymentDrivers/BaseDriver.php | 4 +- app/Services/Credit/SendEmail.php | 2 +- app/Services/Invoice/SendEmail.php | 2 +- app/Services/Quote/SendEmail.php | 2 +- tests/Feature/CancelInvoiceTest.php | 4 +- tests/Feature/RefundTest.php | 53 +++++++++++++++++++++ 8 files changed, 62 insertions(+), 9 deletions(-) diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 63ce5c2e03..d19ebd3847 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -116,7 +116,7 @@ class SendRecurring implements ShouldQueue nlog("Invoice {$invoice->number} created"); $invoice->invitations->each(function ($invitation) use ($invoice) { - if ($invitation->contact && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) { + if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) { try{ EmailEntity::dispatch($invitation, $invoice->company); diff --git a/app/Jobs/Util/SendFailedEmails.php b/app/Jobs/Util/SendFailedEmails.php index bffe66caa6..ae8f11be1d 100644 --- a/app/Jobs/Util/SendFailedEmails.php +++ b/app/Jobs/Util/SendFailedEmails.php @@ -63,7 +63,7 @@ class SendFailedEmails implements ShouldQueue $invitation = $job_meta_array['entity_name']::where('key', $job_meta_array['invitation_key'])->with('contact')->first(); if ($invitation->invoice) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { EmailEntity::dispatch($invitation, $invitation->company, $job_meta_array['reminder_template']); } } diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 65705fc19c..c715a9d464 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -394,7 +394,7 @@ class BaseDriver extends AbstractPaymentDriver $invoices->first()->invitations->each(function ($invitation) use ($nmo){ - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { $nmo->to_user = $invitation->contact; NinjaMailerJob::dispatch($nmo); @@ -456,7 +456,7 @@ class BaseDriver extends AbstractPaymentDriver $invoices->first()->invitations->each(function ($invitation) use ($nmo){ - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { $nmo->to_user = $invitation->contact; NinjaMailerJob::dispatch($nmo); diff --git a/app/Services/Credit/SendEmail.php b/app/Services/Credit/SendEmail.php index 3915b75035..55599323f1 100644 --- a/app/Services/Credit/SendEmail.php +++ b/app/Services/Credit/SendEmail.php @@ -44,7 +44,7 @@ class SendEmail } $this->credit->invitations->each(function ($invitation) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { $email_builder = (new CreditEmail())->build($invitation, $this->reminder_template); // EmailCredit::dispatchNow($email_builder, $invitation, $invitation->company); diff --git a/app/Services/Invoice/SendEmail.php b/app/Services/Invoice/SendEmail.php index 54cd0f3266..f96b95cbaa 100644 --- a/app/Services/Invoice/SendEmail.php +++ b/app/Services/Invoice/SendEmail.php @@ -44,7 +44,7 @@ class SendEmail extends AbstractService } $this->invoice->invitations->each(function ($invitation) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template); } }); diff --git a/app/Services/Quote/SendEmail.php b/app/Services/Quote/SendEmail.php index 91ff695865..8fb1c30024 100644 --- a/app/Services/Quote/SendEmail.php +++ b/app/Services/Quote/SendEmail.php @@ -42,7 +42,7 @@ class SendEmail } $this->quote->invitations->each(function ($invitation) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { EmailEntity::dispatchNow($invitation, $invitation->company, $this->reminder_template); } }); diff --git a/tests/Feature/CancelInvoiceTest.php b/tests/Feature/CancelInvoiceTest.php index a4d5a8104c..5bdaf24e6b 100644 --- a/tests/Feature/CancelInvoiceTest.php +++ b/tests/Feature/CancelInvoiceTest.php @@ -54,11 +54,11 @@ class CancelInvoiceTest extends TestCase $this->assertEquals(Invoice::STATUS_SENT, $this->invoice->status_id); - $this->invoice->service()->handleCancellation()->save(); + $this->invoice->fresh()->service()->handleCancellation()->save(); $this->assertEquals(0, $this->invoice->fresh()->balance); $this->assertEquals($this->client->fresh()->balance, ($client_balance - $invoice_balance)); $this->assertNotEquals($client_balance, $this->client->fresh()->balance); - $this->assertEquals(Invoice::STATUS_CANCELLED, $this->invoice->status_id); + $this->assertEquals(Invoice::STATUS_CANCELLED, $this->invoice->fresh()->status_id); } } diff --git a/tests/Feature/RefundTest.php b/tests/Feature/RefundTest.php index 1e4af1b635..8c33246569 100644 --- a/tests/Feature/RefundTest.php +++ b/tests/Feature/RefundTest.php @@ -14,6 +14,7 @@ use App\Factory\ClientFactory; use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Helpers\Invoice\InvoiceSum; +use App\Models\ClientContact; use App\Models\Invoice; use App\Models\Payment; use App\Utils\Traits\MakesHash; @@ -63,6 +64,14 @@ class RefundTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id $this->invoice->client_id = $client->id; $this->invoice->status_id = Invoice::STATUS_SENT; @@ -138,6 +147,15 @@ class RefundTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id $this->invoice->client_id = $client->id; $this->invoice->status_id = Invoice::STATUS_SENT; @@ -227,6 +245,14 @@ class RefundTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id $this->invoice->client_id = $client->id; $this->invoice->status_id = Invoice::STATUS_SENT; @@ -303,6 +329,15 @@ class RefundTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id $this->invoice->client_id = $client->id; $this->invoice->status_id = Invoice::STATUS_SENT; @@ -388,6 +423,15 @@ class RefundTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id $this->invoice->client_id = $client->id; $this->invoice->status_id = Invoice::STATUS_SENT; @@ -497,6 +541,15 @@ class RefundTest extends TestCase $client = ClientFactory::create($this->company->id, $this->user->id); $client->save(); + $contact = ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + 'send_email' => true, + ]); + + $this->invoice = InvoiceFactory::create($this->company->id, $this->user->id); //stub the company and user_id $this->invoice->client_id = $client->id; $this->invoice->status_id = Invoice::STATUS_SENT; From 146e1b764fc9895e89ec1bfa1394ea3487e87317 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 11:23:02 +1000 Subject: [PATCH 57/66] Set correct column type --- .../components/livewire/payment-methods-table.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php b/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php index c6216e4484..767ed49017 100644 --- a/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php @@ -41,7 +41,7 @@ - + {{ ctrans('texts.payment_type_id') }} From ded9e25c02d818b88fcec10fc42dc8e19ed3fe1d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 13:12:17 +1000 Subject: [PATCH 58/66] Minor fixes for recurring invoices auto bill --- .../UpdateRecurringInvoiceRequest.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index 7f2d1faad9..4421cf5d69 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -101,8 +101,8 @@ class UpdateRecurringInvoiceRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } - if (isset($input['auto_bill'])) { - $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + if (array_key_exists('auto_bill', $input) && isset($input['auto_bill']) && $this->setAutoBillFlag($input['auto_bill'])) { + $input['auto_bill_enabled'] = true; } if (array_key_exists('documents', $input)) { @@ -123,13 +123,8 @@ class UpdateRecurringInvoiceRequest extends Request */ private function setAutoBillFlag($auto_bill) :bool { - if ($auto_bill == 'always') { + if ($auto_bill == 'always') return true; - } - - // if($auto_bill == '') - // off / optin / optout will reset the status of this field to off to allow - // the client to choose whether to auto_bill or not. return false; } From 700fd6bf99635e35d35d6a2aaaaa84be3f97fef8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 14:09:12 +1000 Subject: [PATCH 59/66] Fixes for password protection route - always check if a password is presented! --- app/Http/Controllers/CompanyController.php | 3 ++- app/Http/Middleware/PasswordProtection.php | 3 ++- routes/api.php | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index af8bb9522e..25800eae7e 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -69,11 +69,12 @@ class CompanyController extends BaseController */ public function __construct(CompanyRepository $company_repo) { + parent::__construct(); $this->company_repo = $company_repo; - // $this->middleware('password_protected')->only(['destroy']); + $this->middleware('password_protected')->only(['destroy']); } diff --git a/app/Http/Middleware/PasswordProtection.php b/app/Http/Middleware/PasswordProtection.php index 54163fac47..fdcbfd3dfd 100644 --- a/app/Http/Middleware/PasswordProtection.php +++ b/app/Http/Middleware/PasswordProtection.php @@ -52,7 +52,8 @@ class PasswordProtection $x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64')); } - if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in')) { + // If no password supplied - then we just check if their authentication is in cache // + if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in') && !$x_api_password) { Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout); diff --git a/routes/api.php b/routes/api.php index 38ac51ac38..0eceb6ef2a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -47,7 +47,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('companies/purge/{company}', 'MigrationController@purgeCompany')->middleware('password_protected'); Route::post('companies/purge_save_settings/{company}', 'MigrationController@purgeCompanySaveSettings')->middleware('password_protected'); + Route::resource('companies', 'CompanyController'); // name = (companies. index / create / show / update / destroy / edit + Route::put('companies/{company}/upload', 'CompanyController@upload'); Route::get('company_ledger', 'CompanyLedgerController@index')->name('company_ledger.index'); From a183f058d4c5fff0cf770ca9f826b1025da0de78 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 15:50:34 +1000 Subject: [PATCH 60/66] Password protection on company tests --- tests/Feature/CompanyTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index 538ef194ff..f427e88ef2 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -117,6 +117,7 @@ class CompanyTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, + 'X-API-PASSWORD' => 'ALongAndBriliantPassword', ])->delete('/api/v1/companies/'.$this->encodePrimaryKey($company->id)) ->assertStatus(200); } From 0aea5eb7ccf15c668d89692fdf031b40a7e126f2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 18:21:06 +1000 Subject: [PATCH 61/66] Fixes for tests --- composer.json | 2 +- tests/Feature/CompanyTest.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 77996d3fcc..958fa7e31a 100644 --- a/composer.json +++ b/composer.json @@ -145,4 +145,4 @@ }, "minimum-stability": "dev", "prefer-stable": true -} +} \ No newline at end of file diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index f427e88ef2..af604cbb67 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -11,6 +11,7 @@ namespace Tests\Feature; use App\DataMapper\CompanySettings; +use App\Http\Middleware\PasswordProtection; use App\Models\Company; use App\Models\CompanyToken; use App\Utils\Traits\MakesHash; @@ -47,6 +48,8 @@ class CompanyTest extends TestCase public function testCompanyList() { + $this->withoutMiddleware(PasswordProtection::class); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, From 64c9d8bb24a67f2976adb0028bc7d147ae618634 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 31 Aug 2021 20:21:29 +1000 Subject: [PATCH 62/66] Fixes for basedriver --- app/PaymentDrivers/BaseDriver.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 219c851c9c..9cbb365eeb 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -389,8 +389,9 @@ class BaseDriver extends AbstractPaymentDriver $invoices->each(function ($invoice) { - if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) { - $invoice->service()->deletePdf(); + if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) + $invoice->service()->deletePdf(); + }); $invoices->first()->invitations->each(function ($invitation) use ($nmo) { @@ -400,7 +401,10 @@ class BaseDriver extends AbstractPaymentDriver $nmo->to_user = $invitation->contact; NinjaMailerJob::dispatch($nmo); } + }); + + } From c075a81326c9de27750c5d143333317385da1af8 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 31 Aug 2021 21:29:18 +1000 Subject: [PATCH 63/66] Slack notifications for email quotas --- app/Jobs/Account/CreateAccount.php | 3 - .../SendVerificationNotification.php | 1 - app/Models/Account.php | 9 ++ .../Ninja/EmailQuotaNotification.php | 88 +++++++++++++++++++ config/ninja.php | 2 +- 5 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 app/Notifications/Ninja/EmailQuotaNotification.php diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index d45b0423fd..4dfe7e0bf2 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -114,9 +114,6 @@ class CreateAccount $spaa9f78->fresh(); - //todo implement SLACK notifications - //$sp035a66->notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja(); - if(Ninja::isHosted()) \Modules\Admin\Jobs\Account\NinjaUser::dispatch([], $sp035a66); diff --git a/app/Listeners/SendVerificationNotification.php b/app/Listeners/SendVerificationNotification.php index cec4f916b0..4f6beef409 100644 --- a/app/Listeners/SendVerificationNotification.php +++ b/app/Listeners/SendVerificationNotification.php @@ -17,7 +17,6 @@ use App\Jobs\Mail\NinjaMailerObject; use App\Libraries\MultiDB; use App\Mail\Admin\VerifyUserObject; use App\Mail\User\UserAdded; -use App\Notifications\Ninja\VerifyUser; use App\Utils\Ninja; use Exception; use Illuminate\Broadcasting\InteractsWithSockets; diff --git a/app/Models/Account.php b/app/Models/Account.php index 54766d341f..5301d90cbd 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -15,11 +15,13 @@ use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerObject; use App\Mail\Ninja\EmailQuotaExceeded; use App\Models\Presenters\AccountPresenter; +use App\Notifications\Ninja\EmailQuotaNotification; use App\Utils\Ninja; use App\Utils\Traits\MakesHash; use Carbon\Carbon; use DateTime; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; use Laracasts\Presenter\PresentableTrait; @@ -384,6 +386,10 @@ class Account extends BaseModel if(is_null(Cache::get("throttle_notified:{$this->key}"))) { + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($this->companies()->first()->settings)); + $nmo = new NinjaMailerObject; $nmo->mailable = new EmailQuotaExceeded($this->companies()->first()); $nmo->company = $this->companies()->first(); @@ -392,6 +398,9 @@ class Account extends BaseModel NinjaMailerJob::dispatch($nmo); Cache::put("throttle_notified:{$this->key}", true, 60 * 24); + + if(config('ninja.notification.slack')) + $this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja(); } return true; diff --git a/app/Notifications/Ninja/EmailQuotaNotification.php b/app/Notifications/Ninja/EmailQuotaNotification.php new file mode 100644 index 0000000000..71c4b00644 --- /dev/null +++ b/app/Notifications/Ninja/EmailQuotaNotification.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 quota exceeded by 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/config/ninja.php b/config/ninja.php index 16d0f9c9be..1bbd2c67b1 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -115,7 +115,7 @@ return [ //'fonts' => 'App\Models\Font', ], 'notification' => [ - 'slack' => env('SLACK_WEBHOOK_URL', ''), + 'slack' => env('SLACK_WEBHOOK_URL', false), 'mail' => env('HOSTED_EMAIL', ''), ], 'themes' => [ From 7a9baae85baa53a1b5f5ac419ad9afc3abf94ac1 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 31 Aug 2021 22:19:30 +1000 Subject: [PATCH 64/66] Fixes for client emails --- app/Models/Presenters/ClientPresenter.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/Models/Presenters/ClientPresenter.php b/app/Models/Presenters/ClientPresenter.php index 824393a7ba..f7a3e7f265 100644 --- a/app/Models/Presenters/ClientPresenter.php +++ b/app/Models/Presenters/ClientPresenter.php @@ -48,7 +48,15 @@ class ClientPresenter extends EntityPresenter public function email() { - return $this->entity->primary_contact->first() !== null ? $this->entity->primary_contact->first()->email : 'No Email Set'; + $primary_contact = $this->entity->primary_contact->first(); + + if($primary_contact && strlen($primary_contact->email) > 1) + return $primary_contact->email; + + $contact = $this->entity->contacts->whereNotNull('email')->first(); + + return $contact ? $contact->email : 'No Email Set'; + } public function address() From 03ed1c3aead67e7ff6993b186271aa1e666e1a90 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 1 Sep 2021 08:22:24 +1000 Subject: [PATCH 65/66] Search for stripe customers by email --- app/PaymentDrivers/StripePaymentDriver.php | 32 ++++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 7231cebbff..7deb0b433d 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -315,20 +315,36 @@ class StripePaymentDriver extends BaseDriver $client_gateway_token = ClientGatewayToken::whereClientId($this->client->id)->whereCompanyGatewayId($this->company_gateway->id)->first(); + //Search by customer reference if ($client_gateway_token && $client_gateway_token->gateway_customer_reference) { + $customer = Customer::retrieve($client_gateway_token->gateway_customer_reference, $this->stripe_connect_auth); - } else { - $data['name'] = $this->client->present()->name(); - $data['phone'] = $this->client->present()->phone(); - - if (filter_var($this->client->present()->email(), FILTER_VALIDATE_EMAIL)) { - $data['email'] = $this->client->present()->email(); - } + if($customer) + return $customer; - $customer = Customer::create($data, $this->stripe_connect_auth); + } + + //Search by email + $searchResults = \Stripe\Customer::all([ + "email" => $this->client->present()->email(), + "limit" => 2, + "starting_after" => null + ],$this->stripe_connect_auth); + + if(count($searchResults) == 1) + return $searchResults->data[0]; + + //Else create a new record + $data['name'] = $this->client->present()->name(); + $data['phone'] = $this->client->present()->phone(); + + if (filter_var($this->client->present()->email(), FILTER_VALIDATE_EMAIL)) { + $data['email'] = $this->client->present()->email(); } + $customer = Customer::create($data, $this->stripe_connect_auth); + if (!$customer) { throw new Exception('Unable to create gateway customer'); } From cc0833b93999851dd0df613acf2a16a89565a910 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 1 Sep 2021 08:42:34 +1000 Subject: [PATCH 66/66] v5.3.2 --- VERSION.txt | 2 +- config/ninja.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION.txt b/VERSION.txt index 7d3cdbf0dd..ba09cfd650 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.1 \ No newline at end of file +5.3.2 \ No newline at end of file diff --git a/config/ninja.php b/config/ninja.php index 1bbd2c67b1..0ee4e37881 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.1', - 'app_tag' => '5.3.1', + 'app_version' => '5.3.2', + 'app_tag' => '5.3.2', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''),