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;