Merge pull request #27 from M-E-Development-Design/v5-develop

V5 develop
This commit is contained in:
Kendall Arneaud 2024-08-09 17:55:48 -04:00 committed by GitHub
commit ae75f7228c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
127 changed files with 8762 additions and 10782 deletions

View File

@ -1 +1 @@
5.10.16
5.10.19

View File

@ -15,7 +15,6 @@ use App\Jobs\Cron\AutoBillCron;
use App\Jobs\Cron\RecurringExpensesCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Cron\UpdateCalculatedFields;
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\BankTransactionSync;
@ -33,6 +32,7 @@ use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\PaymentDrivers\Rotessa\Jobs\TransactionReport;
use App\Utils\Ninja;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -65,12 +65,12 @@ class Kernel extends ConsoleKernel
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Checks Rotessa Transactions */
$schedule->job(new TransactionReport())->dailyAt('01:48')->withoutOverlapping()->name('rotessa-transaction-report')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder())->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new UpdateCalculatedFields())->hourlyAt(40)->withoutOverlapping()->name('update-calculated-fields-job')->onOneServer();
/* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck())->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();

View File

@ -1,4 +1,13 @@
<?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\DataProviders;

View File

@ -1,19 +0,0 @@
<?php
namespace App\DataProviders;
use Omnipay\Rotessa\Object\Frequency;
final class Frequencies
{
public static function get() : array {
return Frequency::getTypes();
}
public static function getFromType() {
}
public static function getOnePayment() {
return Frequency::ONCE;
}
}

View File

@ -22,5 +22,5 @@
*/
function ctrans(string $string, $replace = [], $locale = null): string
{
return trans($string, $replace, $locale);
return html_entity_decode(trans($string, $replace, $locale));
}

View File

@ -80,7 +80,7 @@ class NinjaPlanController extends Controller
$data['intent'] = $setupIntent;
$data['client'] = Auth::guard('contact')->user()->client;
return $this->render('plan.trial', $data);
}
@ -88,6 +88,8 @@ class NinjaPlanController extends Controller
{
$trial_started = "Trial Started @ ".now()->format('Y-m-d H:i:s');
auth()->guard('contact')->user()->fill($request->only(['first_name','last_name']))->save();
$client = auth()->guard('contact')->user()->client;
$client->private_notes = $trial_started;
$client->fill($request->all());

View File

@ -567,9 +567,9 @@ class CompanyGatewayController extends BaseController
{
//Throttle here
if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) {
return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
}
// if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) {
// return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
// }
dispatch(function () use ($company_gateway) {
MultiDB::setDb($company_gateway->company->db);

View File

@ -59,9 +59,9 @@ class ExportController extends BaseController
/** @var \App\Models\User $user */
$user = auth()->user();
$hash = Str::uuid();
$hash = Str::uuid()->toString();
$url = \Illuminate\Support\Facades\URL::temporarySignedRoute('protected_download', now()->addHour(), ['hash' => $hash]);
Cache::put($hash, $url, now()->addHour());
Cache::put($hash, $url, 3600);
CompanyExport::dispatch($user->getCompany(), $user, $hash);

View File

@ -298,6 +298,9 @@ class PreviewController extends BaseController
->mock();
} catch(SyntaxError $e) {
}
catch(\Exception $e) {
return response()->json(['message' => 'invalid data access', 'errors' => ['design.design.body' => $e->getMessage()]], 422);
}
if (request()->query('html') == 'true') {
return $ts->getHtml();

View File

@ -14,7 +14,7 @@ namespace App\Http\Controllers\VendorPortal;
use App\Http\Controllers\Controller;
use App\Models\VendorContact;
use App\Utils\Traits\MakesHash;
use App\Utils\TranslationHelper;
use Illuminate\Http\Request;
class VendorContactController extends Controller
{
@ -58,14 +58,14 @@ class VendorContactController extends Controller
'settings' => $vendor_contact->vendor->company->settings,
'company' => $vendor_contact->vendor->company,
'sidebar' => $this->sidebarMenu(),
'countries' => TranslationHelper::getCountries(),
'countries' => app('countries'),
]);
}
public function update(VendorContact $vendor_contact)
public function update(Request $request, VendorContact $vendor_contact)
{
$vendor_contact->fill(request()->all());
$vendor_contact->vendor->fill(request()->all());
$vendor_contact->fill($request->all());
$vendor_contact->vendor->fill($request->all());
$vendor_contact->push();
return back()->withSuccess(ctrans('texts.profile_updated_successfully'));
@ -76,16 +76,10 @@ class VendorContactController extends Controller
$enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules;
$data = [];
// TODO: Enable dashboard once it's completed.
// $this->settings->enable_client_portal_dashboard
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) {
$data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text'];
}
// $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
return $data;
}
}

View File

@ -79,7 +79,7 @@ class ContactRegister
// As a fallback for self-hosted, it will use default company in the system
// if key isn't provided in the url.
if (! $request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Account::query()->first()->default_company;
$company = Account::query()->first()->default_company ?? Account::query()->first()->companies->first();
if (! $company->client_can_register) {
abort(400, 'Registration disabled');

View File

@ -4,15 +4,15 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Limiters\DurationLimiter;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottleRequestsWithPredis extends ThrottleRequests
class ThrottleRequestsWithPredis extends \Illuminate\Routing\Middleware\ThrottleRequests
{
/**
* The Redis factory implementation.
*
* @var \Illuminate\Redis\Connections\Connection
* @var \Illuminate\Contracts\Redis\Factory
*/
protected $redis;
@ -32,14 +32,14 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
/** @phpstan-ignore-next-line */
public function __construct(RateLimiter $limiter, Redis $redis)
{
parent::__construct($limiter);
/** @phpstan-ignore-next-line */
$this->redis = \Illuminate\Support\Facades\Redis::connection('sentinel-cache');
}
@ -56,7 +56,7 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
}
@ -79,16 +79,13 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
*
* @param string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return mixed
*/
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
{
$limiter = new DurationLimiter(
$this->redis,
$key,
$maxAttempts,
$decayMinutes * 60
$this->getRedisConnection(), $key, $maxAttempts, $decaySeconds
);
return tap(! $limiter->acquire(), function () use ($key, $limiter) {
@ -121,4 +118,13 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
{
return $this->decaysAt[$key] - $this->currentTime();
}
/**
* Get the Redis connection that should be used for throttling.
*
*/
protected function getRedisConnection()
{
return $this->redis;
}
}

View File

@ -31,12 +31,8 @@ class TwigLint implements ValidationRule
try {
$twig->parse($twig->tokenize(new \Twig\Source(preg_replace('/<!--.*?-->/s', '', $value), '')));
} catch (\Twig\Error\SyntaxError $e) {
// echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
nlog($e->getMessage());
$fail($e->getMessage());
}
}
}

View File

@ -1,4 +1,13 @@
<?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\Http\ViewComposers\Components\Rotessa;
@ -27,16 +36,13 @@ class AccountComponent extends Component
'routing_number' => null,
'institution_number' => null,
'transit_number' => null,
'bank_name' => ' ',
'bank_name' => null,
'account_number' => null,
'country' => 'US',
"authorization_type" => 'Online'
];
public array $account;
public function __construct(array $account) {
$this->account = $account;
public function __construct(public array $account) {
$this->attributes = $this->newAttributeBag(Arr::only($this->account, $this->fields) );
}

View File

@ -1,4 +1,13 @@
<?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\Http\ViewComposers\Components\Rotessa;
@ -25,10 +34,7 @@ class AddressComponent extends Component
'country' => 'US'
];
public array $address;
public function __construct(array $address) {
$this->address = $address;
public function __construct(public array $address) {
if(strlen($this->address['state']) > 2 ) {
$this->address['state'] = $this->address['country'] == 'US' ? array_search($this->address['state'], USStates::$states) : CAProvinces::getAbbreviation($this->address['state']);
}

View File

@ -1,4 +1,13 @@
<?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\Http\ViewComposers\Components\Rotessa;
@ -18,9 +27,9 @@ class ContactComponent extends Component
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' =>$contact->client->phone,
'custom_identifier' => $contact->client->number,
'custom_identifier' => $contact->client->client_hash,
'name' =>$contact->client->name,
'id' => $contact->client->contact_key,
'id' => null,
] )->all();
$this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) );

View File

@ -1,123 +0,0 @@
<?php
namespace App\Http\ViewComposers\Components;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
use Illuminate\View\Component;
use App\Models\ClientContact;
use Illuminate\Support\Arr;
use Illuminate\View\View;
// Contact Component
class ContactComponent extends Component
{
public function __construct(ClientContact $contact) {
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' =>$contact->client->phone,
'custom_identifier' => $contact->client->number,
'name' =>$contact->client->name,
'id' => null
] )->all();
$this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) );
}
private $fields = [
'name',
'email',
'home_phone',
'phone',
'custom_identifier',
'customer_type' ,
'id'
];
private $defaults = [
'customer_type' => "Business",
'customer_identifier' => null,
'id' => null
];
public function render()
{
return render('gateways.rotessa.components.contact', array_merge($this->defaults, $this->attributes->getAttributes() ) );
}
}
// Address Component
class AddressComponent extends Component
{
private $fields = [
'address_1',
'address_2',
'city',
'postal_code',
'province_code',
'country'
];
private $defaults = [
'country' => 'US'
];
public array $address;
public function __construct(array $address) {
$this->address = $address;
if(strlen($this->address['state']) > 2 ) {
$this->address['state'] = $this->address['country'] == 'US' ? array_search($this->address['state'], USStates::$states) : CAProvinces::getAbbreviation($this->address['state']);
}
$this->attributes = $this->newAttributeBag(
Arr::only(Arr::mapWithKeys($this->address, function ($item, $key) {
return in_array($key, ['address1','address2','state'])?[ (['address1'=>'address_1','address2'=>'address_2','state'=>'province_code'])[$key] => $item ] :[ $key => $item ];
}),
$this->fields) );
}
public function render()
{
return render('gateways.rotessa.components.address',array_merge( $this->defaults, $this->attributes->getAttributes() ) );
}
}
// AmericanBankInfo Component
class AccountComponent extends Component
{
private $fields = [
'bank_account_type',
'routing_number',
'institution_number',
'transit_number',
'bank_name',
'country',
'account_number'
];
private $defaults = [
'bank_account_type' => null,
'routing_number' => null,
'institution_number' => null,
'transit_number' => null,
'bank_name' => ' ',
'account_number' => null,
'country' => 'US',
"authorization_type" => 'Online'
];
public array $account;
public function __construct(array $account) {
$this->account = $account;
$this->attributes = $this->newAttributeBag(Arr::only($this->account, $this->fields) );
}
public function render()
{
return render('gateways.rotessa.components.account', array_merge($this->attributes->getAttributes(), $this->defaults) );
}
}

View File

