mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
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:
parent
abe8bbcd5d
commit
114b58cdc4
@ -17,7 +17,7 @@ use App\Models\Subscription;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
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 array $context;
|
||||
@ -28,10 +28,7 @@ class Methods extends Component
|
||||
{
|
||||
$total = collect($this->context['products'])->sum('total_raw');
|
||||
|
||||
$methods = auth()->guard('contact')->user()->client->service()->getPaymentMethods(
|
||||
$total,
|
||||
);
|
||||
|
||||
$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()
|
||||
$this->methods = $methods;
|
||||
}
|
||||
|
||||
|
@ -199,7 +199,8 @@ class BillingPortalPurchasev2 extends Component
|
||||
$this->data = [];
|
||||
|
||||
$this->price = $this->subscription->price; // ?
|
||||
|
||||
$this->float_amount_total = $this->price;
|
||||
|
||||
$this->recurring_products = $this->subscription->service()->recurring_products();
|
||||
$this->products = $this->subscription->service()->products();
|
||||
$this->optional_recurring_products = $this->subscription->service()->optional_recurring_products();
|
||||
@ -244,7 +245,8 @@ class BillingPortalPurchasev2 extends Component
|
||||
Auth::guard('contact')->loginUsingId($contact->id, true);
|
||||
$this->contact = $contact;
|
||||
} else {
|
||||
$this->createClientContact();
|
||||
// $this->createClientContact();
|
||||
$this->createBlankClient();
|
||||
}
|
||||
|
||||
$this->getPaymentMethods();
|
||||
@ -767,6 +769,8 @@ class BillingPortalPurchasev2 extends Component
|
||||
if ($currency) {
|
||||
$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)) {
|
||||
@ -785,8 +789,12 @@ class BillingPortalPurchasev2 extends Component
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -52,11 +52,11 @@ class UnderOverPayment extends Component
|
||||
|
||||
$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.
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int $user_id
|
||||
* @property int|null $assigned_user_id
|
||||
* @property int $company_id
|
||||
* @property int $remaining_cycles
|
||||
* @property string|null $product_ids
|
||||
* @property int|null $frequency_id
|
||||
* @property string|null $auto_bill
|
||||
@ -117,6 +118,7 @@ class Subscription extends BaseModel
|
||||
'optional_recurring_product_ids',
|
||||
'use_inventory_management',
|
||||
'steps',
|
||||
'remaining_cycles',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
@ -76,7 +76,11 @@ class CreditCard implements LivewireMethodInterface
|
||||
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
|
||||
|
||||
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();
|
||||
@ -262,11 +266,8 @@ class CreditCard implements LivewireMethodInterface
|
||||
nlog($r->body());
|
||||
|
||||
if($r->failed()){
|
||||
|
||||
$error_payload = $this->getErrorFromResponse($r);
|
||||
|
||||
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();
|
||||
@ -305,7 +306,8 @@ class CreditCard implements LivewireMethodInterface
|
||||
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
|
||||
|
||||
if ($r->failed()) {
|
||||
return $this->processUnsuccessfulPayment($r);
|
||||
$error_payload = $this->getErrorFromResponse($r);
|
||||
return response()->json(['message' => $error_payload[0]], 400);
|
||||
}
|
||||
|
||||
$charge = $r->json();
|
||||
@ -447,6 +449,7 @@ class CreditCard implements LivewireMethodInterface
|
||||
$error_message = "Unknown error";
|
||||
|
||||
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,
|
||||
"UnfulfilledCondition" => $error_message = $error_object->error->message,
|
||||
"transaction_declined" => $error_message = $error_object->error->details[0]->status_code_description,
|
||||
|
@ -167,6 +167,9 @@ class CBAPowerBoardPaymentDriver extends BaseDriver
|
||||
$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;
|
||||
}
|
||||
|
||||
|
@ -1055,8 +1055,8 @@ class SubscriptionService
|
||||
$recurring_invoice->line_items = $subscription_repo->generateLineItems($this->subscription, true, false);
|
||||
$recurring_invoice->subscription_id = $this->subscription->id;
|
||||
$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->remaining_cycles = -1;
|
||||
$recurring_invoice->auto_bill = $client->getSetting('auto_bill');
|
||||
$recurring_invoice->auto_bill_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
|
||||
$recurring_invoice->due_date_days = 'terms';
|
||||
@ -1089,7 +1089,7 @@ class SubscriptionService
|
||||
$recurring_invoice->subscription_id = $this->subscription->id;
|
||||
$recurring_invoice->frequency_id = $this->subscription->frequency_id ?: RecurringInvoice::FREQUENCY_MONTHLY;
|
||||
$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_enabled = $this->setAutoBillFlag($recurring_invoice->auto_bill);
|
||||
$recurring_invoice->due_date_days = 'terms';
|
||||
|
@ -73,6 +73,7 @@ class SubscriptionTransformer extends EntityTransformer
|
||||
'optional_product_ids' => (string) $subscription->optional_product_ids,
|
||||
'registration_required' => (bool) $subscription->registration_required,
|
||||
'steps' => $subscription->steps,
|
||||
'remaining_cycles' => (int) $subscription->remaining_cycles,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -3889,7 +3889,7 @@ $lang = array(
|
||||
'payment_method_saving_failed' => 'Payment method can\'t be saved for future use.',
|
||||
'pay_with' => 'Pay with',
|
||||
'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',
|
||||
'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.',
|
||||
|
1
public/build/assets/app-23f93261.css
vendored
Normal file
1
public/build/assets/app-23f93261.css
vendored
Normal file
File diff suppressed because one or more lines are too long
13
public/build/assets/powerboard-credit-card-f720a335.js
vendored
Normal file
13
public/build/assets/powerboard-credit-card-f720a335.js
vendored
Normal 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());
|
@ -146,7 +146,7 @@
|
||||
"isEntry": true,
|
||||
"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",
|
||||
"imports": [
|
||||
"_wait-8f4ae121.js"
|
||||
|
@ -210,6 +210,7 @@ async function process3ds() {
|
||||
'errors'
|
||||
).textContent = `Sorry, your transaction could not be processed...\n\n${error}`;
|
||||
document.getElementById('errors').hidden = false;
|
||||
pay();
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,7 +268,8 @@ async function get3dsToken() {
|
||||
document.getElementById('errors').hidden = false;
|
||||
|
||||
console.error('Fetch error:', error); // Log error for debugging
|
||||
throw error; //
|
||||
pay();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,6 +304,14 @@
|
||||
<span>{{ $total }}</span>
|
||||
</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>
|
||||
<h2 class="text-2xl font-bold tracking-wide border-b-2 pb-4">{{ $heading_text ?? ctrans('texts.checkout') }}</h2>
|
||||
@if (session()->has('message'))
|
||||
|
@ -13,8 +13,11 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end items-center 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="flex flex-col items-end px-4 py-4">
|
||||
<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>
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
<div x-text="errors" class="alert alert-failure mb-4"></div>
|
||||
</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') }}:
|
||||
{{ $settings->client_portal_under_payment_minimum }}</span>
|
||||
@endif
|
||||
|
@ -52,8 +52,8 @@
|
||||
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.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('opacity-25');
|
||||
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('pointer-events-none');
|
||||
|
@ -16,10 +16,10 @@
|
||||
<dl>
|
||||
<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">
|
||||
{{ ctrans('texts.start_date') }}
|
||||
{{ ctrans('texts.last_sent') }}
|
||||
</dt>
|
||||
<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>
|
||||
</div>
|
||||
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||
|
Loading…
x
Reference in New Issue
Block a user