Merge pull request #10001 from turbo124/v5-develop

Powerboard payment driver
This commit is contained in:
David Bomba 2024-09-17 10:44:41 +10:00 committed by GitHub
commit 984a8666aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
267 changed files with 3129 additions and 682 deletions

View File

@ -24,6 +24,7 @@ QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MAIL_MAILER=smtp
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

28
app/Helpers/Sanitizer.php Normal file
View File

@ -0,0 +1,28 @@
<?php
/**
* 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
*/
namespace App\Helpers;
class Sanitizer
{
public static function removeBlanks($input): array
{
foreach ($input as &$value) {
if (is_array($value)) {
// Recursively apply the filter to nested arrays
$value = self::removeBlanks($value);
}
}
// Use array_filter to remove empty or null values
return array_filter($input);
}
}

View File

@ -70,7 +70,7 @@ class SetupController extends Controller
return response('Oops, something went wrong. Check your logs.'); /* We should never reach this block, but just in case. */
}
$mail_driver = $request->input('mail_driver');
$mail_driver = $request->input('mail_driver', 'smtp');
$url = $request->input('url');
$db_host = $request->input('db_host');

View File

@ -98,9 +98,10 @@ class UpdateCompanyRequest extends Request
$input['portal_domain'] = rtrim(strtolower($input['portal_domain']), "/");
}
if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
unset($input['expense_mailbox']);
}
// /** Disabled on the hosted platform */
// if (isset($input['expense_mailbox']) && Ninja::isHosted() && !($this->company->account->isPaid() && $this->company->account->plan == 'enterprise')) {
// unset($input['expense_mailbox']);
// }
if (isset($input['settings'])) {
$input['settings'] = (array) $this->filterSaveableSettings($input['settings']);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -50,7 +50,7 @@ class InvoiceSummary extends Component
public function downloadDocument($invoice_hashed_id)
{
nlog("here");
$contact = $this->getContext()['contact'];
$_invoices = $this->getContext()['invoices'];
$i = $_invoices->first(function ($i) use($invoice_hashed_id){
@ -61,11 +61,6 @@ class InvoiceSummary extends Component
$file = (new \App\Jobs\Entity\CreateRawPdf($i->invitations()->where('client_contact_id', $contact->id)->first()))->handle();
nlog("here");
nlog($file);
$headers = ['Content-Type' => 'application/pdf'];
return response()->streamDownload(function () use ($file) {

View File

@ -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);
}
}

View File

@ -194,13 +194,14 @@ class RequiredClientInfo extends Component
public function mount()
{
MultiDB::setDb($this->db);
$contact = ClientContact::withTrashed()->with(['client' => function ($query) {
$query->without('gateway_tokens', 'documents', 'contacts.company', 'contacts'); // Exclude 'grandchildren' relation of 'client'
}])->find($this->contact_id);
$this->company_gateway = CompanyGateway::withTrashed()->with('company')->find($this->company_gateway_id);
$company = $this->company_gateway->company;
$this->client_name = $contact->client->name;
@ -240,6 +241,13 @@ class RequiredClientInfo extends Component
$this->checkFields();
}
if (count($this->fields) === 0) {
$this->dispatch(
'passed-required-fields-check',
client_postal_code: $contact->client->postal_code
);
}
if($this->unfilled_fields > 0 || ($this->company_gateway->always_show_required_fields || $this->is_subscription)) {
$this->show_form = true;
}
@ -385,7 +393,6 @@ class RequiredClientInfo extends Component
public function checkFields()
{
MultiDB::setDb($this->db);
$_contact = ClientContact::withTrashed()->find($this->contact_id);

View File

@ -371,7 +371,7 @@ class Company extends BaseModel
'tax_data',
'e_invoice_certificate_passphrase',
'expense_mailbox_active',
'expense_mailbox', // TODO: @turbo124 custom validation: self-hosted => free change, hosted => not changeable, only changeable with env-mask
'expense_mailbox',
'inbound_mailbox_allow_company_users',
'inbound_mailbox_allow_vendors',
'inbound_mailbox_allow_clients',

View File

@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property bool $update_details
* @property bool $is_deleted
* @property string $config
* @property object $settings
* @property mixed $fees_and_limits
* @property string|null $custom_value1
* @property string|null $custom_value2
@ -74,6 +75,7 @@ class CompanyGateway extends BaseModel
protected $casts = [
'fees_and_limits' => 'object',
'settings' => 'object',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
@ -156,6 +158,7 @@ class CompanyGateway extends BaseModel
'80af24a6a691230bbec33e930ab40666' => 323,
'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay
'91be24c7b792230bced33e930ac61676' => 325,
'b67581d804dbad1743b61c57285142ad' => 326, //Powerboard
];
protected $touches = [];
@ -483,6 +486,18 @@ class CompanyGateway extends BaseModel
return $fee;
}
public function getSettings()
{
// return $this->settings;
return $this->settings ?? new \stdClass;
}
public function setSettings($settings)
{
$this->settings = $settings;
$this->save();
}
public function webhookUrl()
{
return route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $this->hashed_id]);

View File

@ -235,6 +235,10 @@ class Gateway extends StaticModel
],
GatewayType::ACSS => ['refund' => false, 'token_billing' => true, 'webhooks' => []]
]; // Rotessa
case 64: //b67581d804dbad1743b61c57285142ad - powerboard
return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
];
default:
return [];
}

View File

