diff --git a/VERSION.txt b/VERSION.txt
index bf827dcc0b..bdbc6ff294 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-5.11.65
\ No newline at end of file
+5.11.66
\ No newline at end of file
diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php
index 304abf3839..be26557707 100644
--- a/app/Export/CSV/BaseExport.php
+++ b/app/Export/CSV/BaseExport.php
@@ -1323,7 +1323,7 @@ class BaseExport
$this->start_date = $fin_year_start->format('Y-m-d');
$this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d');
- return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC');
+ return $query->whereBetween($this->date_key, [$this->start_date, $this->end_date])->orderBy($this->date_key, 'ASC');
case 'last_year':
$first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1;
@@ -1336,7 +1336,7 @@ class BaseExport
$this->start_date = $fin_year_start->format('Y-m-d');
$this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d');
- return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC');
+ return $query->whereBetween($this->date_key, [$this->start_date, $this->end_date])->orderBy($this->date_key, 'ASC');
case 'custom':
$this->start_date = $custom_start_date->format('Y-m-d');
$this->end_date = $custom_end_date->format('Y-m-d');
diff --git a/app/Http/Controllers/ClientPortal/QuoteController.php b/app/Http/Controllers/ClientPortal/QuoteController.php
index 96df72da52..4d1a41787d 100644
--- a/app/Http/Controllers/ClientPortal/QuoteController.php
+++ b/app/Http/Controllers/ClientPortal/QuoteController.php
@@ -178,6 +178,7 @@ class QuoteController extends Controller
->where('client_id', auth()->guard('contact')->user()->client->id)
->where('company_id', auth()->guard('contact')->user()->client->company_id)
->whereIn('status_id', [Quote::STATUS_DRAFT, Quote::STATUS_SENT])
+ ->whereNull('invoice_id')
->where(function ($q) {
$q->whereNull('due_date')->orWhere('due_date', '>=', now());
})
diff --git a/app/Models/Presenters/CompanyPresenter.php b/app/Models/Presenters/CompanyPresenter.php
index 53b61e3bff..afc8959cd5 100644
--- a/app/Models/Presenters/CompanyPresenter.php
+++ b/app/Models/Presenters/CompanyPresenter.php
@@ -51,7 +51,8 @@ class CompanyPresenter extends EntityPresenter
$settings = $this->entity->settings;
}
- $basename = basename($this->settings->company_logo);
+ // $basename = basename($this->settings->company_logo);
+ $basename = basename($settings->company_logo);
$logo = Storage::get("{$this->company_key}/{$basename}");
diff --git a/app/Models/Task.php b/app/Models/Task.php
index fafa773f71..f8af3f0e2f 100644
--- a/app/Models/Task.php
+++ b/app/Models/Task.php
@@ -373,9 +373,6 @@ class Task extends BaseModel
$hours = ctrans('texts.hours');
$parts = [];
-
- // $parts[] = '
';
-
$date_time = [];
if ($this->company->invoice_task_datelog) {
@@ -409,10 +406,14 @@ class Task extends BaseModel
$parts[] = $interval_description;
}
- // $parts[] = '
';
+ //need to return early if there is nothing, otherwise we end up injecting a blank new line.
+ if(count($parts) == 1 && empty($parts[0])) {
+ return '';
+ }
return implode(PHP_EOL, $parts);
})
+ ->filter()//filters any empty strings.
->implode(PHP_EOL);
$body = '';
diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php
index 9bea6f6b27..b574a86576 100644
--- a/app/PaymentDrivers/Braintree/ACH.php
+++ b/app/PaymentDrivers/Braintree/ACH.php
@@ -140,6 +140,10 @@ class ACH implements MethodInterface, LivewireMethodInterface
->where('id', $this->decodePrimaryKey($request->source))
->firstOrFail();
+ $total_taxes = \App\Models\Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->braintree->payment_hash->invoices(), 'invoice_id')))->withTrashed()->sum('total_taxes');
+ $invoice = $this->braintree->payment_hash->fee_invoice;
+ $po_number = $invoice->po_number ?? $invoice->number ?? '';
+
$result = $this->braintree->gateway->transaction()->sale([
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
'paymentMethodToken' => $token->token,
@@ -147,6 +151,8 @@ class ACH implements MethodInterface, LivewireMethodInterface
'options' => [
'submitForSettlement' => true,
],
+ 'tax_amount' => $total_taxes,
+ 'purchase_order_number' => $po_number,
]);
if ($result->success) {
diff --git a/app/PaymentDrivers/Braintree/CreditCard.php b/app/PaymentDrivers/Braintree/CreditCard.php
index e61df63ca1..d3c50d11cf 100644
--- a/app/PaymentDrivers/Braintree/CreditCard.php
+++ b/app/PaymentDrivers/Braintree/CreditCard.php
@@ -12,19 +12,21 @@
namespace App\PaymentDrivers\Braintree;
-use App\Exceptions\PaymentFailed;
-use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
+use App\Models\Payment;
+use App\Models\SystemLog;
+use App\Models\GatewayType;
+use App\Models\PaymentType;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
-use App\Models\GatewayType;
-use App\Models\Payment;
-use App\Models\PaymentType;
-use App\Models\SystemLog;
+use App\Utils\Traits\MakesHash;
+use App\Exceptions\PaymentFailed;
use App\PaymentDrivers\BraintreePaymentDriver;
use App\PaymentDrivers\Common\LivewireMethodInterface;
+use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class CreditCard implements LivewireMethodInterface
{
+ use MakesHash;
/**
* @var BraintreePaymentDriver
*/
@@ -108,6 +110,10 @@ class CreditCard implements LivewireMethodInterface
$token = $this->getPaymentToken($request->all(), $customer->id);
+ $total_taxes = \App\Models\Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->braintree->payment_hash->invoices(), 'invoice_id')))->withTrashed()->sum('total_taxes');
+ $invoice = $this->braintree->payment_hash->fee_invoice;
+ $po_number = $invoice->po_number ?? $invoice->number ?? '';
+
$data = [
'amount' => $this->braintree->payment_hash->data->amount_with_fee, //@phpstan-ignore-line
'paymentMethodToken' => $token,
@@ -122,7 +128,9 @@ class CreditCard implements LivewireMethodInterface
'locality' => $this->braintree->client->city ?: '',
'postalCode' => $this->braintree->client->postal_code ?: '',
'countryCodeAlpha2' => $this->braintree->client->country ? $this->braintree->client->country->iso_3166_2 : 'US',
- ]
+ ],
+ 'tax_amount' => $total_taxes,
+ 'purchase_order_number' => $po_number,
];
if ($this->braintree->company_gateway->getConfigField('merchantAccountId')) {
diff --git a/app/PaymentDrivers/Braintree/PayPal.php b/app/PaymentDrivers/Braintree/PayPal.php
index 724e8578bc..ecac7c1b89 100644
--- a/app/PaymentDrivers/Braintree/PayPal.php
+++ b/app/PaymentDrivers/Braintree/PayPal.php
@@ -11,9 +11,11 @@ use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\BraintreePaymentDriver;
use App\PaymentDrivers\Common\LivewireMethodInterface;
+use App\Utils\Traits\MakesHash;
class PayPal implements LivewireMethodInterface
{
+ use MakesHash;
/**
* @var BraintreePaymentDriver
*/
@@ -68,6 +70,10 @@ class PayPal implements LivewireMethodInterface
$token = $this->getPaymentToken($request->all(), $customer->id);
+ $total_taxes = \App\Models\Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->braintree->payment_hash->invoices(), 'invoice_id')))->withTrashed()->sum('total_taxes');
+ $invoice = $this->braintree->payment_hash->fee_invoice;
+ $po_number = $invoice->po_number ?? $invoice->number ?? '';
+
$result = $this->braintree->gateway->transaction()->sale([
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
'paymentMethodToken' => $token,
@@ -79,6 +85,8 @@ class PayPal implements LivewireMethodInterface
'description' => 'Meaningful description.',
],
],
+ 'tax_amount' => $total_taxes,
+ 'purchase_order_number' => $po_number,
]);
if ($result->success) {
diff --git a/app/PaymentDrivers/BraintreePaymentDriver.php b/app/PaymentDrivers/BraintreePaymentDriver.php
index 87b89c9cd1..10acf28607 100644
--- a/app/PaymentDrivers/BraintreePaymentDriver.php
+++ b/app/PaymentDrivers/BraintreePaymentDriver.php
@@ -222,6 +222,7 @@ class BraintreePaymentDriver extends BaseDriver
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$invoice = Invoice::query()->whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
+ $total_taxes = Invoice::query()->whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->sum('total_taxes');
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->client->present()->name()}";
@@ -235,9 +236,12 @@ class BraintreePaymentDriver extends BaseDriver
'amount' => $amount,
'paymentMethodToken' => $cgt->token,
'deviceData' => '',
+ 'channel' => 'invoiceninja_BT',
'options' => [
'submitForSettlement' => true,
],
+ 'tax_amount' => $total_taxes,
+ 'purchase_order_number' => $invoice->po_number ?? $invoice->number,
]);
if ($result->success) {
diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php
index 30c6214851..b89c9d60e8 100644
--- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php
+++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php
@@ -142,8 +142,6 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
if ($r->status() == 422) {
- //handle conditions where the client may need to try again.
- // return $this->handleRetry($r, $request);
$r = $this->handleDuplicateInvoiceId($orderID);
@@ -161,7 +159,12 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$response = $r;
nlog("Process response =>");
- nlog($response->json());
+
+ if(method_exists($response, 'json')) {
+ nlog($response->json());
+ } else {
+ nlog($response);
+ }
if (isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) {
diff --git a/app/Repositories/TaskRepository.php b/app/Repositories/TaskRepository.php
index 8ac69d8d27..f497d7f0ca 100644
--- a/app/Repositories/TaskRepository.php
+++ b/app/Repositories/TaskRepository.php
@@ -109,7 +109,9 @@ class TaskRepository extends BaseRepository
$data['time_log'] = json_encode($timeLog);
}
- if (isset($data['time_log'])) {
+ if(isset($data['time_log']) && is_array($data['time_log'])) {
+ $time_log = $data['time_log'];
+ } elseif (isset($data['time_log'])) {
$time_log = json_decode($data['time_log']);
} elseif ($task->time_log) {
$time_log = json_decode($task->time_log);
diff --git a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php
index 08333c46ef..389783979c 100644
--- a/app/Services/Quickbooks/Transformers/InvoiceTransformer.php
+++ b/app/Services/Quickbooks/Transformers/InvoiceTransformer.php
@@ -198,7 +198,7 @@ class InvoiceTransformer extends BaseTransformer
$item->discount = (float)data_get($item, 'DiscountRate', data_get($qb_item, 'DiscountAmount', 0));
$item->is_amount_discount = data_get($qb_item, 'DiscountAmount', 0) > 0 ? true : false;
$item->type_id = stripos(data_get($qb_item, 'ItemAccountRef.name') ?? '', 'Service') !== false ? '2' : '1';
- $item->tax_id = $taxCodeRef == 'NON' ? Product::PRODUCT_TYPE_EXEMPT : $item->type_id;
+ $item->tax_id = $taxCodeRef == 'NON' ? (string)Product::PRODUCT_TYPE_EXEMPT : $item->type_id;
$item->tax_rate1 = (float)$taxCodeRef == 'NON' ? 0 : $tax_array[0];
$item->tax_name1 = $taxCodeRef == 'NON' ? '' : $tax_array[1];
@@ -219,7 +219,7 @@ class InvoiceTransformer extends BaseTransformer
$item->tax_name1 = $include_discount == 'true' ? $tax_array[1] : '';
$item->type_id = '1';
- $item->tax_id = Product::PRODUCT_TYPE_PHYSICAL;
+ $item->tax_id = (string)Product::PRODUCT_TYPE_PHYSICAL;
$items[] = (object)$item;
}
diff --git a/app/Services/Report/TaxSummaryReport.php b/app/Services/Report/TaxSummaryReport.php
index cd720e505e..a4c39d5bdf 100644
--- a/app/Services/Report/TaxSummaryReport.php
+++ b/app/Services/Report/TaxSummaryReport.php
@@ -103,6 +103,11 @@ class TaxSummaryReport extends BaseExport
$accrual_invoice_map = [];
$cash_invoice_map = [];
+ // Initialize cash variables
+ $cash_gross_sales = 0;
+ $cash_taxable_sales = 0;
+ $cash_exempt_sales = 0;
+
$gross_sales = round($query->sum('amount'), 2);
$taxable_sales = round($query->where('total_taxes', '>', 0)->sum('amount'), 2);
$exempt_sales = round(($gross_sales - $taxable_sales), 2);
@@ -145,10 +150,6 @@ class TaxSummaryReport extends BaseExport
$cash_map[$key]['tax_amount'] = 0;
}
- $cash_gross_sales = 0;
- $cash_taxable_sales = 0;
- $cash_exempt_sales = 0;
-
if (in_array($invoice->status_id, [Invoice::STATUS_PARTIAL,Invoice::STATUS_PAID])) {
try {
diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php
index 633cdc383a..72279397b4 100644
--- a/app/Services/Template/TemplateService.php
+++ b/app/Services/Template/TemplateService.php
@@ -149,7 +149,7 @@ class TemplateService
$allowedTags = ['if', 'for', 'set', 'filter'];
- $allowedFilters = ['capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html'];
+ $allowedFilters = ['capitalize', 'abs', 'date_modify', 'keys', 'join', 'reduce', 'format_date','json_decode','date_modify','trim','round','format_spellout_number','split', 'reduce','replace', 'escape', 'e', 'reverse', 'shuffle', 'slice', 'batch', 'title', 'sort', 'split', 'upper', 'lower', 'capitalize', 'filter', 'length', 'merge','format_currency', 'format_number','format_percent_number','map', 'join', 'first', 'date', 'sum', 'number_format','nl2br','striptags','markdown_to_html'];
$allowedFunctions = ['range', 'cycle', 'constant', 'date','img','t'];
$allowedProperties = ['type_id'];
// $allowedMethods = ['img','t'];
diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php
index b7d362c477..94c39a3000 100644
--- a/app/Utils/HtmlEngine.php
+++ b/app/Utils/HtmlEngine.php
@@ -585,7 +585,6 @@ class HtmlEngine
$logo_url = $this->company->present()->logo($this->settings);
-
$data['$company.logo'] = ['value' => $logo ?: ' ', 'label' => ctrans('texts.logo')];
$data['$company_logo'] = &$data['$company.logo'];
diff --git a/config/ninja.php b/config/ninja.php
index 6794fbbdd7..1c4d09b39d 100644
--- a/config/ninja.php
+++ b/config/ninja.php
@@ -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.11.65'),
- 'app_tag' => env('APP_TAG', '5.11.65'),
+ 'app_version' => env('APP_VERSION', '5.11.66'),
+ 'app_tag' => env('APP_TAG', '5.11.66'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
diff --git a/tests/Feature/ReminderTest.php b/tests/Feature/ReminderTest.php
index 0a4338c223..1f3704635f 100644
--- a/tests/Feature/ReminderTest.php
+++ b/tests/Feature/ReminderTest.php
@@ -159,6 +159,83 @@ class ReminderTest extends TestCase
}
+ public function testReminderLogic()
+ {
+
+ $account = Account::factory()->create([
+ 'hosted_client_count' => 1000,
+ 'hosted_company_count' => 1000,
+ ]);
+
+ $account->num_users = 3;
+ $account->save();
+
+ $user = User::factory()->create([
+ 'account_id' => $this->account->id,
+ 'confirmation_code' => 'xyz123',
+ 'email' => $this->faker->unique()->safeEmail(),
+ ]);
+
+ $settings = CompanySettings::defaults();
+ $settings->client_online_payment_notification = false;
+ $settings->client_manual_payment_notification = false;
+ $settings->send_reminders = true;
+ $settings->enable_reminder1 = true;
+ $settings->enable_reminder2 = true;
+ $settings->enable_reminder3 = true;
+ $settings->enable_reminder_endless = false;
+ $settings->num_days_reminder1 = 1;
+ $settings->num_days_reminder2 = 14;
+ $settings->num_days_reminder3 = 21;
+ $settings->schedule_reminder1 = 'after_due_date';
+ $settings->schedule_reminder2 = 'after_due_date';
+ $settings->schedule_reminder3 = 'after_due_date';
+ $settings->reminder_send_time = 0;
+ $settings->late_fee_amount1 = 0;
+ $settings->late_fee_amount2 = 49;
+ $settings->late_fee_amount3 = 0;
+ $settings->late_fee_percent1 = 0;
+ $settings->late_fee_percent2 = 0;
+ $settings->late_fee_percent3 = 1.01;
+ $settings->endless_reminder_frequency_id = '0';
+
+ $company = Company::factory()->create([
+ 'account_id' => $this->account->id,
+ 'settings' => $settings,
+ ]);
+
+ $company->settings = $settings;
+ $company->save();
+
+ $cu = CompanyUserFactory::create($user->id, $company->id, $account->id);
+ $cu->is_owner = true;
+ $cu->is_admin = true;
+ $cu->is_locked = false;
+ $cu->save();
+
+ $token = \Illuminate\Support\Str::random(64);
+
+ $c_settings = \App\DataMapper\ClientSettings::defaults();
+ $c_settings->send_reminders = false;
+ $c_settings->currency_id = '1';
+
+ $client = Client::factory()->create([
+ 'user_id' => $user->id,
+ 'company_id' => $company->id,
+ 'is_deleted' => 0,
+ 'name' => 'bob',
+ 'address1' => '1234',
+ 'balance' => 100,
+ 'paid_to_date' => 50,
+ 'settings' => $c_settings,
+ ]);
+
+ $this->assertTrue($client->getSetting('enable_reminder1'));
+ $this->assertFalse($client->getSetting('send_reminders'));
+
+ $account->delete();
+ }
+
public function testReminderScheduleNy()
{
diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php
index e0923497e2..eb4171f5f9 100644
--- a/tests/Feature/TaskApiTest.php
+++ b/tests/Feature/TaskApiTest.php
@@ -104,6 +104,36 @@ class TaskApiTest extends TestCase
}
}
+ public function testPutTaskTimeLog()
+ {
+
+ $task = Task::factory()->create([
+ 'client_id' => $this->client->id,
+ 'user_id' => $this->user->id,
+ 'company_id' => $this->company->id,
+ 'description' => 'Test Task',
+ ]);
+
+ $data = [
+ 'time_log' => [
+ [
+ 1744546939,
+ 1744552309,
+ "Stijlelementen verfijnd en visuele consistentie verbeterd binnen het Platform Alain concept.",
+ true
+ ]
+ ]
+ ];
+
+ $response = $this->withHeaders([
+ 'X-API-SECRET' => config('ninja.api_secret'),
+ 'X-API-TOKEN' => $this->token,
+ ])->putJson("/api/v1/tasks/{$task->hashed_id}", $data);
+
+ $response->assertStatus(200);
+
+ }
+
public function testTaskLogGenerationforInvoices()
{
$this->company->invoice_task_datelog = true;