1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00

Merge pull request #9851 from turbo124/v5-develop

v5.10.17
This commit is contained in:
David Bomba 2024-08-03 14:09:20 +10:00 committed by GitHub
commit b707671cd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 7313 additions and 7226 deletions

View File

@ -1 +1 @@
5.10.16
5.10.17

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

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

@ -27,16 +27,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

@ -25,10 +25,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

@ -18,9 +18,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

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

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

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

@ -137,7 +137,7 @@ class Task extends BaseModel
// 'project',
];
protected $touches = [];
protected $touches = ['project'];
public function getEntityType()
{

View File

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

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

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

@ -37,11 +37,9 @@ use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class PaymentMethod implements MethodInterface
{
protected RotessaPaymentDriver $rotessa;
public function __construct(RotessaPaymentDriver $rotessa)
public function __construct(protected RotessaPaymentDriver $rotessa)
{
$this->rotessa = $rotessa;
$this->rotessa->init();
}
@ -60,9 +58,6 @@ class PaymentMethod implements MethodInterface
'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,
@ -86,7 +81,7 @@ class PaymentMethod implements MethodInterface
'country' => ['required'],
'name' => ['required'],
'address_1' => ['required'],
'address_2' => ['required'],
// 'address_2' => ['required'],
'city' => ['required'],
'email' => ['required','email:filter'],
'province_code' => ['required','size:2','alpha'],
@ -95,7 +90,7 @@ class PaymentMethod implements MethodInterface
'account_number' => ['required'],
'bank_name' => ['required'],
'phone' => ['required'],
'home_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'],
@ -104,6 +99,7 @@ class PaymentMethod implements MethodInterface
'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'));
@ -112,7 +108,7 @@ class PaymentMethod implements MethodInterface
return $this->rotessa->processInternallyFailedPayment($this->rotessa, new ClientPortalAuthorizationException( get_class( $e) . " : {$e->getMessage()}", (int) $e->getCode() ));
}
return back()->withMessage(ctrans('texts.unable_to_verify_payment_method'));
// return back()->withMessage(ctrans('texts.unable_to_verify_payment_method'));
}
/**
@ -138,12 +134,13 @@ 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'],
@ -153,17 +150,23 @@ class PaymentMethod implements MethodInterface
$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());
$response = $this->rotessa->gatewayRequest('post','transaction_schedules', $transaction);
if($response->failed())
$response->throw();
return $this->processPendingPayment($response->getParameter('id'), (float) $response->getParameter('amount'), (int) $customer->gateway_type_id , $customer->token);
$response = $response->json();
nlog($response);
return $this->processPendingPayment($response['id'], (float) $response['amount'], PaymentType::ACSS , $customer->token);
} catch(\Throwable $e) {
$this->processUnsuccessfulPayment( new InvalidResponseException($e->getMessage(), (int) $e->getCode()) );
}
@ -194,7 +197,7 @@ class PaymentMethod implements MethodInterface
/**
* Handle unsuccessful payment for Rotessa.
*
* @param Exception $exception
* @param \Exception $exception
* @throws PaymentFailed
* @return void
*/

View File

@ -72,9 +72,9 @@ class Client extends HttpClient
$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);
throw new \Exception($networkException->getMessage());
} catch (\Exception $exception) {
throw new RequestException($exception->getMessage(), $request, $exception);
throw new \Exception($exception->getMessage());
}
return $response;

View File