@ -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 = [

View File

@ -156,6 +156,8 @@ class SystemLog extends Model
public const TYPE_ROTESSA = 325;
public const TYPE_POWERBOARD = 326;
public const TYPE_QUOTA_EXCEEDED = 400;
public const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -410,6 +410,7 @@ class BaseDriver extends AbstractPaymentDriver
if($invoice && $fee_count == 0){
nlog("apparently no fee, so injecting here!");
if(!$invoice->uses_inclusive_taxes){ //must account for taxes! ? line item taxes also
@ -771,6 +772,18 @@ class BaseDriver extends AbstractPaymentDriver
);
}
public function logUnsuccessfulGatewayResponse($response, $gateway_const)
{
SystemLogger::dispatch(
$response,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
$gateway_const,
$this->client,
$this->client->company,
);
}
public function genericWebhookUrl()
{
return route('payment_notification_webhook', [

View File

@ -0,0 +1,491 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Jobs\Util\SystemLogger;
use App\Exceptions\PaymentFailed;
use Illuminate\Http\Client\RequestException;
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
use App\PaymentDrivers\CBAPowerBoard\Models\Charge;
use App\PaymentDrivers\Common\LivewireMethodInterface;
use App\PaymentDrivers\CBAPowerBoard\Models\PaymentSource;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
class CreditCard implements LivewireMethodInterface
{
private Gateway $cba_gateway;
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
{
$this->cba_gateway = $this->powerboard->settings()->getPaymentGatewayConfiguration(GatewayType::CREDIT_CARD);
}
public function authorizeView(array $data)
{
$data['payment_method_id'] = GatewayType::CREDIT_CARD;
$data['threeds'] = $this->powerboard->company_gateway->getConfigField('threeds');
return render('gateways.powerboard.credit_card.authorize', $this->paymentData($data));
}
public function authorizeResponse($request)
{
if($request->browser_details)
{
$payment_source = $this->storePaymentSource($request);
nlog($payment_source);
$browser_details = json_decode($request->browser_details, true);
$payload = [
"capture" => false,
"amount" => 1,
"currency" => $this->powerboard->client->currency()->code,
"description" => "Card authorization",
"customer" => [
"payment_source" => [
"vault_token" => $payment_source->vault_token,
"gateway_id" => $this->powerboard->settings()->getGatewayId(GatewayType::CREDIT_CARD),
],
],
"_3ds" => [
"browser_details" => $browser_details,
],
];
nlog($payload);
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if ($r->failed()) {
$error_payload = $this->getErrorFromResponse($r);
return response()->json(['message' => $error_payload[0]], 400);
// return $this->processUnsuccessfulPayment($r);
}
$charge = $r->json();
nlog($charge['resource']['data']);
return response()->json($charge['resource']['data'], 200);
}
elseif($request->charge) {
$charge_request = json_decode($request->charge, true);
nlog("we have the charge request");
nlog($charge_request);
$payload = [
'_3ds' => [
'id' => array_key_exists('charge_3ds_id', $charge_request) ? $charge_request['charge_3ds_id'] : $charge_request['_3ds']['id'],
],
"capture" => false,
"authorization" => true,
"amount"=> 1,
"currency"=> $this->powerboard->client->currency()->code,
"store_cvv"=> true,
];
nlog($payload);
$r = $this->powerboard->gatewayRequest("/v1/charges", (\App\Enum\HttpVerb::POST)->value, $payload, []);
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();
nlog($charge);
if ($charge->status == 'complete') {
$this->powerboard->logSuccessfulGatewayResponse(['response' => $charge, 'data' => $this->powerboard->payment_hash], SystemLog::TYPE_POWERBOARD);
$vt = $charge->customer->payment_source->vault_token;
$data = [
"payment_source" => [
"vault_token" => $vt,
],
];
$customer = $this->powerboard->customer()->findOrCreateCustomer($data);
$cgt = $this->powerboard->customer()->storePaymentMethod($charge->customer->payment_source, $charge->customer);
return redirect()->route('client.payment_methods.show', ['payment_method' => $cgt->hashed_id]);
}
}
elseif($request->charge_no3d){
nlog($request->all());
$payment_source = $this->storePaymentSource($request);
nlog($payment_source);
$data = [
"payment_source" => [
"vault_token" => $payment_source->vault_token,
],
];
$customer = $this->powerboard->customer()->findOrCreateCustomer($data);
$cgt = $this->powerboard->customer()->storePaymentMethod($payment_source, $customer);
$cgt->gateway_customer_reference = $this->powerboard->settings()->getGatewayId(GatewayType::CREDIT_CARD);
$cgt->save();
return redirect()->route('client.payment_methods.show', ['payment_method' => $cgt->hashed_id]);
}
return redirect()->route('client.payment_methods.index');
}
private function getCustomer(): array
{
$data = [
'first_name' => $this->powerboard->client->present()->first_name(),
'last_name' => $this->powerboard->client->present()->first_name(),
'email' => $this->powerboard->client->present()->email(),
// 'phone' => $this->powerboard->client->present()->phone(),
// 'type' => 'card',
'address_line1' => $this->powerboard->client->address1 ?? '',
'address_line2' => $this->powerboard->client->address2 ?? '',
'address_state' => $this->powerboard->client->state ?? '',
'address_country' => $this->powerboard->client->country->iso_3166_3 ?? '',
'address_city' => $this->powerboard->client->city ?? '',
'address_postcode' => $this->powerboard->client->postal_code ?? '',
];
return \App\Helpers\Sanitizer::removeBlanks($data);
}
private function storePaymentSource($request)
{
$this->powerboard->init();
$payment_source = $request->gateway_response;
$payload = array_merge($this->getCustomer(), [
'token' => $payment_source,
"vault_type" => "permanent",
'store_ccv' => true,
]);
nlog($payload);
$r = $this->powerboard->gatewayRequest('/v1/vault/payment_sources', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed())
return $this->powerboard->processInternallyFailedPayment($this->powerboard, $r->throw());
nlog($r->object());
$source = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(PaymentSource ::class, $r->object()->resource->data);
return $source;
}
public function paymentData(array $data): array
{
$this->powerboard->init();
if($this->cba_gateway->verification_status != "completed")
throw new PaymentFailed("This payment method is not configured as yet. Reference Powerboard portal for further information", 400);
$merge = [
'public_key' => $this->powerboard->company_gateway->getConfigField('publicKey'),
'widget_endpoint' => $this->powerboard->widget_endpoint,
'gateway' => $this->powerboard,
'environment' => $this->powerboard->environment,
'gateway_id' => $this->cba_gateway->_id,
];
return array_merge($data, $merge);
}
public function paymentView(array $data)
{
$data = $this->paymentData($data);
return render('gateways.powerboard.credit_card.pay', $data);
}
public function livewirePaymentView(array $data): string
{
return 'gateways.powerboard.credit_card.pay_livewire';
}
public function tokenBilling($request, $cgt, $client_present = false)
{
$payload = [
"amount" => $this->powerboard->payment_hash->data->amount_with_fee,
"currency" => $this->powerboard->client->currency()->code,
"customer" => [
"payment_source" => [
"vault_token" => $cgt->token,
"gateway_id" => $cgt->gateway_customer_reference
]
]
];
$r = $this->powerboard->gatewayRequest('/v1/charges', (\App\Enum\HttpVerb::POST)->value, $payload, []);
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();
nlog($charge);
$this->powerboard->logSuccessfulGatewayResponse(['response' => $charge, 'data' => $this->powerboard->payment_hash], SystemLog::TYPE_POWERBOARD);
return $this->processSuccessfulPayment($charge);
}
private function get3dsToken(PaymentSource $source, $request)
{
$payment_hash = PaymentHash::query()->where('hash', $request->payment_hash)->first();
$browser_details = json_decode($request->browser_details,true);
$payload = [
"amount" => $payment_hash->data->amount_with_fee,
"currency" => $this->powerboard->client->currency()->code,
"description" => $this->powerboard->getDescription(),
"customer" => [
"payment_source" => [
"vault_token" => $source->vault_token,
"gateway_id" => $this->powerboard->settings()->getGatewayId(GatewayType::CREDIT_CARD),
],
],
"_3ds" => [
"browser_details" => $browser_details,
],
];
nlog($payload);
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if ($r->failed()) {
$error_payload = $this->getErrorFromResponse($r);
return response()->json(['message' => $error_payload[0]], 400);
}
$charge = $r->json();
return response()->json($charge['resource']['data'], 200);
}
public function paymentResponse(PaymentResponseRequest $request)
{
nlog($request->all());
$this->powerboard->payment_hash->data = array_merge((array) $this->powerboard->payment_hash->data, ['response' => $request->all()]);
$this->powerboard->payment_hash->save();
$payload = [];
/** Token Payment */
if($request->input('token', false))
{
$cgt = $this->powerboard
->client
->gateway_tokens()
->where('company_gateway_id', $this->powerboard->company_gateway->id)
->where('token', $request->token)
->first();
return $this->tokenBilling($request, $cgt, true);
}
elseif($request->browser_details)
{
$payment_source = $this->storePaymentSource($request);
nlog($payment_source);
return $this->get3dsToken($payment_source, $request);
}
elseif($request->charge) {
$charge_request = json_decode($request->charge, true);
nlog($charge_request);
$payload = [
'_3ds' => [
'id' => array_key_exists('charge_3ds_id', $charge_request) ? $charge_request['charge_3ds_id'] : $charge_request['_3ds']['id'],
],
"amount"=> $this->powerboard->payment_hash->data->amount_with_fee, //@phpstan-ignore-line
"currency"=> $this->powerboard->client->currency()->code,
"store_cvv"=> true,
];
$r = $this->powerboard->gatewayRequest("/v1/charges", (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed())
return $this->processUnsuccessfulPayment($r);
$charge = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Charge::class, $r->object()->resource->data) ?? $r->throw();
nlog($charge);
if ($charge->status == 'complete') {
$this->powerboard->logSuccessfulGatewayResponse(['response' => $charge, 'data' => $this->powerboard->payment_hash], SystemLog::TYPE_POWERBOARD);
$vt = $charge->customer->payment_source->vault_token;
if($request->store_card){
$data = [
"payment_source" => [
"vault_token" => $vt,
],
];
$customer = $this->powerboard->customer()->findOrCreateCustomer($data);
$cgt = $this->powerboard->customer()->storePaymentMethod($charge->customer->payment_source, $charge->customer);
}
return $this->processSuccessfulPayment($charge);
}
elseif($charge->error){
$this->powerboard->logUnsuccessfulGatewayResponse($charge, SystemLog::TYPE_POWERBOARD);
throw new PaymentFailed($charge->error->message, $charge->status);
}
}
session()->flash('message', ctrans('texts.payment_token_not_found'));
return redirect()->back();
}
public function processSuccessfulPayment(Charge $charge)
{
$data = [
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'amount' => $this->powerboard->payment_hash->data->amount_with_fee,
'transaction_reference' => $charge->_id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment = $this->powerboard->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $charge, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_POWERBOARD,
$this->powerboard->client,
$this->powerboard->client->company,
);
if ($payment->invoices()->whereHas('subscription')->exists()) {
$subscription = $payment->invoices()->first()->subscription;
if ($subscription && array_key_exists('return_url', $subscription->webhook_configuration) && strlen($subscription->webhook_configuration['return_url']) >= 1) {
return redirect($subscription->webhook_configuration['return_url']);
}
}
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
private function getErrorFromResponse($response)
{
try {
$response->throw();
} catch (RequestException $exception) {
$error_object = $exception->response->object();
$this->powerboard->logUnsuccessfulGatewayResponse($error_object, SystemLog::TYPE_POWERBOARD);
$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,
default => $error_message = $error_object->error->message ?? "Unknown error",
};
return [$error_message, $exception->getCode()];
}
}
public function processUnsuccessfulPayment($response)
{
$error = $this->getErrorFromResponse($response);
$this->powerboard->sendFailureMail($error[0]);
// $message = [
// 'server_response' => $server_response,
// 'data' => $this->stripe->payment_hash->data,
// ];
SystemLogger::dispatch(
$error[0],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_POWERBOARD,
$this->powerboard->client,
$this->powerboard->client->company,
);
if (request()->wantsJson()) {
return response()->json($error[0], 200);
}
throw new PaymentFailed('Failed to process the payment.', $error[1]);
}
}

View File

@ -0,0 +1,186 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard;
use App\Helpers\Sanitizer;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\CBAPowerBoard\Models\Customer as ModelsCustomer;
use App\PaymentDrivers\CBAPowerBoard\Models\PaymentSource;
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
class Customer
{
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
{
}
public function findOrCreateCustomer(array $customer_data): mixed
{
$token = $this->powerboard
->client
->gateway_tokens()
->whereNotNull('gateway_customer_reference')
->where('company_gateway_id', $this->powerboard->company_gateway->id)
->first();
if($token && $customer = $this->getCustomer($token->gateway_customer_reference)){
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $customer->resource->data);
}
if($customer = $this->findCustomer())
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $customer);
return $this->createCustomer($customer_data);
}
public function getCustomer(string $id): mixed
{
$uri = "/v1/customers/{$id}";
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::GET)->value, [], []);
nlog($r->json());
if($r->successful())
return $r->object();
return false;
}
public function findCustomer(): mixed
{
$uri = '/v1/customers';
$query = [
'reference' => $this->powerboard->client->client_hash,
];
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::GET)->value, $query, []);
$search_results = $r->object();
nlog($search_results);
$customers = $search_results->resource->data;
return reset($customers); // returns first element or false
}
public function createCustomer(array $data = []): object
{
$payload = [
'company_name' => $this->powerboard->client->present()->name(),
'first_name' => $this->powerboard->client->present()->first_name(),
'last_name' => $this->powerboard->client->present()->first_name(),
'email' => $this->powerboard->client->present()->email(),
'reference' => $this->powerboard->client->client_hash,
// 'phone' => $this->powerboard->client->present()->phone(),
];
$payload = array_merge($payload, $data);
$payload = Sanitizer::removeBlanks($payload);
nlog($payload);
$uri = "/v1/customers";
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed())
$r->throw();
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $r->object()->resource->data) ?? $r->throw();
}
public function storePaymentMethod(?PaymentSource $payment_source = null, ?ModelsCustomer $customer = null): ClientGatewayToken
{
/** @var PaymentSource $source */
$source = $payment_source ? $payment_source : end($customer->payment_sources);
$payment_meta = new \stdClass();
$payment_meta->exp_month = (string) $source->expire_month;
$payment_meta->exp_year = (string) $source->expire_year;
$payment_meta->brand = (string) $source->card_scheme;
$payment_meta->last4 = (string) $source->card_number_last4;
$payment_meta->gateway_id = $source->gateway_id ?? null;
$payment_meta->type = \App\Models\GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $source->vault_token,
'payment_method_id' => \App\Models\GatewayType::CREDIT_CARD,
];
$cgt = $this->powerboard->storeGatewayToken($data, ['gateway_customer_reference' => $source->gateway_id]);
return $cgt;
}
public function addTokenToCustomer(string $token, ModelsCustomer $customer): mixed
{
nlog("add token to customer");
$uri = "/v1/customers/{$customer->_id}";
$payload = [
'payment_source' => [
'vault_token' => $token,
]
];
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed()){
nlog($r->body());
return $r->throw();
}
nlog($r->object());
$customer = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $r->object()->resource->data);
$source = collect($customer->payment_sources)->first(function (PaymentSource $source) use ($token){
return $token == $source->vault_token;
});
nlog("i found the source");
nlog($source);
$cgt = $this->powerboard
->client
->gateway_tokens()
->where('token', $token)
->first();
nlog($cgt->id);
$meta = $cgt->meta;
$meta->gateway_id = $source->gateway_id;
$cgt->meta = $meta;
$cgt->save();
return $r->object();
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Charge
{
/** @var ?string */
public ?string $external_id;
/** @var ?string */
public ?string $_id;
/** @var ?string */
public ?string $created_at;
/** @var ?string */
public ?string $updated_at;
/** @var ?string */
public ?string $remittance_date;
/** @var ?string */
public ?string $company_id;
/** @var float */
public float $amount;
/** @var ?string */
public ?string $currency;
/** @var ?int */
public ?int $__v;
/** @var Transaction[] */
public array $transactions;
/** @var ?bool */
public ?bool $one_off;
/** @var ?bool */
public ?bool $archived;
/** @var Customer */
public Customer $customer;
/** @var ?bool */
public ?bool $capture;
/** @var ?string */
public? string $status;
/** @var ?array */
public ?array $items;
public function __construct(
?string $external_id,
?string $_id,
?string $created_at,
?string $updated_at,
?string $remittance_date,
?string $company_id,
float $amount,
?string $currency,
?int $__v,
array $transactions,
?bool $one_off,
?bool $archived,
Customer $customer,
?bool $capture,
?string $status,
?array $items,
) {
$this->external_id = $external_id;
$this->_id = $_id;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->remittance_date = $remittance_date;
$this->company_id = $company_id;
$this->amount = $amount;
$this->currency = $currency;
$this->__v = $__v;
$this->transactions = $transactions;
$this->one_off = $one_off;
$this->archived = $archived;
$this->customer = $customer;
$this->capture = $capture;
$this->status = $status;
$this->items = $items;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Customer
{
/** @var ?string */
public ?string $_id;
/** @var ?string */
public ?string $_source_ip_address;
/** @var ?string */
public ?string $first_name;
/** @var ?string */
public ?string $last_name;
/** @var ?string */
public ?string $email;
/** @var ?string */
public ?string $reference;
/** @var ?string */
public ?string $default_source;
/** @var ?string */
public ?string $status;
/** @var ?bool */
public ?bool $archived;
/** @var ?string */
public ?string $created_at;
/** @var ?string */
public ?string $updated_at;
/** @var ?bool */
public ?bool $_check_expire_date;
/** @var ?PaymentSource[] */
public ?array $payment_sources;
/** @var ?PaymentSource */
public ?PaymentSource $payment_source;
/** @var ?array */
public ?array $payment_destinations;
/** @var ?string */
public ?string $company_id;
public function __construct(
?string $_id,
?string $_source_ip_address,
?string $first_name,
?string $last_name,
?string $email,
?string $reference,
?string $default_source,
?string $status,
?bool $archived,
?string $created_at,
?string $updated_at,
?bool $_check_expire_date,
?array $payment_sources,
?array $payment_destinations,
?string $company_id,
?PaymentSource $payment_source
) {
$this->_id = $_id;
$this->_source_ip_address = $_source_ip_address;
$this->first_name = $first_name;
$this->last_name = $last_name;
$this->email = $email;
$this->reference = $reference;
$this->default_source = $default_source;
$this->status = $status;
$this->archived = $archived;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->_check_expire_date = $_check_expire_date;
$this->payment_sources = $payment_sources;
$this->payment_destinations = $payment_destinations;
$this->company_id = $company_id;
$this->payment_source = $payment_source;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Gateway
{
/** @var string */
public string $_id;
/** @var string */
public string $name;
/** @var string */
public string $type;
/** @var string */
public string $mode;
/** @var string */
public string $created_at;
/** @var string */
public string $updated_at;
/** @var bool */
public bool $archived;
/** @var bool */
public bool $default;
/** @var string */
public string $verification_status;
public function __construct(
string $_id,
string $name,
string $type,
string $mode,
string $created_at,
string $updated_at,
bool $archived,
bool $default,
string $verification_status
) {
$this->_id = $_id;
$this->name = $name;
$this->type = $type;
$this->mode = $mode;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->archived = $archived;
$this->default = $default;
$this->verification_status = $verification_status;
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
class Parse
{
public function __construct()
{
}
public function encode($object_type, $document)
{
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// list of PropertyTypeExtractorInterface (any iterable)
$typeExtractors = [$reflectionExtractor,$phpDocExtractor];
// list of PropertyDescriptionExtractorInterface (any iterable)
$descriptionExtractors = [$phpDocExtractor];
// list of PropertyInitializableExtractorInterface (any iterable)
$propertyInitializableExtractors = [$reflectionExtractor];
$propertyInfo = new PropertyInfoExtractor(
$propertyInitializableExtractors,
$descriptionExtractors,
$typeExtractors,
);
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$normalizer = new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter, null, $propertyInfo);
$normalizers = [new DateTimeNormalizer(), $normalizer, new ArrayDenormalizer()];
$encoders = [new JsonEncoder()];
$serializer = new Serializer($normalizers, $encoders);
$data = $serializer->deserialize(json_encode($document), $object_type, 'json', [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
return $data;
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class PaymentSource
{
/** @var ?string */
public ?string $_id;
/** @var string */
public string $type;
/** @var string */
public string $vault_token;
/** @var string */
public string $card_name;
/** @var string */
public string $card_number_bin;
/** @var string */
public string $card_number_last4;
/** @var string */
public string $card_scheme;
/** @var string|null */
public ?string $address_line1;
/** @var string|null */
public ?string $address_line2;
/** @var string|null */
public ?string $address_city;
/** @var string|null */
public ?string $address_country;
/** @var string|null */
public ?string $address_state;
/** @var int */
public int $expire_month;
/** @var int */
public int $expire_year;
/** @var ?string */
public ?string $status;
/** @var ?string */
public ?string $created_at;
/** @var ?string */
public ?string $updated_at;
/** @var ?string */
public ?string $vault_type;
/** @var ?string */
public ?string $gateway_id;
public function __construct(
?string $_id,
string $type,
string $vault_token,
string $card_name,
string $card_number_bin,
string $card_number_last4,
string $card_scheme,
?string $address_line1,
?string $address_line2,
?string $address_city,
?string $address_country,
?string $address_state,
int $expire_month,
int $expire_year,
?string $status,
?string $created_at,
?string $updated_at,
?string $vault_type,
?string $gateway_id
) {
$this->_id = $_id;
$this->type = $type;
$this->vault_token = $vault_token;
$this->card_name = $card_name;
$this->card_number_bin = $card_number_bin;
$this->card_number_last4 = $card_number_last4;
$this->card_scheme = $card_scheme;
$this->address_line1 = $address_line1;
$this->address_line2 = $address_line2;
$this->address_city = $address_city;
$this->address_country = $address_country;
$this->address_state = $address_state;
$this->expire_month = $expire_month;
$this->expire_year = $expire_year;
$this->status = $status;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->vault_type = $vault_type;
$this->gateway_id = $gateway_id;
}
}

View File

@ -0,0 +1,18 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class PaymentSources
{
/** @var \App\PaymentDrivers\CBAPowerBoard\Models\PaymentSources[] */
public array $payment_sources;
}

View File

@ -0,0 +1,79 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Threeds
{
public function __construct(public ?string $token){}
}
class Transaction
{
public ?Threeds $_3ds;
public ?string $gateway_specific_code;
public ?string $gateway_specific_description;
public ?string $error_message;
public ?string $error_code;
public ?string $status_code;
public ?string $status_code_description;
public ?string $type;
public ?string $status;
public float $amount;
public ?string $currency;
public ?string $_id;
public ?string $created_at;
public ?string $updated_at;
public ?string $processed_at;
public ?string $external_id;
public ?string $external_reference;
public ?string $authorization_code;
public function __construct(
?Threeds $_3ds,
?string $gateway_specific_code,
?string $gateway_specific_description,
?string $error_message,
?string $error_code,
?string $status_code,
?string $status_code_description,
?string $type,
?string $status,
float $amount,
?string $currency,
?string $_id,
?string $created_at,
?string $updated_at,
?string $processed_at,
?string $external_id,
?string $external_reference,
?string $authorization_code
) {
$this->_3ds = $_3ds;
$this->gateway_specific_code = $gateway_specific_code;
$this->gateway_specific_description = $gateway_specific_description;
$this->error_message = $error_message;
$this->error_code = $error_code;
$this->status_code = $status_code;
$this->status_code_description = $status_code_description;
$this->type = $type;
$this->status = $status;
$this->amount = $amount;
$this->currency = $currency;
$this->_id = $_id;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->processed_at = $processed_at;
$this->external_id = $external_id;
$this->external_reference = $external_reference;
$this->authorization_code = $authorization_code;
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers\CBAPowerBoard;
use App\PaymentDrivers\CBAPowerBoard\Models\Gateways;
use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
class Settings
{
protected const GATEWAY_CBA = 'MasterCard';
protected const GATEWAY_AFTERPAY = 'Afterpay';
protected const GATEWAY_PAYPAL = 'Paypal';
protected const GATEWAY_ZIP = 'Zipmoney';
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
{
}
public function getGateways()
{
$r = $this->powerboard->gatewayRequest('/v1/gateways', (\App\Enum\HttpVerb::GET)->value, [], []);
if($r->failed())
$r->throw();
nlog($r->object());
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $r->object()->resource->data);
}
/** We will need to have a process that updates this at intervals */
public function updateSettings():self
{
$gateways = $this->getGateways();
$settings = $this->powerboard->company_gateway->getSettings();
$settings->gateways = $gateways;
$this->powerboard->company_gateway->setSettings($settings);
return $this;
}
public function getSettings(): mixed
{
return $this->powerboard->company_gateway->getSettings();
}
public function getPaymentGatewayConfiguration(int $gateway_type_id): mixed
{
$type = self::GATEWAY_CBA;
match($gateway_type_id){
\App\Models\GatewayType::CREDIT_CARD => $type = self::GATEWAY_CBA,
default => $type = self::GATEWAY_CBA,
};
return $this->getGatewayByType($type);
}
private function getGatewayByType(string $gateway_type_const): mixed
{
$settings = $this->getSettings();
if(!property_exists($settings,'gateways')){
$this->updateSettings();
$settings = $this->getSettings();
}
$gateways = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $settings->gateways);
return collect($gateways)->first(function (Gateway $gateway) use ($gateway_type_const){
return $gateway->type == $gateway_type_const;
});
}
public function getGatewayId(int $gateway_type_id): string
{
$gateway = $this->getPaymentGatewayConfiguration($gateway_type_id);
return $gateway->_id;
}
}

View File

@ -0,0 +1,231 @@
<?php
/**
* 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
*/
namespace App\PaymentDrivers;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Utils\HtmlEngine;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Http;
use App\PaymentDrivers\CBAPowerBoard\CreditCard;
use App\PaymentDrivers\CBAPowerBoard\Customer;
use App\PaymentDrivers\CBAPowerBoard\Settings;
/**
* Class CBAPowerBoardPaymentDriver.
*/
class CBAPowerBoardPaymentDriver extends BaseDriver
{
use MakesHash;
public $token_billing = true;
public $can_authorise_credit_card = false;
public $refundable = true;
public string $api_endpoint = 'https://api.powerboard.commbank.com.au';
public string $widget_endpoint = 'https://widget.powerboard.commbank.com.au/sdk/latest/widget.umd.min.js';
public string $environment = 'production_cba';
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_POWERBOARD;
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
];
/**
* Returns the gateway types.
*/
public function gatewayTypes(): array
{
$types = [];
if ($this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['AUS'])
&& in_array($this->client->currency()->code, ['AUD'])
) {
$types[] = GatewayType::CREDIT_CARD;
}
return $types;
}
public function init(): self
{
if($this->company_gateway->getConfigField('testMode')) {
$this->widget_endpoint = 'https://widget.preproduction.powerboard.commbank.com.au/sdk/latest/widget.umd.min.js';
$this->api_endpoint = 'https://api.preproduction.powerboard.commbank.com.au';
$this->environment = 'preproduction_cba';
}
return $this;
}
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
/**
* Proxy method to pass the data into payment method authorizeView().
*
* @param array $data
* @return \Illuminate\Http\RedirectResponse|mixed
*/
public function authorizeView(array $data)
{
$this->init();
return $this->payment_method->authorizeView($data);
}
/**
* Processes the gateway response for credit card authorization.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|mixed
*/
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
/**
* View for displaying custom content of the driver.
*
* @param array $data
* @return mixed
*/
public function processPaymentView($data)
{
$this->init();
return $this->payment_method->paymentView($data);
}
/**
* Processing method for payment. Should never be reached with this driver.
*
* @return mixed
*/
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request);
}
/**
* Detach payment method from custom payment driver.
*
* @param ClientGatewayToken $token
* @return bool
*/
public function detach(ClientGatewayToken $token): bool
{
// Driver doesn't support this feature.
return true;
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
}
public function processWebhookRequest($request)
{
}
public function getClientRequiredFields(): array
{
$fields = [];
if ($this->company_gateway->require_client_name) {
$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;
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
}
public function importCustomers()
{
}
public function auth(): bool
{
$this->init();
return true;
// try {
// $this->verifyConnect();
// return true;
// } catch(\Exception $e) {
// }
// return false;
}
public function gatewayRequest(string $uri, string $verb, array $payload, array $headers = [])
{
$this->init();
$r = Http::withHeaders($this->getHeaders($headers))
->{$verb}($this->api_endpoint.$uri, $payload);
nlog($r->body());
return $r;
}
public function getHeaders(array $headers = []): array
{
return array_merge([
'x-user-secret-key' => $this->company_gateway->getConfigField('secretKey'),
'Content-Type' => 'application/json',
],
$headers);
}
public function customer(): Customer
{
return new Customer($this);
}
public function settings(): Settings
{
return new Settings($this);
}
}

View File

@ -390,6 +390,14 @@ class Peppol extends AbstractService
$this->p_invoice = $this->e->decode('Peppol', json_encode($this->invoice->e_invoice->Invoice), 'json');
$this->gateway
->mutator
->setInvoice($this->invoice)
->setPeppol($this->p_invoice)
->setClientSettings($this->_client_settings)
->setCompanySettings($this->_company_settings);
return $this;
}

View File

@ -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';

View File

@ -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,
];
}
}

View File

@ -119,7 +119,7 @@
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^10",
"phpunit/phpunit": "^11",
"spatie/laravel-ignition": "^2.0",
"spaze/phpstan-stripe": "^3.0"
},

483
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
<?php
use App\Models\Gateway;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Model::unguard();
$fields = new \stdClass();
$fields->publicKey = '';
$fields->secretKey = '';
$fields->testMode = false;
$fields->threeds = false;
$powerboard = new Gateway();
$powerboard->id = 64;
$powerboard->name = 'CBA PowerBoard';
$powerboard->provider = 'CBAPowerBoard';
$powerboard->key = 'b67581d804dbad1743b61c57285142ad';
$powerboard->sort_order = 4543;
$powerboard->is_offsite = false;
$powerboard->visible = true;
$powerboard->fields = json_encode($fields);
$powerboard->save();
Schema::table("company_gateways", function (\Illuminate\Database\Schema\Blueprint $table){
$table->text('settings')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->integer('remaining_cycles')->nullable()->default(-1);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -88,7 +88,8 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 60, 'name' => 'PayPal REST', 'provider' => 'PayPal_Rest', 'key' => '80af24a6a691230bbec33e930ab40665', 'fields' => '{"clientId":"","secret":"","signature":"","testMode":false}'],
['id' => 61, 'name' => 'PayPal Platform', 'provider' => 'PayPal_PPCP', 'key' => '80af24a6a691230bbec33e930ab40666', 'fields' => '{"testMode":false}'],
['id' => 62, 'name' => 'BTCPay', 'provider' => 'BTCPay', 'key' => 'vpyfbmdrkqcicpkjqdusgjfluebftuva', 'fields' => '{"btcpayUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'],
['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":""}'],
['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":false}'],
['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "Threeds":true}'],
];
foreach ($gateways as $gateway) {

View File

@ -3891,7 +3891,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.',

View File

@ -1,7 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false"
bootstrap="vendor/autoload.php" colors="true" processIsolation="true" stopOnError="true" stopOnFailure="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
cacheDirectory=".phpunit.cache" backupStaticProperties="false">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
bootstrap="vendor/autoload.php"
colors="true"
processIsolation="true"
stopOnError="true"
stopOnFailure="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>

File diff suppressed because one or more lines are too long

1
public/build/assets/app-9c3f71d4.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,13 @@
import{i as a,w as d}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
*/async function i(){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"))),gateway_response:Array.from(document.querySelectorAll("input[name=gateway_response]")).find(e=>e.value).value}),o=document.querySelector("meta[name=store_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;let r=document.getElementById("authorize-card");throw r.disabled=!1,r.querySelector("svg").classList.add("hidden"),r.querySelector("span").classList.remove("hidden"),console.error("Fetch error:",e),e}}async function c(){try{const t=await i();if(t.status==="not_authenticated"||t==="not_authenticated")throw s(),new Error("There was an issue authenticating this payment method.");if(t.status==="authentication_not_supported")return document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(t),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),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,s()}),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;let n=document.getElementById("authorize-card");n.disabled=!1,n.querySelector("svg").classList.add("hidden"),n.querySelector("span").classList.remove("hidden")}}function u(){l();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.load();let r=document.getElementById("authorize-card");return r.disabled=!1,r.querySelector("svg").classList.add("hidden"),r.querySelector("span").classList.remove("hidden"),e}function l(){var t;document.querySelector("#widget").innerHTML="",(t=document.querySelector("#widget"))==null||t.classList.remove("hidden"),document.querySelector("#widget-3dsecure").innerHTML=""}function s(){const t=u();function n(){let r=document.getElementById("pay-now");r.disabled=t.isInvalidForm()}t.trigger("tab",n),t.trigger("submit_form",n),t.trigger("tab",n),t.on("finish",function(r){document.getElementById("errors").hidden=!0,c()});const o=document.querySelector('input[name="payment-type"]');o&&o.click();let e=document.getElementById("authorize-card");e.addEventListener("click",()=>{const r=document.getElementById("widget");if(t.getValidationState(),!t.isValidForm()&&r.offsetParent!==null){e.disabled=!1,e.querySelector("svg").classList.add("hidden"),e.querySelector("span").classList.remove("hidden");return}e.disabled=!0,e.querySelector("svg").classList.remove("hidden"),e.querySelector("span").classList.add("hidden"),r.offsetParent!==null?document.getElementById("stepone_submit").click():document.getElementById("server-response").submit()})}a()?s():d("#").then(s);

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){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

@ -48,6 +48,14 @@
"isEntry": true,
"src": "resources/js/clients/payment_methods/authorize-checkout-card.js"
},
"resources/js/clients/payment_methods/authorize-powerboard-card.js": {
"file": "assets/authorize-powerboard-card-9db77713.js",
"imports": [
"_wait-8f4ae121.js"
],
"isEntry": true,
"src": "resources/js/clients/payment_methods/authorize-powerboard-card.js"
},
"resources/js/clients/payment_methods/authorize-stripe-acss.js": {
"file": "assets/authorize-stripe-acss-f6bd46c1.js",
"imports": [
@ -141,6 +149,14 @@
"isEntry": true,
"src": "resources/js/clients/payments/paytrace-credit-card.js"
},
"resources/js/clients/payments/powerboard-credit-card.js": {
"file": "assets/powerboard-credit-card-9c79019c.js",
"imports": [
"_wait-8f4ae121.js"
],
"isEntry": true,
"src": "resources/js/clients/payments/powerboard-credit-card.js"
},
"resources/js/clients/payments/razorpay-aio.js": {
"file": "assets/razorpay-aio-f8e8c7f0.js",
"imports": [
@ -348,7 +364,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
"file": "assets/app-23f93261.css",
"file": "assets/app-9c3f71d4.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}

View File

@ -0,0 +1,239 @@
/**
* 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
*/
import { instant, wait } from '../wait';
async function get3dsToken() {
const browserDetails = {
name: navigator.userAgent.substring(0, 100), // The full user agent string, which contains the browser name and version
java_enabled: navigator.javaEnabled() ? 'true' : 'false', // Indicates if Java is enabled in the browser
language: navigator.language || navigator.userLanguage, // The browser language
screen_height: window.screen.height.toString(), // Screen height in pixels
screen_width: window.screen.width.toString(), // Screen width in pixels
time_zone: (new Date().getTimezoneOffset() * -1).toString(), // Timezone offset in minutes (negative for behind UTC)
color_depth: window.screen.colorDepth.toString(), // Color depth in bits per pixel
};
document.querySelector('input[name="browser_details"]').value =
JSON.stringify(browserDetails);
const formData = JSON.stringify({
...Object.fromEntries(
new FormData(document.getElementById('server-response'))
),
gateway_response: Array.from(document.querySelectorAll('input[name=gateway_response]')).find(input => input.value).value,
});
const paymentsRoute = document.querySelector('meta[name=store_route]');
try {
// Return the fetch promise to handle it externally
const response = await fetch(paymentsRoute.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: formData,
});
if (!response.ok) {
return await response.json().then((errorData) => {
throw new Error(errorData.message ?? 'Unknown error.');
});
// const text = await response.text();
// throw new Error(`Network response was not ok: ${response.statusText}. Response text: ${text}`);
}
return await response.json();
} catch (error) {
document.getElementById(
'errors'
).textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
document.getElementById('errors').hidden = false;
let payNow = document.getElementById('authorize-card');
payNow.disabled = false;
payNow.querySelector('svg').classList.add('hidden');
payNow.querySelector('span').classList.remove('hidden');
console.error('Fetch error:', error); // Log error for debugging
throw error; //
}
}
async function process3ds() {
try {
const resource = await get3dsToken();
if (resource.status === 'not_authenticated' || resource === 'not_authenticated') {
authorize();
throw new Error(
'There was an issue authenticating this payment method.'
);
}
if (resource.status === 'authentication_not_supported') {
document.querySelector('input[name="browser_details"]').value =
null;
document.querySelector('input[name="charge"]').value =
JSON.stringify(resource);
return document.getElementById('server-response').submit();
}
const canvas = new cba.Canvas3ds(
'#widget-3dsecure',
resource._3ds.token
);
canvas.load();
let widget = document.getElementById('widget');
widget.classList.add('hidden');
canvas.on('chargeAuthSuccess', function (data) {
document.querySelector('input[name="browser_details"]').value =
null;
document.querySelector('input[name="charge"]').value =
JSON.stringify(data);
document.getElementById('server-response').submit();
});
canvas.on('chargeAuthReject', function (data) {
document.getElementById(
'errors'
).textContent = `Sorry, your transaction could not be processed...`;
document.getElementById('errors').hidden = false;
authorize();
});
canvas.load();
} catch (error) {
console.error('Error fetching 3DS Token:', error);
document.getElementById(
'errors'
).textContent = `Sorry, your transaction could not be processed...\n\n${error}`;
document.getElementById('errors').hidden = false;
let payNow = document.getElementById('authorize-card');
payNow.disabled = false;
payNow.querySelector('svg').classList.add('hidden');
payNow.querySelector('span').classList.remove('hidden');
}
}
function setup() {
reload();
const publicKey = document.querySelector('meta[name=public_key]');
const gatewayId = document.querySelector('meta[name=gateway_id]');
const env = document.querySelector('meta[name=environment]');
const widget = new cba.HtmlWidget(
'#widget',
publicKey?.content,
gatewayId?.content
);
widget.setEnv(env?.content);
widget.useAutoResize();
widget.interceptSubmitForm('#stepone');
widget.onFinishInsert(
'#server-response input[name="gateway_response"]',
'payment_source'
);
widget.setFormFields(['card_name*']);
widget.load();
let payNow = document.getElementById('authorize-card');
payNow.disabled = false;
payNow.querySelector('svg').classList.add('hidden');
payNow.querySelector('span').classList.remove('hidden');
return widget;
}
function reload() {
document.querySelector('#widget').innerHTML = '';
document.querySelector('#widget')?.classList.remove('hidden');
document.querySelector('#widget-3dsecure').innerHTML = '';
}
export function authorize() {
const widget = setup();
function handleTrigger() {
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
}
widget.trigger('tab', handleTrigger);
widget.trigger('submit_form', handleTrigger);
widget.trigger('tab', handleTrigger);
widget.on('finish', function (data) {
document.getElementById('errors').hidden = true;
process3ds();
});
const first = document.querySelector('input[name="payment-type"]');
if (first) {
first.click();
}
let authorizeCard = document.getElementById('authorize-card');
authorizeCard.addEventListener('click', () => {
const div = document.getElementById('widget');
widget.getValidationState();
if (!widget.isValidForm() && div.offsetParent !== null) {
authorizeCard.disabled = false;
authorizeCard.querySelector('svg').classList.add('hidden');
authorizeCard.querySelector('span').classList.remove('hidden');
return;
}
authorizeCard.disabled = true;
authorizeCard.querySelector('svg').classList.remove('hidden');
authorizeCard.querySelector('span').classList.add('hidden');
if (div.offsetParent !== null) {
document.getElementById('stepone_submit').click();
} else {
document.getElementById('server-response').submit();
}
});
}
instant() ? authorize() : wait('#').then(authorize);

View File

@ -0,0 +1,276 @@
/**
* 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
*/
import { instant, wait } from '../wait';
function setup() {
const publicKey = document.querySelector('meta[name=public_key]');
const gatewayId = document.querySelector('meta[name=gateway_id]');
const env = document.querySelector('meta[name=environment]');
const widget = new cba.HtmlWidget(
'#widget',
publicKey?.content,
gatewayId?.content
);
widget.setEnv(env?.content);
widget.useAutoResize();
widget.interceptSubmitForm('#stepone');
widget.onFinishInsert(
'#server-response input[name="gateway_response"]',
'payment_source'
);
widget.setFormFields(['card_name*']);
widget.reload();
let payNow = document.getElementById('pay-now');
payNow.disabled = false;
payNow.querySelector('svg').classList.add('hidden');
payNow.querySelector('span').classList.remove('hidden');
document.querySelector(
'#server-response input[name="gateway_response"]'
).value = '';
return widget;
}
function reload() {
document.querySelector('#widget')?.replaceChildren();
document.querySelector('#widget')?.classList.remove('hidden');
document.querySelector('#widget-3dsecure')?.replaceChildren();
}
function pay() {
reload();
const widget = setup();
widget.on('finish', () => {
document.getElementById('errors').hidden = true;
process3ds();
});
widget.on('submit', function (data) {
document.getElementById('errors').hidden = true;
});
let payNow = document.getElementById('pay-now');
payNow.addEventListener('click', () => {
const div = document.getElementById('widget');
widget.getValidationState();
if (!widget.isValidForm() && div.offsetParent !== null) {
payNow.disabled = false;
payNow.querySelector('svg').classList.add('hidden');
payNow.querySelector('span').classList.remove('hidden');
return;
}
payNow.disabled = true;
payNow.querySelector('svg').classList.remove('hidden');
payNow.querySelector('span').classList.add('hidden');
let storeCard = document.querySelector(
'input[name=token-billing-checkbox]:checked'
);
if (storeCard) {
document.getElementById('store_card').value = storeCard.value;
}
if (div.offsetParent !== null)
document.getElementById('stepone_submit').click();
else document.getElementById('server-response').submit();
});
document
.getElementById('toggle-payment-with-credit-card')
.addEventListener('click', (element) => {
let widget = document.getElementById('widget');
widget.classList.remove('hidden');
document.getElementById('save-card--container').style.display =
'grid';
document.querySelector('input[name=token]').value = '';
document
.querySelector('#powerboard-payment-container')
?.classList.remove('hidden');
});
Array.from(
document.getElementsByClassName('toggle-payment-with-token')
).forEach((element) =>
element.addEventListener('click', (element) => {
document.getElementById('widget').classList.add('hidden');
document.getElementById('save-card--container').style.display =
'none';
document.querySelector('input[name=token]').value =
element.target.dataset.token;
document
.querySelector('#powerboard-payment-container')
?.classList.add('hidden');
})
);
const first = document.querySelector('input[name="payment-type"]');
if (first) {
first.click();
}
}
async function process3ds() {
try {
const resource = await get3dsToken();
if (resource.status === 'not_authenticated' || resource === 'not_authenticated') {
pay();
throw new Error(
'There was an issue authenticating this payment method.'
);
}
if (resource.status === 'authentication_not_supported') {
document.querySelector('input[name="browser_details"]').value =
null;
document.querySelector('input[name="charge"]').value =
JSON.stringify(resource);
let storeCard = document.querySelector(
'input[name=token-billing-checkbox]:checked'
);
if (storeCard) {
document.getElementById('store_card').value = storeCard.value;
}
return document.getElementById('server-response').submit();
}
const canvas = new cba.Canvas3ds(
'#widget-3dsecure',
resource._3ds.token
);
canvas.load();
let widget = document.getElementById('widget');
widget.classList.add('hidden');
canvas.on('chargeAuthSuccess', function (data) {
document.querySelector('input[name="browser_details"]').value =
null;
document.querySelector('input[name="charge"]').value =
JSON.stringify(data);
let storeCard = document.querySelector(
'input[name=token-billing-checkbox]:checked'
);
if (storeCard) {
document.getElementById('store_card').value = storeCard.value;
}
document.getElementById('server-response').submit();
});
canvas.on('chargeAuthReject', function (data) {
document.getElementById(
'errors'
).textContent = `Sorry, your transaction could not be processed...`;
document.getElementById('errors').hidden = false;
pay();
});
canvas.load();
} catch (error) {
document.getElementById(
'errors'
).textContent = `Sorry, your transaction could not be processed...\n\n${error}`;
document.getElementById('errors').hidden = false;
pay();
}
}
async function get3dsToken() {
const browserDetails = {
name: navigator.userAgent.substring(0, 100), // The full user agent string, which contains the browser name and version
java_enabled: navigator.javaEnabled() ? 'true' : 'false', // Indicates if Java is enabled in the browser
language: navigator.language || navigator.userLanguage, // The browser language
screen_height: window.screen.height.toString(), // Screen height in pixels
screen_width: window.screen.width.toString(), // Screen width in pixels
time_zone: (new Date().getTimezoneOffset() * -1).toString(), // Timezone offset in minutes (negative for behind UTC)
color_depth: window.screen.colorDepth.toString(), // Color depth in bits per pixel
};
document.querySelector('input[name="browser_details"]').value =
JSON.stringify(browserDetails);
const formData = JSON.stringify(
Object.fromEntries(
new FormData(document.getElementById('server-response'))
)
);
const paymentsRoute = document.querySelector('meta[name=payments_route]');
try {
// Return the fetch promise to handle it externally
const response = await fetch(paymentsRoute.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: formData,
});
if (!response.ok) {
return await response.json().then((errorData) => {
throw new Error(errorData.message ?? 'Unknown error.');
});
// const text = await response.text();
// throw new Error(`Network response was not ok: ${response.statusText}. Response text: ${text}`);
}
return await response.json();
} catch (error) {
document.getElementById(
'errors'
).textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
document.getElementById('errors').hidden = false;
console.error('Fetch error:', error); // Log error for debugging
pay();
}
}
instant() ? pay() : wait('#powerboard-credit-card-payment').then(() => pay());

View File

@ -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'))

View File

@ -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>

View File

@ -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

View File

@ -93,7 +93,7 @@ inset: 6px;
document.getElementById("gateway_response").value =JSON.stringify( data );
formData = JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server_response")))),
fetch('{{ route('client.payments.response') }}', {
method: 'POST',
headers: {

View File

@ -0,0 +1,54 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
@section('gateway_head')
<meta name="instant-payment" content="yes" />
<meta name="public_key" content="{{ $public_key }}" />
<meta name="gateway_id" content="{{ $gateway_id }}" />
<meta name="store_route" content="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" />
<meta name="environment" content="{{ $environment }}" />
@endsection
@section('gateway_content')
<form action="javascript:void(0);" id="stepone">
<input type="hidden" name="gateway_response">
<button type="submit" class="hidden" id="stepone_submit">Submit</button>
</form>
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="browser_details">
<input type="hidden" name="charge">
<button type="submit" class="hidden" id="stub">Submit</button>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div id="powerboard-payment-container" class="w-full p-4" style="background-color: rgb(249, 249, 249);">
<div id="widget" style="block"></div>
<div id="widget-3dsecure"></div>
</div>
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@section('gateway_footer')
<style>
iframe {
border: 0;
width: 100%;
height: 400px;
}
</style>
<script src="{{ $widget_endpoint }}"></script>
@vite('resources/js/clients/payment_methods/authorize-powerboard-card.js')
@endsection

View File

@ -0,0 +1,99 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
@section('gateway_head')
<meta name="instant-payment" content="yes" />
<meta name="public_key" content="{{ $public_key }}" />
<meta name="gateway_id" content="{{ $gateway_id }}" />
<meta name="environment" content="{{ $environment }}">
<meta name="payments_route" content="{{ route('client.payments.response') }}" />
@endsection
@section('gateway_content')
<form action="javascript:void(0);" id="stepone">
<input type="hidden" name="gateway_response">
<button type="submit" class="hidden" id="stepone_submit">Submit</button>
</form>
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card" id="store_card"/>
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="token">
<input type="hidden" name="browser_details">
<input type="hidden" name="charge">
<input type="hidden" name="charge_3ds_id">
<button type="submit" class="hidden" id="stub">Submit</button>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@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', ['title' => ctrans('texts.pay_with')])
<ul class="list-none">
@if(count($tokens) > 0)
@foreach($tokens as $token)
<li class="py-2 cursor-pointer">
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-check-input text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
</li>
@endforeach
@endisset
<li class="py-2 cursor-pointer">
<label>
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-check-input text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.includes.save_card')
<div id="powerboard-payment-container" class="w-full p-4 hidden" style="background-color: rgb(249, 249, 249);">
<div id="widget" style="block" class="hidden"></div>
<div id="widget-3dsecure"></div>
</div>
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@section('gateway_footer')
<style>
iframe {
border: 0;
width: 100%;
height: 400px;
}
</style>
<script src="{{ $widget_endpoint }}"></script>
@vite('resources/js/clients/payments/powerboard-credit-card.js')
@endsection

View File

@ -0,0 +1,90 @@
<div class="rounded-lg border bg-card text-card-foreground shadow-sm overflow-hidden py-5 bg-white sm:gap-4"
id="powerboard-credit-card-payment">
<meta name="public_key" content="{{ $public_key }}" />
<meta name="gateway_id" content="{{ $gateway_id }}" />
<meta name="environment" content="{{ $environment }}">
<meta name="payments_route" content="{{ route('client.payments.response') }}" />
<form action="javascript:void(0);" id="stepone">
<input type="hidden" name="gateway_response">
<button type="submit" class="hidden" id="stepone_submit">Submit</button>
</form>
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card" id="store_card"/>
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="token">
<input type="hidden" name="browser_details">
<input type="hidden" name="charge">
<input type="hidden" name="charge_3ds_id">
<button type="submit" class="hidden" id="stub">Submit</button>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@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', ['title' => ctrans('texts.pay_with')])
<ul class="list-none">
@if(count($tokens) > 0)
@foreach($tokens as $token)
<li class="py-2 cursor-pointer">
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-check-input text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
</li>
@endforeach
@endisset
<li class="py-2 cursor-pointer">
<label>
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-check-input text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.includes.save_card')
<div id="powerboard-payment-container" class="w-full p-4 hidden" style="background-color: rgb(249, 249, 249);">
<div id="widget" style="block" class="hidden"></div>
<div id="widget-3dsecure"></div>
</div>
@include('portal.ninja2020.gateways.includes.pay_now')
<style>
iframe {
border: 0;
width: 100%;
height: 400px;
}
</style>
</div>
@assets
<script src="{{ $widget_endpoint }}"></script>
@vite('resources/js/clients/payments/powerboard-credit-card.js')
@endassets

View File

@ -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');

View File

@ -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">

View File

@ -17,10 +17,7 @@ use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\ActivityController
*/
class ActivityApiTest extends TestCase
{
use DatabaseTransactions;

View File

@ -17,10 +17,6 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\ApplePayDomainController
*/
class ApplePayDomainMerchantUrlTest extends TestCase
{
use DatabaseTransactions;

View File

@ -18,10 +18,7 @@ use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\BankIntegrationController
*/
class BankIntegrationApiTest extends TestCase
{
use MakesHash;

View File

@ -21,10 +21,7 @@ use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\BankTransactionController
*/
class BankTransactionApiTest extends TestCase
{
use MakesHash;

View File

@ -18,10 +18,7 @@ use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\BankTransactionRuleController
*/
class BankTransactionRuleApiTest extends TestCase
{
use MakesHash;

View File

@ -54,10 +54,6 @@ use Illuminate\Testing\Fluent\AssertableJson;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\BaseController
*/
class BaseApiTest extends TestCase
{
use MockAccountData;

View File

@ -19,10 +19,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Services\Invoice\HandleCancellation
*/
class CancelInvoiceTest extends TestCase
{
use MakesHash;

View File

@ -17,7 +17,7 @@ use Tests\MockUnitData;
use Tests\TestCase;
/**
* @test
*
*/
class ClassificationTest extends TestCase
{

View File

@ -33,10 +33,6 @@ use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\ClientController
*/
class ClientApiTest extends TestCase
{
use MakesHash;

View File

@ -19,10 +19,6 @@ use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\InvoiceController
*/
class ClientDeletedInvoiceCreationTest extends TestCase
{
use MakesHash;

View File

@ -20,10 +20,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
* @covers App\Http\Controllers\ClientGatewayTokenController
*/
class ClientGatewayTokenApiTest extends TestCase
{
use MakesHash;

View File

@ -18,10 +18,6 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Models\Client
*/
class ClientModelTest extends TestCase
{
use MockAccountData;

View File

@ -16,8 +16,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Models\Presenters\ClientPresenter
*
* App\Models\Presenters\ClientPresenter
*/
class ClientPresenterTest extends TestCase
{

View File

@ -37,8 +37,8 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
* @covers App\Http\Controllers\ClientController
*
* App\Http\Controllers\ClientController
*/
class ClientTest extends TestCase
{
@ -405,7 +405,7 @@ class ClientTest extends TestCase
}
/*
* @covers ClientController
* ClientController
*/
public function testClientRestEndPoints()
{

View File

@ -24,8 +24,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Models\CompanyGateway
*
* App\Models\CompanyGateway
*/
class CompanyGatewayApiTest extends TestCase
{

View File

@ -23,7 +23,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class CompanyGatewayResolutionTest extends TestCase
{
@ -105,7 +105,7 @@ class CompanyGatewayResolutionTest extends TestCase
}
/**
* @covers \App\Models\CompanyGateway::calcGatewayFee()
* \App\Models\CompanyGateway::calcGatewayFee()
*/
public function testGatewayResolution()
{
@ -114,7 +114,7 @@ class CompanyGatewayResolutionTest extends TestCase
}
/**
* @covers \App|Models\Client::validGatewayForAmount()
* \App|Models\Client::validGatewayForAmount()
*/
public function testValidationForGatewayAmount()
{

View File

@ -21,8 +21,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Models\CompanyGateway
*
* App\Models\CompanyGateway
*/
class CompanyGatewayTest extends TestCase
{

View File

@ -22,8 +22,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Utils\Traits\CompanySettingsSaver
*
* App\Utils\Traits\CompanySettingsSaver
*/
class CompanySettingsTest extends TestCase
{

View File

@ -11,29 +11,30 @@
namespace Tests\Feature;
use App\DataMapper\CompanySettings;
use App\Http\Middleware\PasswordProtection;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\TaxRate;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
use App\Models\Account;
use App\Models\Company;
use App\Models\TaxRate;
use Tests\MockAccountData;
use App\Models\CompanyToken;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\UploadedFile;
use App\DataMapper\CompanySettings;
use Illuminate\Support\Facades\Log;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Session;
use App\Http\Middleware\PasswordProtection;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
* @covers App\Http\Controllers\CompanyController
*
* App\Http\Controllers\CompanyController
*/
class CompanyTest extends TestCase
{
use MakesHash;
use MockAccountData;
use DatabaseTransactions;
// use DatabaseTransactions;
public $faker;
@ -41,30 +42,29 @@ class CompanyTest extends TestCase
{
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();
}
public function testCompanyExpenseMailbox()
{
$safeEmail = $this->faker->safeEmail();
// Test valid email address
$company_update = [
'expense_mailbox' => 'valid@example.com',
'expense_mailbox' => $safeEmail,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update);
])->putJson('/api/v1/companies/'.$this->company->hashed_id, $company_update);
$response->assertStatus(200);
$this->assertEquals('valid@example.com', $response->json('data.expense_mailbox'));
$arr = $response->json();
$this->assertEquals($safeEmail, $arr['data']['expense_mailbox']);
// Test invalid email address
$company_update = [
@ -260,5 +260,11 @@ class CompanyTest extends TestCase
->assertStatus(200);
}
public function tearDown(): void
{
Account::query()->where('id', $this->company->account_id)->delete();
parent::tearDown();
}
}

View File

@ -22,8 +22,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\TokenController
*
* App\Http\Controllers\TokenController
*/
class CompanyTokenApiTest extends TestCase
{

View File

@ -23,7 +23,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class DeleteInvoiceTest extends TestCase
{
@ -302,7 +302,7 @@ class DeleteInvoiceTest extends TestCase
}
/**
* @covers App\Services\Invoice\MarkInvoiceDeleted
* App\Services\Invoice\MarkInvoiceDeleted
*/
public function testInvoiceDeletion()
{
@ -410,7 +410,7 @@ class DeleteInvoiceTest extends TestCase
}
/**
* @covers App\Services\Invoice\HandleRestore
* App\Services\Invoice\HandleRestore
*/
public function testInvoiceDeletionAndRestoration()
{

View File

@ -21,8 +21,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\DesignController
*
* App\Http\Controllers\DesignController
*/
class DesignApiTest extends TestCase
{

View File

@ -21,8 +21,8 @@ use Illuminate\Support\Facades\Session;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
* @covers App\Http\Controllers\DocumentController
*
* App\Http\Controllers\DocumentController
*/
class DocumentsApiTest extends TestCase
{

View File

@ -17,7 +17,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class FacturaeTest extends TestCase
{

View File

@ -29,7 +29,7 @@ use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaBodyType\FatturaEle
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaHeaderType\FatturaElettronicaHeader;
/**
* @test
*
*/
class FatturaPATest extends TestCase
{

View File

@ -34,7 +34,7 @@ use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaBodyType\FatturaEle
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaHeaderType\FatturaElettronicaHeader;
/**
* @test
*
*/
class PeppolTest extends TestCase
{

View File

@ -23,7 +23,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class EntityPaidToDateTest extends TestCase
{

View File

@ -23,8 +23,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\ExpenseController
*
* App\Http\Controllers\ExpenseController
*/
class ExpenseApiTest extends TestCase
{

View File

@ -19,8 +19,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\ExpenseCategoryController
*
* App\Http\Controllers\ExpenseCategoryController
*/
class ExpenseCategoryApiTest extends TestCase
{

View File

@ -25,7 +25,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
*
*/
class ArDetailReportTest extends TestCase
{

View File

@ -24,7 +24,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
*
*/
class ArSummaryReportTest extends TestCase
{

View File

@ -24,7 +24,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
*
*/
class ClientBalanceReportTest extends TestCase
{

View File

@ -17,7 +17,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class ClientCsvTest extends TestCase
{

View File

@ -24,7 +24,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
*
*/
class ClientSalesReportTest extends TestCase
{

View File

@ -18,7 +18,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class ExportCompanyTest extends TestCase
{

View File

@ -20,7 +20,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class ExportCsvTest extends TestCase
{

View File

@ -26,8 +26,8 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
* @covers App\Services\Report\ProductSalesExport
*
* App\Services\Report\ProductSalesExport
*/
class ProductSalesReportTest extends TestCase
{

View File

@ -28,8 +28,8 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
* @covers App\Services\Report\ProfitLoss
*
* App\Services\Report\ProfitLoss
*/
class ProfitAndLossReportTest extends TestCase
{

View File

@ -17,7 +17,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class ReportApiTest extends TestCase
{

View File

@ -34,7 +34,7 @@ use League\Csv\Reader;
use Tests\TestCase;
/**
* @test
*
*/
class ReportCsvGenerationTest extends TestCase
{

View File

@ -33,7 +33,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class ReportPreviewTest extends TestCase
{

View File

@ -24,7 +24,7 @@ use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/**
* @test
*
*/
class TaxSummaryReportTest extends TestCase
{

View File

@ -26,7 +26,7 @@ use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
/**
* @test
*
*/
class UserSalesReportTest extends TestCase
{

View File

@ -17,8 +17,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\FilterController
*
* App\Http\Controllers\FilterController
*/
class FilterApiTest extends TestCase
{

View File

@ -24,7 +24,7 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*
*/
class GoCardlessInstantBankPaymentTest extends TestCase
{

View File

@ -24,8 +24,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Import\Transformer\BaseTransformer
*
* App\Import\Transformer\BaseTransformer
*/
class BaseTransformerTest extends TestCase
{

View File

@ -25,8 +25,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Import\Providers\Csv
*
* App\Import\Providers\Csv
*/
class CsvImportTest extends TestCase
{

View File

@ -22,8 +22,8 @@ use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Import\Providers\Csv
*
* App\Import\Providers\Csv
*/
class TaskImportTest extends TestCase
{

Some files were not shown because too many files have changed in this diff Show More