Beganovich 1513 powerboard (#103)

* make container nicer

* assets rebuild

* authorize powerband card (3ds)

* add reference to build file

* update authorize (3ds) view

* assets rebuild

* unify 3ds and non-3ds auth/pay

* assets rebuild

* authorize

* pay

* update vite refs

* pay

* hide authorize button

* intercepting form on authorize

* assets build

* wip

* init powerboard in data ref

* fixes for blank placeholders

* reset the form on failed 3ds

* handling unsuccessful errors

* send email on payment failed

* fixes for 3ds fail on auth

* assets rebuild

* make card_name required

* make card_name required (on auth)

* fixes for blocked pay-now button

* fixes for reload

* fixes for reload

* build

* Fixes for broken powerboard

* make client name required

* skip fields checking if no required fields

* on request, return json response

* check for plain not_authenticated response

* flash message when no action is present

* fixes for exec order on token

* assets build

* check for plain not_authenticated response (pay)

* assets build

* adjustments for minimum payments

* Add text decoration to terms button

* Improvements for subscriptions and new payment flow

---------

Co-authored-by: Benjamin Beganović <k1pstabug@gmail.com>
This commit is contained in:
David Bomba 2024-09-17 10:16:10 +10:00 committed by GitHub
parent abe8bbcd5d
commit 114b58cdc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 69 additions and 28 deletions

View File

@ -17,7 +17,7 @@ use App\Models\Subscription;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
class Methods extends Component class Methods extends Component
{ {//@todo this breaks down when the cart is in front of the login - we have no context on the user - nor their country/currency
public Subscription $subscription; public Subscription $subscription;
public array $context; public array $context;
@ -28,10 +28,7 @@ class Methods extends Component
{ {
$total = collect($this->context['products'])->sum('total_raw'); $total = collect($this->context['products'])->sum('total_raw');
$methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods( $methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods($total); //@todo this breaks down when the cart is in front of the login - we have no context on the user - nor their country/currency()
$total,
);
$this->methods = $methods; $this->methods = $methods;
} }

View File

@ -199,6 +199,7 @@ class BillingPortalPurchasev2 extends Component
$this->data = []; $this->data = [];
$this->price = $this->subscription->price; // ? $this->price = $this->subscription->price; // ?
$this->float_amount_total = $this->price;
$this->recurring_products = $this->subscription->service()->recurring_products(); $this->recurring_products = $this->subscription->service()->recurring_products();
$this->products = $this->subscription->service()->products(); $this->products = $this->subscription->service()->products();
@ -244,7 +245,8 @@ class BillingPortalPurchasev2 extends Component
Auth::guard('contact')->loginUsingId($contact->id, true); Auth::guard('contact')->loginUsingId($contact->id, true);
$this->contact = $contact; $this->contact = $contact;
} else { } else {
$this->createClientContact(); // $this->createClientContact();
$this->createBlankClient();
} }
$this->getPaymentMethods(); $this->getPaymentMethods();
@ -767,6 +769,8 @@ class BillingPortalPurchasev2 extends Component
if ($currency) { if ($currency) {
$data['settings']->currency_id = $currency->id; $data['settings']->currency_id = $currency->id;
} }
}else {
$data['settings']->currency_id = $this->subscription->company->getSetting('currency_id');
} }
if (array_key_exists('locale', $this->request_data)) { if (array_key_exists('locale', $this->request_data)) {
@ -785,8 +789,12 @@ class BillingPortalPurchasev2 extends Component
} }
$client = $client_repo->save($data, ClientFactory::create($company->id, $user->id)); $client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
$contact = $client->fresh()->contacts->first();
$this->contact = $contact;
return $client->fresh()->contacts->first(); Auth::guard('contact')->loginUsingId($contact->id, true);
return $contact;
} }

View File