@ -13,7 +13,7 @@ class PatchTransactionSchedulesId extends BaseRequest implements RequestInterfac
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
public function setAmount($value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {

View File

@ -15,7 +15,7 @@ class PostTransactionSchedulesUpdateViaPost extends BaseRequest implements Reque
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
public function setAmount($value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {

View File

@ -11,6 +11,7 @@
namespace App\PaymentDrivers;
use App\DataMapper\ClientSettings;
use Omnipay\Omnipay;
use App\Models\Client;
use App\Models\Payment;
@ -29,6 +30,7 @@ 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
{
@ -53,11 +55,6 @@ class RotessaPaymentDriver extends BaseDriver
public function init(): self
{
$this->gateway = Omnipay::create(
$this->company_gateway->gateway->provider
);
$this->gateway->initialize((array) $this->company_gateway->getConfig());
return $this;
}
@ -116,30 +113,42 @@ 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',[]);
if($result->failed())
$result->throw();
$customers = collect($result->json())->unique('email');
$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();
function($contact) {
// $result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send();
$contact = (object)$contact;
$result = $this->gatewayRequest("get","customers/{$contact->id}");
$result = $result->json();
$this->client = Client::find($contact->client_id);
$customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$customer = (new Customer($result))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
}
);
@ -149,8 +158,8 @@ class RotessaPaymentDriver extends BaseDriver
$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();
$customer = $this->gatewayRequest("get", "customers/{$customer['id']}")->json();
/**
{
"account_number": "11111111"
@ -183,6 +192,9 @@ class RotessaPaymentDriver extends BaseDriver
"updated_at": "2015-02-10T23:50:45.000-06:00"
}
*/
$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 +204,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();
@ -234,26 +247,34 @@ class RotessaPaymentDriver extends BaseDriver
$existing = ClientGatewayToken::query()
->where('company_gateway_id', $this->company_gateway->id)
->where('client_id', $this->client->id)
->where('is_deleted',0)
->orWhere(function (Builder $query) use ($data) {
$query->where('token', encrypt(join(".", Arr::only($data, 'id','custom_identifier'))) )
$query->where('token', join(".", Arr::only($data, ['id','custom_identifier'])))
->where('gateway_customer_reference', Arr::only($data,'id'));
})
->exists();
if ($existing) return true;
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());
// $result = $this->gateway->authorize($data)->send();
// if (!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
$customer = new Customer($result->getData());
$result = $this->gatewayRequest('post', 'customers', $data);
if($result->failed())
$result->throw();
$customer = new Customer($result->json());
$data = array_filter($customer->resolve());
}
// $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' => $data + ['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']
@ -261,7 +282,6 @@ class RotessaPaymentDriver extends BaseDriver
return $data['id'];
throw new \Exception($result->getMessage(), (int) $result->getCode());
} catch (\Throwable $th) {
$data = [
@ -269,7 +289,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 +297,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

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

58
composer.lock generated
View File

@ -535,16 +535,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.316.10",
"version": "3.317.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "eeb8df6ff6caa428e8bcd631ad2a96430900a249"
"reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eeb8df6ff6caa428e8bcd631ad2a96430900a249",
"reference": "eeb8df6ff6caa428e8bcd631ad2a96430900a249",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dc1e3031c2721a25beb2e8fbb175b576e3d60ab9",
"reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9",
"shasum": ""
},
"require": {
@ -624,9 +624,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.316.10"
"source": "https://github.com/aws/aws-sdk-php/tree/3.317.1"
},
"time": "2024-07-30T18:10:20+00:00"
"time": "2024-08-02T18:09:42+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -972,16 +972,16 @@
},
{
"name": "checkout/checkout-sdk-php",
"version": "3.2.1",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/checkout/checkout-sdk-php.git",
"reference": "91797beb18fd9b1581b1cfe5b96a551c0009417c"
"reference": "ac757648271894e3c30b7bc58ff08ba1b5b84de8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/91797beb18fd9b1581b1cfe5b96a551c0009417c",
"reference": "91797beb18fd9b1581b1cfe5b96a551c0009417c",
"url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/ac757648271894e3c30b7bc58ff08ba1b5b84de8",
"reference": "ac757648271894e3c30b7bc58ff08ba1b5b84de8",
"shasum": ""
},
"require": {
@ -1034,9 +1034,9 @@
],
"support": {
"issues": "https://github.com/checkout/checkout-sdk-php/issues",
"source": "https://github.com/checkout/checkout-sdk-php/tree/3.2.1"
"source": "https://github.com/checkout/checkout-sdk-php/tree/3.2.2"
},
"time": "2024-07-09T16:07:18+00:00"
"time": "2024-08-02T08:07:53+00:00"
},
{
"name": "clue/stream-filter",
@ -4758,16 +4758,16 @@
},
{
"name": "laravel/pint",
"version": "v1.17.0",
"version": "v1.17.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5"
"reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5",
"reference": "4dba80c1de4b81dc4c4fb10ea6f4781495eb29f5",
"url": "https://api.github.com/repos/laravel/pint/zipball/b5b6f716db298671c1dfea5b1082ec2c0ae7064f",
"reference": "b5b6f716db298671c1dfea5b1082ec2c0ae7064f",
"shasum": ""
},
"require": {
@ -4820,7 +4820,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2024-07-23T16:40:20+00:00"
"time": "2024-08-01T09:06:33+00:00"
},
{
"name": "laravel/prompts",
@ -16968,16 +16968,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.11.8",
"version": "1.11.9",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec"
"reference": "e370bcddadaede0c1716338b262346f40d296f82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec",
"reference": "6adbd118e6c0515dd2f36b06cde1d6da40f1b8ec",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e370bcddadaede0c1716338b262346f40d296f82",
"reference": "e370bcddadaede0c1716338b262346f40d296f82",
"shasum": ""
},
"require": {
@ -17022,7 +17022,7 @@
"type": "github"
}
],
"time": "2024-07-24T07:01:22+00:00"
"time": "2024-08-01T16:25:18+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -19031,16 +19031,16 @@
},
{
"name": "spatie/flare-client-php",
"version": "1.7.0",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/flare-client-php.git",
"reference": "097040ff51e660e0f6fc863684ac4b02c93fa234"
"reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/097040ff51e660e0f6fc863684ac4b02c93fa234",
"reference": "097040ff51e660e0f6fc863684ac4b02c93fa234",
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122",
"reference": "180f8ca4c0d0d6fc51477bd8c53ce37ab5a96122",
"shasum": ""
},
"require": {
@ -19058,7 +19058,7 @@
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"spatie/phpunit-snapshot-assertions": "^4.0|^5.0"
"spatie/pest-plugin-snapshots": "^1.0|^2.0"
},
"type": "library",
"extra": {
@ -19088,7 +19088,7 @@
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
"source": "https://github.com/spatie/flare-client-php/tree/1.7.0"
"source": "https://github.com/spatie/flare-client-php/tree/1.8.0"
},
"funding": [
{
@ -19096,7 +19096,7 @@
"type": "github"
}
],
"time": "2024-06-12T14:39:14+00:00"
"time": "2024-08-01T08:27:26+00:00"
},
{
"name": "spatie/ignition",

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.17'),
'app_tag' => env('APP_TAG', '5.10.17'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

File diff suppressed because it is too large Load Diff

View File

@ -998,7 +998,7 @@ paths:
post:
tags:
- bank_transactions
summary: "Performs bulk actions on an array of bank_transations"
summary: "Bulk actions"
description: ""
operationId: bulkBankTransactions
parameters:
@ -1042,8 +1042,30 @@ paths:
post:
tags:
- bank_transactions
summary: "Performs match actions on an array of bank_transactions"
description: ""
summary: "Match transactions"
description: |
Matching invoices or a payment to a bank transactions.
The API expects the id of the transaction along with either a comma separated list of invoice ids OR the payment id to associate the transaction to.
Example for matching a transaction to two invoices:
{"transactions":[{"id":"olejRl5ejN","invoice_ids":"JxboYBLegw,JxboYBLeXX"}]}
Example for matching a transaction and a paymente:
{"transactions":[{"id":"olejRl5ejN","payment_id":"JxboYBLeXf"}]}
Matching expenses.
You can match an existing expense within Invoice Ninja - or - create a new expense using the following:
{"transactions":[{"id":"open5pld7A","vendor_id":"gl9avJnaG1","ninja_category_id":""}]}
To match to an existing expense:
{"transactions":[{"id":"Jxbo2qKagw","expense_id":"7N1aMM1aWm"}]}
operationId: matchBankTransactions
parameters:
- $ref: "#/components/parameters/X-API-TOKEN"

View File

@ -22,6 +22,7 @@
color-scheme: light dark;
supported-color-schemes: light dark;
}
@if(isset($settings) && $settings->email_style === 'dark')
body {
background-color: #1a1a1a !important;
@ -48,6 +49,13 @@
hr {
border-color: #474849 !important;
}
.file_icon {
filter: invert(1);
}
@else
.file_icon {
filter: invert(1);
}
@endif
/** Content-specific styles. **/
#content .button {
@ -171,9 +179,12 @@
@isset($links)
<div>
<ul style="list-style-type: none;">
<ul style="list-style-type: none;">
@if(count($links) > 0)
<li>{{ ctrans('texts.download_files')}}</li>
@endif
@foreach($links as $link)
<li>{!! $link ?? '' !!} <img height="15px" src="{{ asset('images/svg/dark/file.svg') }}"></li>
<li>{!! $link ?? '' !!} <img height="15px" src="{{ asset('images/svg/dark/file.svg') }}" class="file_icon"></li>
@endforeach
</ul>
</div>

View File

@ -22,7 +22,7 @@
{{ ctrans('texts.address2') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="address_2" name="address_2" type="text" placeholder="Address Line 2" required value="{{ old('address_2', $address_2) }}">
<input class="input w-full" id="address_2" name="address_2" type="text" placeholder="Address Line 2" value="{{ old('address_2', $address_2) }}">
</dd>
</div>