Merge pull request #9935 from turbo124/v5-develop

v5.10.25
This commit is contained in:
David Bomba 2024-08-22 17:19:43 +10:00 committed by GitHub
commit 13b24cc03e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
530 changed files with 11968 additions and 4755 deletions

View File

@ -1 +1 @@
5.10.24
5.10.25

View File

@ -516,9 +516,10 @@ class CompanySettings extends BaseSettings
public $quote_late_fee_amount1 = 0;
public $quote_late_fee_percent1 = 0;
public string $payment_flow = 'default'; //smooth
public static $casts = [
'payment_flow' => 'string',
'enable_quote_reminder1' => 'bool',
'quote_num_days_reminder1' => 'int',
'quote_schedule_reminder1' => 'string',

View File

@ -131,7 +131,8 @@ class BaseRule implements RuleInterface
return $this;
}
public function shouldCalcTax(): bool {
public function shouldCalcTax(): bool
{
return $this->should_calc_tax && $this->checkIfInvoiceLocked();
}
/**
@ -404,8 +405,9 @@ class BaseRule implements RuleInterface
{
$lock_invoices = $this->client->getSetting('lock_invoices');
if($this->invoice instanceof RecurringInvoice)
if($this->invoice instanceof RecurringInvoice) {
return true;
}
switch ($lock_invoices) {
case 'off':

View File

@ -251,8 +251,7 @@ class Rule extends BaseRule implements RuleInterface
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
$this->defaultForeign();
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || $this->client->has_valid_vat_number)) { //eu country / no valid vat
// if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) {
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || !$this->client->has_valid_vat_number)) { //eu country / no valid vat
if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// nlog("eu zone with sales above threshold");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;

View File

@ -48,8 +48,7 @@ class TaxModel
public function migrate(): self
{
if($this->version == 'alpha')
{
if($this->version == 'alpha') {
$this->regions->EU->subregions->PL = new \stdClass();
$this->regions->EU->subregions->PL->tax_rate = 23;
$this->regions->EU->subregions->PL->tax_name = 'VAT';

View File

@ -11,7 +11,8 @@
namespace App\DataProviders;
final class CAProvinces {
final class CAProvinces
{
/**
* The provinces and territories of Canada
*
@ -39,7 +40,8 @@ final class CAProvinces {
* @param string $abbreviation
* @return string
*/
public static function getName($abbreviation) {
public static function getName($abbreviation)
{
return self::$provinces[$abbreviation];
}
@ -48,7 +50,8 @@ final class CAProvinces {
*
* @return array
*/
public static function get() {
public static function get()
{
return self::$provinces;
}
@ -58,7 +61,8 @@ final class CAProvinces {
* @param string $name
* @return string
*/
public static function getAbbreviation($name) {
public static function getAbbreviation($name)
{
return array_search(ucwords($name), self::$provinces);
}
}

View File

@ -269,8 +269,7 @@ class ExpenseExport extends BaseExport
if($expense->uses_inclusive_taxes) {
$entity['expense.net_amount'] = round($expense->amount, $precision) - $total_tax_amount;
}
else {
} else {
$entity['expense.net_amount'] = round($expense->amount, $precision);
}

View File

@ -162,8 +162,9 @@ class ExpenseFilters extends QueryFilters
{
$categories_exploded = explode(",", $categories);
if(empty($categories) || count(array_filter($categories_exploded)) == 0)
if(empty($categories) || count(array_filter($categories_exploded)) == 0) {
return $this->builder;
}
$categories_keys = $this->transformKeys($categories_exploded);

View File

@ -254,17 +254,20 @@ class ActivityController extends BaseController
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
// no break
case Task::class:
$activity->task_id = $entity->id;
$activity->expense_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
// no break
case Payment::class:
$activity->payment_id = $entity->id;
$activity->expense_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
// no break
default:
# code...
break;

View File

@ -41,8 +41,9 @@ class ContactLoginController extends Controller
$company = false;
$account = false;
if($request->query('intended'))
if($request->query('intended')) {
$request->session()->put('url.intended', $request->query('intended'));
}
if ($request->session()->has('company_key')) {
MultiDB::findAndSetDbByCompanyKey($request->session()->get('company_key'));
@ -142,8 +143,9 @@ class ContactLoginController extends Controller
$this->setRedirectPath();
if($intended)
if($intended) {
$this->redirectTo = $intended;
}
return $request->wantsJson()
? new JsonResponse([], 204)

View File

@ -19,7 +19,6 @@ use Illuminate\Http\Request;
*/
class BrevoController extends BaseController
{
public function __construct()
{
}

View File

@ -62,6 +62,7 @@ class InvoiceController extends Controller
$invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first();
// @phpstan-ignore-next-line
if ($invitation && auth()->guard('contact') && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
@ -77,13 +78,17 @@ class InvoiceController extends Controller
'key' => $invitation ? $invitation->key : false,
'hash' => $hash,
'variables' => $variables,
'invoices' => [$invoice->hashed_id],
'db' => $invoice->company->db,
];
if ($request->query('mode') === 'fullscreen') {
return render('invoices.show-fullscreen', $data);
}
return $this->render('invoices.show', $data);
return auth()->guard('contact')->user()->client->getSetting('payment_flow') == 'default' ? $this->render('invoices.show', $data) : $this->render('invoices.show_smooth', $data);
// return $this->render('invoices.show_smooth', $data);
}
public function showBlob($hash)
@ -235,9 +240,12 @@ class InvoiceController extends Controller
'hashed_ids' => $invoices->pluck('hashed_id'),
'total' => $total,
'variables' => $variables,
'invitation' => $invitation,
'db' => $invitation->company->db,
];
return $this->render('invoices.payment', $data);
// return $this->render('invoices.payment', $data);
return auth()->guard('contact')->user()->client->getSetting('payment_flow') === 'default' ? $this->render('invoices.payment', $data) : $this->render('invoices.show_smooth_multi', $data);
}
/**

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\Quickbooks\AuthorizedQuickbooksRequest;
use \Closure;
use Closure;
use App\Utils\Ninja;
use App\Models\Company;
use App\Libraries\MultiDB;
@ -19,7 +19,6 @@ use App\Services\Import\Quickbooks\QuickbooksService;
class ImportQuickbooksController extends BaseController
{
private array $import_entities = [
'client' => 'Customer',
'invoice' => 'Invoice',
@ -46,7 +45,6 @@ class ImportQuickbooksController extends BaseController
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
{
@ -74,15 +72,17 @@ class ImportQuickbooksController extends BaseController
$this->getData($data);
}
protected function getData($data) {
protected function getData($data)
{
$entity = $this->import_entities[$data['type']];
$cache_name = "{$data['hash']}-{$data['type']}";
// TODO: Get or put cache or DB?
if(! Cache::has($cache_name) )
{
if(! Cache::has($cache_name)) {
$contents = call_user_func([$this->service, "fetch{$entity}s"]);
if($contents->isEmpty()) return;
if($contents->isEmpty()) {
return;
}
Cache::put($cache_name, base64_encode($contents->toJson()), 600);
}
@ -117,20 +117,19 @@ class ImportQuickbooksController extends BaseController
*/
public function import(Request $request)
{
$hash = Str::random(32);
foreach($request->input('import_types') as $type)
{
$this->preimport($type, $hash);
}
/** @var \App\Models\User $user */
// $user = auth()->user() ?? Auth::loginUsingId(60);
$data = ['import_types' => $request->input('import_types') ] + compact('hash');
if (Ninja::isHosted()) {
QuickbooksIngest::dispatch( $data , $user->company() );
} else {
QuickbooksIngest::dispatch($data, $user->company() );
}
// $hash = Str::random(32);
// foreach($request->input('import_types') as $type) {
// $this->preimport($type, $hash);
// }
// /** @var \App\Models\User $user */
// // $user = auth()->user() ?? Auth::loginUsingId(60);
// $data = ['import_types' => $request->input('import_types') ] + compact('hash');
// if (Ninja::isHosted()) {
// QuickbooksIngest::dispatch($data, $user->company());
// } else {
// QuickbooksIngest::dispatch($data, $user->company());
// }
return response()->json(['message' => 'Processing'], 200);
// return response()->json(['message' => 'Processing'], 200);
}
}

View File

@ -20,7 +20,6 @@ use Illuminate\Http\Request;
*/
class MailgunWebhookController extends BaseController
{
public function __construct()
{
}

View File

@ -22,7 +22,6 @@ use Illuminate\Support\Str;
class OneTimeTokenController extends BaseController
{
public function __construct()
{
parent::__construct();

View File

@ -19,7 +19,6 @@ use Illuminate\Http\Request;
*/
class PostMarkController extends BaseController
{
public function __construct()
{
}

View File

@ -297,8 +297,7 @@ class PreviewController extends BaseController
->setTemplate($design_object)
->mock();
} catch(SyntaxError $e) {
}
catch(\Exception $e) {
} catch(\Exception $e) {
return response()->json(['message' => 'invalid data access', 'errors' => ['design.design.body' => $e->getMessage()]], 422);
}

View File

@ -181,8 +181,9 @@ class SelfUpdateController extends BaseController
public function checkVersion()
{
if(Ninja::isHosted())
if(Ninja::isHosted()) {
return '5.10.SaaS';
}
return trim(file_get_contents(config('ninja.version_url')));
}

View File

@ -85,7 +85,10 @@ class ThrottleRequestsWithPredis extends \Illuminate\Routing\Middleware\Throttle
protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
{
$limiter = new DurationLimiter(
$this->getRedisConnection(), $key, $maxAttempts, $decaySeconds
$this->getRedisConnection(),
$key,
$maxAttempts,
$decaySeconds
);
return tap(! $limiter->acquire(), function () use ($key, $limiter) {

View File

@ -68,8 +68,9 @@ class StoreNoteRequest extends Request
public function getEntity()
{
if(!$this->entity)
if(!$this->entity) {
return false;
}
$class = "\\App\\Models\\".ucfirst(Str::camel(rtrim($this->entity, 's')));
return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id);

View File

@ -66,8 +66,7 @@ class ShowCalculatedFieldRequest extends Request
$input['end_date'] = now()->format('Y-m-d');
}
if(isset($input['period']) && $input['period'] == 'previous')
{
if(isset($input['period']) && $input['period'] == 'previous') {
$dates = $this->calculatePreviousPeriodStartAndEndDates($input, $user->company());
$input['start_date'] = $dates[0];
$input['end_date'] = $dates[1];

View File

@ -40,8 +40,9 @@ class BulkInvoiceRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key))
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key)) {
throw new DuplicatePaymentException('Duplicate request.', 429);
}
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key), true, 1);

View File

@ -80,8 +80,9 @@ class StorePaymentRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('amount', 0)."|".$this->input('client_id', '')."|".$user->company()->company_key))
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('amount', 0)."|".$this->input('client_id', '')."|".$user->company()->company_key)) {
throw new DuplicatePaymentException('Duplicate request.', 429);
}
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('amount', 0)."|".$this->input('client_id', '')."|".$user->company()->company_key), true, 1);

View File

@ -64,6 +64,6 @@ class AuthQuickbooksRequest extends FormRequest
public function getCompany(): ?Company
{
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
return Company::query()->where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
}
}

View File

@ -17,7 +17,6 @@ use Illuminate\Auth\Access\AuthorizationException;
class GenericReportRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*

View File

@ -15,7 +15,6 @@ use App\Http\Requests\Request;
class DisconnectUserMailerRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*

View File