@ -88,11 +88,11 @@ class PortalComposer
$data['sidebar'] = $this->sidebarMenu();
$data['header'] = [];
$data['footer'] = [];
$data['countries'] = TranslationHelper::getCountries();
$data['countries'] = app('countries');
$data['company'] = auth()->guard('contact')->user()->company;
$data['client'] = auth()->guard('contact')->user()->client;
$data['settings'] = $this->settings;
$data['currencies'] = TranslationHelper::getCurrencies();
$data['currencies'] = app('currencies');
$data['contact'] = auth()->guard('contact')->user();
$data['multiple_contacts'] = session()->get('multiple_contacts') ?: collect();
@ -136,11 +136,11 @@ class PortalComposer
$data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity'];
if (Ninja::isHosted() && auth()->guard('contact')->user()->company->id == config('ninja.ninja_default_company_id')) {
// if (Ninja::isHosted() && auth()->guard('contact')->user()->company->id == config('ninja.ninja_default_company_id')) {
$data[] = ['title' => ctrans('texts.plan'), 'url' => 'client.plan', 'icon' => 'credit-card'];
} else {
// } else {
$data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar'];
}
// }
if (auth()->guard('contact')->user()->client->getSetting('client_initiated_payments')) {
$data[] = ['title' => ctrans('texts.pre_payment'), 'url' => 'client.pre_payments.index', 'icon' => 'dollar-sign'];

View File

@ -1,4 +1,13 @@
<?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
*/
use Illuminate\Support\Facades\View;
use App\DataProviders\CAProvinces;
@ -9,7 +18,6 @@ View::composer(['*.rotessa.components.address','*.rotessa.components.banks.US.ba
$view->with('states', $states);
});
// CAProvinces View Composer
View::composer(['*.rotessa.components.address','*.rotessa.components.banks.CA.bank','*.rotessa.components.dropdowns.country.CA'], function ($view) {
$provinces = CAProvinces::get();
$view->with('provinces', $provinces);

View File

@ -695,7 +695,7 @@ class CompanyExport implements ShouldQueue
$url = Cache::get($this->hash);
Cache::put($this->hash, $storage_path, now()->addHour());
Cache::put($this->hash, $storage_path, 3600);
App::forgetInstance('translator');
$t = app('translator');

View File

@ -48,12 +48,12 @@ class RecurringInvoicesCron
Auth::logout();
if (! config('ninja.db.multi_db_enabled')) {
$recurring_invoices = RecurringInvoice::query()->where('recurring_invoices.status_id', RecurringInvoice::STATUS_ACTIVE)
->where('recurring_invoices.is_deleted', false)
->where('recurring_invoices.remaining_cycles', '!=', '0')
->whereNotNull('recurring_invoices.next_send_date')
->whereNull('recurring_invoices.deleted_at')
->where('recurring_invoices.next_send_date', '<=', now()->toDateTimeString())
$recurring_invoices = RecurringInvoice::query()->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', false)
->where('remaining_cycles', '!=', '0')
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
@ -87,27 +87,18 @@ class RecurringInvoicesCron
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$recurring_invoices = RecurringInvoice::query()->where('recurring_invoices.status_id', RecurringInvoice::STATUS_ACTIVE)
->where('recurring_invoices.is_deleted', false)
->where('recurring_invoices.remaining_cycles', '!=', '0')
->whereNull('recurring_invoices.deleted_at')
->whereNotNull('recurring_invoices.next_send_date')
->where('recurring_invoices.next_send_date', '<=', now()->toDateTimeString())
// ->whereHas('client', function ($query) {
// $query->where('is_deleted', 0)
// ->where('deleted_at', null);
// })
// ->whereHas('company', function ($query) {
// $query->where('is_disabled', 0);
// })
->leftJoin('clients', function ($join) {
$join->on('recurring_invoices.client_id', '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
$recurring_invoices = RecurringInvoice::query()->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', false)
->where('remaining_cycles', '!=', '0')
->whereNull('deleted_at')
->whereNotNull('next_send_date')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->leftJoin('companies', function ($join) {
$join->on('recurring_invoices.company_id', '=', 'companies.id')
->where('companies.is_disabled', 0);
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('company')
->cursor();

View File

@ -1,105 +0,0 @@
<?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\Jobs\Cron;
use App\Libraries\MultiDB;
use App\Models\Project;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Auth;
class UpdateCalculatedFields
{
use Dispatchable;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
nlog("Updating calculated fields");
Auth::logout();
if (! config('ninja.db.multi_db_enabled')) {
Project::query()->with('tasks')->whereHas('tasks', function ($query) {
$query->where('updated_at', '>', now()->subHours(2));
})
->cursor()
->each(function ($project) {
$project->current_hours = $this->calculateDuration($project);
$project->save();
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
Project::query()->with('tasks')->whereHas('tasks', function ($query) {
$query->where('updated_at', '>', now()->subHours(2));
})
->cursor()
->each(function ($project) {
$project->current_hours = $this->calculateDuration($project);
$project->save();
});
//Clean password resets table
\DB::connection($db)->table('password_resets')->where('created_at', '<', now()->subHour())->delete();
}
}
}
private function calculateDuration($project): int
{
$duration = 0;
$project->tasks->each(function ($task) use (&$duration) {
if(is_iterable(json_decode($task->time_log))) {
foreach(json_decode($task->time_log) as $log) {
if(!is_array($log))
continue;
$start_time = $log[0];
$end_time = $log[1] == 0 ? time() : $log[1];
$duration += $end_time - $start_time;
}
}
});
return (int) round(($duration / 60 / 60), 0);
}
}

View File

@ -94,7 +94,9 @@ class ProcessMailgunWebhook implements ShouldQueue
}
MultiDB::findAndSetDbByCompanyKey($this->request['event-data']['tags'][0]);
$company = Company::query()->where('company_key', $this->request['event-data']['tags'][0])->first();
/** @var \App\Models\Company $company */
$company = Company::where('company_key', $this->request['event-data']['tags'][0])->first();
if ($company && $this->request['event-data']['event'] == 'complained' && config('ninja.notification.slack')) {
$company->notification(new EmailSpamNotification($company))->ninja();
@ -195,7 +197,7 @@ class ProcessMailgunWebhook implements ShouldQueue
'date' => \Carbon\Carbon::parse($this->request['event-data']['timestamp'])->format('Y-m-d H:i:s') ?? '',
];
if($sl) {
if($sl instanceof SystemLog) {
$data = $sl->log;
$data['history']['events'][] = $event;
$this->updateSystemLog($sl, $data);

View File

@ -176,8 +176,8 @@ class SendRecurring implements ShouldQueue
private function createRecurringInvitations($invoice): Invoice
{
if ($this->recurring_invoice->invitations->count() == 0) {
$this->recurring_invoice->service()->createInvitations()->save();
$this->recurring_invoice = $this->recurring_invoice->fresh();
$this->recurring_invoice = $this->recurring_invoice->service()->createInvitations()->save();
// $this->recurring_invoice = $this->recurring_invoice->fresh();
}
$this->recurring_invoice->invitations->each(function ($recurring_invitation) use ($invoice) {

View File

@ -95,6 +95,8 @@ class CleanStaleInvoiceOrder implements ShouldQueue
->each(function ($invoice) {
$invoice->service()->removeUnpaidGatewayFees();
});
\DB::connection($db)->table('password_resets')->where('created_at', '<', now()->subHours(12))->delete();
}
}

View File

@ -61,10 +61,10 @@ class QuoteReminderJob implements ShouldQueue
nrlog("Sending quote reminders on ".now()->format('Y-m-d h:i:s'));
Quote::query()
->where('quotes.is_deleted', 0)
->whereIn('quotes.status_id', [Invoice::STATUS_SENT])
->whereNull('quotes.deleted_at')
->where('quotes.next_send_date', '<=', now()->toDateTimeString())
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT])
->whereNull('deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
@ -88,10 +88,10 @@ class QuoteReminderJob implements ShouldQueue
nrlog("Sending quote reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
Quote::query()
->where('quotes.is_deleted', 0)
->whereIn('quotes.status_id', [Invoice::STATUS_SENT])
->whereNull('quotes.deleted_at')
->where('quotes.next_send_date', '<=', now()->toDateTimeString())
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT])
->whereNull('deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);

View File

@ -356,15 +356,14 @@ class BillingPortalPurchase extends Component
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
foreach($this->methods as $method){
if($method['is_paypal'] == '1' && !$this->steps['check_rff']){
$this->rff();
break;
}
}
$method_values = array_column($this->methods, 'is_paypal');
$is_paypal = in_array('1', $method_values);
if($is_paypal && !$this->steps['check_rff'])
$this->rff();
elseif(!$this->steps['check_rff'])
$this->steps['fetched_payment_methods'] = true;
$this->heading_text = ctrans('texts.payment_methods');
return $this;

View File

@ -484,7 +484,7 @@ class BillingPortalPurchasev2 extends Component
$this->methods = [];
}
if ($this->contact && $this->float_amount_total >= 1) {
if ($this->contact && $this->float_amount_total >= 0) {
$this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total);
}

View File

@ -20,8 +20,6 @@ class PersonalAddress extends Component
public $country_id;
public $countries;
public $saved;
protected $rules = [
@ -33,7 +31,7 @@ class PersonalAddress extends Component
'country_id' => ['sometimes'],
];
public function mount($countries)
public function mount()
{
$this->fill([
'profile' => auth()->guard('contact')->user()->client,
@ -43,8 +41,6 @@ class PersonalAddress extends Component
'state' => auth()->guard('contact')->user()->client->state,
'postal_code' => auth()->guard('contact')->user()->client->postal_code,
'country_id' => auth()->guard('contact')->user()->client->country_id,
'countries' => $countries,
'saved' => ctrans('texts.save'),
]);
}

View File

@ -20,8 +20,6 @@ class ShippingAddress extends Component
public $shipping_country_id;
public $countries;
public $saved;
protected $rules = [
@ -33,7 +31,7 @@ class ShippingAddress extends Component
'shipping_country_id' => ['sometimes'],
];
public function mount($countries)
public function mount()
{
$this->fill([
'profile' => auth()->guard('contact')->user()->client,
@ -43,8 +41,6 @@ class ShippingAddress extends Component
'shipping_state' => auth()->guard('contact')->user()->client->shipping_state,
'shipping_postal_code' => auth()->guard('contact')->user()->client->shipping_postal_code,
'shipping_country_id' => auth()->guard('contact')->user()->client->shipping_country_id,
'countries' => $countries,
'saved' => ctrans('texts.save'),
]);
}

View File

@ -65,7 +65,7 @@ class TemplateEmail extends Mailable
}
$link_string = '<ul>';
$link_string .= "<li>{ctrans('texts.download_files')}</li>";
foreach ($this->build_email->getAttachmentLinks() as $link) {
$link_string .= "<li>{$link}</li>";
}

View File

@ -155,6 +155,7 @@ class CompanyGateway extends BaseModel
'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9' => 322,
'80af24a6a691230bbec33e930ab40666' => 323,
'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay
'91be24c7b792230bced33e930ac61676' => 325,
];
protected $touches = [];

View File

@ -53,4 +53,9 @@ class Currency extends StaticModel
'deleted_at' => 'timestamp',
'precision' => 'integer',
];
public function getName(): string
{
return trans('texts.currency_'.$this->name);
}
}

View File

@ -16,6 +16,7 @@ use App\Models\CompanyUser;
use Illuminate\Support\Carbon;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Libraries\Currency\Conversion\CurrencyApi;
/**
* App\Models\Task
@ -137,7 +138,7 @@ class Task extends BaseModel
// 'project',
];
protected $touches = [];
protected $touches = ['project'];
public function getEntityType()
{
@ -159,27 +160,55 @@ class Task extends BaseModel
return $this->morphMany(Document::class, 'documentable');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function assigned_user()
{
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class)->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function client()
{
return $this->belongsTo(Client::class)->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function status()
{
return $this->belongsTo(TaskStatus::class)->withTrashed();
}
public function stringStatus()
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function invoice()
{
return $this->belongsTo(Invoice::class)->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function project()
{
return $this->belongsTo(Project::class)->withTrashed();
}
public function stringStatus(): string
{
if($this->invoice_id) {
return '<h5><span class="badge badge-success">'.ctrans('texts.invoiced').'</span></h5>';
@ -193,16 +222,6 @@ class Task extends BaseModel
}
public function invoice()
{
return $this->belongsTo(Invoice::class)->withTrashed();
}
public function project()
{
return $this->belongsTo(Project::class)->withTrashed();
}
public function calcStartTime()
{
$parts = json_decode($this->time_log) ?: [];
@ -230,7 +249,7 @@ class Task extends BaseModel
public function calcDuration($start_time_cutoff = 0, $end_time_cutoff = 0)
{
$duration = 0;
$parts = json_decode($this->time_log) ?: [];
$parts = json_decode($this->time_log ?? '{}') ?: [];
foreach ($parts as $part) {
$start_time = $part[0];
@ -272,6 +291,26 @@ class Task extends BaseModel
return $this->company->settings->default_task_rate ?? 0;
}
public function taskCompanyValue(): float
{
$client_currency = $this->client->getSetting('currency_id');
$company_currency = $this->company->getSetting('currency_id');
if($client_currency != $company_currency)
{
$converter = new CurrencyApi();
return $converter->convert($this->taskValue(), $client_currency, $company_currency);
}
return $this->taskValue();
}
public function taskValue(): float
{
return round(($this->calcDuration() / 3600) * $this->getRate(),2);
}
public function processLogs()
{

View File

@ -82,6 +82,7 @@ class TaskObserver
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_ARCHIVE_TASK, $task, $task->company)->delay(0);
}
}
/**

View File

@ -20,7 +20,7 @@ abstract class AbstractPaymentDriver
{
abstract public function authorizeView(array $data);
abstract public function authorizeResponse(Request $request);
abstract public function authorizeResponse(\App\Http\Requests\Request | Request $request);
abstract public function processPaymentView(array $data);

View File

@ -586,10 +586,6 @@ class BaseDriver extends AbstractPaymentDriver
$invoices = Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
// $invoices->each(function ($invoice) {
// $invoice->service()->deletePdf();
// });
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
if ((bool) $invitation->contact->send_email !== false && $invitation->contact->email) {
$nmo->to_user = $invitation->contact;

View File

@ -1,249 +0,0 @@
<?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\Exceptions\PaymentFailed;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\Utils\Traits\MakesHash;
use Omnipay\Common\Item;
use Omnipay\Omnipay;
class PayPalExpressPaymentDriver extends BaseDriver
{
use MakesHash;
public $token_billing = false;
public $can_authorise_credit_card = false;
private $omnipay_gateway;
private float $fee = 0;
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL;
public function gatewayTypes()
{
return [
GatewayType::PAYPAL,
];
}
public function init()
{
return $this;
}
/**
* Initialize Omnipay PayPal_Express gateway.
*
* @return void
*/
private function initializeOmnipayGateway(): void
{
$this->omnipay_gateway = Omnipay::create(
$this->company_gateway->gateway->provider
);
$this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig());
}
public function setPaymentMethod($payment_method_id)
{
// PayPal doesn't have multiple ways of paying.
// There's just one, off-site redirect.
return $this;
}
public function authorizeView($payment_method)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function authorizeResponse($request)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function processPaymentView($data)
{
$this->initializeOmnipayGateway();
$this->payment_hash->data = array_merge((array) $this->payment_hash->data, ['amount' => $data['total']['amount_with_fee']]);
$this->payment_hash->save();
$response = $this->omnipay_gateway
->purchase($this->generatePaymentDetails($data))
->setItems($this->generatePaymentItems($data))
->send();
if ($response->isRedirect()) {
return $response->redirect();
}
// $this->sendFailureMail($response->getMessage() ?: '');
$message = [
'server_response' => $response->getMessage(),
'data' => $this->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company,
);
throw new PaymentFailed($response->getMessage(), $response->getCode());
}
public function processPaymentResponse($request)
{
$this->initializeOmnipayGateway();
$response = $this->omnipay_gateway
->completePurchase(['amount' => $this->payment_hash->data->amount, 'currency' => $this->client->getCurrencyCode()])
->send();
if ($response->isCancelled() && $this->client->getSetting('enable_client_portal')) {
return redirect()->route('client.invoices.index')->with('warning', ctrans('texts.status_cancelled'));
} elseif($response->isCancelled() && !$this->client->getSetting('enable_client_portal')) {
redirect()->route('client.invoices.show', ['invoice' => $this->payment_hash->fee_invoice])->with('warning', ctrans('texts.status_cancelled'));
}
if ($response->isSuccessful()) {
$data = [
'payment_method' => $response->getData()['TOKEN'],
'payment_type' => PaymentType::PAYPAL,
'amount' => $this->payment_hash->data->amount,
'transaction_reference' => $response->getTransactionReference(),
'gateway_type_id' => GatewayType::PAYPAL,
];
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => (array) $response->getData(), 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
}
if (! $response->isSuccessful()) {
$data = $response->getData();
$this->sendFailureMail($response->getMessage() ?: '');
$message = [
'server_response' => $data['L_LONGMESSAGE0'],
'data' => $this->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company,
);
throw new PaymentFailed($response->getMessage(), $response->getCode());
}
}
public function generatePaymentDetails(array $data)
{
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
// $this->fee = $this->feeCalc($invoice, $data['total']['amount_with_fee']);
return [
'currency' => $this->client->getCurrencyCode(),
'transactionType' => 'Purchase',
'clientIp' => request()->getClientIp(),
// 'amount' => round(($data['total']['amount_with_fee'] + $this->fee),2),
'amount' => round($data['total']['amount_with_fee'], 2),
'returnUrl' => route('client.payments.response', [
'company_gateway_id' => $this->company_gateway->id,
'payment_hash' => $this->payment_hash->hash,
'payment_method_id' => GatewayType::PAYPAL,
]),
'cancelUrl' => $this->client->company->domain()."/client/invoices/{$invoice->hashed_id}",
'description' => implode(',', collect($this->payment_hash->data->invoices)
->map(function ($invoice) {
return sprintf('%s: %s', ctrans('texts.invoice_number'), $invoice->invoice_number);
})->toArray()),
'transactionId' => $this->payment_hash->hash.'-'.time(),
'ButtonSource' => 'InvoiceNinja_SP',
'solutionType' => 'Sole',
'no_shipping' => $this->company_gateway->require_shipping_address ? 0 : 1,
];
}
public function generatePaymentItems(array $data)
{
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$items = [];
$items[] = new Item([
'name' => ' ',
'description' => ctrans('texts.invoice_number').'# '.$invoice->number,
'price' => $data['total']['amount_with_fee'],
'quantity' => 1,
]);
return $items;
}
private function feeCalc($invoice, $invoice_total)
{
$invoice->service()->removeUnpaidGatewayFees();
$invoice = $invoice->fresh();
$balance = floatval($invoice->balance);
$_updated_invoice = $invoice->service()->addGatewayFee($this->company_gateway, GatewayType::PAYPAL, $invoice_total)->save();
if (floatval($_updated_invoice->balance) > $balance) {
$fee = floatval($_updated_invoice->balance) - $balance;
$this->payment_hash->fee_total = $fee;
$this->payment_hash->save();
return $fee;
}
return 0;
}
}

View File

@ -0,0 +1,155 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Rotessa\Jobs;
use App\Utils\Ninja;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Libraries\MultiDB;
use App\Models\PaymentHash;
use Illuminate\Bus\Queueable;
use App\Models\CompanyGateway;
use App\Jobs\Util\SystemLogger;
use Illuminate\Support\Facades\App;
use App\Jobs\Mail\PaymentFailedMailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class TransactionReport implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $tries = 1; //number of retries
public $deleteWhenMissingModels = true;
public function __construct()
{
}
public function handle()
{
set_time_limit(0);
foreach(MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
CompanyGateway::query()
->where('gateway_key', '91be24c7b792230bced33e930ac61676')
->cursor()
->each(function ($cg){
$driver = $cg->driver()->init();
//Approved Transactions
$transactions = $driver->gatewayRequest("get", "transaction_report", ['page' => 1, 'status' => 'Approved', 'start_date' => now()->subMonths(2)->format('Y-m-d')]);
if($transactions->successful())
{
$transactions = $transactions->json();
nlog($transactions);
Payment::query()
->where('company_id', $cg->company_id)
->where('status_id', Payment::STATUS_PENDING)
->whereIn('transaction_reference', array_column($transactions, "transaction_schedule_id"))
->cursor()
->each(function ($payment) use ($transactions) {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
SystemLogger::dispatch(
['response' => collect($transactions)->where('id', $payment->transaction_reference)->first()->toArray(), 'data' => []],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_ROTESSA,
$payment->client,
$payment->company,
);
});
}
//Declined / Charged Back Transactions
$declined_transactions = $driver->gatewayRequest("get", "transaction_report", ['page' => 1, 'status' => 'Declined', 'start_date' => now()->subMonths(2)->format('Y-m-d')]);
$chargeback_transactions = $driver->gatewayRequest("get", "transaction_report", ['page' => 1, 'status' => 'Chargeback', 'start_date' => now()->subMonths(2)->format('Y-m-d')]);
if($declined_transactions->successful() && $chargeback_transactions->successful()) {
$transactions = array_merge($declined_transactions->json(), $chargeback_transactions->json());
nlog($transactions);
Payment::query()
->where('company_id', $cg->company_id)
->where('status_id', Payment::STATUS_PENDING)
->whereIn('transaction_reference', array_column($transactions, "transaction_schedule_id"))
->cursor()
->each(function ($payment) use ($transactions){
$client = $payment->client;
$payment->service()->deletePayment();
$payment->status_id = Payment::STATUS_FAILED;
$payment->save();
$payment_hash = PaymentHash::query()->where('payment_id', $payment->id)->first();
if ($payment_hash) {
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($client->getMergedSettings()));
App::setLocale($client->locale());
$error = ctrans('texts.client_payment_failure_body', [
'invoice' => implode(',', $payment->invoices->pluck('number')->toArray()),
'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, ]);
} else {
$error = 'Payment for '.$payment->client->present()->name()." for {$payment->amount} failed";
}
PaymentFailedMailer::dispatch(
$payment_hash,
$client->company,
$client,
$error
);
SystemLogger::dispatch(
['response' => collect($transactions)->where('id', $payment->transaction_reference)->first()->toArray(), 'data' => []],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_ROTESSA,
$payment->client,
$payment->company,
);
});
}
});
}
}
}

View File

@ -12,14 +12,11 @@
namespace App\PaymentDrivers\Rotessa;
use Carbon\Carbon;
use App\Models\Client;
use App\Models\Payment;
use App\Models\SystemLog;
use Illuminate\View\View;
use App\Models\GatewayType;
use App\Models\PaymentType;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Exceptions\PaymentFailed;
@ -28,20 +25,20 @@ use App\Models\ClientGatewayToken;
use Illuminate\Http\RedirectResponse;
use App\PaymentDrivers\RotessaPaymentDriver;
use App\PaymentDrivers\Common\MethodInterface;
use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\Resources\Transaction;
use Omnipay\Common\Exception\InvalidRequestException;
use Omnipay\Common\Exception\InvalidResponseException;
use App\Exceptions\Ninja\ClientPortalAuthorizationException;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class PaymentMethod implements MethodInterface
{
protected RotessaPaymentDriver $rotessa;
public function __construct(RotessaPaymentDriver $rotessa)
private array $transaction = [
"financial_transactions" => [],
"frequency" =>'Once',
"installments" =>1
];
public function __construct(protected RotessaPaymentDriver $rotessa)
{
$this->rotessa = $rotessa;
$this->rotessa->init();
}
@ -53,16 +50,13 @@ class PaymentMethod implements MethodInterface
*/
public function authorizeView(array $data): View
{
$data['contact'] = collect($data['client']->contacts->firstWhere('is_primary', 1)->toArray())->merge([
$data['contact'] = collect($data['client']->contacts->first()->toArray())->merge([
'home_phone' => $data['client']->phone,
'custom_identifier' => $data['client']->number,
'name' => $data['client']->name,
'id' => null
] )->all();
$data['gateway'] = $this->rotessa;
// Set gateway type according to client country
// $data['gateway_type_id'] = $data['client']->country->iso_3166_2 == 'US' ? GatewayType::BANK_TRANSFER : ( $data['client']->country->iso_3166_2 == 'CA' ? GatewayType::ACSS : (int) request('method'));
// TODO: detect GatewayType based on client country USA vs CAN
$data['gateway_type_id'] = GatewayType::ACSS ;
$data['account'] = [
'routing_number' => $data['client']->routing_id,
@ -78,41 +72,38 @@ class PaymentMethod implements MethodInterface
* @param Request $request
* @return RedirectResponse
*/
public function authorizeResponse(Request $request): RedirectResponse
public function authorizeResponse($request)
{
try {
$request->validate([
'gateway_type_id' => ['required','integer'],
'country' => ['required'],
'name' => ['required'],
'address_1' => ['required'],
'address_2' => ['required'],
'city' => ['required'],
'email' => ['required','email:filter'],
'province_code' => ['required','size:2','alpha'],
'postal_code' => ['required'],
'authorization_type' => ['required'],
'account_number' => ['required'],
'bank_name' => ['required'],
'phone' => ['required'],
'home_phone' => ['required'],
'bank_account_type'=>['required_if:country,US'],
'routing_number'=>['required_if:country,US'],
'institution_number'=>['required_if:country,CA','numeric'],
'transit_number'=>['required_if:country,CA','numeric'],
'custom_identifier'=>['required_without:customer_id'],
'customer_id'=>['required_without:custom_identifier','integer'],
]);
$customer = new Customer( ['address' => $request->only('address_1','address_2','city','postal_code','province_code','country'), 'custom_identifier' => $request->input('custom_identifier') ] + $request->all());
$this->rotessa->findOrCreateCustomer($customer->resolve());
return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added'));
} catch (\Throwable $e) {
return $this->rotessa->processInternallyFailedPayment($this->rotessa, new ClientPortalAuthorizationException( get_class( $e) . " : {$e->getMessage()}", (int) $e->getCode() ));
}
$request->validate([
'gateway_type_id' => ['required','integer'],
'country' => ['required','in:US,CA,United States,Canada'],
'name' => ['required'],
'address_1' => ['required'],
'city' => ['required'],
'email' => ['required','email:filter'],
'province_code' => ['required','size:2','alpha'],
'postal_code' => ['required'],
'authorization_type' => ['required'],
'account_number' => ['required'],
'bank_name' => ['required'],
'phone' => ['required'],
'home_phone' => ['required','size:10'],
'bank_account_type'=>['required_if:country,US'],
'routing_number'=>['required_if:country,US'],
'institution_number'=>['required_if:country,CA','numeric','digits:3'],
'transit_number'=>['required_if:country,CA','numeric','digits:5'],
'custom_identifier'=>['required_without:customer_id'],
'customer_id'=>['required_without:custom_identifier','integer'],
'customer_type' => ['required', 'in:Personal,Business'],
]);
$customer = array_merge(['address' => $request->only('address_1','address_2','city','postal_code','province_code','country'), 'custom_identifier' => $request->input('custom_identifier') ], $request->all());
$this->rotessa->findOrCreateCustomer($customer);
return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added'));
return back()->withMessage(ctrans('texts.unable_to_verify_payment_method'));
}
/**
@ -128,7 +119,7 @@ class PaymentMethod implements MethodInterface
$data['due_date'] = date('Y-m-d', min(max(strtotime($data['invoices']->max('due_date')), strtotime('now')), strtotime('+1 day')));
$data['process_date'] = $data['due_date'];
$data['currency'] = $this->rotessa->client->getCurrencyCode();
$data['frequency'] = Frequencies::getOnePayment();
$data['frequency'] = 'Once';
$data['installments'] = 1;
$data['invoice_nums'] = $data['invoices']->pluck('invoice_number')->join(', ');
return render('gateways.rotessa.bank_transfer.pay', $data );
@ -138,34 +129,41 @@ class PaymentMethod implements MethodInterface
* Handle payments page for Rotessa.
*
* @param PaymentResponseRequest $request
* @return void
*/
public function paymentResponse(PaymentResponseRequest $request)
{
$response= null;
$customer = null;
try {
$request->validate([
'source' => ['required','string','exists:client_gateway_tokens,token'],
'amount' => ['required','numeric'],
'process_date'=> ['required','date','after_or_equal:today'],
]);
$customer = ClientGatewayToken::query()
->where('company_gateway_id', $this->rotessa->company_gateway->id)
->where('client_id', $this->rotessa->client->id)
->where('is_deleted', 0)
->where('token', $request->input('source'))
->first();
if(!$customer) throw new \Exception('Client gateway token not found!', SystemLog::TYPE_ROTESSA);
$transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date') + ['comment' => $this->rotessa->getDescription(false) ]);
$transaction->additional(['customer_id' => $customer->gateway_customer_reference]);
$transaction = array_filter( $transaction->resolve());
$response = $this->rotessa->gateway->capture($transaction)->send();
if(!$response->isSuccessful()) throw new \Exception($response->getMessage(), (int) $response->getCode());
$transaction = array_merge($this->transaction,[
'amount' => $request->input('amount'),
'process_date' => now()->addSeconds($customer->client->utc_offset())->format('Y-m-d'),
'comment' => $this->rotessa->getDescription(false),
'customer_id' => $customer->gateway_customer_reference,
]);
$response = $this->rotessa->gatewayRequest('post','transaction_schedules', $transaction);
return $this->processPendingPayment($response->getParameter('id'), (float) $response->getParameter('amount'), (int) $customer->gateway_type_id , $customer->token);
if($response->failed())
$response->throw();
$response = $response->json();
return $this->processPendingPayment($response['id'], (float) $response['amount'], PaymentType::ACSS , $customer->token);
} catch(\Throwable $e) {
$this->processUnsuccessfulPayment( new InvalidResponseException($e->getMessage(), (int) $e->getCode()) );
$this->processUnsuccessfulPayment( new \Exception($e->getMessage(), (int) $e->getCode()) );
}
}
@ -194,7 +192,7 @@ class PaymentMethod implements MethodInterface
/**
* Handle unsuccessful payment for Rotessa.
*
* @param Exception $exception
* @param \Exception $exception
* @throws PaymentFailed
* @return void
*/

View File

@ -1,22 +0,0 @@
<?php
namespace App\PaymentDrivers\Rotessa\Resources;
use Illuminate\Http\Request;
use Omnipay\Rotessa\Model\CustomerModel;
use Illuminate\Http\Resources\Json\JsonResource;
class Customer extends JsonResource
{
function __construct($resource) {
parent::__construct( new CustomerModel($resource));
}
function jsonSerialize() : array {
return $this->resource->jsonSerialize();
}
function toArray(Request $request) : array {
return $this->additional + parent::toArray($request);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\PaymentDrivers\Rotessa\Resources;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Omnipay\Rotessa\Model\TransactionScheduleModel;
class Transaction extends JsonResource
{
function __construct($resource) {
parent::__construct( new TransactionScheduleModel( $resource));
}
function jsonSerialize() : array {
return $this->resource->jsonSerialize();
}
function toArray(Request $request) : array {
return $this->additional + parent::toArray($request);
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Common\AbstractGateway;
use Omnipay\Rotessa\ClientInterface;
use Omnipay\Rotessa\Message\RequestInterface;
abstract class AbstractClient extends AbstractGateway implements ClientInterface
{
protected $default_parameters = [];
public function getDefaultParameters() : array {
return $this->default_parameters;
}
public function setDefaultParameters(array $params) {
$this->default_parameters = $params;
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Rotessa\Message\Request\RequestInterface;
trait ApiTrait
{
public function getCustomers() : RequestInterface {
return $this->createRequest('GetCustomers', [] );
}
public function postCustomers(array $params) : RequestInterface {
return $this->createRequest('PostCustomers', $params );
}
public function getCustomersId(array $params) : RequestInterface {
return $this->createRequest('GetCustomersId', $params );
}
public function patchCustomersId(array $params) : RequestInterface {
return $this->createRequest('PatchCustomersId', $params );
}
public function postCustomersShowWithCustomIdentifier(array $params) : RequestInterface {
return $this->createRequest('PostCustomersShowWithCustomIdentifier', $params );
}
public function getTransactionSchedulesId(array $params) : RequestInterface {
return $this->createRequest('GetTransactionSchedulesId', $params );
}
public function deleteTransactionSchedulesId(array $params) : RequestInterface {
return $this->createRequest('DeleteTransactionSchedulesId', $params );
}
public function patchTransactionSchedulesId(array $params) : RequestInterface {
return $this->createRequest('PatchTransactionSchedulesId', $params );
}
public function postTransactionSchedules(array $params) : RequestInterface {
return $this->createRequest('PostTransactionSchedules', $params );
}
public function postTransactionSchedulesCreateWithCustomIdentifier(array $params) : RequestInterface {
return $this->createRequest('PostTransactionSchedulesCreateWithCustomIdentifier', $params );
}
public function postTransactionSchedulesUpdateViaPost(array $params) : RequestInterface {
return $this->createRequest('PostTransactionSchedulesUpdateViaPost', $params );
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Common\GatewayInterface;
use Omnipay\Rotessa\Message\Request\RequestInterface;
interface ClientInterface extends GatewayInterface
{
public function getDefaultParameters(): array;
public function setDefaultParameters(array $data);
}

View File

@ -1,43 +0,0 @@
<?php
namespace Omnipay\Rotessa\Exception;
class BadRequestException extends \Exception {
protected $message = "Your request includes invalid parameters";
protected $code = 400;
}
class UnauthorizedException extends \Exception {
protected $message = "Your API key is not valid or is missing";
protected $code = 401;
}
class NotFoundException extends \Exception {
protected $message = "The specified resource could not be found";
protected $code = 404;
}
class NotAcceptableException extends \Exception {
protected $message = "You requested a format that isnt json";
protected $code = 406;
}
class UnprocessableEntityException extends \Exception {
protected $message = "Your request results in invalid data";
protected $code = 422;
}
class InternalServerErrorException extends \Exception {
protected $message = "We had a problem with our server. Try again later";
protected $code = 500;
}
class ServiceUnavailableException extends \Exception {
protected $message = "We're temporarily offline for maintenance. Please try again later";
protected $code = 503;
}
class ValidationException extends \Exception {
protected $message = "A validation error has occured";
protected $code = 600;
}

View File

@ -1,74 +0,0 @@
<?php
namespace Omnipay\Rotessa;
use Omnipay\Rotessa\ApiTrait;
use Omnipay\Rotessa\AbstractClient;
use Omnipay\Rotessa\ClientInterface;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class Gateway extends AbstractClient implements ClientInterface {
use ApiTrait;
protected $default_parameters = ['api_key' => 1234567890 ];
protected $test_mode = true;
protected $api_key;
public function getName()
{
return 'Rotessa';
}
public function getDefaultParameters() : array
{
return array_merge($this->default_parameters, array('api_key' => $this->api_key, 'test_mode' => $this->test_mode ) );
}
public function setTestMode($value) {
$this->test_mode = $value;
}
public function getTestMode() {
return $this->test_mode;
}
protected function createRequest($class_name, ?array $parameters = [] ) :RequestInterface {
$class = null;
$class_name = "Omnipay\\Rotessa\\Message\\Request\\$class_name";
$parameters = $class_name::hasModel() ? (($parameters = ($class_name::getModel($parameters)))->validate() ? $parameters->jsonSerialize() : null ) : $parameters;
try {
$class = new $class_name($this->httpClient, $this->httpRequest, $this->getDefaultParameters() + $parameters );
} catch (\Throwable $th) {
throw $th;
}
return $class;
}
function setApiKey($value) {
$this->api_key = $value;
}
function getApiKey() {
return $this->api_key;
}
function authorize(array $options = []) : RequestInterface {
return $this->postCustomers($options);
}
function capture(array $options = []) : RequestInterface {
return array_key_exists('customer_id', $options)? $this->postTransactionSchedules($options) : $this->postTransactionSchedulesCreateWithCustomIdentifier($options) ;
}
function updateCustomer(array $options) : RequestInterface {
return $this->patchCustomersId($options);
}
function fetchTransaction($id = null) : RequestInterface {
return $this->getTransactionSchedulesId(compact('id'));
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace Omnipay\Rotessa\Http;
use Omnipay\Common\Http\Client as HttpClient;
use Omnipay\Common\Http\Exception\NetworkException;
use Omnipay\Common\Http\Exception\RequestException;
use Http\Discovery\HttpClientDiscovery;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Message\RequestFactory;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class Client extends HttpClient
{
/**
* The Http Client which implements `public function sendRequest(RequestInterface $request)`
* Note: Will be changed to PSR-18 when released
*
* @var HttpClient
*/
private $httpClient;
/**
* @var RequestFactory
*/
private $requestFactory;
public function __construct($httpClient = null, RequestFactory $requestFactory = null)
{
$this->httpClient = $httpClient ?: HttpClientDiscovery::find();
$this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find();
parent::__construct($httpClient, $requestFactory);
}
/**
* @param $method
* @param $uri
* @param array $headers
* @param string|array|resource|StreamInterface|null $body
* @param string $protocolVersion
* @return ResponseInterface
* @throws \Http\Client\Exception
*/
public function request(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return $this->sendRequest($method, $uri, $headers, $body, $protocolVersion);
}
/**
* @param RequestInterface $request
* @return ResponseInterface
* @throws \Http\Client\Exception
*/
private function sendRequest( $method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1')
{
$response = null;
try {
if( method_exists($this->httpClient, 'sendRequest'))
$response = $this->httpClient->sendRequest( $this->requestFactory->createRequest($method, $uri, $headers, $body, $protocolVersion));
else $response = $this->httpClient->request($method, $uri, compact('body','headers'));
} catch (\Http\Client\Exception\NetworkException $networkException) {
throw new NetworkException($networkException->getMessage(), $request, $networkException);
} catch (\Exception $exception) {
throw new RequestException($exception->getMessage(), $request, $exception);
}
return $response;
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace Omnipay\Rotessa\Http\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
class Response extends JsonResponse
{
protected $reason_phrase = '';
protected $reason_code = '';
public function __construct(mixed $data = null, int $status = 200, array $headers = [])
{
parent::__construct($data , $status, $headers, true);
if(array_key_exists('errors',$data = json_decode( $this->content, true) )) {
$data = $data['errors'][0];
$this->reason_phrase = $data['error_message'] ;
$this->reason_code = $data['error_message'] ;
}
}
public function getReasonPhrase() {
return $this->reason_phrase;
}
public function getReasonCode() {
return $this->reason_code;
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace Omnipay\Rotessa;
trait IsValidTypeTrait {
public static function isValid(string $value) {
return in_array($value, self::getTypes());
}
abstract public static function getTypes() : array;
}

View File

@ -1,52 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Rotessa\Message\Request\RequestInterface;
use Omnipay\Common\Message\AbstractRequest as Request;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
abstract class AbstractRequest extends Request implements RequestInterface
{
protected $test_mode = false;
protected $api_version;
protected $method = 'GET';
protected $endpoint;
protected $api_key;
public function setApiKey(string $value) {
$this->api_key = $value;
}
public function getData() {
try {
if(empty($this->api_key)) throw new \Exception('No Api Key Found!');
$this->validate( ...array_keys($data = $this->getParameters()));
} catch (\Throwable $th) {
throw new \Omnipay\Rotessa\Exception\ValidationException($th->getMessage() , 600, $th);
}
return (array) $data;
}
abstract public function sendData($data) : ResponseInterface;
abstract protected function sendRequest(string $method, string $endpoint, array $headers = [], array $data = [] );
abstract protected function createResponse(array $data) : ResponseInterface;
abstract public function getEndpointUrl(): string;
public function getEndpoint() : string {
return $this->endpoint;
}
public function getTestMode() {
return $this->test_mode;
}
public function setTestMode($mode) {
$this->test_mode = $mode;
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Common\Http\ClientInterface;
use Omnipay\Rotessa\Http\Response\Response;
use Omnipay\Rotessa\Message\Response\BaseResponse;
use Omnipay\Rotessa\Message\Request\RequestInterface;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
use Symfony\Component\HttpFoundation\Request as HttpRequest;
class BaseRequest extends AbstractRequest implements RequestInterface
{
protected $base_url = 'rotessa.com';
protected $api_version = 1;
protected $endpoint = '';
const ENVIRONMENT_SANDBOX = 'sandbox-api';
const ENVIRONMENT_LIVE = 'api';
function __construct(ClientInterface $http_client = null, HttpRequest $http_request, $model ) {
parent::__construct($http_client, $http_request );
$this->initialize($model);
}
protected function sendRequest(string $method, string $endpoint, array $headers = [], array $data = [])
{
/**
* @param $method
* @param $uri
* @param array $headers
* @param string|resource|StreamInterface|null $body
* @param string $protocolVersion
* @return ResponseInterface
* @throws \Http\Client\Exception
*/
$response = $this->httpClient->request($method, $endpoint, $headers, json_encode($data) ) ;
$this->response = new Response ($response->getBody()->getContents(), $response->getStatusCode(), $response->getHeaders(), true);
}
protected function createResponse(array $data): ResponseInterface {
return new BaseResponse($this, $data, $this->response->getStatusCode(), $this->response->getReasonPhrase());
}
protected function replacePlaceholder($string, $array) {
$pattern = "/\{([^}]+)\}/";
$replacement = function($matches) use($array) {
$key = $matches[1];
if (array_key_exists($key, $array)) {
return $array[$key];
} else {
return $matches[0];
}
};
return preg_replace_callback($pattern, $replacement, $string);
}
public function sendData($data) :ResponseInterface {
$headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
'Authorization' => "Token token={$this->api_key}"
];
$this->sendRequest(
$this->method,
$this->getEndpointUrl(),
$headers,
$data);
return $this->createResponse(json_decode($this->response->getContent(), true));
}
public function getEndpoint() : string {
return $this->replacePlaceholder($this->endpoint, $this->getParameters());
}
public function getEndpointUrl() : string {
return sprintf('https://%s.%s/v%d%s',$this->test_mode ? self::ENVIRONMENT_SANDBOX : self::ENVIRONMENT_LIVE ,$this->base_url, $this->api_version, $this->getEndpoint());
}
public static function hasModel() : bool {
return (bool) static::$model;
}
public static function getModel($parameters = []) {
$class_name = static::$model;
$class_name = "Omnipay\\Rotessa\\Model\\{$class_name}Model";
return new $class_name($parameters);
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class DeleteTransactionSchedulesId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/{id}';
protected $method = 'DELETE';
protected static $model = '';
public function setId(string $value) {
$this->setParameter('id',$value);
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class GetCustomers extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers';
protected $method = 'GET';
protected static $model = '';
}

View File

@ -1,19 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class GetCustomersId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers/{id}';
protected $method = 'GET';
protected static $model = '';
public function setId(int $value) {
$this->setParameter('id',$value);
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class GetTransactionSchedulesId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/{id}';
protected $method = 'GET';
protected static $model = '';
public function setId(int $value) {
$this->setParameter('id',$value);
}
}

View File

@ -1,65 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PatchCustomersId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers/{id}';
protected $method = 'PATCH';
protected static $model = 'CustomerPatch';
public function setId(string $value) {
$this->setParameter('id',$value);
}
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
public function setName(string $value) {
$this->setParameter('name',$value);
}
public function setEmail(string $value) {
$this->setParameter('email',$value);
}
public function setCustomerType(string $value) {
$this->setParameter('customer_type',$value);
}
public function setHomePhone(string $value) {
$this->setParameter('home_phone',$value);
}
public function setPhone(string $value) {
$this->setParameter('phone',$value);
}
public function setBankName(string $value) {
$this->setParameter('bank_name',$value);
}
public function setInstitutionNumber(string $value) {
$this->setParameter('institution_number',$value);
}
public function setTransitNumber(string $value) {
$this->setParameter('transit_number',$value);
}
public function setBankAccountType(string $value) {
$this->setParameter('bank_account_type',$value);
}
public function setAuthorizationType(string $value) {
$this->setParameter('authorization_type',$value);
}
public function setRoutingNumber(string $value) {
$this->setParameter('routing_number',$value);
}
public function setAccountNumber(string $value) {
$this->setParameter('account_number',$value);
}
public function setAddress(array $value) {
$this->setParameter('address',$value);
}
public function setTransactionSchedules(array $value) {
$this->setParameter('transaction_schedules',$value);
}
public function setFinancialTransactions(array $value) {
$this->setParameter('financial_transactions',$value);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PatchTransactionSchedulesId extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/{id}';
protected $method = 'PATCH';
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {
$this->setParameter('comment',$value);
}
}

View File

@ -1,60 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostCustomers extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers';
protected $method = 'POST';
protected static $model = 'Customer';
public function setId(string $value) {
$this->setParameter('id',$value);
}
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
public function setName(string $value) {
$this->setParameter('name',$value);
}
public function setEmail(string $value) {
$this->setParameter('email',$value);
}
public function setCustomerType(string $value) {
$this->setParameter('customer_type',$value);
}
public function setHomePhone(string $value) {
$this->setParameter('home_phone',$value);
}
public function setPhone(string $value) {
$this->setParameter('phone',$value);
}
public function setBankName(string $value) {
$this->setParameter('bank_name',$value);
}
public function setInstitutionNumber(string $value = '') {
$this->setParameter('institution_number',$value);
}
public function setTransitNumber(string $value = '') {
$this->setParameter('transit_number',$value);
}
public function setBankAccountType(string $value) {
$this->setParameter('bank_account_type',$value);
}
public function setAuthorizationType(string $value = '') {
$this->setParameter('authorization_type',$value);
}
public function setRoutingNumber(string $value = '') {
$this->setParameter('routing_number',$value);
}
public function setAccountNumber(string $value) {
$this->setParameter('account_number',$value);
}
public function setAddress(array $value) {
$this->setParameter('address',$value);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostCustomersShowWithCustomIdentifier extends BaseRequest implements RequestInterface
{
protected $endpoint = '/customers/show_with_custom_identifier';
protected $method = 'POST';
protected static $model = null;
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostTransactionSchedules extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules';
protected $method = 'POST';
protected static $model = 'TransactionSchedule';
public function setCustomerId(string $value) {
$this->setParameter('customer_id',$value);
}
public function setProcessDate(string $value) {
$this->setParameter('process_date',$value);
}
public function setFrequency(string $value) {
$this->setParameter('frequency',$value);
}
public function setInstallments(int $value) {
$this->setParameter('installments',$value);
}
public function setComment(string $value) {
$this->setParameter('comment',$value);
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostTransactionSchedulesCreateWithCustomIdentifier extends PostTransactionSchedules implements RequestInterface
{
protected $endpoint = '/transaction_schedules/create_with_custom_identifier';
protected $method = 'POST';
public function setCustomIdentifier(string $value) {
$this->setParameter('custom_identifier',$value);
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
// You will need to create this BaseRequest class as abstracted from the AbstractRequest;
use Omnipay\Rotessa\Message\Request\BaseRequest;
use Omnipay\Rotessa\Message\Request\RequestInterface;
class PostTransactionSchedulesUpdateViaPost extends BaseRequest implements RequestInterface
{
protected $endpoint = '/transaction_schedules/update_via_post';
protected $method = 'POST';
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {
$this->setParameter('comment',$value);
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Request;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
use Omnipay\Common\Message\RequestInterface as MessageInterface;
interface RequestInterface extends MessageInterface
{
}

View File

@ -1,16 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Response;
use Omnipay\Common\Message\AbstractResponse as Response;
abstract class AbstractResponse extends Response implements ResponseInterface
{
abstract public function getData();
abstract public function getCode();
abstract public function getMessage();
abstract public function getParameter(string $key);
}

View File

@ -1,44 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Response;
use Omnipay\Rotessa\Message\Request\RequestInterface;
use Omnipay\Rotessa\Message\Response\ResponseInterface;
use Omnipay\Common\Message\AbstractResponse as Response;
class BaseResponse extends Response implements ResponseInterface
{
protected $code = 0;
protected $message = null;
function __construct(RequestInterface $request, array $data = [], int $code = 200, string $message = null ) {
parent::__construct($request, $data);
$this->code = $code;
$this->message = $message;
}
public function getData() {
return $this->getParameters();
}
public function getCode() {
return (int) $this->code;
}
public function isSuccessful() {
return $this->code < 300;
}
public function getMessage() {
return $this->message;
}
protected function getParameters() {
return $this->data;
}
public function getParameter(string $key) {
return $this->getParameters()[$key];
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace Omnipay\Rotessa\Message\Response;
use Omnipay\Common\Message\ResponseInterface as MessageInterface;
interface ResponseInterface extends MessageInterface
{
public function getParameter(string $key) ;
}

View File

@ -1,63 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Common\ParametersTrait;
use Omnipay\Rotessa\Model\ModelInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Omnipay\Rotessa\Exception\ValidationException;
abstract class AbstractModel implements ModelInterface {
use ParametersTrait;
abstract public function jsonSerialize() : array;
public function validate() : bool {
$required = array_diff_key( array_flip($this->required), array_filter($this->getParameters()) );
if(!empty($required)) throw new ValidationException("Could not validate " . implode(",", array_keys($required)) );
return true;
}
public function __get($key) {
return array_key_exists($key, $this->attributes) ? $this->getParameter($key) : null;
}
public function __set($key, $value) {
if(array_key_exists($key, $this->attributes)) $this->setParameter($key, $value);
}
public function __toString() : string {
return json_encode($this);
}
public function toString() : string {
return $this->__toString();
}
public function __toArray() : array {
return $this->getParameters();
}
public function toArray() : array {
return $this->__toArray();
}
public function initialize(array $params = []) {
$this->parameters = new ParameterBag;
$parameters = array_merge($this->defaults, $params);
if ($parameters) {
foreach ($this->attributes as $param => $type) {
$value = @$parameters[$param];
if($value){
settype($value, $type);
$this->setParameter($param, $value);
}
}
}
return $this;
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use \DateTime;
use Omnipay\Rotessa\Model\AbstractModel;
use Omnipay\Rotessa\Model\ModelInterface;
class BaseModel extends AbstractModel implements ModelInterface {
protected $attributes = [
"id" => "string"
];
protected $required = ['id'];
protected $defaults = ['id' => 0 ];
public function __construct($parameters = array()) {
$this->initialize($parameters);
}
public function jsonSerialize() : array {
return array_intersect_key($this->toArray(), array_flip($this->required) );
}
}

View File

@ -1,94 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Object\Country;
use Omnipay\Rotessa\Object\Address;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Object\CustomerType;
use Omnipay\Rotessa\Model\ModelInterface;
use Omnipay\Rotessa\Object\BankAccountType;
use Omnipay\Rotessa\Object\AuthorizationType;
use Omnipay\Rotessa\Exception\ValidationException;
class CustomerModel extends BaseModel implements ModelInterface {
protected $attributes = [
"id" => "string",
"custom_identifier" => "string",
"name" => "string",
"email" => "string",
"customer_type" => "string",
"home_phone" => "string",
"phone" => "string",
"bank_name" => "string",
"institution_number" => "string",
"transit_number" => "string",
"bank_account_type" => "string",
"authorization_type" => "string",
"routing_number" => "string",
"account_number" => "string",
"address" => "object",
"transaction_schedules" => "array",
"financial_transactions" => "array",
"active" => "bool"
];
protected $defaults = ["active" => false,"customer_type" =>'Business',"bank_account_type" =>'Savings',"authorization_type" =>'Online',];
protected $required = ["name","email","customer_type","home_phone","phone","bank_name","institution_number","transit_number","bank_account_type","authorization_type","routing_number","account_number","address",'custom_identifier'];
public function validate() : bool {
try {
$country = $this->address->country;
if(!self::isValidCountry($country)) throw new \Exception("Invalid country!");
$this->required = array_diff($this->required, Country::isAmerican($country) ? ["institution_number", "transit_number"] : ["bank_account_type", "routing_number"]);
parent::validate();
if(Country::isCanadian($country) ) {
if(!self::isValidTransitNumber($this->getParameter('transit_number'))) throw new \Exception("Invalid transit number!");
if(!self::isValidInstitutionNumber($this->getParameter('institution_number'))) throw new \Exception("Invalid institution number!");
}
if(!self::isValidCustomerType($this->getParameter('customer_type'))) throw new \Exception("Invalid customer type!");
if(!self::isValidBankAccountType($this->getParameter('bank_account_type'))) throw new \Exception("Invalid bank account type!");
if(!self::isValidAuthorizationType($this->getParameter('authorization_type'))) throw new \Exception("Invalid authorization type!");
} catch (\Throwable $th) {
throw new ValidationException($th->getMessage());
}
return true;
}
public static function isValidCountry(string $country ) : bool {
return Country::isValidCountryCode($country) || Country::isValidCountryName($country);
}
public static function isValidTransitNumber(string $value ) : bool {
return strlen($value) == 5;
}
public static function isValidInstitutionNumber(string $value ) : bool {
return strlen($value) == 3;
}
public static function isValidCustomerType(string $value ) : bool {
return CustomerType::isValid($value);
}
public static function isValidBankAccountType(string $value ) : bool {
return BankAccountType::isValid($value);
}
public static function isValidAuthorizationType(string $value ) : bool {
return AuthorizationType::isValid($value);
}
public function toArray() : array {
return [ 'address' => (array) $this->getParameter('address') ] + parent::toArray();
}
public function jsonSerialize() : array {
$address = (array) $this->getParameter('address');
unset($address['country']);
return compact('address') + parent::jsonSerialize();
}
}

View File

@ -1,16 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Object\Country;
use Omnipay\Rotessa\Object\Address;
use Omnipay\Rotessa\Object\CustomerType;
use Omnipay\Rotessa\Model\ModelInterface;
use Omnipay\Rotessa\Object\BankAccountType;
use Omnipay\Rotessa\Object\AuthorizationType;
use Omnipay\Rotessa\Exception\ValidationException;
class CustomerPatchModel extends CustomerModel implements ModelInterface {
protected $required = ["id","custom_identifier","name","email","customer_type","home_phone","phone","bank_name","institution_number","transit_number","bank_account_type","authorization_type","routing_number","account_number","address"];
}

View File

@ -1,8 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
interface ModelInterface extends \JsonSerializable
{
public function __toArray();
public function __toString();
}

View File

@ -1,84 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use \DateTime;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Object\Frequency;
use Omnipay\Rotessa\Model\ModelInterface;
use Omnipay\Rotessa\Exception\ValidationException;
class TransactionScheduleModel extends BaseModel implements ModelInterface {
protected $properties;
protected $attributes = [
"id" => "string",
"amount" => "float",
"comment" => "string",
"created_at" => "date",
"financial_transactions" => "array",
"frequency" => "string",
"installments" => "integer",
"next_process_date" => "date",
"process_date" => "date",
"updated_at" => "date",
"customer_id" => "string",
"custom_identifier" => "string",
];
public const DATE_FORMAT = 'F j, Y';
protected $defaults = ["amount" =>0.00,"comment" =>' ',"financial_transactions" =>0,"frequency" =>'Once',"installments" =>1];
protected $required = ["amount","comment","frequency","installments","process_date"];
public function validate() : bool {
try {
parent::validate();
if(!self::isValidDate($this->process_date)) throw new \Exception("Could not validate date ");
if(!self::isValidFrequency($this->frequency)) throw new \Exception("Invalid frequency");
if(is_null($this->customer_id) && is_null($this->custom_identifier)) throw new \Exception("customer id or custom identifier is invalid");
} catch (\Throwable $th) {
throw new ValidationException($th->getMessage());
}
return true;
}
public function jsonSerialize() : array {
return ['customer_id' => $this->getParameter('customer_id'), 'custom_identifier' => $this->getParameter('custom_identifier') ] + parent::jsonSerialize() ;
}
public function __toArray() : array {
return parent::__toArray() ;
}
public function initialize(array $params = [] ) {
$o_params = array_intersect_key(
$params = array_intersect_key($params, $this->attributes),
($attr = array_filter($this->attributes, fn($p) => $p != "date"))
);
parent::initialize($o_params);
$d_params = array_diff_key($params, $attr);
array_walk($d_params, function($v,$k) {
$this->setParameter($k, self::formatDate( $v) );
}, );
return $this;
}
public static function isValidDate($date) : bool {
$d = DateTime::createFromFormat(self::DATE_FORMAT, $date);
// Check if the date is valid and matches the format
return $d && $d->format(self::DATE_FORMAT) === $date;
}
public static function isValidFrequency($value) : bool {
return Frequency::isValid($value);
}
protected static function formatDate($date) : string {
$d = new DateTime($date);
return $d->format(self::DATE_FORMAT);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Model\ModelInterface;
class TransactionSchedulesIdBodyModel extends BaseModel implements ModelInterface {
protected $properties;
protected $attributes = [
"amount" => "int",
"comment" => "string",
];
public const DATE_FORMAT = 'Y-m-d H:i:s';
private $_is_error = false;
protected $defaults = ["amount" =>0,"comment" =>'0',];
protected $required = ["amount","comment",];
}

View File

@ -1,24 +0,0 @@
<?php
namespace Omnipay\Rotessa\Model;
use Omnipay\Rotessa\Model\BaseModel;
use Omnipay\Rotessa\Model\ModelInterface;
class TransactionSchedulesUpdateViaPostBodyModel extends BaseModel implements ModelInterface {
protected $properties;
protected $attributes = [
"id" => "int",
"amount" => "int",
"comment" => "string",
];
public const DATE_FORMAT = 'Y-m-d H:i:s';
private $_is_error = false;
protected $defaults = ["amount" =>0,"comment" =>'0',];
protected $required = ["amount","comment",];
}

View File

@ -1,53 +0,0 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Common\ParametersTrait;
final class Address implements \JsonSerializable {
use ParametersTrait;
protected $attributes = [
"address_1" => "string",
"address_2" => "string",
"city" => "string",
"id" => "int",
"postal_code" => "string",
"province_code" => "string",
"country" => "string"
];
protected $required = ["address_1","address_2","city","postal_code","province_code",];
public function jsonSerialize() {
return array_intersect_key($this->getParameters(), array_flip($this->required));
}
public function getCountry() : string {
return $this->getParameter('country');
}
public function initialize(array $parameters) {
foreach($this->attributes as $param => $type) {
$value = @$parameters[$param] ;
settype($value, $type);
$value = $value ?? null;
$this->parameters->set($param, $value);
}
}
public function __toArray() : array {
return $this->getParameters();
}
public function __toString() : string {
return $this->getFullAddress();
}
public function getFullAddress() :string {
$full_address = $this->getParameters();
extract($full_address);
return "$address_1 $address_2, $city, $postal_code $province_code, $country";
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class AuthorizationType {
use isValidTypeTrait;
const IN_PERSON = "In Person";
const ONLINE = "Online";
public static function isInPerson($value) {
return $value === self::IN_PERSON;
}
public static function isOnline($value) {
return $value === self::ONLINE;
}
public static function getTypes() : array {
return [
self::IN_PERSON,
self::ONLINE
];
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class BankAccountType {
use IsValidTypeTrait;
const SAVINGS = "Savings";
const CHECKING = "Checking";
public static function isSavings($value) {
return $value === self::SAVINGS;
}
public static function isChecking($value) {
return $value === self::Checking;
}
public static function getTypes() : array {
return [
self::SAVINGS,
self::CHECKING
];
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class Country {
use IsValidTypeTrait;
protected static $codes = ['CA','US'];
protected static $names = ['United States', 'Canada'];
public static function isValidCountryName(string $value) {
return in_array($value, self::$names);
}
public static function isValidCountryCode(string $value) {
return in_array($value, self::$codes);
}
public static function isAmerican(string $value) : bool {
return $value == 'US' || $value == 'United States';
}
public static function isCanadian(string $value) : bool {
return $value == 'CA' || $value == 'Canada';
}
public static function getTypes() : array {
return $codes + $names;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class CustomerType {
use IsValidTypeTrait;
const PERSONAL = "Personal";
const BUSINESS = "Business";
public static function isPersonal($value) {
return $value === self::PERSONAL;
}
public static function isBusiness($value) {
return $value === self::BUSINESS;
}
public static function getTypes() : array {
return [
self::PERSONAL,
self::BUSINESS
];
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace Omnipay\Rotessa\Object;
use Omnipay\Rotessa\IsValidTypeTrait;
final class Frequency {
use IsValidTypeTrait;
const ONCE = "Once";
const WEEKLY = "Weekly";
const OTHER_WEEK = "Every Other Week";
const MONTHLY= "Monthly";
const OTHER_MONTH = "Every Other Month";
const QUARTERLY = "Quarterly";
const SEMI_ANNUALLY = "Semi-Annually";
const YEARLY = "Yearly";
public static function isOnce($value) {
return $value === self::ONCE;
}
public static function isWeekly($value) {
return $value === self::WEEKLY;
}
public static function isOtherWeek($value) {
return $value === self::OTHER_WEEK;
}
public static function isMonthly($value) {
return $value === self::MONTHLY;
}
public static function isOtherMonth($value) {
return $value === self::OTHER_MONTH;
}
public static function isQuarterly($value) {
return $value === self::QUARTERLY;
}
public static function isSemiAnnually($value) {
return $value === self::SEMI_ANNUALLY;
}
public static function isYearly($value) {
return $value === self::YEARLY;
}
public static function getTypes() : array {
return [
self::ONCE,
self::WEEKLY,
self::OTHER_WEEK,
self::MONTHLY,
self::OTHER_MONTH,
self::QUARTERLY,
self::SEMI_ANNUALLY,
self::YEARLY
];
}
}

View File

@ -11,24 +11,19 @@
namespace App\PaymentDrivers;
use Omnipay\Omnipay;
use App\DataMapper\ClientSettings;
use App\Models\Client;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\PaymentHash;
use Illuminate\Support\Arr;
use App\Models\GatewayType;
use Omnipay\Rotessa\Gateway;
use App\Models\ClientContact;
use App\Utils\Traits\MakesHash;
use App\Jobs\Util\SystemLogger;
use App\PaymentDrivers\BaseDriver;
use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Builder;
use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\PaymentMethod as Acss;
use App\PaymentDrivers\Rotessa\PaymentMethod as BankTransfer;
use Illuminate\Support\Facades\Http;
class RotessaPaymentDriver extends BaseDriver
{
@ -40,24 +35,15 @@ class RotessaPaymentDriver extends BaseDriver
public $can_authorise_credit_card = true;
public Gateway $gateway;
public $payment_method;
public static $methods = [
GatewayType::BANK_TRANSFER => BankTransfer::class,
//GatewayType::BACS => Bacs::class,
GatewayType::ACSS => Acss::class,
// GatewayType::DIRECT_DEBIT => DirectDebit::class
];
public function init(): self
{
$this->gateway = Omnipay::create(
$this->company_gateway->gateway->provider
);
$this->gateway->initialize((array) $this->company_gateway->getConfig());
return $this;
}
@ -116,73 +102,61 @@ class RotessaPaymentDriver extends BaseDriver
}
public function importCustomers() {
$this->init();
try {
if(!$result = Cache::has("rotessa-import_customers-{$this->company_gateway->company->company_key}")) {
$result = $this->gateway->getCustomers()->send();
if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
// cache results
Cache::put("rotessa-import_customers-{$this->company_gateway->company->company_key}", $result->getData(), 60 * 60 * 24);
}
$result = Cache::get("rotessa-import_customers-{$this->company_gateway->company->company_key}");
$customers = collect($result)->unique('email');
$result = $this->gatewayRequest('get','customers',[]); //Rotessa customers
if($result->failed())
$result->throw();
$customers = collect($result->json())->unique('email'); //Rotessa customer emails
$client_emails = $customers->pluck('email')->all();
$company_id = $this->company_gateway->company->id;
// get existing customers
$client_contacts = ClientContact::where('company_id', $company_id)->whereIn('email', $client_emails )->whereNull('deleted_at')->get();
$client_contacts = ClientContact::where('company_id', $company_id)
->whereIn('email', $client_emails )
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->whereNull('deleted_at')
->get();
$client_contacts = $client_contacts->map(function($item, $key) use ($customers) {
return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number, 'client_id' => $item->client->id ]);
return array_merge($customers->firstWhere("email", $item->email),['custom_identifier' => $item->client->number, 'identifier' => $item->client->number, 'client_id' => $item->client->id ]);
} );
// create payment methods
$client_contacts->each(
function($contact) use ($customers) {
$result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send();
$this->client = Client::find($contact->client_id);
$customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
collect($client_contacts)->each(
function($contact) {
$contact = (object)$contact;
$result = $this->gatewayRequest("get","customers/{$contact->id}");
$result = $result->json();
$this->client = Client::query()->find($contact->client_id);
$customer = array_merge($result, ['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ]);
$this->findOrCreateCustomer($customer);
}
);
// create new clients from rotessa customers
$client_emails = $client_contacts->pluck('email')->all();
$client_contacts = $customers->filter(function ($value, $key) use ($client_emails) {
return !in_array(((object) $value)->email, $client_emails);
})->each( function($customer) use ($company_id) {
// create new client contact from rotess customer
$customer = (object) $this->gateway->getCustomersId(['id' => ($customer = (object) $customer)->id])->send()->getData();
/**
{
"account_number": "11111111"
"active": true,
"address": {
"address_1": "123 Main Street",
"address_2": "Unit 4",
"city": "Birmingham",
"id": 114397,
"postal_code": "36016",
"province_code": "AL"
},
"authorization_type": "Online",
"bank_account_type": "Checking",
"bank_name": "Scotiabank",
"created_at": "2015-02-10T23:50:45.000-06:00",
"custom_identifier": "Mikey",
"customer_type": "Personal",
"email": "mikesmith@test.com",
"financial_transactions": [],
"home_phone": "(204) 555 5555",
"id": 1,
"identifier": "Mikey",
"institution_number": "",
"name": "Mike Smith",
"phone": "(204) 555 4444",
"routing_number": "111111111",
"transaction_schedules": [],
"transit_number": "",
"updated_at": "2015-02-10T23:50:45.000-06:00"
}
*/
$customer = $this->gatewayRequest("get", "customers/{$customer['id']}")->json();
$settings = ClientSettings::defaults();
$settings->currency_id = $this->company_gateway->company->getSetting('currency_id');
$customer = (object)$customer;
$client = (\App\Factory\ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id))->fill(
[
'address1' => $customer->address['address_1'] ?? '',
@ -192,7 +166,8 @@ class RotessaPaymentDriver extends BaseDriver
'state' => $customer->address['province_code'] ?? '',
'country_id' => empty($customer->transit_number) ? 840 : 124,
'routing_id' => empty(($r = $customer->routing_number))? null : $r,
"number" => str_pad($customer->account_number,3,'0',STR_PAD_LEFT)
"number" => str_pad($customer->account_number,3,'0',STR_PAD_LEFT),
"settings" => $settings,
]
);
$client->saveQuietly();
@ -207,8 +182,7 @@ class RotessaPaymentDriver extends BaseDriver
$client->contacts()->saveMany([$contact]);
$contact = $client->contacts()->first();
$this->client = $client;
$customer = (new Customer((array) $customer))->additional(['id' => $customer->id, 'custom_identifier' => $customer->custom_identifier ?? $contact->id ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
});
} catch (\Throwable $th) {
$data = [
@ -228,40 +202,46 @@ class RotessaPaymentDriver extends BaseDriver
public function findOrCreateCustomer(array $data)
{
nlog($data);
$result = null;
try {
$existing = ClientGatewayToken::query()
->where('company_gateway_id', $this->company_gateway->id)
->where('client_id', $this->client->id)
->orWhere(function (Builder $query) use ($data) {
$query->where('token', encrypt(join(".", Arr::only($data, 'id','custom_identifier'))) )
->where('gateway_customer_reference', Arr::only($data,'id'));
})
->where('is_deleted',0)
->where('gateway_customer_reference', Arr::only($data,'id'))
->exists();
if ($existing) return true;
else if(!Arr::has($data,'id')) {
$result = $this->gateway->authorize($data)->send();
if (!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
$customer = new Customer($result->getData());
$data = array_filter($customer->resolve());
if ($existing)
return true;
if(!isset($data['id'])) {
nlog("no id, lets goo");
$result = $this->gatewayRequest('post', 'customers', $data);
if($result->failed())
$result->throw();
$data = $result->json();
nlog($data);
}
// $payment_method_id = Arr::has($data,'address.postal_code') && ((int) $data['address']['postal_code'])? GatewayType::BANK_TRANSFER: GatewayType::ACSS;
// TODO: Check/ Validate postal code between USA vs CAN
$payment_method_id = GatewayType::ACSS;
$gateway_token = $this->storeGatewayToken( [
'payment_meta' => $data + ['brand' => 'Rotessa', 'last4' => $data['bank_name'], 'type' => $data['bank_account_type'] ],
'token' => encrypt(join(".", Arr::only($data, 'id','custom_identifier'))),
'payment_meta' => ['brand' => 'Bank Transfer', 'last4' => substr($data['account_number'], -4), 'type' => GatewayType::ACSS ],
'token' => join(".", Arr::only($data, ['id','custom_identifier'])),
'payment_method_id' => $payment_method_id ,
], ['gateway_customer_reference' =>
$data['id']
, 'routing_number' => Arr::has($data,'routing_number') ? $data['routing_number'] : $data['transit_number'] ]);
], [
'gateway_customer_reference' => $data['id'],
'routing_number' => Arr::has($data,'routing_number') ? $data['routing_number'] : $data['transit_number']
]);
return $data['id'];
throw new \Exception($result->getMessage(), (int) $result->getCode());
} catch (\Throwable $th) {
$data = [
@ -269,7 +249,7 @@ class RotessaPaymentDriver extends BaseDriver
'transaction_response' => $th->getMessage(),
'success' => false,
'description' => $th->getMessage(),
'code' =>(int) $th->getCode()
'code' => 500
];
SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company);
@ -277,4 +257,20 @@ class RotessaPaymentDriver extends BaseDriver
throw $th;
}
}
public function gatewayRequest($verb, $uri, $payload = [])
{
$r = Http::withToken($this->company_gateway->getConfigField('apiKey'))
->{$verb}($this->getUrl().$uri, $payload);
nlog($r->body());
return $r;
}
private function getUrl(): string
{
return $this->company_gateway->getConfigField('testMode') ? 'https://sandbox-api.rotessa.com/v1/' : 'https://api.rotessa.com/v1/';
}
}

View File

@ -59,6 +59,11 @@ class ChargeRefunded implements ShouldQueue
$payment_hash_key = $source['metadata']['payment_hash'] ?? null;
if(is_null($payment_hash_key)){
nlog("charge.refunded not found");
return;
}
$payment_hash = PaymentHash::query()->where('hash', $payment_hash_key)->first();
$company_gateway = $payment_hash->payment->company_gateway;

View File

@ -59,38 +59,22 @@ class PaymentIntentProcessingWebhook implements ShouldQueue
/* Stub processing payment intents with a pending payment */
public function handle()
{
nlog($this->stripe_request);
// The first payment will always be a PI payment - subsequent are PY
MultiDB::findAndSetDbByCompanyKey($this->company_key);
$company = Company::query()->where('company_key', $this->company_key)->first();
foreach ($this->stripe_request as $transaction) {
$payment = Payment::query()
->where('company_id', $company->id)
->where(function ($query) use ($transaction) {
if(isset($transaction['payment_intent'])) {
$query->where('transaction_reference', $transaction['payment_intent']);
}
if(isset($transaction['payment_intent']) && isset($transaction['id'])) {
$query->orWhere('transaction_reference', $transaction['id']);
}
if(!isset($transaction['payment_intent']) && isset($transaction['id'])) {
$query->where('transaction_reference', $transaction['id']);
}
})
->where('transaction_reference', $transaction['id'])
->first();
if ($payment) {
$payment->status_id = Payment::STATUS_PENDING;
$payment->save();
nlog("found payment");
$this->payment_completed = true;
}

View File

@ -33,7 +33,6 @@ class ComposerServiceProvider extends ServiceProvider
$view->with('states', $states);
});
// CAProvinces View Composer
view()->composer(['*.rotessa.components.address','*.rotessa.components.banks.CA.bank','*.rotessa.components.dropdowns.country.CA'], function ($view) {
$provinces = CAProvinces::get();
$view->with('provinces', $provinces);

View File

@ -11,17 +11,17 @@
namespace App\Providers;
use App\Http\Middleware\ThrottleRequestsWithPredis;
use App\Models\Scheduler;
use App\Utils\Ninja;
use App\Models\Scheduler;
use Illuminate\Http\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Route;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use App\Http\Middleware\ThrottleRequestsWithPredis;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
@ -65,7 +65,7 @@ class RouteServiceProvider extends ServiceProvider
if (Ninja::isSelfHost()) {
return Limit::none();
} else {
return Limit::perMinute(300)->by($request->ip());
return Limit::perMinute(500)->by($request->ip());
}
});

View File

@ -46,16 +46,16 @@ class TaskRepository extends BaseRepository
$this->new_task = false;
}
if(isset($data['assigned_user_id']) && $data['assigned_user_id'] != $task->assigned_user_id){
TaskAssigned::dispatch($task, $task->company->db)->delay(2);
}
if(!is_numeric($task->rate) && !isset($data['rate']))
$data['rate'] = 0;
$task->fill($data);
$task->saveQuietly();
if(isset($data['assigned_user_id']) && $data['assigned_user_id'] != $task->assigned_user_id) {
TaskAssigned::dispatch($task, $task->company->db)->delay(2);
}
$this->init($task);
if ($this->new_task && ! $task->status_id) {
@ -155,6 +155,8 @@ class TaskRepository extends BaseRepository
$this->saveDocuments($data['documents'], $task);
}
$this->calculateProjectDuration($task);
return $task;
}
@ -261,6 +263,8 @@ class TaskRepository extends BaseRepository
$task->saveQuietly();
}
$this->calculateProjectDuration($task);
return $task;
}
@ -302,7 +306,10 @@ class TaskRepository extends BaseRepository
$task->saveQuietly();
}
$this->calculateProjectDuration($task);
return $task;
}
public function triggeredActions($request, $task)
@ -348,4 +355,67 @@ class TaskRepository extends BaseRepository
return $task->number;
}
private function calculateProjectDuration(Task $task)
{
if($task->project) {
$duration = 0;
$task->project->tasks->each(function ($task) use (&$duration) {
if(is_iterable(json_decode($task->time_log))) {
foreach(json_decode($task->time_log) as $log) {
if(!is_array($log)) {
continue;
}
$start_time = $log[0];
$end_time = $log[1] == 0 ? time() : $log[1];
$duration += $end_time - $start_time;
}
}
});
$task->project->current_hours = (int) round(($duration / 60 / 60), 0);
$task->push();
}
}
/**
* @param $entity
*/
public function restore($task)
{
if (!$task->trashed()) {
return;
}
parent::restore($task);
$this->calculateProjectDuration($task);
}
/**
* @param $entity
*/
public function delete($task)
{
if ($task->is_deleted) {
return;
}
parent::delete($task);
$this->calculateProjectDuration($task);
}
}

View File

@ -11,9 +11,12 @@
namespace App\Services\Chart;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Quote;
use App\Models\Task;
use Illuminate\Contracts\Database\Eloquent\Builder;
/**
* Class ChartCalculations.
@ -170,4 +173,215 @@ trait ChartCalculations
return $result;
}
public function getLoggedTasks($data): int|float
{
$q = $this->taskQuery($data);
return $this->taskCalculations($q, $data);
}
public function getPaidTasks($data): int|float
{
$q = $this->taskQuery($data);
$q->whereHas('invoice', function ($query){
$query->where('status_id', 4)->where('is_deleted', 0);
});
return $this->taskCalculations($q, $data);
}
public function getInvoicedTasks($data): int|float
{
$q = $this->taskQuery($data);
$q->whereHas('invoice');
return $this->taskCalculations($q, $data);
}
/**
* All Expenses
*/
public function getLoggedExpenses($data): int|float
{
$q = $this->expenseQuery($data);
return $this->expenseCalculations($q, $data);
}
/**
* Expenses that should be invoiced - but are not yet invoiced.
*/
public function getPendingExpenses($data): int|float
{
$q = $this->expenseQuery($data);
$q->where('should_be_invoiced', true)->whereNull('invoice_id');
return $this->expenseCalculations($q, $data);
}
/**
* Invoiced.
*/
public function getInvoicedExpenses($data): int|float
{
$q = $this->expenseQuery($data);
$q->whereNotNull('invoice_id');
return $this->expenseCalculations($q, $data);
}
/**
* Paid.
*/
public function getPaidExpenses($data): int|float
{
$q = $this->expenseQuery($data);
$q->whereNotNull('payment_date');
return $this->expenseCalculations($q, $data);
}
/**
* Paid.
*/
public function getInvoicedPaidExpenses($data): int|float
{
$q = $this->expenseQuery($data);
$q->whereNotNull('invoice_id')->whereNotNull('payment_date');
return $this->expenseCalculations($q, $data);
}
private function expenseCalculations(Builder $query, array $data): int|float
{
$result = 0;
$calculated = $this->expenseCalculator($query, $data);
match ($data['calculation']) {
'sum' => $result = $calculated->sum(),
'avg' => $result = $calculated->avg(),
'count' => $result = $query->count(),
default => $result = 0,
};
return $result;
}
private function expenseCalculator(Builder $query, array $data)
{
return $query->get()
->when($data['currency_id'] == '999', function ($collection) {
$collection->map(function ($e) {
/** @var \App\Models\Expense $e */
return $e->amount * $e->exchange_rate;
});
})
->when($data['currency_id'] != '999', function ($collection) {
$collection->map(function ($e) {
/** @var \App\Models\Expense $e */
return $e->amount;
});
});
}
private function expenseQuery($data): Builder
{
$query = Expense::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
if(in_array($data['period'], ['current,previous'])) {
$query->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
return $query;
}
////////////////////////////////////////////////////////////////
private function taskMoneyCalculator($query, $data)
{
return $query->get()
->when($data['currency_id'] == '999', function ($collection) {
$collection->map(function ($t) {
return $t->taskCompanyValue();
});
})
->when($data['currency_id'] != '999', function ($collection) {
$collection->map(function ($t) {
return $t->taskValue();
});
});
}
private function taskQuery($data): Builder
{
$q = Task::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('calculated_start_date', [$data['start_date'], $data['end_date']]);
}
return $q;
}
private function taskCalculations(Builder $q, array $data): int|float
{
$result = 0;
$calculated = collect();
if($data['calculation'] != 'count' && $data['format'] == 'money') {
if($data['currency_id'] != '999') {
$q->whereHas('client', function ($query) use ($data) {
$query->where('settings->currency_id', $data['currency_id']);
});
}
$calculated = $this->taskMoneyCalculator($q, $data);
}
if($data['calculation'] != 'count' && $data['format'] == 'time') {
$calculated = $q->get()->map(function ($t) {
return $t->calcDuration();
});
}
match ($data['calculation']) {
'sum' => $result = $calculated->sum(),
'avg' => $result = $calculated->avg(),
'count' => $result = $q->count(),
default => $result = 0,
};
return $result;
}
}

View File

@ -224,6 +224,8 @@ class ChartService
* period - current/previous
* calculation - sum/count/avg
*
* May require currency_id
*
* date_range - this_month
* or
* start_date - end_date
@ -234,18 +236,18 @@ class ChartService
match($data['field']){
'active_invoices' => $results = $this->getActiveInvoices($data),
'outstanding_invoices' => $results = 0,
'completed_payments' => $results = 0,
'refunded_payments' => $results = 0,
'active_quotes' => $results = 0,
'unapproved_quotes' => $results = 0,
'logged_tasks' => $results = 0,
'invoiced_tasks' => $results = 0,
'paid_tasks' => $results = 0,
'logged_expenses' => $results = 0,
'pending_expenses' => $results = 0,
'invoiced_expenses' => $results = 0,
'invoice_paid_expenses' => $results = 0,
'outstanding_invoices' => $results = $this->getOutstandingInvoices($data),
'completed_payments' => $results = $this->getCompletedPayments($data),
'refunded_payments' => $results = $this->getRefundedPayments($data),
'active_quotes' => $results = $this->getActiveQuotes($data),
'unapproved_quotes' => $results = $this->getUnapprovedQuotes($data),
'logged_tasks' => $results = $this->getLoggedTasks($data),
'invoiced_tasks' => $results = $this->getInvoicedTasks($data),
'paid_tasks' => $results = $this->getPaidTasks($data),
'logged_expenses' => $results = $this->getLoggedExpenses($data),
'pending_expenses' => $results = $this->getPendingExpenses($data),
'invoiced_expenses' => $results = $this->getInvoicedExpenses($data),
'invoice_paid_expenses' => $results = $this->getInvoicedPaidExpenses($data),
default => $results = 0,
};

File diff suppressed because one or more lines are too long

View File

@ -124,7 +124,7 @@ class TemplateService
$this->twig->addFilter($filter);
$allowedTags = ['if', 'for', 'set', 'filter'];
$allowedFilters = ['escape', 'e', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format'];
$allowedFilters = ['replace', 'escape', 'e', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br'];
$allowedFunctions = ['range', 'cycle', 'constant', 'date',];
$allowedProperties = ['type_id'];
$allowedMethods = ['img','t'];
@ -323,6 +323,9 @@ class TemplateService
$template = $template->render($this->data);
$f = $this->document->createDocumentFragment();
$template = htmlspecialchars($template, ENT_XML1, 'UTF-8');
$f->appendXML(html_entity_decode($template));
$replacements[] = $f;
@ -482,6 +485,8 @@ class TemplateService
default => $processed = [],
};
// nlog(json_encode($processed));
return $processed;
})->toArray();

View File

@ -17,77 +17,67 @@ use Illuminate\Support\Str;
class TranslationHelper
{
public static function getIndustries()
{
// public static function getIndustries()
// {
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$industries = app('industries');
// /** @var \Illuminate\Support\Collection<\App\Models\Currency> */
// $industries = app('industries');
return $industries->each(function ($industry) {
$industry->name = ctrans('texts.industry_'.$industry->name);
})->sortBy(function ($industry) {
return $industry->name;
});
}
// return $industries->each(function ($industry) {
// $industry->name = ctrans('texts.industry_'.$industry->name);
// })->sortBy(function ($industry) {
// return $industry->name;
// });
// }
public static function getCountries()
{
/** @var \Illuminate\Support\Collection<\App\Models\Country> */
// $countries = app('countries');
return app('countries');
return \App\Models\Country::all()->each(function ($country) {
$country->name = ctrans('texts.country_'.$country->name);
})->sortBy(function ($country) {
return $country->iso_3166_2;
});
}
public static function getPaymentTypes()
{
// public static function getPaymentTypes()
// {
/** @var \Illuminate\Support\Collection<\App\Models\PaymentType> */
// $payment_types = app('payment_types');
// /** @var \Illuminate\Support\Collection<\App\Models\PaymentType> */
// // $payment_types = app('payment_types');
return \App\Models\PaymentType::all()->each(function ($pType) {
$pType->name = ctrans('texts.payment_type_'.$pType->name);
})->sortBy(function ($pType) {
return $pType->name;
});
}
// return \App\Models\PaymentType::all()->each(function ($pType) {
// $pType->name = ctrans('texts.payment_type_'.$pType->name);
// })->sortBy(function ($pType) {
// return $pType->name;
// });
// }
public static function getLanguages()
{
// public static function getLanguages()
// {
/** @var \Illuminate\Support\Collection<\App\Models\Language> */
// $languages = app('languages');
// /** @var \Illuminate\Support\Collection<\App\Models\Language> */
// // $languages = app('languages');
return \App\Models\Language::all()->each(function ($lang) {
$lang->name = ctrans('texts.lang_'.$lang->name);
})->sortBy(function ($lang) {
return $lang->name;
});
}
// return \App\Models\Language::all()->each(function ($lang) {
// $lang->name = ctrans('texts.lang_'.$lang->name);
// })->sortBy(function ($lang) {
// return $lang->name;
// });
// }
public static function getCurrencies()
{
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
// $currencies = app('currencies');
return app('currencies');
return \App\Models\Currency::all()->each(function ($currency) {
$currency->name = ctrans('texts.currency_'.Str::slug($currency->name, '_'));
})->sortBy(function ($currency) {
return $currency->name;
});
}
public static function getPaymentTerms()
{
return PaymentTerm::getCompanyTerms()->map(function ($term) {
$term['name'] = ctrans('texts.payment_terms_net').' '.$term['num_days'];
// public static function getPaymentTerms()
// {
// return PaymentTerm::getCompanyTerms()->map(function ($term) {
// $term['name'] = ctrans('texts.payment_terms_net').' '.$term['num_days'];
return $term;
});
}
// return $term;
// });
// }
}

View File

@ -74,14 +74,12 @@
"league/csv": "^9.6",
"league/flysystem-aws-s3-v3": "^3.0",
"league/fractal": "^0.20.0",
"league/omnipay": "^3.1",
"livewire/livewire": "^3",
"microsoft/microsoft-graph": "^1.69",
"mollie/mollie-api-php": "^2.36",
"nelexa/zip": "^4.0",
"nordigen/nordigen-php": "^1.1",
"nwidart/laravel-modules": "^11.0",
"omnipay/paypal": "^3.0",
"phpoffice/phpspreadsheet": "^1.29",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^2",
@ -133,7 +131,8 @@
"app/Helpers/Generic.php",
"app/Helpers/ClientPortal.php"
],
"classmap": ["app/PaymentDrivers/Rotessa/vendor/karneaud/omnipay-rotessa/src/Omnipay/Rotessa/"]
"classmap": [
]
},
"autoload-dev": {
"psr-4": {
@ -186,7 +185,7 @@
"url": "https://github.com/turbo124/apple"
},
{
"type":"vcs",
"type": "vcs",
"url": "https://github.com/invoiceninja/einvoice"
},
{
@ -204,4 +203,4 @@
],
"minimum-stability": "dev",
"prefer-stable": true
}
}

833
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.10.16'),
'app_tag' => env('APP_TAG', '5.10.16'),
'app_version' => env('APP_VERSION', '5.10.19'),
'app_tag' => env('APP_TAG', '5.10.19'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@ -12,18 +12,18 @@ return new class extends Migration
*/
public function up(): void
{
Company::whereNotNull('tax_data')
->cursor()
->each(function($company){
// Company::whereNotNull('tax_data')
// ->cursor()
// ->each(function($company){
if($company->tax_data?->version == 'alpha')
{
// if($company->tax_data?->version == 'alpha' && ($company->tax_data->seller_subregion ?? false))
// {
$company->update(['tax_data' => new \App\DataMapper\Tax\TaxModel($company->tax_data)]);
// $company->update(['tax_data' => new \App\DataMapper\Tax\TaxModel($company->tax_data)]);
}
// }
});
// });
}
/**

View File

@ -0,0 +1,41 @@
<?php
use App\Utils\Ninja;
use App\Models\Company;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if(Ninja::isSelfHost())
{
Company::whereNotNull('tax_data')
->cursor()
->each(function ($company) {
if($company->tax_data?->version == 'alpha' && ($company->tax_data->seller_subregion ?? false)) {
$company->update(['tax_data' => new \App\DataMapper\Tax\TaxModel($company->tax_data)]);
}
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -5309,7 +5309,8 @@ $lang = array(
'account_holder_information' => 'Account Holder Information',
'enter_information_for_the_account_holder' => 'Enter Information for the Account Holder',
'customer_type' => 'Customer Type',
'process_date' => 'Process Date'
'process_date' => 'Process Date',
'forever_free' => 'Forever Free',
);
return $lang;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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