@ -52,11 +52,11 @@ class UnderOverPayment extends Component
$input_amount = collect($payableInvoices)->sum('amount'); $input_amount = collect($payableInvoices)->sum('amount');
if($settings->client_portal_allow_under_payment && $settings->client_portal_under_payment_minimum != 0) if($settings->client_portal_allow_under_payment)
{ {
if($input_amount <= $settings->client_portal_under_payment_minimum){ if($input_amount <= $settings->client_portal_under_payment_minimum || $input_amount <= 0){
// return error message under payment too low. // return error message under payment too low.
$this->errors = ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]); $this->errors = ctrans('texts.minimum_required_payment', ['amount' => max($settings->client_portal_under_payment_minimum, 1)]);
$this->dispatch('errorMessageUpdate', errors: $this->errors); $this->dispatch('errorMessageUpdate', errors: $this->errors);
} }
} }

View File

@ -25,6 +25,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $user_id * @property int $user_id
* @property int|null $assigned_user_id * @property int|null $assigned_user_id
* @property int $company_id * @property int $company_id
* @property int $remaining_cycles
* @property string|null $product_ids * @property string|null $product_ids
* @property int|null $frequency_id * @property int|null $frequency_id
* @property string|null $auto_bill * @property string|null $auto_bill
@ -117,6 +118,7 @@ class Subscription extends BaseModel
'optional_recurring_product_ids', 'optional_recurring_product_ids',
'use_inventory_management', 'use_inventory_management',
'steps', 'steps',
'remaining_cycles',
]; ];
protected $casts = [ protected $casts = [

View File

@ -76,7 +76,11 @@ class CreditCard implements LivewireMethodInterface
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []); $r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if ($r->failed()) { if ($r->failed()) {
return $this->processUnsuccessfulPayment($r);
$error_payload = $this->getErrorFromResponse($r);
return response()->json(['message' => $error_payload[0]], 400);
// return $this->processUnsuccessfulPayment($r);
} }
$charge = $r->json(); $charge = $r->json();
@ -262,11 +266,8 @@ class CreditCard implements LivewireMethodInterface
nlog($r->body()); nlog($r->body());
if($r->failed()){ if($r->failed()){
$error_payload = $this->getErrorFromResponse($r); $error_payload = $this->getErrorFromResponse($r);
throw new PaymentFailed($error_payload[0], $error_payload[1]); throw new PaymentFailed($error_payload[0], $error_payload[1]);
} }
$charge = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Charge::class, $r->object()->resource->data) ?? $r->throw(); $charge = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Charge::class, $r->object()->resource->data) ?? $r->throw();
@ -305,7 +306,8 @@ class CreditCard implements LivewireMethodInterface
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []); $r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if ($r->failed()) { if ($r->failed()) {
return $this->processUnsuccessfulPayment($r); $error_payload = $this->getErrorFromResponse($r);
return response()->json(['message' => $error_payload[0]], 400);
} }
$charge = $r->json(); $charge = $r->json();
@ -447,6 +449,7 @@ class CreditCard implements LivewireMethodInterface
$error_message = "Unknown error"; $error_message = "Unknown error";
match($error_object->error->code) { match($error_object->error->code) {
"UnfulfilledCondition" => $error_message = $error_object->error->details->messages[0] ?? $error_object->error->message ?? "Unknown error",
"GatewayError" => $error_message = $error_object->error->message, "GatewayError" => $error_message = $error_object->error->message,
"UnfulfilledCondition" => $error_message = $error_object->error->message, "UnfulfilledCondition" => $error_message = $error_object->error->message,
"transaction_declined" => $error_message = $error_object->error->details[0]->status_code_description, "transaction_declined" => $error_message = $error_object->error->details[0]->status_code_description,

View File

@ -167,6 +167,9 @@ class CBAPowerBoardPaymentDriver extends BaseDriver
$fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required']; $fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required'];
} }
$fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required'];
return $fields; return $fields;
} }

View File

