From b324c4292bb9ec94cc3b6e91145886c4aae6a95a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 23 Jul 2021 13:42:13 +0200 Subject: [PATCH 01/36] Inject Mollie in CreateSingleAccount & ninja config --- app/Console/Commands/CreateSingleAccount.php | 21 ++++++++++++++++++++ config/ninja.php | 1 + 2 files changed, 22 insertions(+) diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index b53626595..4a8b1236e 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -742,6 +742,27 @@ class CreateSingleAccount extends Command $cg->fees_and_limits = $fees_and_limits; $cg->save(); } + + if (config('ninja.testvars.mollie') && ($this->gateway == 'all' || $this->gateway == 'mollie')) { + $cg = new CompanyGateway; + $cg->company_id = $company->id; + $cg->user_id = $user->id; + $cg->gateway_key = '1bd651fb213ca0c9d66ae3c336dc77e8'; + $cg->require_cvv = true; + $cg->require_billing_address = true; + $cg->require_shipping_address = true; + $cg->update_details = true; + $cg->config = encrypt(config('ninja.testvars.mollie')); + $cg->save(); + + $gateway_types = $cg->driver(new Client)->gatewayTypes(); + + $fees_and_limits = new stdClass; + $fees_and_limits->{$gateway_types[0]} = new FeesAndLimits; + + $cg->fees_and_limits = $fees_and_limits; + $cg->save(); + } } private function createRecurringInvoice($client) diff --git a/config/ninja.php b/config/ninja.php index 878174e2f..c0d7d1721 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -84,6 +84,7 @@ return [ 'test_email' => env('TEST_EMAIL', 'test@example.com'), 'wepay' => env('WEPAY_KEYS', ''), 'braintree' => env('BRAINTREE_KEYS', ''), + 'mollie' => env('MOLLIE_KEYS', ''), ], 'contact' => [ 'email' => env('MAIL_FROM_ADDRESS'), From aa88f067e77ff01faf80e5e4513e192eb887d46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 23 Jul 2021 13:46:28 +0200 Subject: [PATCH 02/36] Install mollie/mollie-api-php --- composer.json | 1 + composer.lock | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 151dd8239..8595adacb 100644 --- a/composer.json +++ b/composer.json @@ -62,6 +62,7 @@ "league/omnipay": "^3.1", "livewire/livewire": "^2.4", "maennchen/zipstream-php": "^1.2", + "mollie/mollie-api-php": "^2.36", "nwidart/laravel-modules": "^8.0", "omnipay/paypal": "^3.0", "payfast/payfast-php-sdk": "^1.1", diff --git a/composer.lock b/composer.lock index 15b58365b..89eea29d9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d2beb37ff5fbee59ad4bb792e944eb10", + "content-hash": "275a9dd3910b6ec79607b098406dc6c7", "packages": [ { "name": "asm/php-ansible", @@ -4386,6 +4386,97 @@ }, "time": "2019-07-17T11:01:58+00:00" }, + { + "name": "mollie/mollie-api-php", + "version": "v2.36.1", + "source": { + "type": "git", + "url": "https://github.com/mollie/mollie-api-php.git", + "reference": "19f69c116d47a3600f0ed629e0df925a43d3a8f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/19f69c116d47a3600f0ed629e0df925a43d3a8f5", + "reference": "19f69c116d47a3600f0ed629e0df925a43d3a8f5", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.1", + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "php": ">=5.6" + }, + "require-dev": { + "eloquent/liberator": "^2.0", + "friendsofphp/php-cs-fixer": "^3.0", + "guzzlehttp/guzzle": "^6.3 || ^7.0", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.1 || ^8.5" + }, + "suggest": { + "mollie/oauth2-mollie-php": "Use OAuth to authenticate with the Mollie API. This is needed for some endpoints. Visit https://docs.mollie.com/ for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Mollie\\Api\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Mollie B.V.", + "email": "info@mollie.com" + } + ], + "description": "Mollie API client library for PHP. Mollie is a European Payment Service provider and offers international payment methods such as Mastercard, VISA, American Express and PayPal, and local payment methods such as iDEAL, Bancontact, SOFORT Banking, SEPA direct debit, Belfius Direct Net, KBC Payment Button and various gift cards such as Podiumcadeaukaart and fashioncheque.", + "homepage": "https://www.mollie.com/en/developers", + "keywords": [ + "Apple Pay", + "CBC", + "Przelewy24", + "api", + "bancontact", + "banktransfer", + "belfius", + "belfius direct net", + "charges", + "creditcard", + "direct debit", + "fashioncheque", + "gateway", + "gift cards", + "ideal", + "inghomepay", + "intersolve", + "kbc", + "klarna", + "mistercash", + "mollie", + "paylater", + "payment", + "payments", + "paypal", + "paysafecard", + "podiumcadeaukaart", + "recurring", + "refunds", + "sepa", + "service", + "sliceit", + "sofort", + "sofortbanking", + "subscriptions" + ], + "support": { + "issues": "https://github.com/mollie/mollie-api-php/issues", + "source": "https://github.com/mollie/mollie-api-php/tree/v2.36.1" + }, + "time": "2021-06-23T12:55:50+00:00" + }, { "name": "moneyphp/money", "version": "v3.3.1", From 99d0259365d803ee98a7858b18857240ed5fd417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 23 Jul 2021 13:47:01 +0200 Subject: [PATCH 03/36] Scaffold MolliePaymentDriver --- app/PaymentDrivers/MolliePaymentDriver.php | 116 +++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 app/PaymentDrivers/MolliePaymentDriver.php diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php new file mode 100644 index 000000000..03879c42f --- /dev/null +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -0,0 +1,116 @@ + CreditCard::class, + ]; + + const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; + + public function init() + { + return $this; + } + + /* Returns an array of gateway types for the payment gateway */ + public function gatewayTypes(): array + { + $types = []; + + $types[] = GatewayType::CREDIT_CARD; + + return $types; + } + + public function setPaymentMethod($payment_method_id) + { + $class = self::$methods[$payment_method_id]; + $this->payment_method = new $class($this); + return $this; + } + + public function authorizeView(array $data) + { + return $this->payment_method->authorizeView($data); + } + + public function authorizeResponse($request) + { + return $this->payment_method->authorizeResponse($request); + } + + public function processPaymentView(array $data) + { + return $this->payment_method->paymentView($data); + } + + public function processPaymentResponse($request) + { + return $this->payment_method->paymentResponse($request); + } + + public function refund(Payment $payment, $amount, $return_client_response = false) + { + return $this->payment_method->yourRefundImplementationHere(); + } + + public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) + { + return $this->payment_method->yourTokenBillingImplmentation(); + } + + public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null) + { + } +} From a29d4f20759ebfbb7c99356e5d1ff5a20cc4bed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 23 Jul 2021 14:43:32 +0200 Subject: [PATCH 04/36] wip --- app/Models/SystemLog.php | 1 + app/PaymentDrivers/Mollie/CreditCard.php | 63 ++++++ app/PaymentDrivers/MolliePaymentDriver.php | 17 +- public/css/app.css | 2 +- public/mix-manifest.json | 2 +- resources/lang/en/texts.php | 1 + .../gateways/mollie/credit_card/pay.blade.php | 183 ++++++++++++++++++ 7 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 app/PaymentDrivers/Mollie/CreditCard.php create mode 100644 resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index 7614c01ec..a851ed767 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -68,6 +68,7 @@ class SystemLog extends Model const TYPE_BRAINTREE = 307; const TYPE_WEPAY = 309; const TYPE_PAYFAST = 310; + const TYPE_MOLLIE = 311; const TYPE_QUOTA_EXCEEDED = 400; diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php new file mode 100644 index 000000000..3f227c76e --- /dev/null +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -0,0 +1,63 @@ +mollie = $mollie; + + $this->mollie->init(); + } + + /** + * Show the page for credit card payments. + * + * @param array $data + * @return Factory|View + */ + public function paymentView(array $data) + { + $data['gateway'] = $this->mollie; + + return render('gateways.mollie.credit_card.pay', $data); + } + + public function paymentResponse(PaymentResponseRequest $request) + { + try { + $payment = $this->mollie->gateway->payments->create([ + "amount" => [ + "currency" => "USD", + "value" => "10.00" + ], + "description" => "Order #12345", + "redirectUrl" => "https://webshop.example.org/order/12345/", + "webhookUrl" => "https://webshop.example.org/mollie-webhook/", + ]); + + if ($payment->status === 'open') { + return redirect($payment->getCheckoutUrl()); + } + } catch (\Exception $e) { + throw new PaymentFailed($e->getMessage(), $e->getCode()); + } + + dd($payment); + } +} diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 03879c42f..6cdcb8b97 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -18,7 +18,9 @@ use App\Models\GatewayType; use App\Models\Payment; use App\Models\PaymentHash; use App\Models\SystemLog; +use App\PaymentDrivers\Mollie\CreditCard; use App\Utils\Traits\MakesHash; +use Mollie\Api\MollieApiClient; class MolliePaymentDriver extends BaseDriver { @@ -40,7 +42,7 @@ class MolliePaymentDriver extends BaseDriver public $can_authorise_credit_card = true; /** - * @var mixed + * @var MollieApiClient */ public $gateway; @@ -56,14 +58,19 @@ class MolliePaymentDriver extends BaseDriver GatewayType::CREDIT_CARD => CreditCard::class, ]; - const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; + const SYSTEM_LOG_TYPE = SystemLog::TYPE_MOLLIE; - public function init() + public function init(): self { + $this->gateway = new MollieApiClient(); + + $this->gateway->setApiKey( + $this->company_gateway->getConfigField('apiKey'), + ); + return $this; } - /* Returns an array of gateway types for the payment gateway */ public function gatewayTypes(): array { $types = []; @@ -76,7 +83,9 @@ class MolliePaymentDriver extends BaseDriver public function setPaymentMethod($payment_method_id) { $class = self::$methods[$payment_method_id]; + $this->payment_method = new $class($this); + return $this; } diff --git a/public/css/app.css b/public/css/app.css index fbe3808e3..f14af4b99 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-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%}.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-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: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%}.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 b2050020b..cff448c2b 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=f4c07fdabcbe50c9f4be", + "/css/app.css": "/css/app.css?id=3ab7ce803b68a6e66464", "/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", diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index a2642657d..75e6505f6 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4286,6 +4286,7 @@ $LANG = array( 'user_created_user' => ':user created :created_user at :time', 'company_deleted' => 'Company deleted', 'company_deleted_body' => 'Company [ :company ] was deleted by :user', + 'expiry_date' => 'Expiry date', ); return $LANG; diff --git a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php new file mode 100644 index 000000000..bd57d1a5d --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php @@ -0,0 +1,183 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' => +ctrans('texts.credit_card')]) + +@section('gateway_head') + + + +@endsection + +@section('gateway_content') +
+ @csrf + + + + + + + + +
+ + + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')]) + {{ ctrans('texts.credit_card') }} + @endcomponent + + @include('portal.ninja2020.gateways.includes.payment_details') + + @component('portal.ninja2020.components.general.card-element-single') +
+ + + + +
+ + + +
+
+ @endcomponent + + @include('portal.ninja2020.gateways.includes.pay_now') +@endsection + +@section('gateway_footer') + + + +@endsection From 4818ea81bcd5cf1e5d43a80862dc05a9ee3ab9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 23 Jul 2021 16:17:32 +0200 Subject: [PATCH 05/36] Credit card: Pay with new credit card --- app/PaymentDrivers/Mollie/CreditCard.php | 72 ++++++++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 3f227c76e..63c425e22 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -4,12 +4,14 @@ namespace App\PaymentDrivers\Mollie; use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\Jobs\Util\SystemLogger; +use App\Models\GatewayType; +use App\Models\Payment; +use App\Models\PaymentType; +use App\Models\SystemLog; use App\PaymentDrivers\MolliePaymentDriver; use Illuminate\Contracts\View\Factory; use Illuminate\View\View; -use Illuminate\Contracts\Container\BindingResolutionException; - -use function Symfony\Component\String\b; class CreditCard { @@ -38,26 +40,72 @@ class CreditCard return render('gateways.mollie.credit_card.pay', $data); } + /** + * Create a payment object. + * + * @param PaymentResponseRequest $request + * @return mixed + */ public function paymentResponse(PaymentResponseRequest $request) { + $amount = number_format((float) $this->mollie->payment_hash->data->amount_with_fee, 2, '.', ''); + try { $payment = $this->mollie->gateway->payments->create([ - "amount" => [ - "currency" => "USD", - "value" => "10.00" + 'amount' => [ + 'currency' => $this->mollie->client->currency()->code, + 'value' => $amount, ], - "description" => "Order #12345", - "redirectUrl" => "https://webshop.example.org/order/12345/", - "webhookUrl" => "https://webshop.example.org/mollie-webhook/", + 'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash), + 'redirectUrl' => 'https://webshop.example.org/order/12345/', + 'webhookUrl' => 'https://webshop.example.org/mollie-webhook/', + 'cardToken' => $request->token, ]); - if ($payment->status === 'open') { - return redirect($payment->getCheckoutUrl()); + if ($payment->status === 'paid') { + $this->mollie->logSuccessfulGatewayResponse( + ['response' => $payment, 'data' => $this->mollie->payment_hash], + SystemLog::TYPE_MOLLIE + ); + + $this->processSuccessfulPayment($payment); } + + if ($payment->status === 'open') { + // Handle redirect payment + } + + dd($payment); + } catch (\Exception $e) { throw new PaymentFailed($e->getMessage(), $e->getCode()); } + } - dd($payment); + protected function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment) + { + // Check if storing credit card is enabled + + $payment_hash = $this->mollie->payment_hash; + + $data = [ + 'gateway_type_id' => GatewayType::CREDIT_CARD, + 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, + 'payment_type' => PaymentType::CREDIT_CARD_OTHER, + 'transaction_reference' => $payment->id, + ]; + + $payment_record = $this->mollie->createPayment($data, Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); } } From 7dd7a6e4b15a2d6280e3b14a3ed00b5a69d547fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 26 Jul 2021 17:03:15 +0200 Subject: [PATCH 06/36] Scaffold Mollie3dsController --- .../Gateways/Mollie3dsController.php | 24 +++++++++++ .../Gateways/Mollie/Mollie3dsRequest.php | 40 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 app/Http/Controllers/Gateways/Mollie3dsController.php create mode 100644 app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php diff --git a/app/Http/Controllers/Gateways/Mollie3dsController.php b/app/Http/Controllers/Gateways/Mollie3dsController.php new file mode 100644 index 000000000..7f6a0d912 --- /dev/null +++ b/app/Http/Controllers/Gateways/Mollie3dsController.php @@ -0,0 +1,24 @@ +all()); + } +} diff --git a/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php b/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php new file mode 100644 index 000000000..85415e6e8 --- /dev/null +++ b/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php @@ -0,0 +1,40 @@ + Date: Mon, 26 Jul 2021 17:03:28 +0200 Subject: [PATCH 07/36] Add withData() to PaymentHash --- app/Models/PaymentHash.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php index e82845415..0839120f5 100644 --- a/app/Models/PaymentHash.php +++ b/app/Models/PaymentHash.php @@ -20,7 +20,6 @@ class PaymentHash extends Model protected $casts = [ 'data' => 'object', ]; - public function invoices() { @@ -41,4 +40,12 @@ class PaymentHash extends Model { return $this->belongsTo(Invoice::class, 'fee_invoice_id', 'id'); } + + public function withData(string $property, $value): PaymentHash + { + $this->data = array_merge((array) $this->data, [$property => $value]); + $this->save(); + + return $this; + } } From 548405c4d8abd0c7608927c0904ac68e1dcb43bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Mon, 26 Jul 2021 17:03:40 +0200 Subject: [PATCH 08/36] Refactor payment with credit card --- app/PaymentDrivers/Mollie/CreditCard.php | 38 +++++++++++++++++++----- routes/web.php | 1 + 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 63c425e22..c811f24fa 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -4,12 +4,14 @@ namespace App\PaymentDrivers\Mollie; use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\Jobs\Mail\PaymentFailureMailer; use App\Jobs\Util\SystemLogger; use App\Models\GatewayType; use App\Models\Payment; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\MolliePaymentDriver; +use App\Utils\Number; use Illuminate\Contracts\View\Factory; use Illuminate\View\View; @@ -48,17 +50,21 @@ class CreditCard */ public function paymentResponse(PaymentResponseRequest $request) { - $amount = number_format((float) $this->mollie->payment_hash->data->amount_with_fee, 2, '.', ''); + $this->mollie->payment_hash->withData('gateway_type_id', GatewayType::CREDIT_CARD); try { $payment = $this->mollie->gateway->payments->create([ 'amount' => [ 'currency' => $this->mollie->client->currency()->code, - 'value' => $amount, + 'value' => Number::formatValue($this->mollie->payment_hash->data->amount_with_fee, $this->mollie->client->currency()), ], 'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash), 'redirectUrl' => 'https://webshop.example.org/order/12345/', - 'webhookUrl' => 'https://webshop.example.org/mollie-webhook/', + 'webhookUrl' => route('mollie.3ds_redirect', [ + 'company_key' => $this->mollie->client->company->company_key, + 'company_gateway_id' => $this->mollie->company_gateway->hashed_id, + 'hash' => $this->mollie->payment_hash->hash, + ]), 'cardToken' => $request->token, ]); @@ -72,12 +78,11 @@ class CreditCard } if ($payment->status === 'open') { - // Handle redirect payment + return redirect($payment->getCheckoutUrl()); } - - dd($payment); - } catch (\Exception $e) { + $this->processUnsuccessfulPayment($e); + throw new PaymentFailed($e->getMessage(), $e->getCode()); } } @@ -108,4 +113,23 @@ class CreditCard return redirect()->route('client.payments.show', ['payment' => $this->mollie->encodePrimaryKey($payment_record->id)]); } + + public function processUnsuccessfulPayment(\Exception $e) + { + PaymentFailureMailer::dispatch( + $this->mollie->client, + $e->getMessage(), + $this->mollie->client->company, + $this->mollie->payment_hash->data->amount_with_fee + ); + + SystemLogger::dispatch( + $e->getMessage(), + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_MOLLIE, + $this->mollie->client, + $this->mollie->client->company, + ); + } } diff --git a/routes/web.php b/routes/web.php index 3468a99eb..8c3a8e85d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -42,3 +42,4 @@ Route::get('stripe/signup/{token}', 'StripeConnectController@initialize')->name( Route::get('stripe/completed', 'StripeConnectController@completed')->name('stripe_connect.return'); Route::get('checkout/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\Checkout3dsController@index')->name('checkout.3ds_redirect'); +Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', 'Gateways\Mollie3dsController@index')->name('mollie.3ds_redirect'); From 3d12fd80e8370155250ce247add5b9068947df03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 27 Jul 2021 11:46:12 +0200 Subject: [PATCH 09/36] wip --- app/PaymentDrivers/Mollie/CreditCard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index c811f24fa..c7495ca6b 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -74,7 +74,7 @@ class CreditCard SystemLog::TYPE_MOLLIE ); - $this->processSuccessfulPayment($payment); + return $this->processSuccessfulPayment($payment); } if ($payment->status === 'open') { From 1e2e55c9e481932c76ece5479cacf8967de94604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 29 Jul 2021 15:13:38 +0200 Subject: [PATCH 10/36] Credit card 3ds processing --- .../Gateways/Mollie3dsController.php | 5 ++- .../Gateways/Mollie/Mollie3dsRequest.php | 35 ++++++++++++++++++- app/Models/PaymentHash.php | 2 +- app/PaymentDrivers/Mollie/CreditCard.php | 21 +++++++---- app/PaymentDrivers/MolliePaymentDriver.php | 16 +++++++++ 5 files changed, 69 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Gateways/Mollie3dsController.php b/app/Http/Controllers/Gateways/Mollie3dsController.php index 7f6a0d912..0e3d8f8c1 100644 --- a/app/Http/Controllers/Gateways/Mollie3dsController.php +++ b/app/Http/Controllers/Gateways/Mollie3dsController.php @@ -14,11 +14,14 @@ namespace App\Http\Controllers\Gateways; use App\Http\Controllers\Controller; use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest; +use App\Models\PaymentHash; class Mollie3dsController extends Controller { public function index(Mollie3dsRequest $request) { - dd($request->all()); + return $request->getCompanyGateway() + ->driver($request->getClient()) + ->process3dsConfirmation($request); } } diff --git a/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php b/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php index 85415e6e8..8c06791cd 100644 --- a/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php +++ b/app/Http/Requests/Gateways/Mollie/Mollie3dsRequest.php @@ -12,10 +12,18 @@ namespace App\Http\Requests\Gateways\Mollie; +use App\Models\Client; +use App\Models\ClientGatewayToken; +use App\Models\Company; +use App\Models\CompanyGateway; +use App\Models\PaymentHash; +use App\Utils\Traits\MakesHash; use Illuminate\Foundation\Http\FormRequest; class Mollie3dsRequest extends FormRequest { + use MakesHash; + /** * Determine if the user is authorized to make this request. * @@ -23,7 +31,7 @@ class Mollie3dsRequest extends FormRequest */ public function authorize() { - return false; + return true; } /** @@ -37,4 +45,29 @@ class Mollie3dsRequest extends FormRequest // ]; } + + public function getCompany(): ?Company + { + return Company::where('company_key', $this->company_key)->first(); + } + + public function getCompanyGateway(): ?CompanyGateway + { + return CompanyGateway::find($this->decodePrimaryKey($this->company_gateway_id)); + } + + public function getPaymentHash(): ?PaymentHash + { + return PaymentHash::where('hash', $this->hash)->first(); + } + + public function getClient(): ?Client + { + return Client::find($this->getPaymentHash()->data->client_id); + } + + public function getPaymentId(): ?string + { + return $this->getPaymentHash()->data->payment_id; + } } diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php index 0839120f5..c2ea7232b 100644 --- a/app/Models/PaymentHash.php +++ b/app/Models/PaymentHash.php @@ -41,7 +41,7 @@ class PaymentHash extends Model return $this->belongsTo(Invoice::class, 'fee_invoice_id', 'id'); } - public function withData(string $property, $value): PaymentHash + public function withData(string $property, $value): self { $this->data = array_merge((array) $this->data, [$property => $value]); $this->save(); diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index c7495ca6b..38886e65d 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -50,21 +50,26 @@ class CreditCard */ public function paymentResponse(PaymentResponseRequest $request) { - $this->mollie->payment_hash->withData('gateway_type_id', GatewayType::CREDIT_CARD); + // TODO: Unit tests. + $amount = number_format((float) $this->mollie->payment_hash->data->amount_with_fee, 2, '.', ''); + + $this->mollie->payment_hash + ->withData('gateway_type_id', GatewayType::CREDIT_CARD) + ->withData('client_id', $this->mollie->client->id); try { $payment = $this->mollie->gateway->payments->create([ 'amount' => [ 'currency' => $this->mollie->client->currency()->code, - 'value' => Number::formatValue($this->mollie->payment_hash->data->amount_with_fee, $this->mollie->client->currency()), + 'value' => $amount, ], 'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash), - 'redirectUrl' => 'https://webshop.example.org/order/12345/', - 'webhookUrl' => route('mollie.3ds_redirect', [ + 'redirectUrl' => route('mollie.3ds_redirect', [ 'company_key' => $this->mollie->client->company->company_key, 'company_gateway_id' => $this->mollie->company_gateway->hashed_id, 'hash' => $this->mollie->payment_hash->hash, ]), + 'webhookUrl' => 'https://invoiceninja.com', 'cardToken' => $request->token, ]); @@ -78,6 +83,8 @@ class CreditCard } if ($payment->status === 'open') { + $this->mollie->payment_hash->withData('payment_id', $payment->id); + return redirect($payment->getCheckoutUrl()); } } catch (\Exception $e) { @@ -87,10 +94,8 @@ class CreditCard } } - protected function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment) + public function processSuccessfulPayment(\Mollie\Api\Resources\Payment $payment) { - // Check if storing credit card is enabled - $payment_hash = $this->mollie->payment_hash; $data = [ @@ -131,5 +136,7 @@ class CreditCard $this->mollie->client, $this->mollie->client->company, ); + + throw new PaymentFailed($e->getMessage(), $e->getCode()); } } diff --git a/app/PaymentDrivers/MolliePaymentDriver.php b/app/PaymentDrivers/MolliePaymentDriver.php index 6cdcb8b97..88d7883ac 100644 --- a/app/PaymentDrivers/MolliePaymentDriver.php +++ b/app/PaymentDrivers/MolliePaymentDriver.php @@ -12,6 +12,7 @@ namespace App\PaymentDrivers; +use App\Http\Requests\Gateways\Mollie\Mollie3dsRequest; use App\Http\Requests\Payments\PaymentWebhookRequest; use App\Models\ClientGatewayToken; use App\Models\GatewayType; @@ -122,4 +123,19 @@ class MolliePaymentDriver extends BaseDriver public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null) { } + + public function process3dsConfirmation(Mollie3dsRequest $request) + { + $this->init(); + + $this->setPaymentHash($request->getPaymentHash()); + + try { + $payment = $this->gateway->payments->get($request->getPaymentId()); + + return (new CreditCard($this))->processSuccessfulPayment($payment); + } catch(\Mollie\Api\Exceptions\ApiException $e) { + return (new CreditCard($this))->processUnsuccessfulPayment($e); + } + } } From d50c629476f7911c1ea1d26dc062bf95df7db0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 29 Jul 2021 15:26:01 +0200 Subject: [PATCH 11/36] Show a message about preauthorizing credit card --- .../gateways/mollie/credit_card/pay.blade.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php index bd57d1a5d..d6a4d7db4 100644 --- a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php @@ -28,6 +28,31 @@ ctrans('texts.credit_card')]) @include('portal.ninja2020.gateways.includes.payment_details') + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if(count($tokens) > 0) + @foreach($tokens as $token) + + @endforeach + @endif + + + @endcomponent + @component('portal.ninja2020.components.general.card-element-single')
@endcomponent + @component('portal.ninja2020.components.general.card-element-single') + If you want to save the card for future purchases, please click on the + Payment methods page and authorize the credit card manually. + + + After that, come back to this page and select your payment method. + @endcomponent + @include('portal.ninja2020.gateways.includes.pay_now') @endsection From 202cc9d670e7b7e0491bb5a9fffb88dab6a393f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 29 Jul 2021 16:15:27 +0200 Subject: [PATCH 12/36] wip --- app/PaymentDrivers/Mollie/CreditCard.php | 38 ++++++++++++++++++- .../mollie/credit_card/authorize.blade.php | 37 ++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 38886e65d..959ecdc9c 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -11,7 +11,7 @@ use App\Models\Payment; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\MolliePaymentDriver; -use App\Utils\Number; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\View\Factory; use Illuminate\View\View; @@ -50,6 +50,8 @@ class CreditCard */ public function paymentResponse(PaymentResponseRequest $request) { + dd($this->mollie->gateway->mandates->listForId('cst_6S77wEkuQT')); + // TODO: Unit tests. $amount = number_format((float) $this->mollie->payment_hash->data->amount_with_fee, 2, '.', ''); @@ -58,7 +60,16 @@ class CreditCard ->withData('client_id', $this->mollie->client->id); try { + $customer = $this->mollie->gateway->customers->create([ + 'name' => $this->mollie->client->name, + 'metadata' => [ + 'id' => $this->mollie->client->hashed_id, + ], + ]); + $payment = $this->mollie->gateway->payments->create([ + 'customerId' => $customer->id, + 'sequenceType' => 'first', 'amount' => [ 'currency' => $this->mollie->client->currency()->code, 'value' => $amount, @@ -79,7 +90,7 @@ class CreditCard SystemLog::TYPE_MOLLIE ); - return $this->processSuccessfulPayment($payment); + return $this->processSuccessfulPayment($payment); } if ($payment->status === 'open') { @@ -139,4 +150,27 @@ class CreditCard throw new PaymentFailed($e->getMessage(), $e->getCode()); } + + /** + * Show authorization page. + * + * @param array $data + * @return Factory|View + */ + public function authorizeView(array $data) + { + return render('gateways.mollie.credit_card.authorize', $data); + } + + public function authorizeResponse($request) + { + $customer = $this->mollie->gateway->customers->create([ + 'name' => $this->mollie->client->name, + 'metadata' => [ + 'id' => $this->mollie->client->hashed_id, + ], + ]); + + // Save $customer->id to database.. + } } diff --git a/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php b/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php new file mode 100644 index 000000000..5e3ae5d75 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php @@ -0,0 +1,37 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' => +ctrans('texts.credit_card')]) + +@section('gateway_head') + +@endsection + +@section('gateway_content') +
+ @csrf + + {{-- --}} + +
+ + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')]) + {{ ctrans('texts.credit_card') }} + @endcomponent + + @component('portal.ninja2020.components.general.card-element-single') + Click the "Add Payment Method" button to complete test payment. + @endcomponent + + @component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card']) + {{ ctrans('texts.add_payment_method') }} + @endcomponent +@endsection + +@section('gateway_footer') + +@endsection From e3062785471c1355e3f416c29107e9c11fa080f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 30 Jul 2021 14:09:29 +0200 Subject: [PATCH 13/36] Update authentication page --- app/PaymentDrivers/Mollie/CreditCard.php | 21 ++++++------- .../mollie/credit_card/authorize.blade.php | 31 +------------------ 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 959ecdc9c..d67b6f249 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -11,8 +11,8 @@ use App\Models\Payment; use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\MolliePaymentDriver; -use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\View\Factory; +use Illuminate\Http\RedirectResponse; use Illuminate\View\View; class CreditCard @@ -50,8 +50,6 @@ class CreditCard */ public function paymentResponse(PaymentResponseRequest $request) { - dd($this->mollie->gateway->mandates->listForId('cst_6S77wEkuQT')); - // TODO: Unit tests. $amount = number_format((float) $this->mollie->payment_hash->data->amount_with_fee, 2, '.', ''); @@ -162,15 +160,14 @@ class CreditCard return render('gateways.mollie.credit_card.authorize', $data); } - public function authorizeResponse($request) + /** + * Handle authorization response. + * + * @param mixed $request + * @return RedirectResponse + */ + public function authorizeResponse($request): RedirectResponse { - $customer = $this->mollie->gateway->customers->create([ - 'name' => $this->mollie->client->name, - 'metadata' => [ - 'id' => $this->mollie->client->hashed_id, - ], - ]); - - // Save $customer->id to database.. + return redirect()->route('client.payment_methods.index'); } } diff --git a/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php b/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php index 5e3ae5d75..395a8d68b 100644 --- a/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/mollie/credit_card/authorize.blade.php @@ -1,37 +1,8 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' => ctrans('texts.credit_card')]) -@section('gateway_head') - -@endsection - @section('gateway_content') -
- @csrf - - {{-- --}} - -
- - @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')]) - {{ ctrans('texts.credit_card') }} - @endcomponent - @component('portal.ninja2020.components.general.card-element-single') - Click the "Add Payment Method" button to complete test payment. - @endcomponent - - @component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card']) - {{ ctrans('texts.add_payment_method') }} + {{ __('texts.payment_method_cannot_be_authorized_first') }} @endcomponent @endsection - -@section('gateway_footer') - -@endsection From 8af3cfe737c9028147bb8a42dd66eeeb79266a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 30 Jul 2021 14:36:14 +0200 Subject: [PATCH 14/36] Pay with credit card and save for future use --- .../Payments/PaymentResponseRequest.php | 5 ++ app/PaymentDrivers/Mollie/CreditCard.php | 77 +++++++++++++------ .../gateways/mollie/credit_card/pay.blade.php | 36 ++++----- 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php b/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php index d3980f4b8..3a917f339 100644 --- a/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php +++ b/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php @@ -37,6 +37,11 @@ class PaymentResponseRequest extends FormRequest return PaymentHash::whereRaw('BINARY `hash`= ?', [$input['payment_hash']])->first(); } + public function shouldStoreToken(): bool + { + return (bool) $this->store_card; + } + public function prepareForValidation() { if ($this->has('store_card')) { diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index d67b6f249..22806fb44 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -31,9 +31,9 @@ class CreditCard /** * Show the page for credit card payments. - * - * @param array $data - * @return Factory|View + * + * @param array $data + * @return Factory|View */ public function paymentView(array $data) { @@ -44,9 +44,9 @@ class CreditCard /** * Create a payment object. - * - * @param PaymentResponseRequest $request - * @return mixed + * + * @param PaymentResponseRequest $request + * @return mixed */ public function paymentResponse(PaymentResponseRequest $request) { @@ -58,16 +58,7 @@ class CreditCard ->withData('client_id', $this->mollie->client->id); try { - $customer = $this->mollie->gateway->customers->create([ - 'name' => $this->mollie->client->name, - 'metadata' => [ - 'id' => $this->mollie->client->hashed_id, - ], - ]); - - $payment = $this->mollie->gateway->payments->create([ - 'customerId' => $customer->id, - 'sequenceType' => 'first', + $data = [ 'amount' => [ 'currency' => $this->mollie->client->currency()->code, 'value' => $amount, @@ -80,7 +71,26 @@ class CreditCard ]), 'webhookUrl' => 'https://invoiceninja.com', 'cardToken' => $request->token, - ]); + ]; + + if ($request->shouldStoreToken()) { + $customer = $this->mollie->gateway->customers->create([ + 'name' => $this->mollie->client->name, + 'email' => $this->mollie->client->present()->email(), + 'metadata' => [ + 'id' => $this->mollie->client->hashed_id, + ], + ]); + + $data['customerId'] = $customer->id; + $data['sequenceType'] = 'first'; + + $this->mollie->payment_hash + ->withData('mollieCustomerId', $customer->id) + ->withData('shouldStoreToken', true); + } + + $payment = $this->mollie->gateway->payments->create($data); if ($payment->status === 'paid') { $this->mollie->logSuccessfulGatewayResponse( @@ -107,6 +117,27 @@ class CreditCard { $payment_hash = $this->mollie->payment_hash; + if ($payment_hash->data->shouldStoreToken) { + try { + $mandates = \iterator_to_array($this->mollie->gateway->mandates->listForId($payment_hash->data->mollieCustomerId)); + } catch (\Mollie\Api\Exceptions\ApiException $e) { + return $this->processUnsuccessfulPayment($e); + } + + $payment_meta = new \stdClass; + $payment_meta->exp_month = (string) $mandates[0]->details->cardExpiryDate; + $payment_meta->exp_year = (string) ''; + $payment_meta->brand = (string) $mandates[0]->details->cardLabel; + $payment_meta->last4 = (string) $mandates[0]->details->cardNumber; + $payment_meta->type = GatewayType::CREDIT_CARD; + + $this->mollie->storeGatewayToken([ + 'token' => $mandates[0]->id, + 'payment_method_id' => GatewayType::CREDIT_CARD, + 'payment_meta' => $payment_meta, + ]); + } + $data = [ 'gateway_type_id' => GatewayType::CREDIT_CARD, 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, @@ -151,9 +182,9 @@ class CreditCard /** * Show authorization page. - * - * @param array $data - * @return Factory|View + * + * @param array $data + * @return Factory|View */ public function authorizeView(array $data) { @@ -162,9 +193,9 @@ class CreditCard /** * Handle authorization response. - * - * @param mixed $request - * @return RedirectResponse + * + * @param mixed $request + * @return RedirectResponse */ public function authorizeResponse($request): RedirectResponse { diff --git a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php index d6a4d7db4..a99e55c6d 100644 --- a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php @@ -29,26 +29,19 @@ ctrans('texts.credit_card')]) @include('portal.ninja2020.gateways.includes.payment_details') @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) - @if(count($tokens) > 0) - @foreach($tokens as $token) + @if (count($tokens) > 0) + @foreach ($tokens as $token) @endforeach @endif @endcomponent @@ -83,13 +76,7 @@ ctrans('texts.credit_card')]) @endcomponent - @component('portal.ninja2020.components.general.card-element-single') - If you want to save the card for future purchases, please click on the - Payment methods page and authorize the credit card manually. - - - After that, come back to this page and select your payment method. - @endcomponent + @include('portal.ninja2020.gateways.includes.save_card') @include('portal.ninja2020.gateways.includes.pay_now') @endsection @@ -193,6 +180,15 @@ ctrans('texts.credit_card')]) return; } + let tokenBillingCheckbox = document.querySelector( + 'input[name="token-billing-checkbox"]:checked' + ); + + if (tokenBillingCheckbox) { + document.querySelector('input[name="store_card"]').value = + tokenBillingCheckbox.value; + } + document.querySelector('input[name=token]').value = token; document.getElementById('server-response').submit(); }); From 541a1a825fd4eb3463b36fe31ce7ca2202740249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Fri, 30 Jul 2021 16:04:26 +0200 Subject: [PATCH 15/36] Pay with saved credit card --- app/PaymentDrivers/Mollie/CreditCard.php | 42 +++++++++++++++++-- .../gateways/mollie/credit_card/pay.blade.php | 28 ++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/app/PaymentDrivers/Mollie/CreditCard.php b/app/PaymentDrivers/Mollie/CreditCard.php index 22806fb44..c223d1392 100644 --- a/app/PaymentDrivers/Mollie/CreditCard.php +++ b/app/PaymentDrivers/Mollie/CreditCard.php @@ -6,6 +6,7 @@ use App\Exceptions\PaymentFailed; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; 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; @@ -57,6 +58,41 @@ class CreditCard ->withData('gateway_type_id', GatewayType::CREDIT_CARD) ->withData('client_id', $this->mollie->client->id); + if (!empty($request->token)) { + try { + $cgt = ClientGatewayToken::where('token', $request->token)->firstOrFail(); + + $payment = $this->mollie->gateway->payments->create([ + 'amount' => [ + 'currency' => $this->mollie->client->currency()->code, + 'value' => $amount, + ], + 'mandateId' => $request->token, + 'customerId' => $cgt->gateway_customer_reference, + 'sequenceType' => 'recurring', + 'description' => \sprintf('Hash: %s', $this->mollie->payment_hash->hash), + 'webhookUrl' => 'https://invoiceninja.com', + ]); + + if ($payment->status === 'paid') { + $this->mollie->logSuccessfulGatewayResponse( + ['response' => $payment, 'data' => $this->mollie->payment_hash], + SystemLog::TYPE_MOLLIE + ); + + return $this->processSuccessfulPayment($payment); + } + + if ($payment->status === 'open') { + $this->mollie->payment_hash->withData('payment_id', $payment->id); + + return redirect($payment->getCheckoutUrl()); + } + } catch (\Exception $e) { + return $this->processUnsuccessfulPayment($e); + } + } + try { $data = [ 'amount' => [ @@ -70,7 +106,7 @@ class CreditCard 'hash' => $this->mollie->payment_hash->hash, ]), 'webhookUrl' => 'https://invoiceninja.com', - 'cardToken' => $request->token, + 'cardToken' => $request->gateway_response, ]; if ($request->shouldStoreToken()) { @@ -117,7 +153,7 @@ class CreditCard { $payment_hash = $this->mollie->payment_hash; - if ($payment_hash->data->shouldStoreToken) { + if (property_exists($payment_hash->data, 'shouldStoreToken') && $payment_hash->data->shouldStoreToken) { try { $mandates = \iterator_to_array($this->mollie->gateway->mandates->listForId($payment_hash->data->mollieCustomerId)); } catch (\Mollie\Api\Exceptions\ApiException $e) { @@ -135,7 +171,7 @@ class CreditCard 'token' => $mandates[0]->id, 'payment_method_id' => GatewayType::CREDIT_CARD, 'payment_meta' => $payment_meta, - ]); + ], ['gateway_customer_reference' => $payment_hash->data->mollieCustomerId]); } $data = [ diff --git a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php index a99e55c6d..2e89246c3 100644 --- a/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/mollie/credit_card/pay.blade.php @@ -47,7 +47,7 @@ ctrans('texts.credit_card')]) @endcomponent @component('portal.ninja2020.components.general.card-element-single') -
+