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 b53626595059..4a8b1236e4fd 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 878174e2f07a..c0d7d1721274 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 151dd8239ab6..8595adacbbc3 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 15b58365b9a7..89eea29d9e7b 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 000000000000..03879c42fd1e --- /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 7614c01ecccc..a851ed767cfb 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 000000000000..3f227c76e634 --- /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 03879c42fd1e..6cdcb8b97676 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 fbe3808e3bb2..f14af4b99afb 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 b2050020b4b2..cff448c2b916 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 a2642657dead..75e6505f669c 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 000000000000..bd57d1a5d8bc --- /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 3f227c76e634..63c425e2228a 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 000000000000..7f6a0d9121dd --- /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 000000000000..85415e6e8dc9 --- /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 e82845415eab..0839120f5a9a 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 63c425e2228a..c811f24fa0a7 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 3468a99ebbd9..8c3a8e85d0d3 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 c811f24fa0a7..c7495ca6b52a 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 7f6a0d9121dd..0e3d8f8c113f 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 85415e6e8dc9..8c06791cd957 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 0839120f5a9a..c2ea7232b651 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 c7495ca6b52a..38886e65d722 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 6cdcb8b97676..88d7883ac359 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 bd57d1a5d8bc..d6a4d7db4f56 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 38886e65d722..959ecdc9c1c2 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 000000000000..5e3ae5d75ef3 --- /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 959ecdc9c1c2..d67b6f249bf9 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 5e3ae5d75ef3..395a8d68bd11 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 d3980f4b89af..3a917f339e2f 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 d67b6f249bf9..22806fb44e71 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 d6a4d7db4f56..a99e55c6dfca 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 22806fb44e71..c223d1392277 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 a99e55c6dfca..2e89246c3532 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') -
+