@ -1055,8 +1055,8 @@ class SubscriptionService
$recurring_invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true, false); $recurring_invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true, false);
$recurring_invoice->subscription_id = $this->subscription->id; $recurring_invoice->subscription_id = $this->subscription->id;
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY; $recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
$recurring_invoice->remaining_cycles = $this->subscription->remaining_cycles ?? -1;
$recurring_invoice->date = now(); $recurring_invoice->date = now();
$recurring_invoice->remaining_cycles = -1;
$recurring_invoice->auto_bill = $client->getSetting('auto_bill'); $recurring_invoice->auto_bill = $client->getSetting('auto_bill');
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms'; $recurring_invoice->due_date_days = 'terms';
@ -1089,7 +1089,7 @@ class SubscriptionService
$recurring_invoice->subscription_id = $this->subscription->id; $recurring_invoice->subscription_id = $this->subscription->id;
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY; $recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
$recurring_invoice->date = now()->addSeconds($client->timezone_offset()); $recurring_invoice->date = now()->addSeconds($client->timezone_offset());
$recurring_invoice->remaining_cycles = -1; $recurring_invoice->remaining_cycles = $this->subscription->remaining_cycles ?? -1;
$recurring_invoice->auto_bill = $client->getSetting('auto_bill'); $recurring_invoice->auto_bill = $client->getSetting('auto_bill');
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill); $recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
$recurring_invoice->due_date_days = 'terms'; $recurring_invoice->due_date_days = 'terms';

View File

@ -73,6 +73,7 @@ class SubscriptionTransformer extends EntityTransformer
'optional_product_ids' => (string) $subscription->optional_product_ids, 'optional_product_ids' => (string) $subscription->optional_product_ids,
'registration_required' => (bool) $subscription->registration_required, 'registration_required' => (bool) $subscription->registration_required,
'steps' => $subscription->steps, 'steps' => $subscription->steps,
'remaining_cycles' => (int) $subscription->remaining_cycles,
]; ];
} }
} }

View File

