mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 15:44:33 -04:00
commit
13b24cc03e
@ -1 +1 @@
|
||||
5.10.24
|
||||
5.10.25
|
@ -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',
|
||||
|
@ -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':
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -19,7 +19,6 @@ use Illuminate\Http\Request;
|
||||
*/
|
||||
class BrevoController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ use Illuminate\Http\Request;
|
||||
*/
|
||||
class MailgunWebhookController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ use Illuminate\Support\Str;
|
||||
|
||||
class OneTimeTokenController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
@ -19,7 +19,6 @@ use Illuminate\Http\Request;
|
||||
*/
|
||||
class PostMarkController extends BaseController
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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')));
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ use Illuminate\Auth\Access\AuthorizationException;
|
||||
|
||||
class GenericReportRequest extends Request
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
|
@ -15,7 +15,6 @@ use App\Http\Requests\Request;
|
||||
|
||||
class DisconnectUserMailerRequest extends Request
|
||||
{
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
|
@ -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']);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,8 @@ trait CommonTrait
|
||||
{
|
||||
protected $model;
|
||||
|
||||
public function getString($data,$field) {
|
||||
public function getString($data, $field)
|
||||
{
|
||||
return Arr::get($data, $field);
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 = [
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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() : ' ']);
|
||||
|
@ -59,7 +59,6 @@ class Register extends Component
|
||||
|
||||
public function register(array $data)
|
||||
{
|
||||
|
||||
$service = new ClientRegisterService(
|
||||
company: $this->subscription->company,
|
||||
additional: $this->additional_fields,
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
287
app/Livewire/Flow2/InvoicePay.php
Normal file
287
app/Livewire/Flow2/InvoicePay.php
Normal 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');
|
||||
}
|
||||
}
|
47
app/Livewire/Flow2/InvoiceSummary.php
Normal file
47
app/Livewire/Flow2/InvoiceSummary.php
Normal 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,
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
78
app/Livewire/Flow2/PaymentMethod.php
Normal file
78
app/Livewire/Flow2/PaymentMethod.php
Normal 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]);
|
||||
}
|
||||
}
|
86
app/Livewire/Flow2/ProcessPayment.php
Normal file
86
app/Livewire/Flow2/ProcessPayment.php
Normal 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);
|
||||
}
|
||||
}
|
136
app/Livewire/Flow2/RequiredFields.php
Normal file
136
app/Livewire/Flow2/RequiredFields.php
Normal 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'],
|
||||
]);
|
||||
}
|
||||
}
|
23
app/Livewire/Flow2/Signature.php
Normal file
23
app/Livewire/Flow2/Signature.php
Normal 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');
|
||||
}
|
||||
}
|
36
app/Livewire/Flow2/Terms.php
Normal file
36
app/Livewire/Flow2/Terms.php
Normal 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');
|
||||
}
|
||||
}
|
76
app/Livewire/Flow2/UnderOverPayment.php
Normal file
76
app/Livewire/Flow2/UnderOverPayment.php
Normal 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');
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
30
app/PaymentDrivers/Common/LivewireMethodInterface.php
Normal file
30
app/PaymentDrivers/Common/LivewireMethodInterface.php
Normal 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;
|
||||
}
|
261
app/PaymentDrivers/PayPalExpressPaymentDriver.php
Normal file
261
app/PaymentDrivers/PayPalExpressPaymentDriver.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
|
@ -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)) {
|
||||
|
189
app/Services/Client/RFFService.php
Normal file
189
app/Services/Client/RFFService.php
Normal 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;
|
||||
}
|
||||
}
|
263
app/Services/ClientPortal/LivewireInstantPayment.php
Normal file
263
app/Services/ClientPortal/LivewireInstantPayment.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}"]
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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()}");
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -17,7 +17,6 @@ use App\Models\PurchaseOrder;
|
||||
|
||||
class MarkSent
|
||||
{
|
||||
|
||||
public function __construct(public Vendor $vendor, public PurchaseOrder $purchase_order)
|
||||
{
|
||||
}
|
||||
|
@ -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')]);
|
||||
|
1
app/Services/Vendor/VendorService.php
vendored
1
app/Services/Vendor/VendorService.php
vendored
@ -17,7 +17,6 @@ use Illuminate\Database\QueryException;
|
||||
|
||||
class VendorService
|
||||
{
|
||||
|
||||
use GeneratesCounter;
|
||||
|
||||
private bool $completed = true;
|
||||
|
@ -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;
|
||||
|
@ -13,6 +13,7 @@ namespace App\Utils;
|
||||
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class TempFile
|
||||
{
|
||||
public static function path($url): string
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
47
app/Utils/Traits/WithSecureContext.php
Normal file
47
app/Utils/Traits/WithSecureContext.php
Normal 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
24
composer.lock
generated
@ -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",
|
||||
|
@ -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
1215
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
|
1
public/build/assets/app-06521fee.css
vendored
1
public/build/assets/app-06521fee.css
vendored
File diff suppressed because one or more lines are too long
1
public/build/assets/app-fee1da41.css
vendored
Normal file
1
public/build/assets/app-fee1da41.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
public/build/assets/forte-credit-card-payment-7bb15431.js
vendored
Normal file
9
public/build/assets/forte-credit-card-payment-7bb15431.js
vendored
Normal 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();
|
@ -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"
|
||||
}
|
||||
|
633
public/vendor/signature_pad@5/signature_pad.js
vendored
Normal file
633
public/vendor/signature_pad@5/signature_pad.js
vendored
Normal 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
|
1
public/vendor/signature_pad@5/signature_pad.js.map
vendored
Normal file
1
public/vendor/signature_pad@5/signature_pad.js.map
vendored
Normal file
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
Loading…
x
Reference in New Issue
Block a user