mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 02:57:01 +00:00
@@ -1 +1 @@
|
||||
5.12.37
|
||||
5.12.38
|
||||
@@ -184,7 +184,7 @@ class CheckData extends Command
|
||||
private function checkTaskTimeLogs()
|
||||
{
|
||||
\App\Models\Task::query()->cursor()->each(function ($task) {
|
||||
$time_log = json_decode($task->time_log, true) ?? [];
|
||||
$time_log = json_decode(($task->time_log ?? ''), true) ?? [];
|
||||
|
||||
foreach($time_log as &$log){
|
||||
if(count($log) > 4){
|
||||
@@ -521,7 +521,8 @@ class CheckData extends Command
|
||||
$this->logMessage("Add invitation for {$entity_key} - {$entity->id}");
|
||||
}
|
||||
} else {
|
||||
$this->logMessage("No contact present, so cannot add invitation for {$entity_key} - {$entity->id}");
|
||||
$this->logMessage("No contact present ( ? vendor not present ?? ), so cannot add invitation for {$entity_key} - {$entity->id}");
|
||||
|
||||
|
||||
try {
|
||||
$entity->service()->createInvitations()->save();
|
||||
|
||||
@@ -58,7 +58,7 @@ class ChartController extends BaseController
|
||||
$user = auth()->user();
|
||||
$admin_equivalent_permissions = $user->isAdmin() || $user->hasExactPermissionAndAll('view_all') || $user->hasExactPermissionAndAll('edit_all');
|
||||
|
||||
$cs = new ChartService($user->company(), $user, $admin_equivalent_permissions);
|
||||
$cs = new ChartService($user->company(), $user, $admin_equivalent_permissions, $request->input('include_drafts', false));
|
||||
|
||||
return response()->json($cs->totals($request->input('start_date'), $request->input('end_date')), 200);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class ShowChartRequest extends Request
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
/**@var \App\Models\User auth()->user */
|
||||
/** @var \App\Models\User auth()->user */
|
||||
$user = auth()->user();
|
||||
|
||||
return $user->isAdmin() || $user->hasPermission('view_dashboard');
|
||||
@@ -38,6 +38,7 @@ class ShowChartRequest extends Request
|
||||
'date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom',
|
||||
'start_date' => 'bail|sometimes|date',
|
||||
'end_date' => 'bail|sometimes|date',
|
||||
'include_drafts' => 'bail|sometimes|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ class UpdateClientRequest extends Request
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $input)) {
|
||||
$input['name'] = strip_tags($input['name']);
|
||||
$input['name'] = strip_tags($input['name'] ?? '');
|
||||
}
|
||||
|
||||
// allow setting country_id by iso code
|
||||
@@ -225,6 +225,9 @@ class UpdateClientRequest extends Request
|
||||
{
|
||||
$account = $this->client->company->account;
|
||||
|
||||
// Do not allow a user to force pdf variables on the client settings.
|
||||
unset($settings['pdf_variables']);
|
||||
|
||||
if (! $account->isFreeHostedClient()) {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ class StoreGroupSettingRequest extends Request
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
unset($settings->pdf_variables);
|
||||
|
||||
$settings_data = new SettingsData();
|
||||
$settings = $settings_data->cast($settings)->toObject();
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ class UpdateGroupSettingRequest extends Request
|
||||
$settings_data = new SettingsData();
|
||||
$settings = $settings_data->cast($settings)->toObject();
|
||||
|
||||
|
||||
// Do not allow a user to force pdf variables on the client settings.
|
||||
unset($settings->pdf_variables);
|
||||
|
||||
if (! $user->account->isFreeHostedClient()) {
|
||||
return (array)$settings;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class StorePaymentRequest extends Request
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$invoices = $this->input('invoices', []);
|
||||
$invoices = $this->input('invoices') ?? [];
|
||||
$clientId = $this->input('client_id');
|
||||
$invCollection = Invoice::withTrashed()
|
||||
->whereIn('id', array_column($invoices, 'invoice_id'))
|
||||
@@ -84,6 +84,11 @@ class StorePaymentRequest extends Request
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists('invoice_id', $invoice)) {
|
||||
$validator->errors()->add("invoices.{$index}.invoice_id", ctrans('texts.invoice_id') . ' required');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find invoice
|
||||
$inv = $invCollection->firstWhere('id', $invoice['invoice_id']);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ class BlackListRule implements ValidationRule
|
||||
{
|
||||
/** Bad domains +/- disposable email domains */
|
||||
private array $blacklist = [
|
||||
"edu.pk",
|
||||
"bablace.com",
|
||||
"moonfee.com",
|
||||
"edus2.us",
|
||||
|
||||
@@ -81,7 +81,7 @@ class BulkInvoiceJob implements ShouldQueue
|
||||
|
||||
$invoice->service()->markSent()->save();
|
||||
|
||||
if($invoice->verifactuEnabled() && !$invoice->hasSentAeat()) {
|
||||
if($invoice->company->verifactuEnabled() && !$invoice->hasSentAeat()) {
|
||||
$invoice->invitations()->update(['email_error' => 'primed']); // Flag the invitations as primed for AEAT submission
|
||||
$invoice->service()->sendVerifactu();
|
||||
return false;
|
||||
|
||||
@@ -324,7 +324,7 @@ class NinjaMailerJob implements ShouldQueue
|
||||
*/
|
||||
private function entityEmailFailed($message): void
|
||||
{
|
||||
$class = get_class($this->nmo->entity);
|
||||
$class = $this->nmo->entity ? get_class($this->nmo->entity) : false;
|
||||
|
||||
switch ($class) {
|
||||
case Invoice::class:
|
||||
|
||||
@@ -179,7 +179,7 @@ class InvoiceTransactionEventEntry
|
||||
$tax_detail = [
|
||||
'tax_name' => $tax['name'],
|
||||
'tax_rate' => $tax['tax_rate'],
|
||||
'line_total' => $tax['base_amount'],
|
||||
'line_total' => ($tax['base_amount'] ?? $calc->getNetSubtotal()),
|
||||
'total_tax' => $tax['total'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) - ($previousLine->line_total ?? 0),
|
||||
'tax_amount' => $tax['total'] - ($previousLine->total_tax ?? 0),
|
||||
@@ -234,7 +234,7 @@ class InvoiceTransactionEventEntry
|
||||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * $this->paid_ratio * -1,
|
||||
'tax_amount' => ($tax['total'] * $this->paid_ratio * -1),
|
||||
'line_total' => $tax['base_amount'] * $this->paid_ratio * -1,
|
||||
'line_total' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * $this->paid_ratio * -1,
|
||||
'total_tax' => $tax['total'] * $this->paid_ratio * -1,
|
||||
'postal_code' => $postal_code,
|
||||
|
||||
@@ -280,7 +280,7 @@ class InvoiceTransactionEventEntry
|
||||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * $this->paid_ratio,
|
||||
'tax_amount' => ($tax['total'] * $this->paid_ratio),
|
||||
'line_total' => $tax['base_amount'] * $this->paid_ratio,
|
||||
'line_total' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * $this->paid_ratio,
|
||||
'total_tax' => $tax['total'] * $this->paid_ratio,
|
||||
'postal_code' => $postal_code,
|
||||
];
|
||||
@@ -323,7 +323,7 @@ class InvoiceTransactionEventEntry
|
||||
'tax_rate' => $tax['tax_rate'],
|
||||
'taxable_amount' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * -1,
|
||||
'tax_amount' => $tax['total'] * -1,
|
||||
'line_total' => $tax['base_amount'] * -1,
|
||||
'line_total' => ($tax['base_amount'] ?? $calc->getNetSubtotal()) * -1,
|
||||
'total_tax' => $tax['total'] * -1,
|
||||
'postal_code' => $postal_code,
|
||||
];
|
||||
|
||||
@@ -113,8 +113,10 @@ class PaymentNotification implements ShouldQueue
|
||||
//new check, IF the payment is on a recurring invoice AND the user had notifications disabled for recurring invoices. then we disable the notification for this payment.
|
||||
$disabled_recurring_invoice_notifications = $this->findUserEntityNotificationType($payment, $company_user,['disable_recurring_payment_notification']);
|
||||
|
||||
if ($payment->invoices->first()->recurring_id && (array_search('mail', $disabled_recurring_invoice_notifications) !== false)) {
|
||||
continue;
|
||||
$invoice = $payment->invoices->first();
|
||||
|
||||
if ($invoice && $invoice->recurring_id && (array_search('mail', $disabled_recurring_invoice_notifications) !== false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nmo = new NinjaMailerObject();
|
||||
|
||||
@@ -269,7 +269,7 @@ class BillingPortalPurchase extends Component
|
||||
$data = [
|
||||
'name' => '',
|
||||
'contacts' => [
|
||||
['email' => $this->email],
|
||||
['email' => $this->email, 'is_primary' => true],
|
||||
],
|
||||
'client_hash' => Str::random(40),
|
||||
'settings' => ClientSettings::defaults(),
|
||||
|
||||
@@ -76,6 +76,8 @@ class BillingPortalPurchasev2 extends Component
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
public $price;
|
||||
|
||||
/**
|
||||
* List of payment methods fetched from client.
|
||||
*
|
||||
|
||||
@@ -95,9 +95,9 @@ class InvoicePay extends Component
|
||||
public $required_fields = false;
|
||||
|
||||
#[On('update.context')]
|
||||
public function handleContext(string $property, $value): self
|
||||
public function handleContext(string $key, string $property, $value): self
|
||||
{
|
||||
$this->setContext(property: $property, value: $value);
|
||||
$this->setContext($key, $property, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ class InvoicePay extends Component
|
||||
$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;
|
||||
$this->setContext($invite->key, 'signature', $base64); // $this->context['signature'] = $base64;
|
||||
$invite->save();
|
||||
|
||||
}
|
||||
@@ -124,15 +124,17 @@ class InvoicePay extends Component
|
||||
#[On('payable-amount')]
|
||||
public function payableAmount($payable_amount)
|
||||
{
|
||||
$this->setContext('amount', $payable_amount);
|
||||
$invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id);
|
||||
$this->setContext($invite->key, 'amount', $payable_amount);
|
||||
$this->under_over_payment = false;
|
||||
}
|
||||
|
||||
#[On('payment-method-selected')]
|
||||
public function paymentMethodSelected($company_gateway_id, $gateway_type_id, $amount)
|
||||
{
|
||||
$invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id);
|
||||
|
||||
$this->bulkSetContext([
|
||||
$this->bulkSetContext($invite->key, [
|
||||
'company_gateway_id' => $company_gateway_id,
|
||||
'gateway_type_id' => $gateway_type_id,
|
||||
'amount' => $amount,
|
||||
@@ -160,14 +162,14 @@ class InvoicePay extends Component
|
||||
|
||||
private function checkRequiredFields(CompanyGateway $company_gateway)
|
||||
{
|
||||
$invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id);
|
||||
|
||||
/** @var \App\Models\ClientContact $contact */
|
||||
$contact = $this->getContext()['contact'];
|
||||
$contact = $this->getContext($invite->key)['contact'];
|
||||
|
||||
$fields = $company_gateway->driver($contact->client)->getClientRequiredFields();
|
||||
|
||||
$this->setContext('fields', $fields); // $this->context['fields'] = $fields;
|
||||
|
||||
$this->setContext($invite->key,'fields', $fields); // $this->context['fields'] = $fields;
|
||||
|
||||
foreach ($fields as $index => $field) {
|
||||
$_field = $this->mappings[$field['name']];
|
||||
@@ -233,7 +235,7 @@ class InvoicePay extends Component
|
||||
public function mount()
|
||||
{
|
||||
|
||||
$this->resetContext();
|
||||
// $this->resetContext();
|
||||
|
||||
MultiDB::setDb($this->db);
|
||||
|
||||
@@ -243,7 +245,7 @@ class InvoicePay extends Component
|
||||
$client = $invite->contact->client;
|
||||
$settings = $client->getMergedSettings();
|
||||
|
||||
$this->bulkSetContext([
|
||||
$this->bulkSetContext($invite->key, [
|
||||
'contact' => $invite->contact,
|
||||
'settings' => $settings,
|
||||
'db' => $this->db,
|
||||
@@ -286,7 +288,7 @@ class InvoicePay extends Component
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$this->bulkSetContext([
|
||||
$this->bulkSetContext($invite->key, [
|
||||
'variables' => $this->variables,
|
||||
'invoices' => $invoices,
|
||||
'settings' => $settings,
|
||||
@@ -300,7 +302,10 @@ class InvoicePay extends Component
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('flow2.invoice-pay');
|
||||
//@phpstan-ignore-next-line
|
||||
$invite = \App\Models\InvoiceInvitation::with('contact.client', 'company')->withTrashed()->find($this->invitation_id);
|
||||
|
||||
return render('flow2.invoice-pay', ['_key' => $invite->key]);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation)
|
||||
|
||||
@@ -30,6 +30,8 @@ class InvoiceSummary extends Component
|
||||
|
||||
public $isReady = false;
|
||||
|
||||
public $_key;
|
||||
|
||||
#[On(self::CONTEXT_READY)]
|
||||
public function onContextReady(): void
|
||||
{
|
||||
@@ -39,7 +41,7 @@ class InvoiceSummary extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
if (!empty($_context)) {
|
||||
$this->isReady = true;
|
||||
@@ -49,7 +51,7 @@ class InvoiceSummary extends Component
|
||||
|
||||
private function loadContextData(): void
|
||||
{
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
if (empty($_context)) {
|
||||
return;
|
||||
@@ -76,7 +78,7 @@ class InvoiceSummary extends Component
|
||||
public function downloadDocument($invoice_hashed_id)
|
||||
{
|
||||
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
$invitation_id = $_context['invitation_id'];
|
||||
|
||||
@@ -98,7 +100,7 @@ class InvoiceSummary extends Component
|
||||
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
$contact = $_context['contact'] ?? auth()->guard('contact')->user();
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class PaymentMethod extends Component
|
||||
|
||||
public $amount = 0;
|
||||
|
||||
public $_key;
|
||||
public function placeholder()
|
||||
{
|
||||
return <<<'HTML'
|
||||
@@ -56,7 +57,7 @@ class PaymentMethod extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
$this->variables = $_context['variables'];
|
||||
$this->amount = array_sum(array_column($_context['payable_invoices'], 'amount'));
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
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\Exceptions\PaymentFailed;
|
||||
use App\Models\InvoiceInvitation;
|
||||
use App\Utils\Traits\WithSecureContext;
|
||||
use App\Services\ClientPortal\LivewireInstantPayment;
|
||||
|
||||
class ProcessPayment extends Component
|
||||
@@ -29,15 +29,15 @@ class ProcessPayment extends Component
|
||||
private array $payment_data_payload = [];
|
||||
|
||||
public $isLoading = true;
|
||||
|
||||
public $_key;
|
||||
public function mount()
|
||||
{
|
||||
|
||||
MultiDB::setDb($this->getContext()['db']);
|
||||
MultiDB::setDb($this->getContext($this->_key)['db']);
|
||||
|
||||
$invitation = InvoiceInvitation::find($this->getContext()['invitation_id']);
|
||||
$invitation = InvoiceInvitation::find($this->getContext($this->_key)['invitation_id']);
|
||||
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
$data = [
|
||||
'company_gateway_id' => $_context['company_gateway_id'],
|
||||
@@ -65,7 +65,7 @@ class ProcessPayment extends Component
|
||||
$gateway_fee = data_get($responder_data, 'payload.total.fee_total', false);
|
||||
$amount = data_get($responder_data, 'payload.total.amount_with_fee', 0);
|
||||
|
||||
$this->bulkSetContext([
|
||||
$this->bulkSetContext($this->_key, [
|
||||
'amount' => $amount,
|
||||
'gateway_fee' => $gateway_fee,
|
||||
]);
|
||||
@@ -116,7 +116,7 @@ class ProcessPayment extends Component
|
||||
$bag->add('gateway_error', $e->getMessage());
|
||||
session()->put('errors', $errors->put('default', $bag));
|
||||
|
||||
$invoice_id = $this->getContext()['payable_invoices'][0]['invoice_id'];
|
||||
$invoice_id = $this->getContext($this->_key)['payable_invoices'][0]['invoice_id'];
|
||||
$this->redirectRoute('client.invoice.show', ['invoice' => $invoice_id]);
|
||||
$stopPropagation();
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@ class RequiredFields extends Component
|
||||
public bool $is_loading = true;
|
||||
|
||||
public array $errors = [];
|
||||
|
||||
public $_key;
|
||||
public function mount(): void
|
||||
{
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
MultiDB::setDB(
|
||||
$_context['db'],
|
||||
@@ -114,7 +114,7 @@ class RequiredFields extends Component
|
||||
|
||||
$rff = new RFFService(
|
||||
fields: $this->fields,
|
||||
database: $this->getContext()['db'],
|
||||
database: $this->getContext($this->_key)['db'],
|
||||
company_gateway_id: (string)$this->company_gateway->id,
|
||||
);
|
||||
|
||||
@@ -134,7 +134,7 @@ class RequiredFields extends Component
|
||||
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
{
|
||||
return render('flow2.required-fields', [
|
||||
'contact' => $this->getContext()['contact'],
|
||||
'contact' => $this->getContext($this->_key)['contact'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,16 +21,16 @@ class Terms extends Component
|
||||
use WithSecureContext;
|
||||
|
||||
public $variables;
|
||||
|
||||
public $_key;
|
||||
public function mount()
|
||||
{
|
||||
$this->variables = $this->getContext()['variables'];
|
||||
$this->variables = $this->getContext($this->_key)['variables'];
|
||||
}
|
||||
|
||||
#[Computed()]
|
||||
public function invoice()
|
||||
{
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
$invitation_id = $_context['invitation_id'];
|
||||
|
||||
|
||||
@@ -30,10 +30,12 @@ class UnderOverPayment extends Component
|
||||
|
||||
public $payableInvoices = [];
|
||||
|
||||
public $_key;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
|
||||
$contact = $_context['contact'] ?? auth()->guard('contact')->user();
|
||||
|
||||
@@ -45,7 +47,7 @@ class UnderOverPayment extends Component
|
||||
public function checkValue(array $payableInvoices)
|
||||
{
|
||||
$this->errors = '';
|
||||
$_context = $this->getContext();
|
||||
$_context = $this->getContext($this->_key);
|
||||
$settings = $_context['settings'];
|
||||
|
||||
$contact = $_context['contact'] ?? auth()->guard('contact')->user();
|
||||
@@ -71,7 +73,7 @@ class UnderOverPayment extends Component
|
||||
}
|
||||
|
||||
if (!$this->errors) {
|
||||
$this->setContext('payable_invoices', $payableInvoices);
|
||||
$this->setContext($this->_key, 'payable_invoices', $payableInvoices);
|
||||
$this->dispatch('payable-amount', payable_amount: $input_amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ class PaymentEmailEngine extends BaseEmailEngine
|
||||
$invoice_list = '';
|
||||
|
||||
foreach ($this->payment->invoices as $invoice) {
|
||||
if (strlen($invoice->po_number) > 1) {
|
||||
if (strlen($invoice->po_number ?? '') > 1) {
|
||||
$invoice_list .= ctrans('texts.po_number')." {$invoice->po_number} <br>";
|
||||
}
|
||||
|
||||
@@ -433,7 +433,7 @@ class PaymentEmailEngine extends BaseEmailEngine
|
||||
$invoice_list = '<br><br>';
|
||||
|
||||
foreach ($this->payment->invoices as $invoice) {
|
||||
if (strlen($invoice->po_number) > 1) {
|
||||
if (strlen($invoice->po_number ?? '') > 1) {
|
||||
$invoice_list .= ctrans('texts.po_number')." {$invoice->po_number} <br>";
|
||||
}
|
||||
|
||||
|
||||
@@ -288,14 +288,13 @@ class Activity extends StaticModel
|
||||
|
||||
public const PURGE_CLIENT = 153;
|
||||
|
||||
public const VERIFACTU_INVOICE_SENT = 154;
|
||||
|
||||
public const VERIFACTU_INVOICE_SENT = 150;
|
||||
public const VERIFACTU_INVOICE_SENT_FAILURE = 155;
|
||||
|
||||
public const VERIFACTU_INVOICE_SENT_FAILURE = 151;
|
||||
public const VERIFACTU_CANCELLATION_SENT = 156;
|
||||
|
||||
public const VERIFACTU_CANCELLATION_SENT = 152;
|
||||
|
||||
public const VERIFACTU_CANCELLATION_SENT_FAILURE = 153;
|
||||
public const VERIFACTU_CANCELLATION_SENT_FAILURE = 157;
|
||||
|
||||
protected $casts = [
|
||||
'is_system' => 'boolean',
|
||||
|
||||
@@ -322,7 +322,8 @@ trait ChartQueries
|
||||
{
|
||||
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
// AND invoices.balance > 0
|
||||
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3)' : 'AND invoices.status_id IN (2,3)';
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
@@ -332,7 +333,7 @@ trait ChartQueries
|
||||
FROM clients
|
||||
JOIN invoices
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3)
|
||||
{$status_filter}
|
||||
AND invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
{$user_filter}
|
||||
@@ -347,6 +348,7 @@ trait ChartQueries
|
||||
{
|
||||
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3)' : 'AND invoices.status_id IN (2,3)';
|
||||
//AND invoices.balance > 0
|
||||
return DB::select("
|
||||
SELECT
|
||||
@@ -355,7 +357,7 @@ trait ChartQueries
|
||||
FROM clients
|
||||
JOIN invoices
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3)
|
||||
{$status_filter}
|
||||
AND invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
{$user_filter}
|
||||
@@ -416,13 +418,14 @@ trait ChartQueries
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
//AND invoices.amount > 0 @2024-12-03 - allow negative invoices to be included
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3,4)' : 'AND invoices.status_id IN (2,3,4)';
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
SUM(invoices.amount / COALESCE(NULLIF(invoices.exchange_rate, 0), 1)) as invoiced_amount
|
||||
FROM clients
|
||||
JOIN invoices ON invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3,4)
|
||||
{$status_filter}
|
||||
AND invoices.company_id = :company_id
|
||||
{$user_filter}
|
||||
|
||||
@@ -441,9 +444,8 @@ trait ChartQueries
|
||||
public function getInvoicesQuery($start_date, $end_date)
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
// $user_filter = $this->is_admin ? '' : 'AND (clients.user_id = '.$this->user->id.' OR clients.assigned_user_id = '.$this->user->id.')';
|
||||
|
||||
//AND invoices.amount > 0 @2024-12-03 - allow negative invoices to be included
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3,4)' : 'AND invoices.status_id IN (2,3,4)';
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
@@ -452,7 +454,7 @@ trait ChartQueries
|
||||
FROM clients
|
||||
JOIN invoices
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3,4)
|
||||
{$status_filter}
|
||||
AND invoices.company_id = :company_id
|
||||
{$user_filter}
|
||||
|
||||
@@ -467,6 +469,8 @@ trait ChartQueries
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3,4)' : 'AND invoices.status_id IN (2,3,4)';
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
SUM(invoices.balance / COALESCE(NULLIF(invoices.exchange_rate, 0), 1)) as total,
|
||||
@@ -474,7 +478,7 @@ trait ChartQueries
|
||||
FROM clients
|
||||
JOIN invoices
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3,4)
|
||||
{$status_filter}
|
||||
AND invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
@@ -492,6 +496,8 @@ trait ChartQueries
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3,4)' : 'AND invoices.status_id IN (2,3,4)';
|
||||
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
@@ -500,7 +506,7 @@ trait ChartQueries
|
||||
FROM clients
|
||||
JOIN invoices
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3,4)
|
||||
{$status_filter}
|
||||
AND invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
@@ -523,6 +529,8 @@ trait ChartQueries
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3,4)' : 'AND invoices.status_id IN (2,3,4)';
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
SUM(invoices.amount / COALESCE(NULLIF(invoices.exchange_rate, 0), 1)) as total,
|
||||
@@ -534,7 +542,7 @@ trait ChartQueries
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND invoices.status_id IN (2,3,4)
|
||||
{$status_filter}
|
||||
AND (invoices.date BETWEEN :start_date AND :end_date)
|
||||
GROUP BY invoices.date
|
||||
", [
|
||||
@@ -548,10 +556,12 @@ trait ChartQueries
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
$status_filter = $this->include_drafts ? 'AND invoices.status_id IN (1,2,3,4)' : 'AND invoices.status_id IN (2,3,4)';
|
||||
|
||||
return DB::select("
|
||||
SELECT
|
||||
sum(invoices.amount) as total,
|
||||
invoices.date
|
||||
invoices.date
|
||||
FROM clients
|
||||
JOIN invoices
|
||||
on invoices.client_id = clients.id
|
||||
@@ -559,7 +569,7 @@ trait ChartQueries
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND invoices.status_id IN (2,3,4)
|
||||
{$status_filter}
|
||||
AND (invoices.date BETWEEN :start_date AND :end_date)
|
||||
AND IFNULL(CAST(JSON_UNQUOTE(JSON_EXTRACT(clients.settings, '$.currency_id')) AS SIGNED), :company_currency) = :currency_id
|
||||
GROUP BY invoices.date
|
||||
|
||||
@@ -27,7 +27,7 @@ class ChartService
|
||||
use ChartQueries;
|
||||
use ChartCalculations;
|
||||
|
||||
public function __construct(public Company $company, private User $user, private bool $is_admin)
|
||||
public function __construct(public Company $company, private User $user, private bool $is_admin, private bool $include_drafts = false)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
namespace App\Services\Credit;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\BcMath;
|
||||
use App\Models\Credit;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Paymentable;
|
||||
use App\Models\PaymentType;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
@@ -35,9 +37,9 @@ class CreditService
|
||||
$this->credit = $credit;
|
||||
}
|
||||
|
||||
public function location(): array
|
||||
public function location(bool $set_countries = true): array
|
||||
{
|
||||
return (new LocationData($this->credit))->run();
|
||||
return (new LocationData($this->credit))->run($set_countries);
|
||||
}
|
||||
|
||||
public function getCreditPdf($invitation)
|
||||
@@ -260,14 +262,60 @@ class CreditService
|
||||
public function deleteCredit()
|
||||
{
|
||||
$paid_to_date = $this->credit->invoice_id ? $this->credit->balance : 0;
|
||||
/** 2025-09-24 - On invoice reversal => credit => delete - we reassign the payment to the credit - so no need to update the paid to date! */
|
||||
|
||||
$this->credit
|
||||
->client
|
||||
->service()
|
||||
// ->updatePaidToDate($paid_to_date)
|
||||
->adjustCreditBalance($this->credit->balance * -1)
|
||||
->save();
|
||||
|
||||
|
||||
/** 2025-12-03 - On credit deletion => credit => delete - if this credit was linked to a previous invoice
|
||||
* reversal, we cannot leave the payment dangling. This has the same net effect as a payment refund.
|
||||
*
|
||||
* At this point, we will assign whatever balance remains as a refund on the payment.
|
||||
*
|
||||
* Need to ensure the refund is never > than payment amount &&
|
||||
* The refund amount should be no more than the invoice amount.
|
||||
* or any remainder on the credit if it has been subsequently used elsewhere.
|
||||
*
|
||||
* Once this credit has been deleted, it cannot be restored?
|
||||
*/
|
||||
|
||||
if($this->credit->invoice_id && $this->credit->balance > 0){
|
||||
|
||||
$this->credit->invoice
|
||||
->payments()
|
||||
->where('is_deleted', 0)
|
||||
->cursor()
|
||||
->each(function ($payment){
|
||||
|
||||
$balance = $this->credit->balance;
|
||||
|
||||
$pivot = $payment->pivot;
|
||||
|
||||
$refundable_amount = min($balance, $pivot->amount);
|
||||
|
||||
$paymentable = new Paymentable();
|
||||
$paymentable->payment_id = $payment->id;
|
||||
$paymentable->paymentable_id = $this->credit->id;
|
||||
$paymentable->paymentable_type = Credit::class;
|
||||
$paymentable->refunded = $refundable_amount;
|
||||
$paymentable->save();
|
||||
|
||||
$payment->refunded += $refundable_amount;
|
||||
|
||||
if(BcMath::comp($payment->amount, $refundable_amount) == 0) {
|
||||
$payment->status_id = Payment::STATUS_REFUNDED;
|
||||
}
|
||||
else {
|
||||
$payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
|
||||
}
|
||||
$payment->save();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -284,6 +332,54 @@ class CreditService
|
||||
->adjustCreditBalance($this->credit->balance)
|
||||
->save();
|
||||
|
||||
|
||||
/**
|
||||
* If we have previously deleted a reversed credit / invoice
|
||||
* we would have applied an adjustment to the payments for the
|
||||
* credit balance remaining. This section reverses the adjustment.
|
||||
*/
|
||||
if($this->credit->invoice_id && $this->credit->balance > 0){
|
||||
|
||||
|
||||
$this->credit->invoice
|
||||
->payments()
|
||||
->where('is_deleted', 0)
|
||||
->whereHas('paymentables', function ($q){
|
||||
$q->where('paymentable_type', Credit::class)
|
||||
->where('paymentable_id', $this->credit->id)
|
||||
->where('refunded', '>', 0);
|
||||
})
|
||||
->cursor()
|
||||
->each(function ($payment) {
|
||||
|
||||
$refund_reversal = 0;
|
||||
|
||||
$payment->paymentables->where('paymentable_type', Credit::class)
|
||||
->where('paymentable_id', $this->credit->id)
|
||||
->each(function ($paymentable) use (&$refund_reversal){
|
||||
$refund_reversal += $paymentable->refunded;
|
||||
|
||||
$paymentable->forceDelete();
|
||||
|
||||
});
|
||||
|
||||
$payment->refunded -= $refund_reversal;
|
||||
$payment->save();
|
||||
|
||||
if(BcMath::comp($payment->refunded, 0) == 0) {
|
||||
$payment->status_id = Payment::STATUS_COMPLETED;
|
||||
}
|
||||
elseif(BcMath::comp($payment->amount, $payment->refunded) == 0) {
|
||||
$payment->status_id = Payment::STATUS_REFUNDED;
|
||||
}
|
||||
else {
|
||||
$payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
|
||||
}
|
||||
$payment->save();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,9 +69,19 @@ class ApplyNumber extends AbstractService
|
||||
{
|
||||
$x = 1;
|
||||
|
||||
/** Peppol Credit Numbers should be labelled as such. */
|
||||
$peppol_enabled =$this->client->peppolSendingEnabled();
|
||||
|
||||
do {
|
||||
try {
|
||||
$this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
|
||||
|
||||
if($peppol_enabled && strlen(trim($this->client->getSetting('credit_number_pattern'))) > 0) {
|
||||
$this->invoice->number = $this->getPeppolCreditNumber($this->client, $this->invoice);
|
||||
}
|
||||
else {
|
||||
$this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice, $this->invoice->recurring_id);
|
||||
}
|
||||
|
||||
$this->invoice->saveQuietly();
|
||||
|
||||
$this->completed = false;
|
||||
|
||||
@@ -622,11 +622,11 @@ class InvoiceService
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function location(): array
|
||||
public function location(bool $set_countries = true): array
|
||||
{
|
||||
return (new LocationData($this->invoice))->run();
|
||||
return (new LocationData($this->invoice))->run($set_countries);
|
||||
}
|
||||
|
||||
|
||||
public function workFlow()
|
||||
{
|
||||
if ($this->invoice->status_id == Invoice::STATUS_PAID && $this->invoice->client->getSetting('auto_archive_invoice')) {
|
||||
@@ -720,13 +720,7 @@ class InvoiceService
|
||||
*
|
||||
*/
|
||||
/** New Invoice - F1 Type */
|
||||
// if (empty($this->invoice->client->vat_number) || !in_array($this->invoice->client->country->iso_3166_2, (new \App\DataMapper\Tax\BaseRule())->eu_country_codes)) {
|
||||
|
||||
// $this->invoice->backup->guid = 'exempt';
|
||||
// $this->invoice->saveQuietly();
|
||||
// return $this;
|
||||
// } else
|
||||
|
||||
|
||||
if ($new_model && $this->invoice->amount >= 0) {
|
||||
$this->invoice->backup->document_type = 'F1';
|
||||
$this->invoice->backup->adjustable_amount = (new \App\Services\EDocument\Standards\Verifactu($this->invoice))->run()->registro_alta->calc->getTotal();
|
||||
|
||||
@@ -37,9 +37,9 @@ class QuoteService
|
||||
$this->quote = $quote;
|
||||
}
|
||||
|
||||
public function location(): array
|
||||
public function location(bool $set_countries = true): array
|
||||
{
|
||||
return (new LocationData($this->quote))->run();
|
||||
return (new LocationData($this->quote))->run($set_countries);
|
||||
}
|
||||
|
||||
public function createInvitations()
|
||||
|
||||
@@ -180,9 +180,9 @@ class RecurringService
|
||||
|
||||
}
|
||||
|
||||
public function location(): array
|
||||
public function location(bool $set_countries = true): array
|
||||
{
|
||||
return (new LocationData($this->recurring_entity))->run();
|
||||
return (new LocationData($this->recurring_entity))->run($set_countries);
|
||||
}
|
||||
|
||||
public function save()
|
||||
|
||||
@@ -208,8 +208,15 @@ class HtmlEngine
|
||||
$data['$location.custom3'] = &$data['$location3'];
|
||||
$data['$location.custom4'] = &$data['$location4'];
|
||||
|
||||
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
|
||||
$data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')];
|
||||
if ($this->entity_string == 'invoice' || $this->entity_string == 'recurring_invoice') {
|
||||
|
||||
if($this->client->peppolSendingEnabled() && $this->entity->amount < 0) {
|
||||
$data['$entity'] = ['value' => ctrans('texts.credit'), 'label' => ctrans('texts.credit')];
|
||||
}
|
||||
else {
|
||||
$data['$entity'] = ['value' => ctrans('texts.invoice'), 'label' => ctrans('texts.invoice')];
|
||||
}
|
||||
|
||||
$data['$number'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')];
|
||||
$data['$invoice'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number')];
|
||||
$data['$number_short'] = ['value' => $this->entity->number ?: ' ', 'label' => ctrans('texts.invoice_number_short')];
|
||||
|
||||
@@ -40,6 +40,13 @@ trait GeneratesCounter
|
||||
//todo in the form validation, we need to ensure that if a prefix and pattern is set we throw a validation error,
|
||||
//only one type is allow else this will cause confusion to the end user
|
||||
|
||||
public function getPeppolCreditNumber(Client $client, Invoice $invoice)
|
||||
{
|
||||
$entity_number = $this->getNextEntityNumber(Credit::class, $client);
|
||||
|
||||
return $this->replaceUserVars($invoice, $entity_number);
|
||||
}
|
||||
|
||||
private function getNextEntityNumber($entity, Client $client, $is_recurring = false)
|
||||
{
|
||||
$prefix = '';
|
||||
|
||||
@@ -19,7 +19,7 @@ trait ActionsInvoice
|
||||
|
||||
public function invoicePayable($invoice): bool
|
||||
{
|
||||
if($invoice->verifactuEnabled() && $invoice->amount < 0) {
|
||||
if($invoice->company->verifactuEnabled() && $invoice->amount < 0) {
|
||||
return false;
|
||||
}
|
||||
return $invoice->isPayable();
|
||||
@@ -28,10 +28,10 @@ trait ActionsInvoice
|
||||
public function invoiceDeletable($invoice): bool
|
||||
{
|
||||
//Cancelled invoices are not deletable if verifactu is enabled
|
||||
if($invoice->verifactuEnabled() && $invoice->status_id == Invoice::STATUS_DRAFT && $invoice->is_deleted == false) {
|
||||
if($invoice->company->verifactuEnabled() && $invoice->status_id == Invoice::STATUS_DRAFT && $invoice->is_deleted == false) {
|
||||
return true;
|
||||
}
|
||||
elseif($invoice->verifactuEnabled()) {
|
||||
elseif($invoice->company->verifactuEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ trait ActionsInvoice
|
||||
|
||||
public function invoiceRestorable($invoice): bool
|
||||
{
|
||||
if($invoice->verifactuEnabled() && !$invoice->is_deleted && $invoice->deleted_at) {
|
||||
if($invoice->company->verifactuEnabled() && !$invoice->is_deleted && $invoice->deleted_at) {
|
||||
return true;
|
||||
}
|
||||
elseif($invoice->verifactuEnabled()) {
|
||||
elseif($invoice->company->verifactuEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ trait ActionsInvoice
|
||||
|
||||
public function invoiceCancellable($invoice): bool
|
||||
{
|
||||
if($invoice->verifactuEnabled() &&
|
||||
if($invoice->company->verifactuEnabled() &&
|
||||
$invoice->backup->document_type === 'F1' &&
|
||||
$invoice->backup->child_invoice_ids->count() == 0 &&
|
||||
in_array($invoice->status_id, [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) &&
|
||||
@@ -64,7 +64,7 @@ trait ActionsInvoice
|
||||
{
|
||||
return true;
|
||||
}
|
||||
elseif($invoice->verifactuEnabled()){
|
||||
elseif($invoice->company->verifactuEnabled()){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ trait ActionsInvoice
|
||||
|
||||
public function invoiceReversable($invoice): bool
|
||||
{
|
||||
if($invoice->verifactuEnabled()){
|
||||
if($invoice->company->verifactuEnabled()){
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,33 +23,33 @@ trait WithSecureContext
|
||||
* @throws \Psr\Container\ContainerExceptionInterface
|
||||
* @throws \Psr\Container\NotFoundExceptionInterface
|
||||
*/
|
||||
public function getContext(): mixed
|
||||
public function getContext(string $key): mixed
|
||||
{
|
||||
$context = \Illuminate\Support\Facades\Cache::get(session()->getId()) ?? [];
|
||||
$context = \Illuminate\Support\Facades\Cache::get($key) ?? [];
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
public function setContext(string $property, $value): array
|
||||
public function setContext(string $key, string $property, $value): array
|
||||
{
|
||||
$clone = $this->getContext();
|
||||
$clone = $this->getContext($key);
|
||||
|
||||
data_set($clone, $property, $value);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put(session()->getId(), $clone, now()->addHour());
|
||||
\Illuminate\Support\Facades\Cache::put($key, $clone, now()->addHour());
|
||||
|
||||
$this->dispatch(self::CONTEXT_UPDATE);
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function bulkSetContext(array $data): array
|
||||
public function bulkSetContext(string $key, array $data): array
|
||||
{
|
||||
|
||||
$clone = $this->getContext();
|
||||
$clone = $this->getContext($key);
|
||||
$clone = array_merge($clone, $data);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put(session()->getId(), $clone, now()->addHour());
|
||||
\Illuminate\Support\Facades\Cache::put($key, $clone, now()->addHour());
|
||||
|
||||
$this->dispatch(self::CONTEXT_UPDATE);
|
||||
|
||||
@@ -57,8 +57,8 @@ trait WithSecureContext
|
||||
|
||||
}
|
||||
|
||||
public function resetContext(): void
|
||||
{
|
||||
\Illuminate\Support\Facades\Cache::forget(session()->getId());
|
||||
}
|
||||
// public function resetContext(): void
|
||||
// {
|
||||
// \Illuminate\Support\Facades\Cache::forget(session()->getId());
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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.12.37'),
|
||||
'app_tag' => env('APP_TAG', '5.12.37'),
|
||||
'app_version' => env('APP_VERSION', '5.12.38'),
|
||||
'app_tag' => env('APP_TAG', '5.12.38'),
|
||||
'minimum_client_version' => '5.0.16',
|
||||
'terms_version' => '1.0.1',
|
||||
'api_secret' => env('API_SECRET', false),
|
||||
|
||||
109
public/build/assets/app-aa93be80.js
vendored
Normal file
109
public/build/assets/app-aa93be80.js
vendored
Normal file
File diff suppressed because one or more lines are too long
104
public/build/assets/app-eee19136.js
vendored
104
public/build/assets/app-eee19136.js
vendored
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
||||
"file": "assets/wait-8f4ae121.js"
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-eee19136.js",
|
||||
"file": "assets/app-aa93be80.js",
|
||||
"imports": [
|
||||
"_index-08e160a7.js",
|
||||
"__commonjsHelpers-725317a4.js"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2">
|
||||
<div class="p-2">
|
||||
@livewire('flow2.invoice-summary')
|
||||
@livewire('flow2.invoice-summary', ['_key' => $_key])
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
@@ -18,6 +18,6 @@
|
||||
session()->forget('errors');
|
||||
@endphp
|
||||
|
||||
@livewire($this->component, [], key($this->componentUniqueId()))
|
||||
@livewire($this->component, ['_key' => $_key], key($this->componentUniqueId()))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,30 @@ class ClientApiTest extends TestCase
|
||||
Model::reguard();
|
||||
}
|
||||
|
||||
public function testPdfVariablesUnset()
|
||||
{
|
||||
$data = [
|
||||
'name' => 'name of client',
|
||||
'settings' => [
|
||||
'pdf_variables' => 'xx',
|
||||
'currency_id' => '2'
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->putJson("/api/v1/clients/".$this->client->hashed_id, $data)
|
||||
->assertStatus(200);
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertEquals("2", $arr['data']['settings']['currency_id']);
|
||||
$this->assertArrayNotHasKey('pdf_variables', $arr['data']['settings']);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function testBulkUpdates()
|
||||
{
|
||||
Client::factory()->count(3)->create([
|
||||
|
||||
@@ -41,6 +41,300 @@ class CreditTest extends TestCase
|
||||
}
|
||||
|
||||
|
||||
public function testCreditDeletionAfterInvoiceReversalAndPaymentRefund()
|
||||
{
|
||||
$c = Client::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'balance' => 0,
|
||||
'paid_to_date' => 0,
|
||||
]);
|
||||
|
||||
|
||||
$ii = new InvoiceItem();
|
||||
$ii->cost = 100;
|
||||
$ii->quantity = 1;
|
||||
$ii->product_key = 'xx';
|
||||
$ii->notes = 'yy';
|
||||
|
||||
$i = \App\Models\Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $c->id,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
$ii
|
||||
],
|
||||
'status_id' => 1,
|
||||
]);
|
||||
|
||||
$repo = new InvoiceRepository();
|
||||
$repo->save([], $i);
|
||||
|
||||
$i = $i->calc()->getInvoice();
|
||||
$i = $i->service()->markPaid()->save(); //paid
|
||||
|
||||
$payment = $i->payments()->first();
|
||||
|
||||
$this->assertNotNull($payment);
|
||||
|
||||
$this->assertEquals(0, $i->balance);
|
||||
$this->assertEquals(100, $i->amount);
|
||||
|
||||
$credit_array = $i->withoutRelations()->toArray();
|
||||
$credit_array['invoice_id'] = $i->hashed_id;
|
||||
$credit_array['client_id'] = $c->hashed_id;
|
||||
unset($credit_array['backup']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->post('/api/v1/credits', $credit_array);
|
||||
|
||||
$response->assertStatus(200); //reversal - credit created.
|
||||
|
||||
$arr = $response->json();
|
||||
$credit = \App\Models\Credit::find($this->decodePrimaryKey($arr['data']['id']));
|
||||
|
||||
$this->assertNotNull($credit);
|
||||
$payment = $payment->fresh();
|
||||
|
||||
$i = $i->fresh();
|
||||
|
||||
$this->assertEquals(\App\Models\Invoice::STATUS_REVERSED, $i->status_id);
|
||||
|
||||
$client = $i->client;
|
||||
|
||||
$this->assertEquals(100, $client->credit_balance);
|
||||
$this->assertEquals(0, $client->paid_to_date);
|
||||
$this->assertEquals(0, $client->balance);
|
||||
|
||||
|
||||
//delete the credit!!
|
||||
|
||||
$data = [
|
||||
'ids' => [$credit->hashed_id],
|
||||
'action' => 'delete',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/credits/bulk', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$payment = $payment->fresh();
|
||||
|
||||
$this->assertEquals($payment->amount, $payment->refunded);
|
||||
$this->assertEquals(\App\Models\Payment::STATUS_REFUNDED, $payment->status_id);
|
||||
$this->assertTrue($payment->paymentables()->where('paymentable_type', Credit::class)->where('paymentable_id', $credit->id)->exists());
|
||||
|
||||
|
||||
//lets restore the credit!!
|
||||
$data = [
|
||||
'ids' => [$credit->hashed_id],
|
||||
'action' => 'restore',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/credits/bulk', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$payment = $payment->fresh();
|
||||
$this->assertEquals(0, $payment->refunded);
|
||||
$this->assertEquals(\App\Models\Payment::STATUS_COMPLETED, $payment->status_id);
|
||||
$this->assertFalse($payment->paymentables()->where('paymentable_type', Credit::class)->where('paymentable_id', $credit->id)->exists());
|
||||
|
||||
}
|
||||
|
||||
public function testInvoiceWithMultiplePaymentsAndSingleCreditDeletionPostInvoiceReversal()
|
||||
{
|
||||
$c = Client::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'balance' => 0,
|
||||
'paid_to_date' => 0,
|
||||
]);
|
||||
|
||||
|
||||
$ii = new InvoiceItem();
|
||||
$ii->cost = 100;
|
||||
$ii->quantity = 1;
|
||||
$ii->product_key = 'xx';
|
||||
$ii->notes = 'yy';
|
||||
|
||||
$i = \App\Models\Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $c->id,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
$ii
|
||||
],
|
||||
'status_id' => 1,
|
||||
]);
|
||||
|
||||
$repo = new InvoiceRepository();
|
||||
$repo->save([], $i);
|
||||
|
||||
$i = $i->calc()->getInvoice();
|
||||
|
||||
$data =[
|
||||
'date' => now()->format('Y-m-d'),
|
||||
'client_id' => $c->hashed_id,
|
||||
'invoices' => [
|
||||
[
|
||||
'invoice_id' => $i->hashed_id,
|
||||
'amount' => 10,
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/payments', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
sleep(1);
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/payments', $data);
|
||||
sleep(1);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/payments', $data);
|
||||
sleep(1);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/payments', $data);
|
||||
sleep(1);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/payments', $data);
|
||||
sleep(1);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
|
||||
// At this stage we have 5 payments for half of the invoice amount.
|
||||
//create the reversal invoice for half of the invoice.
|
||||
|
||||
$credit_array = $i->withoutRelations()->toArray();
|
||||
$credit_array['invoice_id'] = $i->hashed_id;
|
||||
$credit_array['client_id'] = $c->hashed_id;
|
||||
|
||||
$ii = new InvoiceItem();
|
||||
$ii->cost = 50;
|
||||
$ii->quantity = 1;
|
||||
$ii->product_key = 'xx';
|
||||
$ii->notes = 'yy';
|
||||
|
||||
$credit_array['line_items'] = [$ii];
|
||||
unset($credit_array['backup']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->post('/api/v1/credits', $credit_array);
|
||||
|
||||
$response->assertStatus(200); //reversal - credit created.
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertEquals(50, $arr['data']['balance']);
|
||||
$this->assertEquals(50, $arr['data']['amount']);
|
||||
$this->assertEquals($i->hashed_id, $arr['data']['invoice_id']);
|
||||
$this->assertEquals(Credit::STATUS_SENT, $arr['data']['status_id']);
|
||||
|
||||
$credit = Credit::withTrashed()->find($this->decodePrimaryKey($arr['data']['id']));
|
||||
|
||||
//delete the credit!!
|
||||
$data = [
|
||||
'ids' => [$arr['data']['id']],
|
||||
'action' => 'delete',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/credits/bulk', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$invoice = $i->fresh();
|
||||
|
||||
$invoice->payments()->each(function ($payment) use ($credit){
|
||||
|
||||
$this->assertEquals($payment->amount, $payment->refunded);
|
||||
$this->assertEquals(\App\Models\Payment::STATUS_REFUNDED, $payment->status_id);
|
||||
$this->assertTrue($payment->paymentables()->where('paymentable_type', Credit::class)->where('paymentable_id', $credit->id)->exists());
|
||||
|
||||
});
|
||||
|
||||
$client = $invoice->fresh()->client->fresh();
|
||||
|
||||
$this->assertEquals(0, $client->credit_balance);
|
||||
$this->assertEquals(0, $client->paid_to_date);
|
||||
$this->assertEquals(0, $client->balance);
|
||||
|
||||
//restore the credit!!
|
||||
|
||||
$data = [
|
||||
'ids' => [$arr['data']['id']],
|
||||
'action' => 'restore',
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/credits/bulk', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$invoice = $i->fresh();
|
||||
|
||||
$invoice->payments()->each(function ($payment) use ($credit){
|
||||
|
||||
$this->assertEquals(0, $payment->refunded);
|
||||
$this->assertEquals(\App\Models\Payment::STATUS_COMPLETED, $payment->status_id);
|
||||
$this->assertFalse($payment->paymentables()->where('paymentable_type', Credit::class)->where('paymentable_id', $credit->id)->exists());
|
||||
|
||||
});
|
||||
|
||||
$client = $invoice->fresh()->client->fresh();
|
||||
|
||||
$this->assertEquals(50, $client->credit_balance);
|
||||
$this->assertEquals(0, $client->paid_to_date);
|
||||
$this->assertEquals(0, $client->balance);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testClientPaidToDateStateAfterCreditCreatedForPaidInvoice()
|
||||
{
|
||||
$c = Client::factory()->create([
|
||||
|
||||
@@ -33,6 +33,46 @@ class GroupSettingTest extends TestCase
|
||||
$this->makeTestData();
|
||||
}
|
||||
|
||||
|
||||
public function testPdfVariablesUnset()
|
||||
{
|
||||
$settings = new \stdClass();
|
||||
$settings->pdf_variables = 'xx';
|
||||
|
||||
$data = [
|
||||
'name' => 'testX',
|
||||
'settings' => $settings,
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->postJson('/api/v1/group_settings', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertArrayNotHasKey('pdf_variables', $arr['data']['settings']);
|
||||
|
||||
$data = [
|
||||
'name' => 'testX',
|
||||
'settings' => $settings,
|
||||
];
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
])->putJson('/api/v1/group_settings/'.$arr['data']['id'], $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
$this->assertArrayNotHasKey('pdf_variables', $arr['data']['settings']);
|
||||
|
||||
}
|
||||
|
||||
public function testCastingMagic()
|
||||
{
|
||||
|
||||
|
||||
@@ -424,7 +424,7 @@ class TemplateTest extends TestCase
|
||||
$invoices = Invoice::orderBy('id', 'desc')->where('client_id', $this->client->id)->take(10)->get()->map(function ($c) {
|
||||
return $c->service()->markSent()->applyNumber()->save();
|
||||
})->map(function ($i) {
|
||||
return ['invoice_id' => $i->hashed_id, 'amount' => rand(0, $i->balance)];
|
||||
return ['invoice_id' => $i->hashed_id, 'amount' => rand(1, $i->balance)];
|
||||
})->toArray();
|
||||
|
||||
Credit::factory()->count(2)->create([
|
||||
@@ -439,7 +439,7 @@ class TemplateTest extends TestCase
|
||||
$credits = Credit::orderBy('id', 'desc')->where('client_id', $this->client->id)->take(2)->get()->map(function ($c) {
|
||||
return $c->service()->markSent()->applyNumber()->save();
|
||||
})->map(function ($i) {
|
||||
return ['credit_id' => $i->hashed_id, 'amount' => rand(0, $i->balance)];
|
||||
return ['credit_id' => $i->hashed_id, 'amount' => rand(1, $i->balance)];
|
||||
})->toArray();
|
||||
|
||||
$data = [
|
||||
|
||||
Reference in New Issue
Block a user