@ -77,8 +77,7 @@ class UpdateUserRequest extends Request
unset($input['oauth_user_token']);
}
if(isset($input['password']) && is_string($input['password']))
{
if(isset($input['password']) && is_string($input['password'])) {
$input['password'] = trim($input['password']);
}

View File

@ -34,8 +34,7 @@ class ValidClientScheme implements ValidationRule, ValidatorAwareRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(isset($value['Invoice']))
{
if(isset($value['Invoice'])) {
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], ClientLevel::class);

View File

@ -24,7 +24,6 @@ use Illuminate\Contracts\Validation\ValidatorAwareRule;
*/
class ValidCompanyScheme implements ValidationRule, ValidatorAwareRule
{
/**
* The validator instance.
*
@ -35,8 +34,7 @@ class ValidCompanyScheme implements ValidationRule, ValidatorAwareRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(isset($value['Invoice']))
{
if(isset($value['Invoice'])) {
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], CompanyLevel::class);

View File

@ -42,7 +42,8 @@ class AccountComponent extends Component
"authorization_type" => 'Online'
];
public function __construct(public array $account) {
public function __construct(public array $account)
{
$this->attributes = $this->newAttributeBag(Arr::only($this->account, $this->fields));
}

View File

@ -34,16 +34,20 @@ class AddressComponent extends Component
'country' => 'US'
];
public function __construct(public array $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']);
}
$this->attributes = $this->newAttributeBag(
Arr::only(Arr::mapWithKeys($this->address, function ($item, $key) {
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) );
$this->fields
)
);
}

View File

@ -18,12 +18,11 @@ use App\Models\ClientContact;
use Illuminate\Support\Arr;
use Illuminate\View\View;
// Contact Component
class ContactComponent extends Component
{
public function __construct(ClientContact $contact) {
public function __construct(ClientContact $contact)
{
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' => $contact->client->phone,

View File

@ -12,23 +12,24 @@
namespace App\Import\Providers;
use App\Models\Invoice;
use App\Factory\ProductFactory;
use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Factory\ProductFactory;
use App\Import\ImportException;
use Illuminate\Support\Facades\Cache;
use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\ProductRepository;
use App\Repositories\PaymentRepository;
use App\Repositories\ProductRepository;
use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Product\StoreProductRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Payment\StorePaymentRequest;
use App\Http\Requests\Product\StoreProductRequest;
use App\Import\Transformer\Quickbooks\ClientTransformer;
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
use App\Import\Transformer\Quickbooks\ProductTransformer;
use App\Import\Transformer\Quickbooks\PaymentTransformer;
use App\Import\Transformer\Quickbooks\ProductTransformer;
class Quickbooks extends BaseImport
{
@ -94,10 +95,11 @@ class Quickbooks extends BaseImport
$this->entity_count['products'] = $count;
}
public function getData($type) {
public function getData($type)
{
// get the data from cache? file? or api ?
return json_decode(base64_decode(Cache::get("{$this->hash}-$type")), 1);
return json_decode(base64_decode(Cache::get("{$this->hash}-{$type}")), true);
}
public function payment()
@ -198,8 +200,7 @@ class Quickbooks extends BaseImport
'error' => $validator->errors()->all(),
];
} else {
if(!Invoice::where('number',$invoice_data['number'])->get()->first())
{
if(!Invoice::where('number', $invoice_data['number'])->first()) {
$invoice = InvoiceFactory::create(
$this->company->id,
$this->company->owner()->id

View File

@ -244,8 +244,7 @@ class Wave extends BaseImport implements ImportInterface
if (empty($expense_data['vendor_id'])) {
$vendor_data['user_id'] = $this->getUserIDForRecord($expense_data);
if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor']))
{
if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor'])) {
$vendor_repository->save(
['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])],
$vendor = VendorFactory::create(

View File

@ -119,8 +119,9 @@ class BaseTransformer
{
$code = array_key_exists($key, $data) ? $data[$key] : false;
if(!$code)
if(!$code) {
return $this->company->settings->currency_id;
}
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$currencies = app('currencies');

View File

@ -24,7 +24,6 @@ use Illuminate\Support\Str;
*/
class ClientTransformer extends BaseTransformer
{
use CommonTrait {
transform as preTransform;
}
@ -49,7 +48,7 @@ class ClientTransformer extends BaseTransformer
{
parent::__construct($company);
$this->model = new Model;
$this->model = new Model();
}
@ -73,7 +72,8 @@ class ClientTransformer extends BaseTransformer
return $transformed_data;
}
protected function getContacts($data) {
protected function getContacts($data)
{
return (new ClientContact())->fill([
'first_name' => $this->getString($data, 'GivenName'),
'last_name' => $this->getString($data, 'FamilyName'),
@ -84,11 +84,13 @@ class ClientTransformer extends BaseTransformer
}
public function getShipAddrCountry($data,$field) {
public function getShipAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
}
public function getBillAddrCountry($data,$field) {
public function getBillAddrCountry($data, $field)
{
return is_null(($c = $this->getString($data, $field))) ? null : $this->getCountryId($c);
}

View File

@ -8,7 +8,8 @@ trait CommonTrait
{
protected $model;
public function getString($data,$field) {
public function getString($data, $field)
{
return Arr::get($data, $field);
}

View File

@ -11,10 +11,11 @@
namespace App\Import\Transformer\Quickbooks;
use Illuminate\Support\Str;
use App\Models\Invoice;
use Illuminate\Support\Arr;
use App\Import\ImportException;
use Illuminate\Support\Str;
use App\DataMapper\InvoiceItem;
use App\Import\ImportException;
use App\Models\Invoice as Model;
use App\Import\Transformer\BaseTransformer;
use App\Import\Transformer\Quickbooks\CommonTrait;
@ -48,7 +49,7 @@ class InvoiceTransformer extends BaseTransformer
{
parent::__construct($company);
$this->model = new Model;
$this->model = new Model();
}
public function getInvoiceStatus($data)
@ -73,18 +74,19 @@ class InvoiceTransformer extends BaseTransformer
'description' => $this->getString($item, 'Description'),
'product_key' => $this->getString($item, 'Description'),
'quantity' => (int) $this->getString($item, 'SalesItemLineDetail.Qty'),
'unit_price' =>(double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
'line_total' => (double) $this->getString($item,'Amount'),
'cost' =>(double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
'product_cost' => (double) $this->getString($item,'SalesItemLineDetail.UnitPrice'),
'tax_amount' => (double) $this->getString($item,'TxnTaxDetail.TotalTax'),
'unit_price' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
'line_total' => (float) $this->getString($item, 'Amount'),
'cost' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
'product_cost' => (float) $this->getString($item, 'SalesItemLineDetail.UnitPrice'),
'tax_amount' => (float) $this->getString($item, 'TxnTaxDetail.TotalTax'),
];
}, array_filter($this->getString($data, 'Line'), function ($item) {
return $this->getString($item, 'DetailType') !== 'SubTotalLineDetail';
}));
}
public function getInvoiceClient($data, $field = null) {
public function getInvoiceClient($data, $field = null)
{
/**
* "CustomerRef": {
"value": "23",
@ -145,8 +147,7 @@ class InvoiceTransformer extends BaseTransformer
"BillAddr" => array_combine(['City','CountrySubDivisionCode','PostalCode'], array_pad($address, 3, 'N/A')) + ['Line1' => $has_company ? $bill_address->Line3 : $bill_address->Line2 ],
"ShipAddr" => $ship_address
] + $customer + ['PrimaryEmailAddr' => ['Address' => $this->getString($data, 'BillEmail.Address') ]];
if($this->hasClient($client['CompanyName']))
{
if($this->hasClient($client['CompanyName'])) {
$client_id = $this->getClient($client['CompanyName'], $this->getString($client, 'PrimaryEmailAddr.Address'));
}
@ -161,12 +162,12 @@ class InvoiceTransformer extends BaseTransformer
public function getDeposit($data)
{
return (double) $this->getString($data,'Deposit');
return (float) $this->getString($data, 'Deposit');
}
public function getBalance($data)
{
return (double) $this->getString($data,'Balance');
return (float) $this->getString($data, 'Balance');
}
public function getCustomerMemo($data)
@ -176,7 +177,8 @@ class InvoiceTransformer extends BaseTransformer
public function getDocNumber($data, $field = null)
{
return sprintf("%s-%s",
return sprintf(
"%s-%s",
$this->getString($data, 'DocNumber'),
$this->getString($data, 'Id.value')
);
@ -185,7 +187,9 @@ class InvoiceTransformer extends BaseTransformer
public function getLinkedTxn($data)
{
$payments = $this->getString($data, 'LinkedTxn');
if(empty($payments)) return [];
if(empty($payments)) {
return [];
}
return [[
'amount' => $this->getTotalAmt($data),

View File

@ -44,10 +44,11 @@ class PaymentTransformer extends BaseTransformer
{
parent::__construct($company);
$this->model = new Model;
$this->model = new Model();
}
public function getTotalAmt($data, $field = null) {
public function getTotalAmt($data, $field = null)
{
return (float) $this->getString($data, $field);
}
@ -70,8 +71,12 @@ class PaymentTransformer extends BaseTransformer
{
$invoices = [];
$invoice = $this->getString($data, 'Line.LinkedTxn.TxnType');
if(is_null($invoice) || $invoice !== 'Invoice') return $invoices;
if( is_null( ($invoice_id = $this->getInvoiceId($this->getString($data, 'Line.LinkedTxn.TxnId.value')))) ) return $invoices;
if(is_null($invoice) || $invoice !== 'Invoice') {
return $invoices;
}
if(is_null(($invoice_id = $this->getInvoiceId($this->getString($data, 'Line.LinkedTxn.TxnId.value'))))) {
return $invoices;
}
return [[
'amount' => (float) $this->getString($data, 'Line.Amount'),
@ -88,7 +93,9 @@ class PaymentTransformer extends BaseTransformer
{
$invoice = Invoice::query()->where('company_id', $this->company->id)
->where('is_deleted', false)
->where("number", "LIKE",
->where(
"number",
"LIKE",
"%-$invoice_number%",
)
->first();

View File

@ -22,7 +22,6 @@ use App\Import\ImportException;
*/
class ProductTransformer extends BaseTransformer
{
use CommonTrait;
protected $fillable = [
@ -41,19 +40,22 @@ class ProductTransformer extends BaseTransformer
{
parent::__construct($company);
$this->model = new Model;
$this->model = new Model();
}
public function getQtyOnHand($data, $field = null) {
public function getQtyOnHand($data, $field = null)
{
return (int) $this->getString($data, $field);
}
public function getPurchaseCost($data, $field = null) {
return (double) $this->getString($data, $field);
public function getPurchaseCost($data, $field = null)
{
return (float) $this->getString($data, $field);
}
public function getUnitPrice($data, $field = null) {
public function getUnitPrice($data, $field = null)
{
return (float) $this->getString($data, $field);
}
}

View File

@ -38,12 +38,13 @@ class ExpenseTransformer extends BaseTransformer
$tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0;
if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1)
if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1) {
$public_notes = $data['Notes / Memo'];
elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1)
} elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1) {
$public_notes = $data['Transaction Description'];
else
} else {
$public_notes = '';
}
$transformed = [

View File

@ -114,8 +114,7 @@ class RecurringExpensesCron
$exchange_rate = new CurrencyApi();
$expense->exchange_rate = $exchange_rate->exchangeRate($expense->currency_id, (int)$expense->company->settings->currency_id, Carbon::parse($expense->date));
}
else {
} else {
$expense->exchange_rate = 1;
}

View File

@ -12,7 +12,10 @@ use Illuminate\Foundation\Bus\Dispatchable;
class QuickbooksIngest implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
protected $engine;
protected $request;

View File

@ -48,8 +48,7 @@ class TaskAssigned implements ShouldQueue
$company_user = $this->task->assignedCompanyUser();
if(($company_user instanceof CompanyUser) && $this->findEntityAssignedNotification($company_user, 'task'))
{
if(($company_user instanceof CompanyUser) && $this->findEntityAssignedNotification($company_user, 'task')) {
$mo = new EmailObject();
$mo->subject = ctrans('texts.task_assigned_subject', ['task' => $this->task->number, 'date' => now()->setTimeZone($this->task->company->timezone()->name)->format($this->task->company->date_format()) ]);
$mo->body = ctrans('texts.task_assigned_body', ['task' => $this->task->number, 'description' => $this->task->description ?? '', 'client' => $this->task->client ? $this->task->client->present()->name() : ' ']);

View File

@ -59,7 +59,6 @@ class Register extends Component
public function register(array $data)
{
$service = new ClientRegisterService(
company: $this->subscription->company,
additional: $this->additional_fields,

View File

@ -363,10 +363,11 @@ class BillingPortalPurchase extends Component
$method_values = array_column($this->methods, 'is_paypal');
$is_paypal = in_array('1', $method_values);
if($is_paypal && !$this->steps['check_rff'])
if($is_paypal && !$this->steps['check_rff']) {
$this->rff();
elseif(!$this->steps['check_rff'])
} elseif(!$this->steps['check_rff']) {
$this->steps['fetched_payment_methods'] = true;
}
$this->heading_text = ctrans('texts.payment_methods');

View File

@ -515,8 +515,7 @@ class BillingPortalPurchasev2 extends Component
strlen($this->contact_email ?? '') == 0 ||
strlen($this->client_city ?? '') == 0 ||
strlen($this->client_postal_code ?? '') == 0
)
{
) {
$this->check_rff = true;
}

View File

@ -0,0 +1,287 @@
<?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\Livewire\Flow2;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\WithSecureContext;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
class InvoicePay extends Component
{
use MakesDates;
use MakesHash;
use WithSecureContext;
private $mappings = [
'client_name' => 'name',
'client_website' => 'website',
'client_phone' => 'phone',
'client_address_line_1' => 'address1',
'client_address_line_2' => 'address2',
'client_city' => 'city',
'client_state' => 'state',
'client_postal_code' => 'postal_code',
'client_country_id' => 'country_id',
'client_shipping_address_line_1' => 'shipping_address1',
'client_shipping_address_line_2' => 'shipping_address2',
'client_shipping_city' => 'shipping_city',
'client_shipping_state' => 'shipping_state',
'client_shipping_postal_code' => 'shipping_postal_code',
'client_shipping_country_id' => 'shipping_country_id',
'client_custom_value1' => 'custom_value1',
'client_custom_value2' => 'custom_value2',
'client_custom_value3' => 'custom_value3',
'client_custom_value4' => 'custom_value4',
'contact_first_name' => 'first_name',
'contact_last_name' => 'last_name',
'contact_email' => 'email',
// 'contact_phone' => 'phone',
];
public $client_address_array = [
'address1',
'address2',
'city',
'state',
'postal_code',
'country_id',
'shipping_address1',
'shipping_address2',
'shipping_city',
'shipping_state',
'shipping_postal_code',
'shipping_country_id',
];
public $invitation_id;
public $invoices;
public $variables;
public $db;
public $settings;
public $terms_accepted = false;
public $signature_accepted = false;
public $payment_method_accepted = false;
public $under_over_payment = false;
public $required_fields = false;
#[On('update.context')]
public function handleContext(string $property, $value): self
{
$this->setContext(property: $property, value: $value);
return $this;
}
#[On('terms-accepted')]
public function termsAccepted()
{
nlog("Terms accepted");
// $this->invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id)->withoutRelations();
$this->terms_accepted = true;
}
#[On('signature-captured')]
public function signatureCaptured($base64)
{
nlog("signature captured");
$this->signature_accepted = true;
$invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id);
$invite->signature_base64 = $base64;
$invite->signature_date = now()->addSeconds($invite->contact->client->timezone_offset());
$this->setContext('signature', $base64); // $this->context['signature'] = $base64;
$invite->save();
}
#[On('payable-amount')]
public function payableAmount($payable_amount)
{
// $this->setContext('payable_invoices.0.amount', Number::parseFloat($payable_amount)); // $this->context['payable_invoices'][0]['amount'] = Number::parseFloat($payable_amount); //TODO DB: check parseFloat()
$this->under_over_payment = false;
}
#[On('payment-method-selected')]
public function paymentMethodSelected($company_gateway_id, $gateway_type_id, $amount)
{
$this->setContext('company_gateway_id', $company_gateway_id);
$this->setContext('gateway_type_id', $gateway_type_id);
$this->setContext('amount', $amount);
$this->setContext('pre_payment', false);
$this->setContext('is_recurring', false);
$this->setContext('invitation_id', $this->invitation_id);
$this->payment_method_accepted = true;
$company_gateway = CompanyGateway::query()->find($company_gateway_id);
$this->checkRequiredFields($company_gateway);
}
#[On('required-fields')]
public function requiredFieldsFilled()
{
$this->required_fields = false;
}
private function checkRequiredFields(CompanyGateway $company_gateway)
{
$fields = $company_gateway->driver()->getClientRequiredFields();
$this->setContext('fields', $fields); // $this->context['fields'] = $fields;
if ($company_gateway->always_show_required_fields) {
return $this->required_fields = true;
}
$contact = $this->getContext()['contact'];
foreach ($fields as $index => $field) {
$_field = $this->mappings[$field['name']];
if (\Illuminate\Support\Str::startsWith($field['name'], 'client_')) {
if (
empty($contact->client->{$_field})
|| is_null($contact->client->{$_field})
) {
return $this->required_fields = true;
}
}
if (\Illuminate\Support\Str::startsWith($field['name'], 'contact_')) {
if (empty($contact->{$_field}) || is_null($contact->{$_field}) || str_contains($contact->{$_field}, '@example.com')) {
return $this->required_fields = true;
}
}
}
return $this->required_fields = false;
}
#[Computed()]
public function component(): string
{
if (!$this->terms_accepted) {
return Terms::class;
}
if (!$this->signature_accepted) {
return Signature::class;
}
if ($this->under_over_payment) {
return UnderOverPayment::class;
}
if (!$this->payment_method_accepted) {
return PaymentMethod::class;
}
if ($this->required_fields) {
return RequiredFields::class;
}
return ProcessPayment::class;
}
#[Computed()]
public function componentUniqueId(): string
{
return "purchase-" . md5(microtime());
}
public function mount()
{
$this->resetContext();
MultiDB::setDb($this->db);
// @phpstan-ignore-next-line
$invite = \App\Models\InvoiceInvitation::with('contact.client', 'company')->withTrashed()->find($this->invitation_id);
$client = $invite->contact->client;
$settings = $client->getMergedSettings();
$this->setContext('contact', $invite->contact); // $this->context['contact'] = $invite->contact;
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
$this->setContext('db', $this->db); // $this->context['db'] = $this->db;
nlog($this->invoices);
if(is_array($this->invoices)) {
$this->invoices = Invoice::find($this->transformKeys($this->invoices));
}
$invoices = $this->invoices->filter(function ($i) {
$i = $i->service()
->markSent()
->removeUnpaidGatewayFees()
->save();
return $i->isPayable();
});
//under-over / payment
//required fields
$this->terms_accepted = !$settings->show_accept_invoice_terms;
$this->signature_accepted = !$settings->require_invoice_signature;
$this->under_over_payment = $settings->client_portal_allow_over_payment || $settings->client_portal_allow_under_payment;
$this->required_fields = false;
$this->setContext('variables', $this->variables); // $this->context['variables'] = $this->variables;
$this->setContext('invoices', $invoices); // $this->context['invoices'] = $invoices;
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
$this->setContext('invitation', $invite); // $this->context['invitation'] = $invite;
$payable_invoices = $invoices->map(function ($i) {
/** @var \App\Models\Invoice $i */
return [
'invoice_id' => $i->hashed_id,
'amount' => $i->partial > 0 ? $i->partial : $i->balance,
'formatted_amount' => Number::formatValue($i->partial > 0 ? $i->partial : $i->balance, $i->client->currency()),
'number' => $i->number,
'date' => $i->translateDate($i->date, $i->client->date_format(), $i->client->locale())
];
})->toArray();
$this->setContext('payable_invoices', $payable_invoices);
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.invoice-pay');
}
}

View File

@ -0,0 +1,47 @@
<?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\Livewire\Flow2;
use App\Utils\Traits\WithSecureContext;
use Livewire\Attributes\On;
use Livewire\Component;
class InvoiceSummary extends Component
{
use WithSecureContext;
public $invoices;
public function mount()
{
//@TODO for a single invoice - show all details, for multi-invoices, only show the summaries
$this->invoices = $this->getContext()['invoices']; // $this->context['invitation']->invoice;
}
#[On(self::CONTEXT_UPDATE)]
public function onContextUpdate(): void
{
// refactor logic for updating the price for eg if it changes with under/over pay
$this->invoices = $this->getContext()['invoices'];
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.invoices-summary', [
'invoice' => $this->invoices,
'client' => $this->invoices->first()->client,
]);
}
}

View File

@ -0,0 +1,78 @@
<?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\Livewire\Flow2;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
use App\Libraries\MultiDB;
class PaymentMethod extends Component
{
use WithSecureContext;
public $invoice;
public $variables;
public $methods = [];
public $isLoading = true;
public $amount = 0;
public function placeholder()
{
return <<<'HTML'
<div class="flex items-center justify-center min-h-screen">
<svg class="animate-spin h-10 w-10 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
HTML;
}
public function handleSelect(string $company_gateway_id, string $gateway_type_id, string $amount)
{
$this->isLoading = true;
$this->dispatch(
event: 'payment-method-selected',
company_gateway_id: $company_gateway_id,
gateway_type_id: $gateway_type_id,
amount: $amount,
);
}
public function mount()
{
$this->variables = $this->getContext()['variables'];
$this->amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
MultiDB::setDb($this->getContext()['db']);
$this->methods = $this->getContext()['invitation']->contact->client->service()->getPaymentMethods($this->amount);
if (count($this->methods) == 1) {
$this->dispatch('singlePaymentMethodFound', company_gateway_id: $this->methods[0]['company_gateway_id'], gateway_type_id: $this->methods[0]['gateway_type_id'], amount: $this->amount);
} else {
$this->isLoading = false;
$this->dispatch('loadingCompleted');
}
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.payment-method', ['methods' => $this->methods]);
}
}

View File

@ -0,0 +1,86 @@
<?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\Livewire\Flow2;
use App\Exceptions\PaymentFailed;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
use App\Models\InvoiceInvitation;
use App\Services\ClientPortal\LivewireInstantPayment;
class ProcessPayment extends Component
{
use WithSecureContext;
private ?string $payment_view;
private array $payment_data_payload = [];
public $isLoading = true;
public function mount()
{
MultiDB::setDb($this->getContext()['db']);
$invitation = InvoiceInvitation::find($this->getContext()['invitation_id']);
$data = [
'company_gateway_id' => $this->getContext()['company_gateway_id'],
'payment_method_id' => $this->getContext()['gateway_type_id'],
'payable_invoices' => $this->getContext()['payable_invoices'],
'signature' => isset($this->getContext()['signature']) ? $this->getContext()['signature'] : false,
'signature_ip' => isset($this->getContext()['signature_ip']) ? $this->getContext()['signature_ip'] : false,
'pre_payment' => false,
'frequency_id' => false,
'remaining_cycles' => false,
'is_recurring' => false,
// 'hash' => false,
];
$responder_data = (new LivewireInstantPayment($data))->run();
$company_gateway = CompanyGateway::find($this->getContext()['company_gateway_id']);
if (!$responder_data['success']) {
throw new PaymentFailed($responder_data['error'], 400);
}
$driver = $company_gateway
->driver($invitation->contact->client)
->setPaymentMethod($data['payment_method_id'])
->setPaymentHash($responder_data['payload']['ph']);
$this->payment_data_payload = $driver->processPaymentViewData($responder_data['payload']);
$this->payment_view = $driver->livewirePaymentView(
$this->payment_data_payload,
);
$this->isLoading = false;
}
public function render(): \Illuminate\Contracts\View\Factory|string|\Illuminate\View\View
{
if ($this->isLoading) {
return <<<'HTML'
<template></template>
HTML;
}
return render($this->payment_view, $this->payment_data_payload);
}
}

View File

@ -0,0 +1,136 @@
<?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\Livewire\Flow2;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
use App\Services\Client\RFFService;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
class RequiredFields extends Component
{
use WithSecureContext;
public ?CompanyGateway $company_gateway;
public ?string $client_name;
public ?string $contact_first_name;
public ?string $contact_last_name;
public ?string $contact_email;
public ?string $client_phone;
public ?string $client_address_line_1;
public ?string $client_city;
public ?string $client_state;
public ?int $client_country_id;
public ?string $client_postal_code;
public ?string $client_shipping_address_line_1;
public ?string $client_shipping_city;
public ?string $client_shipping_state;
public ?string $client_shipping_postal_code;
public ?int $client_shipping_country_id;
public ?string $client_custom_value1;
public ?string $client_custom_value2;
public ?string $client_custom_value3;
public ?string $client_custom_value4;
/** @var array<int, string> */
public array $fields = [];
public bool $is_loading = true;
public array $errors = [];
public function mount(): void
{
MultiDB::setDB(
$this->getContext()['db'],
);
$this->fields = $this->getContext()['fields'];
$this->company_gateway = CompanyGateway::withTrashed()
->with('company')
->find($this->getContext()['company_gateway_id']);
$contact = auth()->user();
$this->client_name = $contact->client->name;
$this->contact_first_name = $contact->first_name;
$this->contact_last_name = $contact->last_name;
$this->contact_email = $contact->email;
$this->client_phone = $contact->client->phone;
$this->client_address_line_1 = $contact->client->address1;
$this->client_city = $contact->client->city;
$this->client_state = $contact->client->state;
$this->client_country_id = $contact->client->country_id;
$this->client_postal_code = $contact->client->postal_code;
$this->client_shipping_address_line_1 = $contact->client->shipping_address1;
$this->client_shipping_city = $contact->client->shipping_city;
$this->client_shipping_state = $contact->client->shipping_state;
$this->client_shipping_postal_code = $contact->client->shipping_postal_code;
$this->client_shipping_country_id = $contact->client->shipping_country_id;
$this->client_custom_value1 = $contact->client->custom_value1;
$this->client_custom_value2 = $contact->client->custom_value2;
$this->client_custom_value3 = $contact->client->custom_value3;
$this->client_custom_value4 = $contact->client->custom_value4;
$rff = new RFFService(
fields: $this->getContext()['fields'],
database: $this->getContext()['db'],
company_gateway_id: $this->company_gateway->id,
);
/** @var \App\Models\ClientContact $contact */
$rff->check($contact);
if ($rff->unfilled_fields === 0) {
$this->dispatch('required-fields');
}
if ($rff->unfilled_fields > 0) {
$this->is_loading = false;
}
}
public function handleSubmit(array $data)
{
$this->errors = [];
$this->is_loading = true;
$rff = new RFFService(
fields: $this->fields,
database: $this->getContext()['db'],
company_gateway_id: $this->company_gateway->id,
);
$contact = auth()->user();
/** @var \App\Models\ClientContact $contact */
$errors = $rff->handleSubmit($data, $contact, return_errors: true, callback: function () {
$this->dispatch('required-fields');
});
if (is_array($errors) && count($errors)) {
$this->errors = $errors;
$this->is_loading = false;
}
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.required-fields', [
'contact' => $this->getContext()['contact'],
]);
}
}

View File

@ -0,0 +1,23 @@
<?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\Livewire\Flow2;
use Livewire\Component;
class Signature extends Component
{
public function render()
{
return render('components.livewire.signature');
}
}

View File

@ -0,0 +1,36 @@
<?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\Livewire\Flow2;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
class Terms extends Component
{
use WithSecureContext;
public $invoice;
public $variables;
public function mount()
{
$this->invoice = $this->getContext()['invoices']->first();
$this->variables = $this->getContext()['variables'];
}
public function render()
{
return render('components.livewire.terms');
}
}

View File

@ -0,0 +1,76 @@
<?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\Livewire\Flow2;
use App\Utils\Number;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
class UnderOverPayment extends Component
{
use WithSecureContext;
public $payableAmount;
public $currency;
public $invoice_amount;
public $errors = '';
public $payableInvoices = [];
public function mount()
{
$this->invoice_amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
$this->currency = $this->getContext()['invitation']->contact->client->currency();
$this->payableInvoices = $this->getContext()['payable_invoices'];
}
public function checkValue(array $payableInvoices)
{
$this->errors = '';
$settings = $this->getContext()['settings'];
foreach($payableInvoices as $key => $invoice) {
$payableInvoices[$key]['amount'] = Number::parseFloat($invoice['formatted_amount']);
}
$input_amount = collect($payableInvoices)->sum('amount');
if($settings->client_portal_allow_under_payment && $settings->client_portal_under_payment_minimum != 0) {
if($input_amount <= $settings->client_portal_under_payment_minimum) {
// return error message under payment too low.
$this->errors = ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]);
$this->dispatch('errorMessageUpdate', errors: $this->errors);
}
}
if(!$settings->client_portal_allow_over_payment && ($input_amount > $this->invoice_amount)) {
$this->errors = ctrans('texts.over_payments_disabled');
$this->dispatch('errorMessageUpdate', errors: $this->errors);
}
if(!$this->errors) {
$this->setContext('payable_invoices', $payableInvoices);
$this->dispatch('payable-amount', payable_amount: $input_amount);
}
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.under-over-payments');
}
}

View File

@ -449,8 +449,9 @@ class Activity extends StaticModel
$replacements['created_at'] = $this->created_at ?? '';
$replacements['ip'] = $this->ip ?? '';
if($this->activity_type_id == 141)
if($this->activity_type_id == 141) {
$replacements = $this->harvestNoteEntities($replacements);
}
return $replacements;
@ -472,12 +473,12 @@ class Activity extends StaticModel
];
foreach($entities as $entity)
{
foreach($entities as $entity) {
$entity_key = substr($entity, 1);
if($this?->{$entity_key})
if($this?->{$entity_key}) {
$replacements = array_merge($replacements, $this->matchVar($entity));
}
}

View File

@ -376,8 +376,7 @@ class BaseModel extends Model
try {
$pdf = (new PdfMerge($files->flatten()->toArray()))->run();
}
catch(\Exception $e){
} catch(\Exception $e) {
nlog("Exception:: BaseModel:: PdfMerge::" . $e->getMessage());
}

View File

@ -118,7 +118,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property string|null $smtp_port
* @property string|null $smtp_encryption
* @property string|null $smtp_local_domain
* @property string|null $quickbooks
* @property object|null $quickbooks
* @property boolean $smtp_verify_peer
* @property-read \App\Models\Account $account
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities

View File

@ -431,9 +431,9 @@ class Invoice extends BaseModel
public function isPayable(): bool
{
if($this->is_deleted || $this->status_id == self::STATUS_PAID)
if($this->is_deleted || $this->status_id == self::STATUS_PAID) {
return false;
elseif ($this->status_id == self::STATUS_DRAFT && $this->is_deleted == false) {
} elseif ($this->status_id == self::STATUS_DRAFT && $this->is_deleted == false) {
return true;
} elseif ($this->status_id == self::STATUS_SENT && $this->is_deleted == false) {
return true;

View File

@ -435,8 +435,9 @@ class Quote extends BaseModel
*/
public function canRemind(): bool
{
if (in_array($this->status_id, [self::STATUS_DRAFT, self::STATUS_APPROVED, self::STATUS_CONVERTED]) || $this->is_deleted)
if (in_array($this->status_id, [self::STATUS_DRAFT, self::STATUS_APPROVED, self::STATUS_CONVERTED]) || $this->is_deleted) {
return false;
}
return true;

View File

@ -296,8 +296,7 @@ class Task extends BaseModel
$client_currency = $this->client->getSetting('currency_id');
$company_currency = $this->company->getSetting('currency_id');
if($client_currency != $company_currency)
{
if($client_currency != $company_currency) {
$converter = new CurrencyApi();
return $converter->convert($this->taskValue(), $client_currency, $company_currency);
}
@ -374,8 +373,9 @@ class Task extends BaseModel
public function assignedCompanyUser()
{
if(!$this->assigned_user_id)
if(!$this->assigned_user_id) {
return false;
}
return CompanyUser::where('company_id', $this->company_id)->where('user_id', $this->assigned_user_id)->first();
}

View File

@ -0,0 +1,30 @@
<?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\Common;
interface LivewireMethodInterface
{
/**
* Payment page for the gateway method.
*
* @param array $data
*/
public function livewirePaymentView(array $data): string;
/**
* Payment data for the gateway method.
*
* @param array $data
* @return array
*/
public function paymentData(array $data): array;
}

View File

@ -0,0 +1,261 @@
<?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 redirect($response->getRedirectUrl());
}
// $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;
}
public function livewirePaymentView(array $data): string
{
$this->processPaymentView($data);
return ''; // Gateway is offsite.
}
public function processPaymentViewData(array $data): array
{
return $data;
}
}

View File

@ -4,9 +4,9 @@ namespace App\Repositories\Import\Quickbooks\Contracts;
use Illuminate\Support\Collection;
interface RepositoryInterface {
function get(int $max = 100): Collection;
function all(): Collection;
function count(): int;
interface RepositoryInterface
{
public function get(int $max = 100): Collection;
public function all(): Collection;
public function count(): int;
}

View File

@ -9,7 +9,6 @@ use App\Repositories\Import\Quickbooks\Transformers\Transformer as QuickbooksTra
abstract class Repository implements RepositoryInterface
{
protected string $entity;
protected QuickbooksInterface $db;
protected QuickbooksTransformer $transfomer;
@ -20,7 +19,8 @@ abstract class Repository implements RepositoryInterface
$this->transformer = $transfomer;
}
public function count() : int {
public function count(): int
{
return $this->db->totalRecords($this->entity);
}

View File

@ -8,7 +8,9 @@ class Transformer
{
public function transform(array $items, string $type): Collection
{
if(!method_exists($this, ($method = "transform{$type}s"))) throw new \InvalidArgumentException("Unknown type: $type");
if(!method_exists($this, ($method = "transform{$type}s"))) {
throw new \InvalidArgumentException("Unknown type: $type");
}
return call_user_func([$this, $method], $items);
}

View File

@ -46,8 +46,9 @@ class TaskRepository extends BaseRepository
$this->new_task = false;
}
if(!is_numeric($task->rate) && !isset($data['rate']))
if(!is_numeric($task->rate) && !isset($data['rate'])) {
$data['rate'] = 0;
}
$task->fill($data);
$task->saveQuietly();
@ -118,13 +119,15 @@ class TaskRepository extends BaseRepository
$key_values = array_column($time_log, 0);
if(count($key_values) > 0)
if(count($key_values) > 0) {
array_multisort($key_values, SORT_ASC, $time_log);
}
foreach($time_log as $key => $value) {
if(is_array($time_log[$key]) && count($time_log[$key]) >=2)
if(is_array($time_log[$key]) && count($time_log[$key]) >= 2) {
$time_log[$key][1] = $this->roundTimeLog($time_log[$key][0], $time_log[$key][1]);
}
}

View File

@ -72,19 +72,20 @@ class ProcessBankRules extends AbstractService
// $client.custom4
private function matchCredit()
{
$match_set = [];
$this->credit_rules = $this->bank_transaction->company->credit_rules();
foreach ($this->credit_rules as $bank_transaction_rule)
{
foreach ($this->credit_rules as $bank_transaction_rule) {
$match_set = [];
if (!is_array($bank_transaction_rule['rules'])) {
continue;
}
foreach ($bank_transaction_rule['rules'] as $rule) {
$rule_count = count($bank_transaction_rule['rules']);
foreach ($bank_transaction_rule['rules'] as $rule) {
$payments = Payment::query()
->withTrashed()
@ -93,41 +94,74 @@ class ProcessBankRules extends AbstractService
->whereNull('transaction_id')
->get();
match($rule['search_key']){
'$payment.amount' => $results = $this->searchPaymentResource('amount', $rule),
'$payment.transaction_reference' => $results = $this->searchPaymentResource('transaction_reference', $rule),
'$payment.custom1' => $results = $this->searchPaymentResource('custom1', $rule),
'$payment.custom2' => $results = $this->searchPaymentResource('custom2', $rule),
'$payment.custom3' => $results = $this->searchPaymentResource('custom3', $rule),
'$payment.custom4' => $results = $this->searchPaymentResource('custom4', $rule),
'$invoice.amount' => $results = $this->searchInvoiceResource('amount', $rule),
'$invoice.number' => $results = $this->searchInvoiceResource('number', $rule),
'$invoice.po_number' => $results = $this->searchInvoiceResource('po_number', $rule),
'$invoice.custom1' => $results = $this->searchInvoiceResource('custom1', $rule),
'$invoice.custom2' => $results = $this->searchInvoiceResource('custom2', $rule),
'$invoice.custom3' => $results = $this->searchInvoiceResource('custom3', $rule),
'$invoice.custom4' => $results = $this->searchInvoiceResource('custom4', $rule),
'$client.id_number' => $results = $this->searchClientResource('id_number', $rule),
'$client.email' => $results = $this->searchClientResource('email', $rule),
'$client.custom1' => $results = $this->searchClientResource('custom1', $rule),
'$client.custom2' => $results = $this->searchClientResource('custom2', $rule),
'$client.custom3' => $results = $this->searchClientResource('custom3', $rule),
'$client.custom4' => $results = $this->searchClientResource('custom4', $rule),
};
}
}
}
private function searchInvoiceResource(string $column, array $rule)
{
return Invoice::query()
$invoices = Invoice::query()
->withTrashed()
->where('company_id', $this->bank_transaction->company_id)
->whereIn('status_id', [1,2,3])
->where('is_deleted', 0)
->when($rule['search_key'] == 'description', function ($q) use ($rule, $column){
->get();
$results = [];
match($rule['search_key']) {
'$payment.amount' => $results = [Payment::class, $this->searchPaymentResource('amount', $rule, $payments)],
'$payment.transaction_reference' => $results = [Payment::class, $this->searchPaymentResource('transaction_reference', $rule, $payments)],
'$payment.custom1' => $results = [Payment::class, $this->searchPaymentResource('custom1', $rule, $payments)],
'$payment.custom2' => $results = [Payment::class, $this->searchPaymentResource('custom2', $rule, $payments)],
'$payment.custom3' => $results = [Payment::class, $this->searchPaymentResource('custom3', $rule, $payments)],
'$payment.custom4' => $results = [Payment::class, $this->searchPaymentResource('custom4', $rule, $payments)],
'$invoice.amount' => $results = [Invoice::class, $this->searchInvoiceResource('amount', $rule, $invoices)],
'$invoice.number' => $results = [Invoice::class, $this->searchInvoiceResource('number', $rule, $invoices)],
'$invoice.po_number' => $results = [Invoice::class, $this->searchInvoiceResource('po_number', $rule, $invoices)],
'$invoice.custom1' => $results = [Invoice::class, $this->searchInvoiceResource('custom1', $rule, $invoices)],
'$invoice.custom2' => $results = [Invoice::class, $this->searchInvoiceResource('custom2', $rule, $invoices)],
'$invoice.custom3' => $results = [Invoice::class, $this->searchInvoiceResource('custom3', $rule, $invoices)],
'$invoice.custom4' => $results = [Invoice::class, $this->searchInvoiceResource('custom4', $rule, $invoices)],
'$client.id_number' => $results = [Client::class, $this->searchClientResource('id_number', $rule, $invoices, $payments)],
'$client.email' => $results = [Client::class, $this->searchClientResource('email', $rule, $invoices, $payments)],
'$client.custom1' => $results = [Client::class, $this->searchClientResource('custom1', $rule, $invoices, $payments)],
'$client.custom2' => $results = [Client::class, $this->searchClientResource('custom2', $rule, $invoices, $payments)],
'$client.custom3' => $results = [Client::class, $this->searchClientResource('custom3', $rule, $invoices, $payments)],
'$client.custom4' => $results = [Client::class, $this->searchClientResource('custom4', $rule, $invoices, $payments)],
default => $results = [Client::class, [collect([]), Invoice::class]],
};
if($results[0] == 'App\Models\Client') {
$set = $results[1];
$result_set = $set[0];
$entity = $set[1];
if($result_set->count() > 0) {
$match_set[] = [$entity, $result_set->pluck('id')];
}
} elseif($results[1]->count() > 0) {
$match_set[] = $results;
}
}
if (($bank_transaction_rule['matches_on_all'] && (count($match_set) == $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && count($match_set) > 0)) {
$this->bank_transaction->vendor_id = $bank_transaction_rule->vendor_id;
$this->bank_transaction->ninja_category_id = $bank_transaction_rule->category_id;
$this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
$this->bank_transaction->bank_transaction_rule_id = $bank_transaction_rule->id;
$this->bank_transaction->save();
//auto-convert
}
}
}
private function searchInvoiceResource(string $column, array $rule, $invoices)
{
return $invoices->when($rule['search_key'] == 'description', function ($q) use ($rule, $column) {
return $q->cursor()->filter(function ($record) use ($rule, $column) {
return $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
});
@ -140,14 +174,68 @@ class ProcessBankRules extends AbstractService
}
private function searchPaymentResource()
private function searchPaymentResource(string $column, array $rule, $payments)
{
return $payments->when($rule['search_key'] == 'description', function ($q) use ($rule, $column) {
return $q->cursor()->filter(function ($record) use ($rule, $column) {
return $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
});
})
->when($rule['search_key'] == 'amount', function ($q) use ($rule, $column) {
return $q->cursor()->filter(function ($record) use ($rule, $column) {
return $this->matchNumberOperator($this->bank_transaction->amount, $record->{$column}, $rule['operator']);
});
})->pluck("id");
}
private function searchClientResource()
private function searchClientResource(string $column, array $rule, $invoices, $payments)
{
$invoice_matches = Client::query()
->whereIn('id', $invoices->pluck('client_id'))
->when($column == 'email', function ($q) {
return $q->whereHas('contacts', function ($qc) {
$qc->where('email', $this->bank_transaction->description);
});
})
->when($column != 'email', function ($q) use ($rule, $column) {
return $q->cursor()->filter(function ($record) use ($rule, $column) {
return $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
});
})->pluck('id');
$intersection = $invoices->whereIn('client_id', $invoice_matches);
if($intersection->count() > 0) {
return [$intersection, Invoice::class];
}
$payments_matches = Client::query()
->whereIn('id', $payments->pluck('client_id'))
->when($column == 'email', function ($q) {
return $q->whereHas('contacts', function ($qc) {
$qc->where('email', $this->bank_transaction->description);
});
})
->when($column != 'email', function ($q) use ($rule, $column) {
return $q->cursor()->filter(function ($record) use ($rule, $column) {
return $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
});
})->pluck('id');
$intersection = $payments->whereIn('client_id', $payments_matches);
if($intersection->count() > 0) {
return [$intersection, Payment::class];
}
return [Client::class, collect([])];
}
// $payment.amount => "Payment Amount", float
// $payment.transaction_reference => "Payment Transaction Reference", string

View File

@ -23,7 +23,6 @@ use Illuminate\Contracts\Database\Eloquent\Builder;
*/
trait ChartCalculations
{
public function getActiveInvoices($data): int|float
{
$result = 0;
@ -34,8 +33,9 @@ trait ChartCalculations
->where('is_deleted', 0)
->whereIn('status_id', [2,3,4]);
if(in_array($data['period'],['current,previous']))
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
match ($data['calculation']) {
'sum' => $result = $q->sum('amount'),
@ -58,8 +58,9 @@ trait ChartCalculations
->where('is_deleted', 0)
->whereIn('status_id', [2,3]);
if(in_array($data['period'],['current,previous']))
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
match ($data['calculation']) {
'sum' => $result = $q->sum('balance'),
@ -82,8 +83,9 @@ trait ChartCalculations
->where('is_deleted', 0)
->where('status_id', 4);
if(in_array($data['period'],['current,previous']))
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
match ($data['calculation']) {
'sum' => $result = $q->sum('amount'),
@ -106,8 +108,9 @@ trait ChartCalculations
->where('is_deleted', 0)
->whereIn('status_id', [5,6]);
if(in_array($data['period'],['current,previous']))
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
match ($data['calculation']) {
'sum' => $result = $q->sum('refunded'),
@ -133,8 +136,9 @@ trait ChartCalculations
$qq->where('due_date', '>=', now()->toDateString())->orWhereNull('due_date');
});
if(in_array($data['period'],['current,previous']))
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
match ($data['calculation']) {
'sum' => $result = $q->sum('refunded'),
@ -160,8 +164,9 @@ trait ChartCalculations
$qq->where('due_date', '>=', now()->toDateString())->orWhereNull('due_date');
});
if(in_array($data['period'],['current,previous']))
if(in_array($data['period'], ['current,previous'])) {
$q->whereBetween('date', [$data['start_date'], $data['end_date']]);
}
match ($data['calculation']) {
'sum' => $result = $q->sum('refunded'),

View File

@ -36,14 +36,24 @@ class PaymentMethod
{
$this->getGateways()
->getMethods();
// ->buildUrls();
return $this->getPaymentUrls();
}
public function getPaymentUrls()
{
$pu = collect($this->payment_urls);
$keys = $pu->pluck('gateway_type_id');
$contains_both = $keys->contains('1') && $keys->contains('29'); //handle the case where PayPal Advanced cards + regular CC is present
$this->payment_urls = $pu->when($contains_both, function ($methods) {
return $methods->reject(function ($item) {
return $item['gateway_type_id'] == '29';
});
})->toArray();
return $this->payment_urls;
}
public function getPaymentMethods()
@ -148,10 +158,8 @@ class PaymentMethod
$this->payment_methods = $payment_methods_collections->intersectByKeys($payment_methods_collections->flatten(1)->unique());
//@15-06-2024
foreach($this->payment_methods as $key => $type)
{
foreach ($type as $gateway_id => $gateway_type_id)
{
foreach($this->payment_methods as $key => $type) {
foreach ($type as $gateway_id => $gateway_type_id) {
$gate = $this->gateways->where('id', $gateway_id)->first();
$this->buildUrl($gate, $gateway_type_id);
}
@ -168,13 +176,9 @@ class PaymentMethod
foreach ($gateway->driver($this->client)->gatewayTypes() as $type) {
if (isset($gateway->fees_and_limits) && is_object($gateway->fees_and_limits) && property_exists($gateway->fees_and_limits, GatewayType::CREDIT_CARD)) { //@phpstan-ignore-line
if ($this->validGatewayForAmount($gateway->fees_and_limits->{GatewayType::CREDIT_CARD}, $this->amount)) {
// $this->payment_methods[] = [$gateway->id => $type];
// @15-06-2024
$this->buildUrl($gateway, $type);
}
} else {
// $this->payment_methods[] = [$gateway->id => null];
//@15-06-2024
$this->buildUrl($gateway, null);
}
}
@ -225,52 +229,6 @@ class PaymentMethod
return $this;
}
//@deprecated as buildUrl() supercedes
private function buildUrls()
{
foreach ($this->payment_methods as $key => $child_array) {
foreach ($child_array as $gateway_id => $gateway_type_id) {
$gateway = CompanyGateway::query()->find($gateway_id);
$fee_label = $gateway->calcGatewayFeeLabel($this->amount, $this->client, $gateway_type_id);
if (! $gateway_type_id || (GatewayType::CUSTOM == $gateway_type_id)) {
$this->payment_urls[] = [
'label' => $gateway->getConfigField('name').$fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
'is_paypal' => $gateway->isPayPal(),
];
} else {
$this->payment_urls[] = [
'label' => $gateway->getTypeAlias($gateway_type_id).$fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id,
'is_paypal' => $gateway->isPayPal(),
];
}
}
}
if (($this->client->getSetting('use_credits_payment') == 'option' || $this->client->getSetting('use_credits_payment') == 'always') && $this->client->service()->getCreditBalance() > 0) {
// Show credits as only payment option if both statements are true.
if (
$this->client->service()->getCreditBalance() > $this->amount
&& $this->client->getSetting('use_credits_payment') == 'always') {
$payment_urls = [];
}
$this->payment_urls[] = [
'label' => ctrans('texts.apply_credit'),
'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT,
'gateway_type_id' => GatewayType::CREDIT,
'is_paypal' => false,
];
}
return $this;
}
private function validGatewayForAmount($fees_and_limits_for_payment_type): bool
{
if (isset($fees_and_limits_for_payment_type)) {

View File

@ -0,0 +1,189 @@
<?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\Services\Client;
use App\Libraries\MultiDB;
use App\Models\ClientContact;
use App\Models\CompanyGateway;
use Illuminate\Support\Str;
use Validator;
class RFFService
{
public array $mappings = [
'client_name' => 'name',
'client_website' => 'website',
'client_phone' => 'phone',
'client_address_line_1' => 'address1',
'client_address_line_2' => 'address2',
'client_city' => 'city',
'client_state' => 'state',
'client_postal_code' => 'postal_code',
'client_country_id' => 'country_id',
'client_shipping_address_line_1' => 'shipping_address1',
'client_shipping_address_line_2' => 'shipping_address2',
'client_shipping_city' => 'shipping_city',
'client_shipping_state' => 'shipping_state',
'client_shipping_postal_code' => 'shipping_postal_code',
'client_shipping_country_id' => 'shipping_country_id',
'client_custom_value1' => 'custom_value1',
'client_custom_value2' => 'custom_value2',
'client_custom_value3' => 'custom_value3',
'client_custom_value4' => 'custom_value4',
'contact_first_name' => 'first_name',
'contact_last_name' => 'last_name',
'contact_email' => 'email',
// 'contact_phone' => 'phone',
];
public int $unfilled_fields = 0;
public function __construct(
public array $fields,
public string $database,
public string $company_gateway_id,
) {
}
public function check(ClientContact $contact): void
{
$_contact = $contact;
foreach ($this->fields as $index => $field) {
$_field = $this->mappings[$field['name']];
if (Str::startsWith($field['name'], 'client_')) {
if (
empty($_contact->client->{$_field})
|| is_null($_contact->client->{$_field})
) {
// $this->show_form = true;
$this->unfilled_fields++;
} else {
$this->fields[$index]['filled'] = true;
}
}
if (Str::startsWith($field['name'], 'contact_')) {
if (empty($_contact->{$_field}) || is_null($_contact->{$_field}) || str_contains($_contact->{$_field}, '@example.com')) {
$this->unfilled_fields++;
} else {
$this->fields[$index]['filled'] = true;
}
}
}
}
public function handleSubmit(array $data, ClientContact $contact, callable $callback, bool $return_errors = false): bool|array
{
MultiDB::setDb($this->database);
$rules = [];
collect($this->fields)->map(function ($field) use (&$rules) {
if (!array_key_exists('filled', $field)) {
$rules[$field['name']] = array_key_exists('validation_rules', $field)
? $field['validation_rules']
: 'required';
}
});
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
if ($return_errors) {
return $validator->getMessageBag()->getMessages();
}
session()->flash('validation_errors', $validator->getMessageBag()->getMessages());
return false;
}
if ($this->update($data, $contact)) {
$callback();
return true;
}
return false;
}
public function update(array $data, ClientContact $_contact): bool
{
$client = [];
$contact = [];
MultiDB::setDb($this->database);
foreach ($data as $field => $value) {
if (Str::startsWith($field, 'client_')) {
$client[$this->mappings[$field]] = $value;
}
if (Str::startsWith($field, 'contact_')) {
$contact[$this->mappings[$field]] = $value;
}
}
// $_contact->first_name = $data['contact_first_name'] ?? '';
// $_contact->last_name = $data['contact_last_name'] ?? '';
// $_contact->client->name = $data['client_name'] ?? '';
// $_contact->email = $data['contact_email'] ?? '';
// $_contact->client->phone = $data['client_phone'] ?? '';
// $_contact->client->address1 = $data['client_address_line_1'] ?? '';
// $_contact->client->city = $data['client_city'] ?? '';
// $_contact->client->state = $data['client_state'] ?? '';
// $_contact->client->country_id = $data['client_country_id'] ?? '';
// $_contact->client->postal_code = $data['client_postal_code'] ?? '';
// $_contact->client->shipping_address1 = $data['client_shipping_address_line_1'] ?? '';
// $_contact->client->shipping_city = $data['client_shipping_city'] ?? '';
// $_contact->client->shipping_state = $data['client_shipping_state'] ?? '';
// $_contact->client->shipping_postal_code = $data['client_shipping_postal_code'] ?? '';
// $_contact->client->shipping_country_id = $data['client_shipping_country_id'] ?? '';
// $_contact->client->custom_value1 = $data['client_custom_value1'] ?? '';
// $_contact->client->custom_value2 = $data['client_custom_value2'] ?? '';
// $_contact->client->custom_value3 = $data['client_custom_value3'] ?? '';
// $_contact->client->custom_value4 = $data['client_custom_value4'] ?? '';
// $_contact->push();
$_contact
->fill($contact)
->push();
$_contact->client
->fill($client)
->push();
/** @var \App\Models\CompanyGateway $cg */
$cg = CompanyGateway::find(
$this->company_gateway_id,
);
//@phpstan-ignore-next-line
if ($cg && $cg->update_details) {
$payment_gateway = $cg->driver($_contact->client)->init();
if (method_exists($payment_gateway, "updateCustomer")) {
$payment_gateway->updateCustomer();
}
}
return true;
}
}

View File

@ -0,0 +1,263 @@
<?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\Services\ClientPortal;
use App\Exceptions\PaymentFailed;
use App\Jobs\Invoice\CheckGatewayFee;
use App\Jobs\Invoice\InjectSignature;
use App\Jobs\Util\SystemLogger;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
/**
* LivewireInstantPayment
*
* New entry point for livewire component
* payments.
*/
class LivewireInstantPayment
{
use MakesHash;
use MakesDates;
/**
* (bool) success
* (string) error - "displayed back to the user, either in error div, or in with() on redirect"
* (string) redirect - ie client.invoices.index
* (array) payload - the data needed to complete the payment
* (string) component - the payment component to be displayed
*
* @var array $responder
*/
private array $responder = [
'success' => true,
'error' => '',
'redirect' => '',
'payload' => [],
'component' => '',
];
/**
* is_credit_payment
*
* Indicates whether this is a credit payment
* @var bool
*/
private $is_credit_payment = false;
/**
* __construct
*
* contact() guard
* company_gateway_id
* payable_invoices[] ['invoice_id' => '', 'amount' => 0]
* ?signature
* ?signature_ip
* payment_method_id
* ?pre_payment
* ?frequency_id
* ?remaining_cycles
* ?is_recurring
* ?hash
*
* @param array $data
* @return void
*/
public function __construct(public array $data)
{
}
public function run()
{
nlog($this->data);
$company_gateway = CompanyGateway::query()->find($this->data['company_gateway_id']);
if ($this->data['company_gateway_id'] == CompanyGateway::GATEWAY_CREDIT) {
$this->is_credit_payment = true;
}
$payable_invoices = collect($this->data['payable_invoices']);
$tokens = [];
$invoices = Invoice::query()
->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))
->withTrashed()
->get();
$client = $invoices->first()->client;
/* pop non payable invoice from the $payable_invoices array */
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first();
});
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
$payable_invoice_collection = collect();
foreach ($payable_invoices as $payable_invoice) {
$payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']);
$invoice = $invoices->first(function ($inv) use ($payable_invoice) {
return $payable_invoice['invoice_id'] == $inv->hashed_id;
});
$payable_amount = Number::roundValue(Number::parseFloat($payable_invoice['amount']), $client->currency()->precision);
$invoice_balance = Number::roundValue($invoice->balance, $client->currency()->precision);
$payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format());
$payable_invoice['invoice_number'] = $invoice->number;
if (isset($invoice->po_number)) {
$additional_info = $invoice->po_number;
} elseif (isset($invoice->public_notes)) {
$additional_info = $invoice->public_notes;
} else {
$additional_info = $invoice->date;
}
$payable_invoice['additional_info'] = $additional_info;
$payable_invoice_collection->push($payable_invoice);
}
if (isset($this->data['signature']) && $this->data['signature']) {
$contact_id = auth()->guard('contact')->user() ? auth()->guard('contact')->user()->id : null;
$invoices->each(function ($invoice) use ($contact_id) {
InjectSignature::dispatch($invoice, $contact_id, $this->data['signature'], $this->data['signature_ip']);
});
}
$payable_invoices = $payable_invoice_collection;
$payment_method_id = $this->data['payment_method_id'];
$invoice_totals = $payable_invoices->sum('amount');
$first_invoice = $invoices->first();
$credit_totals = in_array($first_invoice->client->getSetting('use_credits_payment'), ['always', 'option']) ? $first_invoice->client->service()->getCreditBalance() : 0;
$starting_invoice_amount = $first_invoice->balance;
if ($company_gateway) {
$first_invoice->service()->addGatewayFee($company_gateway, $payment_method_id, $invoice_totals)->save();
}
/**
* Gateway fee is calculated
* by adding it as a line item, and then subtract
* the starting and finishing amounts of the invoice.
*/
$fee_totals = $first_invoice->balance - $starting_invoice_amount;
if ($company_gateway) {
$tokens = $client->gateway_tokens()
->whereCompanyGatewayId($company_gateway->id)
->whereGatewayTypeId($payment_method_id)
->get();
}
if (! $this->is_credit_payment) {
$credit_totals = 0;
}
/** $hash_data = mixed[] */
$hash_data = [
'invoices' => $payable_invoices->toArray(),
'credits' => $credit_totals,
'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)),
'pre_payment' => $this->data['pre_payment'],
'frequency_id' => $this->data['frequency_id'],
'remaining_cycles' => $this->data['remaining_cycles'],
'is_recurring' => $this->data['is_recurring'],
];
if (isset($this->data['hash'])) {
$hash_data['billing_context'] = Cache::get($this->data['hash']);
} elseif ($old_hash = PaymentHash::query()->where('fee_invoice_id', $first_invoice->id)->whereNull('payment_id')->orderBy('id', 'desc')->first()) {
if (isset($old_hash->data->billing_context)) {
$hash_data['billing_context'] = $old_hash->data->billing_context;
}
}
$payment_hash = new PaymentHash();
$payment_hash->hash = Str::random(32);
$payment_hash->data = $hash_data;
$payment_hash->fee_total = $fee_totals;
$payment_hash->fee_invoice_id = $first_invoice->id;
$payment_hash->save();
if ($this->is_credit_payment) {
$amount_with_fee = max(0, (($invoice_totals + $fee_totals) - $credit_totals));
} else {
$credit_totals = 0;
$amount_with_fee = max(0, $invoice_totals + $fee_totals);
}
$totals = [
'credit_totals' => $credit_totals,
'invoice_totals' => $invoice_totals,
'fee_total' => $fee_totals,
'amount_with_fee' => $amount_with_fee,
];
$data = [
'ph' => $payment_hash,
'payment_hash' => $payment_hash->hash,
'total' => $totals,
'invoices' => $payable_invoices,
'tokens' => $tokens,
'payment_method_id' => $payment_method_id,
'amount_with_fee' => $invoice_totals + $fee_totals,
'client' => $client,
'pre_payment' => $this->data['pre_payment'],
'is_recurring' => $this->data['is_recurring'],
'company_gateway' => $company_gateway,
];
if ($this->is_credit_payment) {
$this->mergeResponder(['success' => true, 'component' => 'CreditPaymentComponent', 'payload' => $data]);
return $this->getResponder();
}
$this->mergeResponder(['success' => true, 'payload' => $data]);
return $this->getResponder();
}
private function getResponder(): array
{
return $this->responder;
}
private function mergeResponder(array $data): self
{
$this->responder = array_merge($this->responder, $data);
return $this;
}
}

View File

@ -23,8 +23,8 @@ enum HttpVerb: string
case DELETE = 'delete';
}
class Storecove {
class Storecove
{
private string $base_url = 'https://api.storecove.com/api/v2/';
private array $peppol_discovery = [
@ -44,7 +44,9 @@ class Storecove {
];
public function __construct(){}
public function __construct()
{
}
//config('ninja.storecove_api_key');
@ -170,8 +172,9 @@ class Storecove {
nlog($r->body());
nlog($r->json());
if($r->successful())
if($r->successful()) {
return $r->json()['guid'];
}
return false;
@ -265,8 +268,9 @@ class Storecove {
$r = $this->httpClient($uri, (HttpVerb::POST)->value, $payload);
if($r->successful())
if($r->successful()) {
return $r->json();
}
return $r;

View File

@ -26,7 +26,8 @@ use horstoeko\zugferd\ZugferdDocumentReader;
use horstoeko\zugferdvisualizer\ZugferdVisualizer;
use horstoeko\zugferdvisualizer\renderer\ZugferdVisualizerLaravelRenderer;
class ZugferdEDocument extends AbstractService {
class ZugferdEDocument extends AbstractService
{
public ZugferdDocumentReader|string $document;
/**
@ -75,7 +76,7 @@ class ZugferdEDocument extends AbstractService {
if ($taxCurrency && $taxCurrency != $invoiceCurrency) {
$expense->private_notes = ctrans("texts.tax_currency_mismatch");
}
$expense->uses_inclusive_taxes = True;
$expense->uses_inclusive_taxes = true;
$expense->amount = $grandTotalAmount;
$counter = 1;
if ($this->document->firstDocumentTax()) {
@ -117,8 +118,7 @@ class ZugferdEDocument extends AbstractService {
$expense->vendor_id = $vendor->id;
}
$expense->transaction_reference = $documentno;
}
else {
} else {
// The document exists as an expense
// Handle accordingly
nlog("Document already exists");
@ -128,4 +128,3 @@ class ZugferdEDocument extends AbstractService {
return $expense;
}
}

View File

@ -341,8 +341,9 @@ class Peppol extends AbstractService
$this->p_invoice->ID = $this->invoice->number;
$this->p_invoice->IssueDate = new \DateTime($this->invoice->date);
if($this->invoice->due_date)
if($this->invoice->due_date) {
$this->p_invoice->DueDate = new \DateTime($this->invoice->due_date);
}
$this->p_invoice->InvoiceTypeCode = 380; //
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
@ -390,10 +391,11 @@ class Peppol extends AbstractService
private function getTotalTaxAmount(): float
{
if(!$this->invoice->total_taxes)
if(!$this->invoice->total_taxes) {
return 0;
elseif($this->invoice->uses_inclusive_taxes)
} elseif($this->invoice->uses_inclusive_taxes) {
return $this->invoice->total_taxes;
}
return $this->calcAmountLineTax($this->invoice->tax_rate1, $this->invoice->amount) ?? 0;
}
@ -746,11 +748,11 @@ class Peppol extends AbstractService
};
//single array
if(is_array($rules) && !is_array($rules[0]))
if(is_array($rules) && !is_array($rules[0])) {
return $rules[2];
}
foreach($rules as $rule)
{
foreach($rules as $rule) {
if(stripos($rule[0], $code) !== false) {
return $rule[2];
}
@ -768,12 +770,13 @@ class Peppol extends AbstractService
if(strlen($this->invoice->client->vat_number ?? '') > 1) {
$pi = new PartyIdentification;
$pi = new PartyIdentification();
$vatID = new ID;
$vatID = new ID();
if($scheme = $this->resolveTaxScheme())
if($scheme = $this->resolveTaxScheme()) {
$vatID->schemeID = $scheme;
}
$vatID->value = $this->invoice->client->vat_number;
$pi->ID = $vatID;
@ -804,7 +807,8 @@ class Peppol extends AbstractService
$physical_location = new PhysicalLocation();
$physical_location->Address = $address;
$party->PhysicalLocation = $physical_location;;
$party->PhysicalLocation = $physical_location;
;
$contact = new Contact();
$contact->ElectronicMail = $this->invoice->client->present()->email();
@ -871,17 +875,15 @@ class Peppol extends AbstractService
if(count($receiver_identifiers) > 1) {
foreach($receiver_identifiers as $ident)
{
if(str_contains($ident[0], $client_classification))
{
foreach($receiver_identifiers as $ident) {
if(str_contains($ident[0], $client_classification)) {
return $ident[3];
}
}
}
elseif(count($receiver_identifiers) == 1)
} elseif(count($receiver_identifiers) == 1) {
return $receiver_identifiers[3];
}
throw new \Exception("e-invoice generation halted:: Could not resolve the Tax Code for this client? {$this->invoice->client->hashed_id}");
@ -914,8 +916,9 @@ class Peppol extends AbstractService
//only scans for top level props
foreach($settings as $prop => $visibility) {
if($prop_value = $this->getSetting($prop))
if($prop_value = $this->getSetting($prop)) {
$this->p_invoice->{$prop} = $prop_value;
}
}
@ -965,8 +968,9 @@ class Peppol extends AbstractService
private function senderSpecificLevelMutators(): self
{
if(method_exists($this, $this->invoice->company->country()->iso_3166_2))
if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) {
$this->{$this->invoice->company->country()->iso_3166_2}();
}
return $this;
}
@ -982,8 +986,9 @@ class Peppol extends AbstractService
private function receiverSpecificLevelMutators(): self
{
if(method_exists($this, "client_{$this->invoice->company->country()->iso_3166_2}"))
if(method_exists($this, "client_{$this->invoice->company->country()->iso_3166_2}")) {
$this->{"client_{$this->invoice->company->country()->iso_3166_2}"}();
}
return $this;
}
@ -999,9 +1004,9 @@ class Peppol extends AbstractService
private function setPaymentMeans(bool $required = false): self
{
if(isset($this->p_invoice->PaymentMeans))
if(isset($this->p_invoice->PaymentMeans)) {
return $this;
elseif($paymentMeans = $this->getSetting('Invoice.PaymentMeans')){
} elseif($paymentMeans = $this->getSetting('Invoice.PaymentMeans')) {
$this->p_invoice->PaymentMeans = is_array($paymentMeans) ? $paymentMeans : [$paymentMeans];
return $this;
}
@ -1022,8 +1027,7 @@ class Peppol extends AbstractService
{
$this->p_invoice->BuyerReference = $this->invoice->po_number ?? '';
if(strlen($this->invoice->po_number ?? '') > 1)
{
if(strlen($this->invoice->po_number ?? '') > 1) {
$order_reference = new OrderReference();
$id = new ID();
$id->value = $this->invoice->po_number;
@ -1064,13 +1068,11 @@ class Peppol extends AbstractService
//@phpstan-ignore-next-line
if(isset($this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID)) {
return $this;
}
elseif($customer_assigned_account_id = $this->getSetting('Invoice.AccountingCustomerParty.CustomerAssignedAccountID')){
} elseif($customer_assigned_account_id = $this->getSetting('Invoice.AccountingCustomerParty.CustomerAssignedAccountID')) {
$this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id;
return $this;
}
elseif(strlen($this->invoice->client->id_number ?? '') > 1){
} elseif(strlen($this->invoice->client->id_number ?? '') > 1) {
$customer_assigned_account_id = new CustomerAssignedAccountID();
$customer_assigned_account_id->value = $this->invoice->client->id_number;
@ -1130,8 +1132,7 @@ class Peppol extends AbstractService
$emails = $meta['routing']['emails'];
array_push($emails, $email);
$meta['routing']['emails'] = $emails;
}
else {
} else {
$meta['routing']['emails'] = [$email];
}
@ -1246,8 +1247,9 @@ class Peppol extends AbstractService
private function ES(): self
{
if(!isset($this->invoice->due_date))
if(!isset($this->invoice->due_date)) {
$this->p_invoice->DueDate = new \DateTime($this->invoice->date);
}
if($this->invoice->client->classification == 'business' && $this->invoice->company->getSetting('classification') == 'business') {
//must have a paymentmeans as credit_transfer
@ -1323,8 +1325,7 @@ class Peppol extends AbstractService
// ["scheme" => 'FR:SIRET', "id" => "0002:{$this->invoice->client->id_number}"]
]));
}
else {
} else {
//SIRET
$this->setStorecoveMeta($this->buildRouting([
["scheme" => 'FR:SIRET', "id" => "{$this->invoice->client->id_number}"]

View File

@ -117,15 +117,18 @@ class RO
];
public function __construct(protected Invoice $invoice){}
public function __construct(protected Invoice $invoice)
{
}
public function getStateCode(?string $state_code): string
{
$state_code = strlen($state_code ?? '') > 1 ? $state_code : $this->invoice->client->state;
//codes are configured by default
if(isset($this->countrySubEntity[$state_code]))
if(isset($this->countrySubEntity[$state_code])) {
return $state_code;
}
$key = array_search($state_code, $this->countrySubEntity);
@ -140,8 +143,9 @@ class RO
{
$client_sector_code = $client_city ?? $this->invoice->client->city;
if(in_array($this->getStateCode($this->invoice->client->state), ['BUCHAREST', 'RO-B']))
if(in_array($this->getStateCode($this->invoice->client->state), ['BUCHAREST', 'RO-B'])) {
return in_array(strtoupper($this->invoice->client->city), array_keys($this->sectorList)) ? strtoupper($this->invoice->client->city) : 'SECTOR1';
}
return $client_sector_code;
}

View File

@ -20,7 +20,8 @@ class PropertyResolver
return self::traverse($object, $pathSegments);
}
private static function traverse($object, array $pathSegments) {
private static function traverse($object, array $pathSegments)
{
if (empty($pathSegments)) {
return null;
}

View File

@ -4,11 +4,11 @@ namespace App\Services\Import\Quickbooks\Contracts;
interface SdkInterface
{
function getAuthorizationUrl(): string;
function accessToken(string $code, string $realm): array;
function refreshToken(): array;
function getAccessToken(): array;
function getRefreshToken(): array;
function totalRecords(string $entity): int;
function fetchRecords(string $entity, int $max): array;
public function getAuthorizationUrl(): string;
public function accessToken(string $code, string $realm): array;
public function refreshToken(): array;
public function getAccessToken();
public function getRefreshToken(): array;
public function totalRecords(string $entity): int;
public function fetchRecords(string $entity, int $max): array;
}

View File

@ -7,19 +7,20 @@ use App\Libraries\MultiDB;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class CompanyTokensRepository {
class CompanyTokensRepository
{
private $company_key;
private $store_key = "quickbooks-token";
public function __construct(string $key = null) {
public function __construct(string $key = null)
{
$this->company_key = $key ?? auth()->user->company()->company_key ?? null;
$this->store_key .= $key;
$this->setCompanyDbByKey();
}
public function save(array $tokens) {
public function save(array $tokens)
{
$this->updateAccessToken($tokens['access_token'], $tokens['access_token_expires']);
$this->updateRefreshToken($tokens['refresh_token'], $tokens['refresh_token_expires'], $tokens['realm']);
}
@ -35,7 +36,8 @@ class CompanyTokensRepository {
MultiDB::findAndSetDbByCompanyKey($this->company_key);
}
public function get() {
public function get()
{
return $this->getAccessToken() + $this->getRefreshToken();
}

View File

@ -6,8 +6,7 @@ use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface
final class SdkWrapper implements QuickbooksInterface
{
const MAXRESULTS = 10000;
public const MAXRESULTS = 10000;
private $sdk;
private $entities = ['Customer','Invoice','Payment','Item'];
@ -33,7 +32,8 @@ final class SdkWrapper implements QuickbooksInterface
return $this->getTokens();
}
public function getRefreshToken(): array{
public function getRefreshToken(): array
{
return $this->getTokens();
}
@ -66,11 +66,13 @@ final class SdkWrapper implements QuickbooksInterface
return $this->getTokens();
}
public function handleCallbacks(array $data): void {
public function handleCallbacks(array $data): void
{
}
public function totalRecords(string $entity) : int {
public function totalRecords(string $entity): int
{
return $this->sdk->Query("select count(*) from $entity");
}
@ -79,9 +81,12 @@ final class SdkWrapper implements QuickbooksInterface
return (array) $this->sdk->Query($query, $start, $limit);
}
public function fetchRecords( string $entity, int $max = 1000): array {
public function fetchRecords(string $entity, int $max = 1000): array
{
if(!in_array($entity, $this->entities)) return [];
if(!in_array($entity, $this->entities)) {
return [];
}
$records = [];
$start = 0;
@ -94,12 +99,16 @@ final class SdkWrapper implements QuickbooksInterface
do {
$limit = min(self::MAXRESULTS, $total - $start);
$recordsChunk = $this->queryData("select * from $entity", $start, $limit);
if(empty($recordsChunk)) break;
if(empty($recordsChunk)) {
break;
}
$records = array_merge($records, $recordsChunk);
$start += $limit;
} while ($start < $total);
if(empty($records)) throw new \Exceptions("No records retrieved!");
if(empty($records)) {
throw new \Exception("No records retrieved!");
}
} catch (\Throwable $th) {
nlog("Fetch Quickbooks API Error: {$th->getMessage()}");

View File

@ -1,17 +1,19 @@
<?php
namespace App\Services\Import\Quickbooks;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use App\Services\Import\Quickbooks\Auth;
use App\Repositories\Import\Quickbooks\Contracts\RepositoryInterface;
use App\Services\Import\QuickBooks\Contracts\SdkInterface as QuickbooksInterface;
use App\Services\Import\Quickbooks\Contracts\SdkInterface as QuickbooksInterface;
final class Service
{
private QuickbooksInterface $sdk;
public function __construct(QuickbooksInterface $quickbooks) {
public function __construct(QuickbooksInterface $quickbooks)
{
$this->sdk = $quickbooks;
}
@ -60,7 +62,8 @@ final class Service
return $this->fetchRecords('Item', $max) ;
}
protected function fetchRecords(string $entity, $max = 100) : Collection {
protected function fetchRecords(string $entity, $max = 100): Collection
{
return (self::RepositoryFactory($entity))->get($max);
}

View File

@ -42,8 +42,9 @@ class CreateInvitations extends AbstractService
public function run()
{
if(!$this->purchase_order->vendor)
if(!$this->purchase_order->vendor) {
return $this->purchase_order;
}
$contacts = $this->purchase_order->vendor->contacts()->get();

View File

@ -17,7 +17,6 @@ use App\Models\PurchaseOrder;
class MarkSent
{
public function __construct(public Vendor $vendor, public PurchaseOrder $purchase_order)
{
}

View File

@ -99,6 +99,13 @@ class ProfitLoss
public function run()
{
MultiDB::setDb($this->company->db);
App::forgetInstance('translator');
App::setLocale($this->company->locale());
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
return $this->build()->getCsv();
}
@ -356,12 +363,6 @@ class ProfitLoss
nlog($this->income_taxes);
nlog(array_sum(array_column($this->expense_break_down, 'total')));
MultiDB::setDb($this->company->db);
App::forgetInstance('translator');
App::setLocale($this->company->locale());
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
$csv = Writer::createFromString();
$csv->insertOne([ctrans('texts.profit_and_loss')]);

View File

@ -17,7 +17,6 @@ use Illuminate\Database\QueryException;
class VendorService
{
use GeneratesCounter;
private bool $completed = true;

View File

@ -40,7 +40,7 @@ class UserTransformer extends EntityTransformer
public function transform(User $user)
{
$ref = new \stdClass;
$ref = new \stdClass();
$ref->free = 0;
$ref->pro = 0;
$ref->enterprise = 0;

View File

@ -13,6 +13,7 @@ namespace App\Utils;
use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
class TempFile
{
public static function path($url): string

View File

@ -84,8 +84,9 @@ trait MakesReminders
{
$interval = $this->addTimeInterval($last_sent_date, $endless_reminder_frequency_id);
if(is_null($interval))
if(is_null($interval)) {
return false;
}
if (Carbon::now()->startOfDay()->eq($interval)) {
return true;

View File

@ -29,8 +29,8 @@ class PDF extends FPDI
try {
$trans = mb_convert_encoding($trans, 'ISO-8859-1', 'UTF-8');
} catch(\Exception $e) {
}
catch(\Exception $e){}
$this->Cell(0, 5, $trans, 0, 0, $this->text_alignment);
}

View File

@ -0,0 +1,47 @@
<?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\Utils\Traits;
use Illuminate\Support\Str;
trait WithSecureContext
{
public const CONTEXT_UPDATE = 'secureContext.updated';
/**
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function getContext(): mixed
{
return session()->get('secureContext.invoice-pay');
}
public function setContext(string $property, $value): array
{
$clone = session()->pull('secureContext.invoice-pay', default: []);
data_set($clone, $property, $value);
session()->put('secureContext.invoice-pay', $clone);
$this->dispatch(self::CONTEXT_UPDATE);
return $clone;
}
public function resetContext(): void
{
session()->forget('secureContext.invoice-pay');
}
}

24
composer.lock generated
View File

@ -535,16 +535,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.320.4",
"version": "3.320.5",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "e6af3e760864d43a30d8b7deb4f9dc6a49a5f66a"
"reference": "afda5aefd59da90208d2f59427ce81e91535b1f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e6af3e760864d43a30d8b7deb4f9dc6a49a5f66a",
"reference": "e6af3e760864d43a30d8b7deb4f9dc6a49a5f66a",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/afda5aefd59da90208d2f59427ce81e91535b1f2",
"reference": "afda5aefd59da90208d2f59427ce81e91535b1f2",
"shasum": ""
},
"require": {
@ -627,9 +627,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.320.4"
"source": "https://github.com/aws/aws-sdk-php/tree/3.320.5"
},
"time": "2024-08-20T18:20:32+00:00"
"time": "2024-08-21T18:14:31+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -8912,16 +8912,16 @@
},
{
"name": "psr/log",
"version": "3.0.0",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
"reference": "79dff0b268932c640297f5208d6298f71855c03e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
"reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
"url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
"reference": "79dff0b268932c640297f5208d6298f71855c03e",
"shasum": ""
},
"require": {
@ -8956,9 +8956,9 @@
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.0"
"source": "https://github.com/php-fig/log/tree/3.0.1"
},
"time": "2021-07-14T16:46:02+00:00"
"time": "2024-08-21T13:31:24+00:00"
},
{
"name": "psr/simple-cache",

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.24'),
'app_tag' => env('APP_TAG', '5.10.24'),
'app_version' => env('APP_VERSION', '5.10.25'),
'app_tag' => env('APP_TAG', '5.10.25'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

1215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"vue-template-compiler": "^2.6.14"
},
"dependencies": {
"@invoiceninja/simple-card": "^0.0.2",
"axios": "^0.25",
"card-js": "^1.0.13",
"card-validator": "^8.1.1",
@ -35,7 +36,8 @@
"lodash": "^4.17.21",
"resolve-url-loader": "^4.0.0",
"sass": "^1.43.4",
"sass-loader": "^12.3.0"
"sass-loader": "^12.3.0",
"signature_pad": "^5.0.2"
},
"type": "module"
}

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
var o=Object.defineProperty;var l=(n,e,t)=>e in n?o(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var d=(n,e,t)=>(l(n,typeof e!="symbol"?e+"":e,t),t);/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/class c{constructor(e){d(this,"handleAuthorization",()=>{var e=$("#my-card"),t={api_login_id:this.apiLoginId,card_number:e.CardJs("cardNumber").replace(/[^\d]/g,""),expire_year:e.CardJs("expiryYear").replace(/[^\d]/g,""),expire_month:e.CardJs("expiryMonth").replace(/[^\d]/g,""),cvv:document.getElementById("cvv").value.replace(/[^\d]/g,"")};return document.getElementById("pay-now")&&(document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden")),forte.createToken(t).success(this.successResponseHandler).error(this.failedResponseHandler),!1});d(this,"successResponseHandler",e=>{document.getElementById("payment_token").value=e.onetime_token,document.getElementById("card_brand").value=e.card_brand,document.getElementById("expire_year").value=e.expire_year,document.getElementById("expire_month").value=e.expire_month,document.getElementById("last_4").value=e.last_4;let t=document.querySelector("input[name=token-billing-checkbox]:checked");return t&&(document.getElementById("store_card").value=t.value),document.getElementById("server_response").submit(),!1});d(this,"failedResponseHandler",e=>{var t='<div class="alert alert-failure mb-4"><ul><li>'+e.response_description+"</li></ul></div>";return document.getElementById("forte_errors").innerHTML=t,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden"),!1});d(this,"handle",()=>{Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(r=>r.addEventListener("click",a=>{document.getElementById("save-card--container").style.display="none",document.getElementById("forte--credit-card-container").style.display="none",document.getElementById("token").value=a.target.dataset.token}));let e=document.getElementById("toggle-payment-with-credit-card");e&&e.addEventListener("click",()=>{document.getElementById("save-card--container").style.display="grid",document.getElementById("forte--credit-card-container").style.display="flex",document.getElementById("token").value=null});let t=document.getElementById("pay-now");return t&&t.addEventListener("click",r=>{let a=document.getElementById("token");a.value?this.handlePayNowAction(a.value):this.handleAuthorization()}),this});this.apiLoginId=e,this.cardHolderName=document.getElementById("cardholder_name")}handlePayNowAction(e){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),document.getElementById("token").value=e,document.getElementById("server_response").submit()}}const i=document.querySelector('meta[name="forte-api-login-id"]').content;new c(i).handle();

View File

@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
"file": "assets/app-06521fee.css",
"file": "assets/app-fee1da41.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}

View File

@ -0,0 +1,633 @@
/*!
* Signature Pad v5.0.2 | https://github.com/szimek/signature_pad
* (c) 2024 Szymon Nowak | Released under the MIT license
*/
class Point {
constructor(x, y, pressure, time) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
distanceTo(start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
}
equals(other) {
return (this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time);
}
velocityFrom(start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}
class Bezier {
static fromPoints(points, widths) {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
static calculateControlPoints(s1, s2, s3) {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
length() {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
const xdiff = cx - px;
const ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
point(t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
}
}
class SignatureEventTarget {
constructor() {
try {
this._et = new EventTarget();
}
catch (error) {
this._et = document;
}
}
addEventListener(type, listener, options) {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._et.dispatchEvent(event);
}
removeEventListener(type, callback, options) {
this._et.removeEventListener(type, callback, options);
}
}
function throttle(fn, wait = 250) {
let previous = 0;
let timeout = null;
let result;
let storedContext;
let storedArgs;
const later = () => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
class SignaturePad extends SignatureEventTarget {
constructor(canvas, options = {}) {
var _a, _b, _c;
super();
this.canvas = canvas;
this._drawingStroke = false;
this._isEmpty = true;
this._lastPoints = [];
this._data = [];
this._lastVelocity = 0;
this._lastWidth = 0;
this._handleMouseDown = (event) => {
if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
return;
}
this._strokeBegin(this._pointerEventToSignatureEvent(event));
};
this._handleMouseMove = (event) => {
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
return;
}
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
};
this._handleMouseUp = (event) => {
if (this._isLeftButtonPressed(event)) {
return;
}
this._strokeEnd(this._pointerEventToSignatureEvent(event));
};
this._handleTouchStart = (event) => {
if (event.targetTouches.length !== 1 || this._drawingStroke) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
this._strokeBegin(this._touchEventToSignatureEvent(event));
};
this._handleTouchMove = (event) => {
if (event.targetTouches.length !== 1) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
if (!this._drawingStroke) {
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
return;
}
this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
};
this._handleTouchEnd = (event) => {
if (event.targetTouches.length !== 0) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this._strokeEnd(this._touchEventToSignatureEvent(event));
};
this._handlePointerDown = (event) => {
if (!this._isLeftButtonPressed(event) || this._drawingStroke) {
return;
}
event.preventDefault();
this._strokeBegin(this._pointerEventToSignatureEvent(event));
};
this._handlePointerMove = (event) => {
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
return;
}
event.preventDefault();
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
};
this._handlePointerUp = (event) => {
if (this._isLeftButtonPressed(event)) {
return;
}
event.preventDefault();
this._strokeEnd(this._pointerEventToSignatureEvent(event));
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = (_a = options.throttle) !== null && _a !== void 0 ? _a : 16;
this.minDistance = (_b = options.minDistance) !== null && _b !== void 0 ? _b : 5;
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this.compositeOperation = options.compositeOperation || 'source-over';
this.canvasContextOptions = (_c = options.canvasContextOptions) !== null && _c !== void 0 ? _c : {};
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d', this.canvasContextOptions);
this.clear();
this.on();
}
clear() {
const { _ctx: ctx, canvas } = this;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
fromDataURL(dataUrl, options = {}) {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = () => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error) => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
toDataURL(type = 'image/png', encoderOptions) {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(this.toSVG(encoderOptions))}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
on() {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
off() {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerDown);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this._removeMoveUpEventListeners();
}
_getListenerFunctions() {
var _a;
const canvasWindow = window.document === this.canvas.ownerDocument
? window
: (_a = this.canvas.ownerDocument.defaultView) !== null && _a !== void 0 ? _a : this.canvas.ownerDocument;
return {
addEventListener: canvasWindow.addEventListener.bind(canvasWindow),
removeEventListener: canvasWindow.removeEventListener.bind(canvasWindow),
};
}
_removeMoveUpEventListeners() {
const { removeEventListener } = this._getListenerFunctions();
removeEventListener('pointermove', this._handlePointerMove);
removeEventListener('pointerup', this._handlePointerUp);
removeEventListener('mousemove', this._handleMouseMove);
removeEventListener('mouseup', this._handleMouseUp);
removeEventListener('touchmove', this._handleTouchMove);
removeEventListener('touchend', this._handleTouchEnd);
}
isEmpty() {
return this._isEmpty;
}
fromData(pointGroups, { clear = true } = {}) {
if (clear) {
this.clear();
}
this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
this._data = this._data.concat(pointGroups);
}
toData() {
return this._data;
}
_isLeftButtonPressed(event, only) {
if (only) {
return event.buttons === 1;
}
return (event.buttons & 1) === 1;
}
_pointerEventToSignatureEvent(event) {
return {
event: event,
type: event.type,
x: event.clientX,
y: event.clientY,
pressure: 'pressure' in event ? event.pressure : 0,
};
}
_touchEventToSignatureEvent(event) {
const touch = event.changedTouches[0];
return {
event: event,
type: event.type,
x: touch.clientX,
y: touch.clientY,
pressure: touch.force,
};
}
_getPointGroupOptions(group) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight: group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
compositeOperation: group && 'compositeOperation' in group
? group.compositeOperation
: this.compositeOperation,
};
}
_strokeBegin(event) {
const cancelled = !this.dispatchEvent(new CustomEvent('beginStroke', { detail: event, cancelable: true }));
if (cancelled) {
return;
}
const { addEventListener } = this._getListenerFunctions();
switch (event.event.type) {
case 'mousedown':
addEventListener('mousemove', this._handleMouseMove);
addEventListener('mouseup', this._handleMouseUp);
break;
case 'touchstart':
addEventListener('touchmove', this._handleTouchMove);
addEventListener('touchend', this._handleTouchEnd);
break;
case 'pointerdown':
addEventListener('pointermove', this._handlePointerMove);
addEventListener('pointerup', this._handlePointerUp);
break;
}
this._drawingStroke = true;
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup = Object.assign(Object.assign({}, pointGroupOptions), { points: [] });
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
_strokeUpdate(event) {
if (!this._drawingStroke) {
return;
}
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
const point = this._createPoint(event.x, event.y, event.pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
}
else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
_strokeEnd(event, shouldUpdate = true) {
this._removeMoveUpEventListeners();
if (!this._drawingStroke) {
return;
}
if (shouldUpdate) {
this._strokeUpdate(event);
}
this._drawingStroke = false;
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
_handlePointerEvents() {
this._drawingStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerDown);
}
_handleMouseEvents() {
this._drawingStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
}
_handleTouchEvents() {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
}
_reset(options) {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
this._ctx.globalCompositeOperation = options.compositeOperation;
}
_createPoint(x, y, pressure) {
const rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
}
_addPoint(point, options) {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2], options);
const curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
}
_calculateCurveWidths(startPoint, endPoint, options) {
const velocity = options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
_strokeWidth(velocity, options) {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
_drawCurveSegment(x, y, width) {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
_drawCurve(curve, options) {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
_drawDot(point, options) {
const ctx = this._ctx;
const width = options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
_fromData(pointGroups, drawCurve, drawDot) {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
}
else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
toSVG({ includeBackgroundColor = false } = {}) {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(pointGroups, (curve, { penColor }) => {
const path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, (point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
});
return svg.outerHTML;
}
}
export { SignaturePad as default };
//# sourceMappingURL=signature_pad.js.map

File diff suppressed because one or more lines are too long

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