@ -3889,7 +3889,7 @@ $lang = array(
'payment_method_saving_failed' => 'Payment method can\'t be saved for future use.', 'payment_method_saving_failed' => 'Payment method can\'t be saved for future use.',
'pay_with' => 'Pay with', 'pay_with' => 'Pay with',
'n/a' => 'N/A', 'n/a' => 'N/A',
'by_clicking_next_you_accept_terms' => 'By clicking "Next step" you accept terms.', 'by_clicking_next_you_accept_terms' => 'By clicking "Next" you accept terms.',
'not_specified' => 'Not specified', 'not_specified' => 'Not specified',
'before_proceeding_with_payment_warning' => 'Before proceeding with payment, you have to fill following fields', 'before_proceeding_with_payment_warning' => 'Before proceeding with payment, you have to fill following fields',
'after_completing_go_back_to_previous_page' => 'After completing, go back to previous page.', 'after_completing_go_back_to_previous_page' => 'After completing, go back to previous page.',

1
public/build/assets/app-23f93261.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
import{i as s,w as c}from"./wait-8f4ae121.js";/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/function i(){const t=document.querySelector("meta[name=public_key]"),n=document.querySelector("meta[name=gateway_id]"),o=document.querySelector("meta[name=environment]"),e=new cba.HtmlWidget("#widget",t==null?void 0:t.content,n==null?void 0:n.content);e.setEnv(o==null?void 0:o.content),e.useAutoResize(),e.interceptSubmitForm("#stepone"),e.onFinishInsert('#server-response input[name="gateway_response"]',"payment_source"),e.setFormFields(["card_name*"]),e.reload();let r=document.getElementById("pay-now");return r.disabled=!1,r.querySelector("svg").classList.add("hidden"),r.querySelector("span").classList.remove("hidden"),document.querySelector('#server-response input[name="gateway_response"]').value="",e}function u(){var t,n,o;(t=document.querySelector("#widget"))==null||t.replaceChildren(),(n=document.querySelector("#widget"))==null||n.classList.remove("hidden"),(o=document.querySelector("#widget-3dsecure"))==null||o.replaceChildren()}function a(){u();const t=i();t.on("finish",()=>{document.getElementById("errors").hidden=!0,l()}),t.on("submit",function(e){document.getElementById("errors").hidden=!0});let n=document.getElementById("pay-now");n.addEventListener("click",()=>{const e=document.getElementById("widget");if(t.getValidationState(),!t.isValidForm()&&e.offsetParent!==null){n.disabled=!1,n.querySelector("svg").classList.add("hidden"),n.querySelector("span").classList.remove("hidden");return}n.disabled=!0,n.querySelector("svg").classList.remove("hidden"),n.querySelector("span").classList.add("hidden");let r=document.querySelector("input[name=token-billing-checkbox]:checked");r&&(document.getElementById("store_card").value=r.value),e.offsetParent!==null?document.getElementById("stepone_submit").click():document.getElementById("server-response").submit()}),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",e=>{var d;document.getElementById("widget").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value="",(d=document.querySelector("#powerboard-payment-container"))==null||d.classList.remove("hidden")}),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(e=>e.addEventListener("click",r=>{var d;document.getElementById("widget").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=r.target.dataset.token,(d=document.querySelector("#powerboard-payment-container"))==null||d.classList.add("hidden")}));const o=document.querySelector('input[name="payment-type"]');o&&o.click()}async function l(){try{const t=await m();if(t.status==="not_authenticated"||t==="not_authenticated")throw a(),new Error("There was an issue authenticating this payment method.");if(t.status==="authentication_not_supported"){document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(t);let e=document.querySelector("input[name=token-billing-checkbox]:checked");return e&&(document.getElementById("store_card").value=e.value),document.getElementById("server-response").submit()}const n=new cba.Canvas3ds("#widget-3dsecure",t._3ds.token);n.load(),document.getElementById("widget").classList.add("hidden"),n.on("chargeAuthSuccess",function(e){document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(e);let r=document.querySelector("input[name=token-billing-checkbox]:checked");r&&(document.getElementById("store_card").value=r.value),document.getElementById("server-response").submit()}),n.on("chargeAuthReject",function(e){document.getElementById("errors").textContent="Sorry, your transaction could not be processed...",document.getElementById("errors").hidden=!1,a()}),n.load()}catch(t){console.error("Error fetching 3DS Token:",t),document.getElementById("errors").textContent=`Sorry, your transaction could not be processed...
${t}`,document.getElementById("errors").hidden=!1,a()}}async function m(){const t={name:navigator.userAgent.substring(0,100),java_enabled:navigator.javaEnabled()?"true":"false",language:navigator.language||navigator.userLanguage,screen_height:window.screen.height.toString(),screen_width:window.screen.width.toString(),time_zone:(new Date().getTimezoneOffset()*-1).toString(),color_depth:window.screen.colorDepth.toString()};document.querySelector('input[name="browser_details"]').value=JSON.stringify(t);const n=JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server-response")))),o=document.querySelector("meta[name=payments_route]");try{const e=await fetch(o.content,{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest",Accept:"application/json","X-CSRF-Token":document.querySelector('meta[name="csrf-token"]').content},body:n});return e.ok?await e.json():await e.json().then(r=>{throw new Error(r.message??"Unknown error.")})}catch(e){document.getElementById("errors").textContent=`Sorry, your transaction could not be processed...
${e.message}`,document.getElementById("errors").hidden=!1,console.error("Fetch error:",e),a()}}s()?a():c("#powerboard-credit-card-payment").then(()=>a());

View File

@ -146,7 +146,7 @@
"isEntry": true, "isEntry": true,
"src": "resources/js/clients/payments/paytrace-credit-card.js" "src": "resources/js/clients/payments/paytrace-credit-card.js"
}, },
"resources/js/clients/payments/powerboard-credit-card.js": { "resources/js/clients/payments/powerboard-credit-card.
"file": "assets/powerboard-credit-card-127361fb.js", "file": "assets/powerboard-credit-card-127361fb.js",
"imports": [ "imports": [
"_wait-8f4ae121.js" "_wait-8f4ae121.js"

View File

@ -210,6 +210,7 @@ async function process3ds() {
'errors' 'errors'
).textContent = `Sorry, your transaction could not be processed...\n\n${error}`; ).textContent = `Sorry, your transaction could not be processed...\n\n${error}`;
document.getElementById('errors').hidden = false; document.getElementById('errors').hidden = false;
pay();
} }
} }
@ -267,7 +268,8 @@ async function get3dsToken() {
document.getElementById('errors').hidden = false; document.getElementById('errors').hidden = false;
console.error('Fetch error:', error); // Log error for debugging console.error('Fetch error:', error); // Log error for debugging
throw error; // pay();
} }
} }

View File

@ -304,6 +304,14 @@
<span>{{ $total }}</span> <span>{{ $total }}</span>
</div> </div>
@if(isset($tax))
<div class="flex font-semibold justify-between py-1 text-sm uppercase border-t-2">
<span>{{ ctrans('texts.tax') }}</span>
<span>{{ $tax }}</span>
</div>
@endif
<div class="mx-auto text-center mt-20 content-center" x-data="{open: @entangle('payment_started').live, toggle: @entangle('payment_confirmed').live, buttonDisabled: false}" x-show.important="open" x-transition> <div class="mx-auto text-center mt-20 content-center" x-data="{open: @entangle('payment_started').live, toggle: @entangle('payment_confirmed').live, buttonDisabled: false}" x-show.important="open" x-transition>
<h2 class="text-2xl font-bold tracking-wide border-b-2 pb-4">{{ $heading_text ?? ctrans('texts.checkout') }}</h2> <h2 class="text-2xl font-bold tracking-wide border-b-2 pb-4">{{ $heading_text ?? ctrans('texts.checkout') }}</h2>
@if (session()->has('message')) @if (session()->has('message'))

View File

@ -13,8 +13,11 @@
@endif @endif
</div> </div>
</div> </div>
<div class="flex justify-end items-center px-4 py-4"> <div class="flex flex-col items-end px-4 py-4">
<button id="accept-terms-button" class="button button-primary bg-primary hover:bg-primary-darken float-end">{{ ctrans('texts.next') }}</button> <div class="w-full flex justify-end mb-2">
<button id="accept-terms-button" class="button button-primary bg-primary hover:bg-primary-darken">{{ ctrans('texts.next') }}</button>
</div>
<span class="text-xs text-gray-600 text-right">{{ ctrans('texts.by_clicking_next_you_accept_terms')}}</span>
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@
<div x-text="errors" class="alert alert-failure mb-4"></div> <div x-text="errors" class="alert alert-failure mb-4"></div>
</template> </template>
@if($settings->client_portal_allow_under_payment) @if($settings->client_portal_allow_under_payment && $settings->client_portal_under_payment_minimum != 0)
<span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.minimum_payment') }}: <span class="mt-1 text-sm text-gray-800">{{ ctrans('texts.minimum_payment') }}:
{{ $settings->client_portal_under_payment_minimum }}</span> {{ $settings->client_portal_under_payment_minimum }}</span>
@endif @endif

View File

@ -52,8 +52,8 @@
Livewire.on('passed-required-fields-check', () => { Livewire.on('passed-required-fields-check', () => {
document.querySelector('div[data-ref="required-fields-container"]').classList.toggle('h-0'); document.querySelector('div[data-ref="required-fields-container"]').classList.toggle('h-0');
// document.querySelector('div[data-ref="required-fields-container"]').classList.add('opacity-25'); document.querySelector('div[data-ref="required-fields-container"]').classList.add('opacity-25');
// document.querySelector('div[data-ref="required-fields-container"]').classList.add('pointer-events-none'); document.querySelector('div[data-ref="required-fields-container"]').classList.add('pointer-events-none');
document.querySelector('div[data-ref="gateway-container"]').classList.remove('opacity-25'); document.querySelector('div[data-ref="gateway-container"]').classList.remove('opacity-25');
document.querySelector('div[data-ref="gateway-container"]').classList.remove('pointer-events-none'); document.querySelector('div[data-ref="gateway-container"]').classList.remove('pointer-events-none');

View File

@ -16,10 +16,10 @@
<dl> <dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500"> <dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.start_date') }} {{ ctrans('texts.last_sent') }}
</dt> </dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2"> <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->translateDate($invoice->start_date, $invoice->client->date_format(), $invoice->client->locale()) }} {{ $invoice->translateDate($invoice->last_sent_date, $invoice->client->date_format(), $invoice->client->locale()) }}
</dd> </dd>
</div> </div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">