diff --git a/VERSION.txt b/VERSION.txt index 25285daaef52..71c2c6e276ca 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.30 \ No newline at end of file +5.7.33 \ No newline at end of file diff --git a/app/DataMapper/Settings/SettingsData.php b/app/DataMapper/Settings/SettingsData.php new file mode 100644 index 000000000000..b76379806ff6 --- /dev/null +++ b/app/DataMapper/Settings/SettingsData.php @@ -0,0 +1,515 @@ + $value) { + + try{ + settype($object->{$key}, gettype($this->{$key})); + } + catch(\Exception | \Error | \Throwable $e){ + + if(property_exists($this, $key)) + $object->{$key} = $this->{$key}; + else + unset($object->{$key}); + + } + + // if(!property_exists($this, $key)) { + // unset($object->{$key}); + // } + // elseif(is_array($object->{$key}) && gettype($this->{$key} != 'array')){ + // $object->{$key} = $this->{$key}; + // } + // else { + // settype($object->{$key}, gettype($this->{$key})); + // } + } + } + $this->object = $object; + + return $this; + } + + public function toObject(): object + { + return (object)$this->object; + } + + public function toArray(): array + { + return (array)$this->object; + } +} \ No newline at end of file diff --git a/app/DataMapper/Tax/AU/Rule.php b/app/DataMapper/Tax/AU/Rule.php index d6536e1a7177..c1d9e7ed218e 100644 --- a/app/DataMapper/Tax/AU/Rule.php +++ b/app/DataMapper/Tax/AU/Rule.php @@ -62,7 +62,7 @@ class Rule extends BaseRule implements RuleInterface public function taxByType($item): self { - if ($this->client->is_tax_exempt) { + if ($this->client->is_tax_exempt || !property_exists($item, 'tax_id')) { return $this->taxExempt($item); } diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 36635429a6d7..ba8169734547 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -297,7 +297,7 @@ class BaseRule implements RuleInterface public function tax($item = null): self { - if ($this->client->is_tax_exempt) { + if ($this->client->is_tax_exempt || !property_exists($item, 'tax_id')) { return $this->taxExempt($item); diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index c82f701496f4..8214188c369f 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -63,7 +63,7 @@ class Rule extends BaseRule implements RuleInterface public function taxByType($item): self { - if ($this->client->is_tax_exempt) { + if ($this->client->is_tax_exempt || !property_exists($item, 'tax_id')) { return $this->taxExempt($item); } diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index e0344792895c..fa6ab80ef33f 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -88,6 +88,7 @@ class BaseExport protected array $client_report_keys = [ "name" => "client.name", + "number" => "client.number", "user" => "client.user", "assigned_user" => "client.assigned_user", "balance" => "client.balance", @@ -168,6 +169,7 @@ class BaseExport 'tax_rate1' => 'invoice.tax_rate1', 'tax_rate2' => 'invoice.tax_rate2', 'tax_rate3' => 'invoice.tax_rate3', + 'recurring_invoice' => 'invoice.recurring_id', ]; protected array $recurring_invoice_report_keys = [ @@ -230,7 +232,7 @@ class BaseExport 'po_number' => 'purchase_order.po_number', 'private_notes' => 'purchase_order.private_notes', 'public_notes' => 'purchase_order.public_notes', - 'status' => 'purchase_order.status_id', + 'status' => 'purchase_order.status', 'tax_name1' => 'purchase_order.tax_name1', 'tax_name2' => 'purchase_order.tax_name2', 'tax_name3' => 'purchase_order.tax_name3', @@ -377,6 +379,7 @@ class BaseExport "custom_value4" => "payment.custom_value4", "user" => "payment.user_id", "assigned_user" => "payment.assigned_user_id", + ]; protected array $expense_report_keys = [ @@ -429,6 +432,14 @@ class BaseExport 'project' => 'task.project_id', ]; + protected array $forced_client_fields = [ + "client.name", + ]; + + protected array $forced_vendor_fields = [ + "vendor.name", + ]; + protected function filterByClients($query) { if (isset($this->input['client_id']) && $this->input['client_id'] != 'all') { @@ -1144,9 +1155,9 @@ class BaseExport $clean_row[$key]['entity'] = $report_keys[0]; $clean_row[$key]['id'] = $report_keys[1] ?? $report_keys[0]; $clean_row[$key]['hashed_id'] = $report_keys[0] == $entity ? null : $resource->{$report_keys[0]}->hashed_id ?? null; - $clean_row[$key]['value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$report_keys[1]]; + $clean_row[$key]['value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$value]; $clean_row[$key]['identifier'] = $value; - $clean_row[$key]['display_value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$report_keys[1]]; + $clean_row[$key]['display_value'] = isset($row[$column_key]) ? $row[$column_key] : $row[$value]; } diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index e99434b19863..16f9ca154d70 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -189,7 +189,7 @@ class ClientExport extends BaseExport $clean_row[$key]['id'] = $report_keys[1] ?? $report_keys[0]; $clean_row[$key]['hashed_id'] = $report_keys[0] == 'client' ? null : $resource->{$report_keys[0]}->hashed_id ?? null; $clean_row[$key]['value'] = $row[$column_key]; - $clean_row[$key]['identifier'] = $key; + $clean_row[$key]['identifier'] = $value; if(in_array($clean_row[$key]['id'], ['paid_to_date', 'balance', 'credit_balance','payment_balance'])) $clean_row[$key]['display_value'] = Number::formatMoney($row[$column_key], $resource); diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 29afbfa16100..c5c05f3c23e2 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -93,6 +93,8 @@ class CreditExport extends BaseExport $this->input['report_keys'] = array_values($this->credit_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Credit::query() ->withTrashed() ->with('client') diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 0a8f3188f2c3..4dc4ee3d8b94 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -50,6 +50,8 @@ class InvoiceExport extends BaseExport $this->input['report_keys'] = array_values($this->invoice_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Invoice::query() ->withTrashed() ->with('client') @@ -142,6 +144,11 @@ class InvoiceExport extends BaseExport if (in_array('invoice.status', $this->input['report_keys'])) { $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); } + + if (in_array('invoice.recurring_id', $this->input['report_keys'])) { + $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; + } + return $entity; } diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 74c1a64c25b0..ba2dc00da4f5 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -62,6 +62,8 @@ class InvoiceItemExport extends BaseExport $this->input['report_keys'] = array_values($this->mergeItemsKeys('invoice_report_keys')); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Invoice::query() ->withTrashed() ->with('client') @@ -135,16 +137,16 @@ class InvoiceItemExport extends BaseExport if (str_contains($key, "item.")) { - $key = str_replace("item.", "", $key); + $tmp_key = str_replace("item.", "", $key); - if($key == 'type_id') - $key = 'type'; + if($tmp_key == 'type_id') + $tmp_key = 'type'; - if($key == 'tax_id') - $key = 'tax_category'; + if($tmp_key == 'tax_id') + $tmp_key = 'tax_category'; - if (property_exists($item, $key)) { - $item_array[$key] = $item->{$key}; + if (property_exists($item, $tmp_key)) { + $item_array[$key] = $item->{$tmp_key}; } else { $item_array[$key] = ''; @@ -154,6 +156,8 @@ class InvoiceItemExport extends BaseExport $transformed_items = array_merge($transformed_invoice, $item_array); $entity = $this->decorateAdvancedFields($invoice, $transformed_items); + + $entity = array_merge(array_flip(array_values($this->input['report_keys'])), $entity); $this->storage_array[] = $entity; @@ -200,6 +204,27 @@ class InvoiceItemExport extends BaseExport $entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']); } + if (in_array('invoice.country_id', $this->input['report_keys'])) { + $entity['invoice.country_id'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ''; + } + + if (in_array('invoice.currency_id', $this->input['report_keys'])) { + $entity['invoice.currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; + } + + if (in_array('invoice.client_id', $this->input['report_keys'])) { + $entity['invoice.client_id'] = $invoice->client->present()->name(); + } + + if (in_array('invoice.status', $this->input['report_keys'])) { + $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); + } + + if (in_array('invoice.recurring_id', $this->input['report_keys'])) { + $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; + } + + return $entity; } diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index 01c8641b0725..7f77563f9b92 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -48,6 +48,8 @@ class PaymentExport extends BaseExport $this->input['report_keys'] = array_values($this->payment_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Payment::query() ->withTrashed() ->where('company_id', $this->company->id) @@ -69,6 +71,8 @@ class PaymentExport extends BaseExport return ['identifier' => $key, 'display_value' => $headerdisplay[$value]]; })->toArray(); + nlog($header); + $report = $query->cursor() ->map(function ($resource) { $row = $this->buildRow($resource); diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php index 88145822b10a..45e8a6adb1b7 100644 --- a/app/Export/CSV/PurchaseOrderExport.php +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -54,7 +54,7 @@ class PurchaseOrderExport extends BaseExport 'po_number' => 'purchase_order.po_number', 'private_notes' => 'purchase_order.private_notes', 'public_notes' => 'purchase_order.public_notes', - 'status' => 'purchase_order.status_id', + 'status' => 'purchase_order.status', 'tax_name1' => 'purchase_order.tax_name1', 'tax_name2' => 'purchase_order.tax_name2', 'tax_name3' => 'purchase_order.tax_name3', @@ -95,6 +95,9 @@ class PurchaseOrderExport extends BaseExport if (count($this->input['report_keys']) == 0) { $this->input['report_keys'] = array_values($this->purchase_order_report_keys); } + + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_vendor_fields, $this->input['report_keys'])); + $query = PurchaseOrder::query() ->withTrashed() ->with('vendor') @@ -181,8 +184,8 @@ class PurchaseOrderExport extends BaseExport $entity['vendor'] = $purchase_order->vendor->present()->name(); } - if (in_array('status_id', $this->input['report_keys'])) { - $entity['status'] = $purchase_order->stringStatus($purchase_order->status_id); + if (in_array('purchase_order.status', $this->input['report_keys'])) { + $entity['purchase_order.status'] = $purchase_order->stringStatus($purchase_order->status_id); } return $entity; diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 9ebf2991c95b..3738ec5b6dd0 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -55,6 +55,8 @@ class PurchaseOrderItemExport extends BaseExport $this->input['report_keys'] = array_values($this->mergeItemsKeys('purchase_order_report_keys')); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_vendor_fields, $this->input['report_keys'])); + $query = PurchaseOrder::query() ->withTrashed() ->with('vendor')->where('company_id', $this->company->id) @@ -125,18 +127,18 @@ class PurchaseOrderItemExport extends BaseExport if (str_contains($key, "item.")) { - $key = str_replace("item.", "", $key); + $tmp_key = str_replace("item.", "", $key); - if($key == 'type_id') { - $keyval = 'type'; + if($tmp_key == 'type_id') { + $tmp_key = 'type'; } - if($key == 'tax_id') { - $keyval = 'tax_category'; + if($tmp_key == 'tax_id') { + $tmp_key = 'tax_category'; } - if (property_exists($item, $key)) { - $item_array[$key] = $item->{$key}; + if (property_exists($item, $tmp_key)) { + $item_array[$key] = $item->{$tmp_key}; } else { $item_array[$key] = ''; } @@ -145,6 +147,7 @@ class PurchaseOrderItemExport extends BaseExport $transformed_items = array_merge($transformed_purchase_order, $item_array); $entity = $this->decorateAdvancedFields($purchase_order, $transformed_items); + $entity = array_merge(array_flip(array_values($this->input['report_keys'])), $entity); $this->storage_array[] = $entity; } diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index 8083c0a71e7a..eba6adbcef26 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -56,6 +56,8 @@ class QuoteExport extends BaseExport $this->input['report_keys'] = array_values($this->quote_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Quote::query() ->withTrashed() ->with('client') diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index f85a82dd4c12..0db08629c664 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -57,6 +57,8 @@ class QuoteItemExport extends BaseExport $this->input['report_keys'] = array_values($this->mergeItemsKeys('quote_report_keys')); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Quote::query() ->withTrashed() ->with('client')->where('company_id', $this->company->id) @@ -131,16 +133,16 @@ class QuoteItemExport extends BaseExport if (str_contains($key, "item.")) { - $key = str_replace("item.", "", $key); + $tmp_key = str_replace("item.", "", $key); - if($key == 'type_id') - $key = 'type'; + if($tmp_key == 'type_id') + $tmp_key = 'type'; - if($key == 'tax_id') - $key = 'tax_category'; + if($tmp_key == 'tax_id') + $tmp_key = 'tax_category'; - if (property_exists($item, $key)) { - $item_array[$key] = $item->{$key}; + if (property_exists($item, $tmp_key)) { + $item_array[$key] = $item->{$tmp_key}; } else { $item_array[$key] = ''; @@ -150,6 +152,7 @@ class QuoteItemExport extends BaseExport $transformed_items = array_merge($transformed_quote, $item_array); $entity = $this->decorateAdvancedFields($quote, $transformed_items); + $entity = array_merge(array_flip(array_values($this->input['report_keys'])), $entity); $this->storage_array[] = $entity; } diff --git a/app/Export/CSV/RecurringInvoiceExport.php b/app/Export/CSV/RecurringInvoiceExport.php index d30359510c80..3234b0674337 100644 --- a/app/Export/CSV/RecurringInvoiceExport.php +++ b/app/Export/CSV/RecurringInvoiceExport.php @@ -48,6 +48,8 @@ class RecurringInvoiceExport extends BaseExport $this->input['report_keys'] = array_values($this->recurring_invoice_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = RecurringInvoice::query() ->withTrashed() ->with('client') @@ -135,8 +137,8 @@ class RecurringInvoiceExport extends BaseExport $entity['client'] = $invoice->client->present()->name(); } - if (in_array('status_id', $this->input['report_keys'])) { - $entity['status'] = $invoice->stringStatus($invoice->status_id); + if (in_array('recurring_invoice.status', $this->input['report_keys'])) { + $entity['recurring_invoice.status'] = $invoice->stringStatus($invoice->status_id); } if (in_array('project_id', $this->input['report_keys'])) { diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 87834c6aee4f..b2eb6425c527 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -60,6 +60,8 @@ class TaskExport extends BaseExport $this->input['report_keys'] = array_values($this->task_report_keys); } + $this->input['report_keys'] = array_merge($this->input['report_keys'], array_diff($this->forced_client_fields, $this->input['report_keys'])); + $query = Task::query() ->withTrashed() ->where('company_id', $this->company->id) diff --git a/app/Factory/UserFactory.php b/app/Factory/UserFactory.php index d31a0019b2c9..9b15037f625c 100644 --- a/app/Factory/UserFactory.php +++ b/app/Factory/UserFactory.php @@ -27,7 +27,8 @@ class UserFactory $user->last_login = now(); $user->failed_logins = 0; $user->signature = ''; - $user->theme_id = 0; + $user->theme_id = 0; + $user->user_logged_in_notification = true; return $user; } diff --git a/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php b/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php index 19ff1974c64d..ceb1417c7530 100644 --- a/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php +++ b/app/Helpers/Bank/Yodlee/Transformer/AccountTransformer.php @@ -140,7 +140,7 @@ class AccountTransformer implements AccountTransformerInterface 'id' => $account->id, 'account_type' => $account->CONTAINER, // 'account_name' => $account->accountName, - 'account_name' => property_exists($account, 'accountName') ? $account->accountName : $account->nickname, + 'account_name' => property_exists($account, 'accountName') ? $account->accountName : ($account->nickname ?? 'Unknown Account'), 'account_status' => $account_status, 'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '', 'provider_account_id' => $account->providerAccountId, diff --git a/app/Helpers/Epc/EpcQrGenerator.php b/app/Helpers/Epc/EpcQrGenerator.php index 62d61a0a15bb..965e2dad1f13 100644 --- a/app/Helpers/Epc/EpcQrGenerator.php +++ b/app/Helpers/Epc/EpcQrGenerator.php @@ -15,6 +15,7 @@ use App\Models\Company; use App\Models\Invoice; use App\Models\RecurringInvoice; use App\Utils\Ninja; +use BaconQrCode\Exception\InvalidArgumentException; use BaconQrCode\Renderer\Image\SvgImageBackEnd; use BaconQrCode\Renderer\ImageRenderer; use BaconQrCode\Renderer\RendererStyle\RendererStyle; @@ -58,10 +59,15 @@ class EpcQrGenerator {$qr}"; } catch(\Throwable $e) { + nlog("EPC QR failure => ".$e->getMessage()); return ''; } catch(\Exception $e) { + nlog("EPC QR failure => ".$e->getMessage()); return ''; - } + } catch( InvalidArgumentException $e) { + nlog("EPC QR failure => ".$e->getMessage()); + return ''; + } } @@ -93,6 +99,7 @@ class EpcQrGenerator if (Ninja::isSelfHost() && isset($this->company?->custom_fields?->company1)) { nlog('The IBAN field is required'); } + } private function formatMoney($value) diff --git a/app/Helpers/SwissQr/SwissQrGenerator.php b/app/Helpers/SwissQr/SwissQrGenerator.php index abacbb57c056..d8c1ad77304c 100644 --- a/app/Helpers/SwissQr/SwissQrGenerator.php +++ b/app/Helpers/SwissQr/SwissQrGenerator.php @@ -174,9 +174,14 @@ class SwissQrGenerator return $html; } catch (\Exception $e) { - foreach ($qrBill->getViolations() as $key => $violation) { - nlog("qr"); - nlog($violation); + + if(is_iterable($qrBill->getViolations())) { + + foreach ($qrBill->getViolations() as $key => $violation) { + nlog("qr"); + nlog($violation); + } + } nlog($e->getMessage()); diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 7b305b09ff65..0bb9508f7646 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -308,7 +308,8 @@ class ClientController extends BaseController */ public function updateTaxData(PurgeClientRequest $request, Client $client) { - (new UpdateTaxData($client, $client->company))->handle(); + if($client->company->account->isPaid()) + (new UpdateTaxData($client, $client->company))->handle(); return $this->itemResponse($client->fresh()); } diff --git a/app/Http/Controllers/EmailHistoryController.php b/app/Http/Controllers/EmailHistoryController.php index 4da69ba7b6dc..f29f1a79bd9d 100644 --- a/app/Http/Controllers/EmailHistoryController.php +++ b/app/Http/Controllers/EmailHistoryController.php @@ -31,11 +31,11 @@ class EmailHistoryController extends BaseController ->where('category_id', SystemLog::CATEGORY_MAIL) ->orderBy('id', 'DESC') ->cursor() - ->map(function ($system_log) { - if(($system_log->log['history'] && $system_log->log['history']['events'] && count($system_log->log['history']['events']) >=1) ?? false) { - return $system_log->log['history']; - } - }); + ->filter(function ($system_log) { + return (isset($system_log->log['history']) && isset($system_log->log['history']['events']) && count($system_log->log['history']['events']) >=1) !== false; + })->map(function ($system_log) { + return $system_log->log['history']; + })->values()->all(); return response()->json($data, 200); @@ -51,16 +51,17 @@ class EmailHistoryController extends BaseController /** @var \App\Models\User $user */ $user = auth()->user(); + $data = SystemLog::where('company_id', $user->company()->id) - ->where('category_id', SystemLog::CATEGORY_MAIL) - ->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id)) - ->orderBy('id', 'DESC') - ->cursor() - ->map(function ($system_log) { - if(($system_log->log['history'] && $system_log->log['history']['events'] && count($system_log->log['history']['events']) >=1) ?? false) { - return $system_log->log['history']; - } - }); + ->where('category_id', SystemLog::CATEGORY_MAIL) + ->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id)) + ->orderBy('id', 'DESC') + ->cursor() + ->filter(function ($system_log) { + return ($system_log->log['history'] && isset($system_log->log['history']['events']) && count($system_log->log['history']['events']) >=1) !== false; + })->map(function ($system_log) { + return $system_log->log['history']; + })->values()->all(); return response()->json($data, 200); diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 0f94c8e9ac1a..8b5d499d8de8 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -11,42 +11,43 @@ namespace App\Http\Controllers; -use App\DataMapper\Analytics\LivePreview; -use App\Factory\CreditFactory; -use App\Factory\InvoiceFactory; -use App\Factory\QuoteFactory; -use App\Factory\RecurringInvoiceFactory; -use App\Http\Requests\Preview\DesignPreviewRequest; -use App\Http\Requests\Preview\PreviewInvoiceRequest; -use App\Jobs\Util\PreviewPdf; -use App\Libraries\MultiDB; +use App\Utils\Ninja; +use App\Models\Quote; use App\Models\Client; -use App\Models\ClientContact; use App\Models\Credit; use App\Models\Invoice; -use App\Models\InvoiceInvitation; -use App\Models\Quote; -use App\Models\RecurringInvoice; -use App\Repositories\CreditRepository; -use App\Repositories\InvoiceRepository; -use App\Repositories\QuoteRepository; -use App\Repositories\RecurringInvoiceRepository; +use App\Utils\HtmlEngine; +use App\Libraries\MultiDB; +use App\Factory\QuoteFactory; +use App\Jobs\Util\PreviewPdf; +use App\Models\ClientContact; use App\Services\Pdf\PdfMock; +use App\Factory\CreditFactory; +use App\Factory\InvoiceFactory; +use App\Utils\Traits\MakesHash; +use App\Models\RecurringInvoice; +use App\Utils\PhantomJS\Phantom; +use App\Models\InvoiceInvitation; use App\Services\PdfMaker\Design; +use App\Utils\HostedPDF\NinjaPdf; +use Illuminate\Support\Facades\DB; +use App\Services\PdfMaker\PdfMaker; +use Illuminate\Support\Facades\App; +use App\Repositories\QuoteRepository; +use Illuminate\Support\Facades\Cache; +use App\Repositories\CreditRepository; +use App\Utils\Traits\MakesInvoiceHtml; +use Turbo124\Beacon\Facades\LightLogs; +use App\Repositories\InvoiceRepository; +use App\Utils\Traits\Pdf\PageNumbering; +use App\Factory\RecurringInvoiceFactory; +use Illuminate\Support\Facades\Response; +use App\DataMapper\Analytics\LivePreview; +use App\Repositories\RecurringInvoiceRepository; +use App\Http\Requests\Preview\DesignPreviewRequest; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; -use App\Services\PdfMaker\PdfMaker; -use App\Utils\HostedPDF\NinjaPdf; -use App\Utils\HtmlEngine; -use App\Utils\Ninja; -use App\Utils\PhantomJS\Phantom; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\MakesInvoiceHtml; -use App\Utils\Traits\Pdf\PageNumbering; -use Illuminate\Support\Facades\App; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Response; -use Turbo124\Beacon\Facades\LightLogs; +use App\Http\Requests\Preview\PreviewInvoiceRequest; class PreviewController extends BaseController { @@ -56,15 +57,179 @@ class PreviewController extends BaseController public function __construct() { - parent::__construct(); + parent::__construct(); + } + + private function purgeCache() + { + Cache::pull("preview_".auth()->user()->id); + } + + /** + * Refactor - 2023-10-19 + * + * New method does not require Transactions. + * + * @param PreviewInvoiceRequest $request + * @return mixed + */ + public function live(PreviewInvoiceRequest $request): mixed + { + + if (Ninja::isHosted() && !in_array($request->getHost(), ['preview.invoicing.co','staging.invoicing.co'])) { + return response()->json(['message' => 'This server cannot handle this request.'], 400); + } + + $start = microtime(true); + + /** Build models */ + $invitation = $request->resolveInvitation(); + $client = $request->getClient(); + $settings = $client->getMergedSettings(); + + /** Set translations */ + App::forgetInstance('translator'); + $t = app('translator'); + App::setLocale($invitation->contact->preferredLocale()); + $t->replace(Ninja::transformTranslations($settings)); + + $entity_prop = str_replace("recurring_", "", $request->entity); + $entity_obj = $invitation->{$request->entity}; + $entity_obj->fill($request->all()); + + /** Update necessary objecty props */ + if(!$entity_obj->id) { + $entity_obj->design_id = intval($this->decodePrimaryKey($settings->{$entity_prop."_design_id"})); + $entity_obj->footer = $settings->{$entity_prop."_footer"}; + $entity_obj->terms = $settings->{$entity_prop."_terms"}; + $entity_obj->public_notes = $request->getClient()->public_notes; + $invitation->{$request->entity} = $entity_obj; + } + + /** Generate variables */ + $html = new HtmlEngine($invitation); + $html->settings = $settings; + $variables = $html->generateLabelsAndValues(); + + + $design = \App\Models\Design::query()->withTrashed()->find($entity_obj->design_id ?? 2); + + /* Catch all in case migration doesn't pass back a valid design */ + if (! $design) { + $design = \App\Models\Design::query()->find(2); + } + + if ($design->is_custom) { + $options = [ + 'custom_partials' => json_decode(json_encode($design->design), true), + ]; + $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); + } else { + $template = new PdfMakerDesign(strtolower($design->name)); + } + + $state = [ + 'template' => $template->elements([ + 'client' => $client, + 'entity' => $entity_obj, + 'pdf_variables' => (array) $settings->pdf_variables, + '$product' => $design->design->product, + 'variables' => $variables, + ]), + 'variables' => $variables, + 'options' => [ + 'all_pages_header' => $client->getSetting('all_pages_header'), + 'all_pages_footer' => $client->getSetting('all_pages_footer'), + ], + 'process_markdown' => $client->company->markdown_enabled, + ]; + + $maker = new PdfMaker($state); + + $maker + ->design($template) + ->build(); + + /** Generate HTML */ + $html = $maker->getCompiledHTML(true); + + if (request()->query('html') == 'true') + return $html; + + //if phantom js...... inject here.. + if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + return (new Phantom)->convertHtmlToPdf($html); + } + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $company = $user->company(); + + if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { + + $pdf = (new NinjaPdf())->build($html); + $numbered_pdf = $this->pageNumbering($pdf, $company); + + if ($numbered_pdf) + $pdf = $numbered_pdf; + + return $pdf; + } + + $pdf = (new PreviewPdf($html, $company))->handle(); + + if (Ninja::isHosted()) { + LightLogs::create(new LivePreview()) + ->increment() + ->batch(); + } + + /** Return PDF */ + return response()->streamDownload(function () use ($pdf) { + echo $pdf; + }, 'preview.pdf', [ + 'Content-Disposition' => 'inline', + 'Content-Type' => 'application/pdf', + 'Cache-Control:' => 'no-cache', + 'Server-Timing' => microtime(true)-$start + ]); + + } + + + /** + * Returns the mocked PDF for the invoice design preview. + * + * Only used in Settings > Invoice Design as a general overview + * + * @param DesignPreviewRequest $request + * @return mixed + */ + public function design(DesignPreviewRequest $request): mixed + { + $start = microtime(true); + + /** @var \App\Models\User $user */ + $user = auth()->user(); + + /** @var \App\Models\Company $company */ + $company = $user->company(); + + $pdf = (new PdfMock($request->all(), $company))->build()->getPdf(); + + $response = Response::make($pdf, 200); + $response->header('Content-Type', 'application/pdf'); + $response->header('Server-Timing', microtime(true)-$start); + + return $response; } /** * Returns a template filled with entity variables. - * + * + * Used in the Custom Designer to preview design changes * @return mixed */ - public function show() { if (request()->has('entity') && @@ -72,6 +237,7 @@ class PreviewController extends BaseController ! empty(request()->input('entity')) && ! empty(request()->input('entity_id')) && request()->has('body')) { + $design_object = json_decode(json_encode(request()->input('design'))); if (! is_object($design_object)) { @@ -128,55 +294,57 @@ class PreviewController extends BaseController return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); } - /** @var \App\Models\User $user */ $user = auth()->user(); - $company = $user->company(); if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); - $numbered_pdf = $this->pageNumbering($pdf, $company); - - if ($numbered_pdf) { + if ($numbered_pdf) $pdf = $numbered_pdf; - } return $pdf; + } - $file_path = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + $pdf = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + + return response()->streamDownload(function () use ($pdf) { + echo $pdf; + }, 'preview.pdf', [ + 'Content-Disposition' => 'inline', + 'Content-Type' => 'application/pdf', + 'Cache-Control:' => 'no-cache', + ]); + - return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true); } return $this->blankEntity(); } - public function design(DesignPreviewRequest $request) + + + /** + * @deprecated due to usage of transactions + * + * @param PreviewInvoiceRequest $request + * @return mixed + */ + public function livex(PreviewInvoiceRequest $request) { - /** @var \App\Models\User $user */ - $user = auth()->user(); - /** @var \App\Models\Company $company */ - $company = $user->company(); + if(Cache::has("preview_".auth()->user()->id)) + return response()->json(['message' => 'Please wait a few seconds before trying again, this many requests are not good.'], 400); - $pdf = (new PdfMock($request->all(), $company))->build()->getPdf(); - - $response = Response::make($pdf, 200); - $response->header('Content-Type', 'application/pdf'); - - return $response; - } - - public function live(PreviewInvoiceRequest $request) - { if (Ninja::isHosted() && !in_array($request->getHost(), ['preview.invoicing.co','staging.invoicing.co'])) { return response()->json(['message' => 'This server cannot handle this request.'], 400); } + Cache::put("preview_".auth()->user()->id, 60); + $start = microtime(true); /** @var \App\Models\User $user */ @@ -287,9 +455,13 @@ class PreviewController extends BaseController DB::connection(config('database.default'))->rollBack(); if (request()->query('html') == 'true') { + $this->purgeCache(); return $maker->getCompiledHTML(); } + } catch(\Exception $e) { + + $this->purgeCache(); DB::connection(config('database.default'))->rollBack(); @@ -302,6 +474,7 @@ class PreviewController extends BaseController //if phantom js...... inject here.. if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + $this->purgeCache(); return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); } @@ -319,7 +492,7 @@ class PreviewController extends BaseController if ($numbered_pdf) { $pdf = $numbered_pdf; } - + $this->purgeCache(); return $pdf; } @@ -335,7 +508,7 @@ class PreviewController extends BaseController $response->header('Content-Type', 'application/pdf'); $response->header('Server-Timing', microtime(true)-$start); - + $this->purgeCache(); return $response; } @@ -523,7 +696,6 @@ class PreviewController extends BaseController $response = Response::make($file_path, 200); $response->header('Content-Type', 'application/pdf'); - return $response; } } diff --git a/app/Http/Controllers/ProtectedDownloadController.php b/app/Http/Controllers/ProtectedDownloadController.php index b8f598a11021..014527bace14 100644 --- a/app/Http/Controllers/ProtectedDownloadController.php +++ b/app/Http/Controllers/ProtectedDownloadController.php @@ -23,7 +23,7 @@ class ProtectedDownloadController extends BaseController public function index(Request $request) { - + /** @var string $hashed_path */ $hashed_path = Cache::pull($request->hash); if (!$hashed_path) { diff --git a/app/Http/Controllers/RecurringInvoiceController.php b/app/Http/Controllers/RecurringInvoiceController.php index d09d31e80220..94c2192d178a 100644 --- a/app/Http/Controllers/RecurringInvoiceController.php +++ b/app/Http/Controllers/RecurringInvoiceController.php @@ -157,6 +157,7 @@ class RecurringInvoiceController extends BaseController $user = auth()->user(); $recurring_invoice = RecurringInvoiceFactory::create($user->company()->id, $user->id); + $recurring_invoice->auto_bill = $user->company()->settings->auto_bill; return $this->itemResponse($recurring_invoice); } diff --git a/app/Http/Livewire/TasksTable.php b/app/Http/Livewire/TasksTable.php index 40106c390bd6..f229db889f46 100644 --- a/app/Http/Livewire/TasksTable.php +++ b/app/Http/Livewire/TasksTable.php @@ -39,11 +39,11 @@ class TasksTable extends Component ->where('is_deleted', false) ->where('client_id', auth()->guard('contact')->user()->client_id); - if ($this->company->getSetting('show_all_tasks_client_portal') === 'invoiced') { + if ( auth()->guard('contact')->user()->client->getSetting('show_all_tasks_client_portal') === 'invoiced') { $query = $query->whereNotNull('invoice_id'); } - if ($this->company->getSetting('show_all_tasks_client_portal') === 'uninvoiced') { + if ( auth()->guard('contact')->user()->client->getSetting('show_all_tasks_client_portal') === 'uninvoiced') { $query = $query->whereNull('invoice_id'); } diff --git a/app/Http/Requests/Client/StoreClientRequest.php b/app/Http/Requests/Client/StoreClientRequest.php index a8ae0fb5fbae..7262b6ef5d0b 100644 --- a/app/Http/Requests/Client/StoreClientRequest.php +++ b/app/Http/Requests/Client/StoreClientRequest.php @@ -180,8 +180,6 @@ class StoreClientRequest extends Request public function messages() { return [ - // 'unique' => ctrans('validation.unique', ['attribute' => ['email','number']), - //'required' => trans('validation.required', ['attribute' => 'email']), 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), 'currency_code' => 'Currency code does not exist', ]; diff --git a/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php b/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php index 1ee0490b8fb6..73724da000c3 100644 --- a/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php +++ b/app/Http/Requests/ClientPortal/Invoices/ProcessInvoicesInBulkRequest.php @@ -28,4 +28,15 @@ class ProcessInvoicesInBulkRequest extends FormRequest 'invoices' => ['array'], ]; } + + public function prepareForValidation() + { + $input = $this->all(); + + if(isset($input['invoices'])){ + $input['invoices'] = array_unique($input['invoices']); + } + + $this->replace($input); + } } diff --git a/app/Http/Requests/ClientPortal/RegisterRequest.php b/app/Http/Requests/ClientPortal/RegisterRequest.php index c2c5e8ecfcea..860628340369 100644 --- a/app/Http/Requests/ClientPortal/RegisterRequest.php +++ b/app/Http/Requests/ClientPortal/RegisterRequest.php @@ -40,7 +40,7 @@ class RegisterRequest extends FormRequest $rules = []; foreach ($this->company()->client_registration_fields as $field) { - if ($field['visible']) { + if ($field['visible'] ?? true) { $rules[$field['key']] = $field['required'] ? ['bail','required'] : ['sometimes']; } } diff --git a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php index ec629276068a..d35e7ac2bc78 100644 --- a/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/StoreGroupSettingRequest.php @@ -11,11 +11,13 @@ namespace App\Http\Requests\GroupSetting; -use App\DataMapper\ClientSettings; -use App\Http\Requests\Request; -use App\Http\ValidationRules\ValidClientGroupSettingsRule; use App\Models\Account; use App\Models\GroupSetting; +use App\Http\Requests\Request; +use App\DataMapper\ClientSettings; +use App\DataMapper\CompanySettings; +use App\DataMapper\Settings\SettingsData; +use App\Http\ValidationRules\ValidClientGroupSettingsRule; class StoreGroupSettingRequest extends Request { @@ -26,12 +28,18 @@ class StoreGroupSettingRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('create', GroupSetting::class) && auth()->user()->account->hasFeature(Account::FEATURE_API); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('create', GroupSetting::class) && $user->account->hasFeature(Account::FEATURE_API); } public function rules() { - $rules['name'] = 'required|unique:group_settings,name,null,null,company_id,'.auth()->user()->companyId(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $rules['name'] = 'required|unique:group_settings,name,null,null,company_id,'.$user->companyId(); $rules['settings'] = new ValidClientGroupSettingsRule(); @@ -42,15 +50,12 @@ class StoreGroupSettingRequest extends Request { $input = $this->all(); - $group_settings = ClientSettings::defaults(); - - if (array_key_exists('settings', $input) && ! empty($input['settings'])) { - foreach ($input['settings'] as $key => $value) { - $group_settings->{$key} = $value; - } + if (array_key_exists('settings', $input)) { + $input['settings'] = $this->filterSaveableSettings($input['settings']); + } + else { + $input['settings'] = (array)ClientSettings::defaults(); } - - $input['settings'] = (array)$group_settings; $this->replace($input); } @@ -61,4 +66,38 @@ class StoreGroupSettingRequest extends Request 'settings' => 'settings must be a valid json structure', ]; } + + /** + * For the hosted platform, we restrict the feature settings. + * + * This method will trim the company settings object + * down to the free plan setting properties which + * are saveable + * + * @param object $settings + * @return array $settings + */ + private function filterSaveableSettings($settings) + { + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $settings_data = new SettingsData(); + $settings = $settings_data->cast($settings)->toObject(); + + if (! $user->account->isFreeHostedClient()) { + return (array)$settings; + } + + $saveable_casts = CompanySettings::$free_plan_casts; + + foreach ($settings as $key => $value) { + if (! array_key_exists($key, $saveable_casts)) { + unset($settings->{$key}); + } + } + + return (array)$settings; + } + } diff --git a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php index 84120cc3475a..a264d74c018d 100644 --- a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php @@ -11,8 +11,9 @@ namespace App\Http\Requests\GroupSetting; -use App\DataMapper\CompanySettings; use App\Http\Requests\Request; +use App\DataMapper\CompanySettings; +use App\DataMapper\Settings\SettingsData; use App\Http\ValidationRules\ValidClientGroupSettingsRule; class UpdateGroupSettingRequest extends Request @@ -62,10 +63,14 @@ class UpdateGroupSettingRequest extends Request */ private function filterSaveableSettings($settings) { - $account = $this->group_setting->company->account; + /** @var \App\Models\User $user */ + $user = auth()->user(); - if (! $account->isFreeHostedClient()) { - return $settings; + $settings_data = new SettingsData(); + $settings = $settings_data->cast($settings)->toObject(); + + if (! $user->account->isFreeHostedClient()) { + return (array)$settings; } $saveable_casts = CompanySettings::$free_plan_casts; @@ -75,7 +80,7 @@ class UpdateGroupSettingRequest extends Request unset($settings->{$key}); } } - + return (array)$settings; } } diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index e51eeb50bc57..b110abcf38ec 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -51,7 +51,7 @@ class StorePaymentRequest extends Request $credits_total = 0; if (isset($input['client_id']) && is_string($input['client_id'])) { - $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + $input['client_id'] = $this->decodePrimaryKey($input['client_id'], true); } if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { diff --git a/app/Http/Requests/Preview/DesignPreviewRequest.php b/app/Http/Requests/Preview/DesignPreviewRequest.php index f3706c9f5b20..ac2b72fb1ad9 100644 --- a/app/Http/Requests/Preview/DesignPreviewRequest.php +++ b/app/Http/Requests/Preview/DesignPreviewRequest.php @@ -32,11 +32,14 @@ class DesignPreviewRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('create', Invoice::class) || - auth()->user()->can('create', Quote::class) || - auth()->user()->can('create', RecurringInvoice::class) || - auth()->user()->can('create', Credit::class) || - auth()->user()->can('create', PurchaseOrder::class); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('create', Invoice::class) || + $user->can('create', Quote::class) || + $user->can('create', RecurringInvoice::class) || + $user->can('create', Credit::class) || + $user->can('create', PurchaseOrder::class); } public function rules() diff --git a/app/Http/Requests/Preview/PreviewInvoiceRequest.php b/app/Http/Requests/Preview/PreviewInvoiceRequest.php index bfa4db546470..57bd0cf22ef0 100644 --- a/app/Http/Requests/Preview/PreviewInvoiceRequest.php +++ b/app/Http/Requests/Preview/PreviewInvoiceRequest.php @@ -11,15 +11,29 @@ namespace App\Http\Requests\Preview; +use App\Models\Quote; +use App\Models\Client; +use App\Models\Credit; +use App\Models\Invoice; use App\Http\Requests\Request; -use App\Utils\Traits\CleanLineItems; +use App\Models\QuoteInvitation; use App\Utils\Traits\MakesHash; +use Illuminate\Validation\Rule; +use App\Models\CreditInvitation; +use App\Models\RecurringInvoice; +use App\Models\InvoiceInvitation; +use App\Utils\Traits\CleanLineItems; +use App\Models\RecurringInvoiceInvitation; class PreviewInvoiceRequest extends Request { use MakesHash; use CleanLineItems; + private string $entity_plural = ''; + + private ?Client $client = null; + /** * Determine if the user is authorized to make this request. * @@ -27,20 +41,32 @@ class PreviewInvoiceRequest extends Request */ public function authorize() : bool { - return auth()->user()->hasIntersectPermissionsOrAdmin(['view_invoice', 'view_quote', 'view_recurring_invoice', 'view_credit', 'create_invoice', 'create_quote', 'create_recurring_invoice', 'create_credit','edit_invoice', 'edit_quote', 'edit_recurring_invoice', 'edit_credit']); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->hasIntersectPermissionsOrAdmin(['view_invoice', 'view_quote', 'view_recurring_invoice', 'view_credit', 'create_invoice', 'create_quote', 'create_recurring_invoice', 'create_credit','edit_invoice', 'edit_quote', 'edit_recurring_invoice', 'edit_credit']); } public function rules() { - $rules = []; + /** @var \App\Models\User $user */ + $user = auth()->user(); - $rules['number'] = ['nullable']; + return [ + 'number' => 'nullable', + 'entity' => 'bail|sometimes|in:invoice,quote,credit,recurring_invoice', + 'entity_id' => ['bail','sometimes','integer',Rule::exists($this->entity_plural, 'id')->where('is_deleted',0)->where('company_id', $user->company()->id)], + 'client_id' => ['required', Rule::exists(Client::class, 'id')->where('is_deleted', 0)->where('company_id', $user->company()->id)], + ]; - return $rules; } public function prepareForValidation() { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $input = $this->all(); $input = $this->decodePrimaryKeys($input); @@ -50,6 +76,112 @@ class PreviewInvoiceRequest extends Request $input['balance'] = 0; $input['number'] = isset($input['number']) ? $input['number'] : ctrans('texts.live_preview').' #'.rand(0, 1000); + if($input['entity_id'] ?? false) + $input['entity_id'] = $this->decodePrimaryKey($input['entity_id'], true); + + $this->convertEntityPlural($input['entity'] ?? 'invoice'); + $this->replace($input); } + + public function resolveInvitation() + { + $invitation = false; + + if(! $this->entity_id ?? false) + return $this->stubInvitation(); + + match($this->entity){ + 'invoice' => $invitation = InvoiceInvitation::withTrashed()->where('invoice_id', $this->entity_id)->first(), + 'quote' => $invitation = QuoteInvitation::withTrashed()->where('quote_id', $this->entity_id)->first(), + 'credit' => $invitation = CreditInvitation::withTrashed()->where('credit_id', $this->entity_id)->first(), + 'recurring_invoice' => $invitation = RecurringInvoiceInvitation::withTrashed()->where('recurring_invoice_id', $this->entity_id)->first(), + }; + + if($invitation) + return $invitation; + + $invitation = $this->stubInvitation(); + } + + public function getClient(): ?Client + { + if(!$this->client) + $this->client = Client::query()->with('contacts', 'company', 'user')->withTrashed()->find($this->client_id); + + return $this->client; + } + + public function setClient(Client $client): self + { + $this->client = $client; + + return $this; + } + + public function stubInvitation() + { + $client = Client::query()->with('contacts', 'company', 'user')->withTrashed()->find($this->client_id); + $this->setClient($client); + $invitation = false; + + match($this->entity) { + 'invoice' => $invitation = InvoiceInvitation::factory()->make(), + 'quote' => $invitation = QuoteInvitation::factory()->make(), + 'credit' => $invitation = CreditInvitation::factory()->make(), + 'recurring_invoice' => $invitation = RecurringInvoiceInvitation::factory()->make(), + default => $invitation = InvoiceInvitation::factory()->make(), + }; + + $entity = $this->stubEntity($client); + + $invitation->make(); + $invitation->setRelation($this->entity, $entity); + $invitation->setRelation('contact', $client->contacts->first()->load('client.company')); + $invitation->setRelation('company', $client->company); + + return $invitation; + } + + private function stubEntity(Client $client) + { + $entity = false; + + match($this->entity){ + 'invoice' => $entity = Invoice::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + 'quote' => $entity = Quote::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + 'credit' => $entity = Credit::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + 'recurring_invoice' => $entity = RecurringInvoice::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + default => $entity = Invoice::factory()->make(['client_id' => $client->id,'user_id' => $client->user_id, 'company_id' => $client->company_id]), + }; + + $entity->setRelation('client', $client); + $entity->setRelation('company', $client->company); + $entity->setRelation('user', $client->user); + $entity->fill($this->all()); + + return $entity; + } + + private function convertEntityPlural(string $entity) :self + { + switch ($entity) { + case 'invoice': + $this->entity_plural = 'invoices'; + return $this; + case 'quote': + $this->entity_plural = 'quotes'; + return $this; + case 'credit': + $this->entity_plural = 'credits'; + return $this; + case 'recurring_invoice': + $this->entity_plural = 'invoices'; + return $this; + default: + $this->entity_plural = 'invoices'; + return $this; + } + } + } diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 2fb9a589e082..8d7eb2acd05b 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -180,7 +180,7 @@ class Request extends FormRequest } //Filter the client contact password - if it is sent with ***** we should ignore it! - if (isset($contact['password'])) { + if (isset($contact['password']) && is_string($contact['password'])) { if (strlen($contact['password']) == 0) { $input['contacts'][$key]['password'] = ''; } else { diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php index f28781bfe080..89a25bdd865c 100644 --- a/app/Jobs/Client/UpdateTaxData.php +++ b/app/Jobs/Client/UpdateTaxData.php @@ -52,7 +52,7 @@ class UpdateTaxData implements ShouldQueue { MultiDB::setDb($this->company->db); - if($this->company->account->isFreeHostedClient()) + if($this->company->account->isFreeHostedClient() || $this->client->country_id != 840) return; $tax_provider = new \App\Services\Tax\Providers\TaxProvider($this->company, $this->client); @@ -73,69 +73,6 @@ class UpdateTaxData implements ShouldQueue nlog("problem getting tax data => ".$e->getMessage()); } - /* - if(!$tax_provider->updatedTaxStatus() && $this->client->country_id == 840){ - - $calculated_state = false; - - if(array_key_exists($this->client->shipping_state, USStates::get())) { - $calculated_state = $this->client->shipping_state; - $calculated_postal_code = $this->client->shipping_postal_code; - $calculated_city = $this->client->shipping_city; - } - elseif(array_key_exists($this->client->state, USStates::get())){ - $calculated_state = $this->client->state; - $calculated_postal_code = $this->client->postal_code; - $calculated_city = $this->client->city; - } - else { - - try{ - $calculated_state = USStates::getState($this->client->shipping_postal_code); - $calculated_postal_code = $this->client->shipping_postal_code; - $calculated_city = $this->client->shipping_city; - } - catch(\Exception $e){ - nlog("could not calculate state from postal code => {$this->client->shipping_postal_code} or from state {$this->client->shipping_state}"); - } - - if(!$calculated_state) { - try { - $calculated_state = USStates::getState($this->client->postal_code); - $calculated_postal_code = $this->client->postal_code; - $calculated_city = $this->client->city; - } catch(\Exception $e) { - nlog("could not calculate state from postal code => {$this->client->postal_code} or from state {$this->client->state}"); - } - } - - if($this->company->tax_data?->seller_subregion) - $calculated_state = $this->company->tax_data?->seller_subregion; - - nlog("i am trying"); - - if(!$calculated_state) { - nlog("could not determine state"); - return; - } - - } - - $data = [ - 'seller_subregion' => $this->company->tax_data?->seller_subregion ?: '', - 'geoPostalCode' => $this->client->postal_code ?? '', - 'geoCity' => $this->client->city ?? '', - 'geoState' => $calculated_state, - 'taxSales' => $this->company->tax_data->regions->US->subregions?->{$calculated_state}?->taxSales ?? 0, - ]; - - $tax_data = new Response($data); - - $this->client->tax_data = $tax_data; - $this->client->saveQuietly(); - - } - */ } public function middleware() diff --git a/app/Jobs/Mail/NinjaMailerObject.php b/app/Jobs/Mail/NinjaMailerObject.php index 1d973cb90275..df33c35c2a11 100644 --- a/app/Jobs/Mail/NinjaMailerObject.php +++ b/app/Jobs/Mail/NinjaMailerObject.php @@ -32,12 +32,12 @@ class NinjaMailerObject /* Variable for cascading notifications */ public $entity_string = false; - /* @var bool | App\Models\InvoiceInvitation | app\Models\QuoteInvitation | app\Models\CreditInvitation | app\Models\RecurringInvoiceInvitation | app\Models\PurchaseOrderInvitation $invitation*/ + /* @var bool | App\Models\InvoiceInvitation | App\Models\QuoteInvitation | App\Models\CreditInvitation | App\Models\RecurringInvoiceInvitation | App\Models\PurchaseOrderInvitation $invitation*/ public $invitation = false; public $template = false; - /* @var bool | App\Models\Invoice | app\Models\Quote | app\Models\Credit | app\Models\RecurringInvoice | app\Models\PurchaseOrder $invitation*/ + /* @var bool | App\Models\Invoice | App\Models\Quote | App\Models\Credit | App\Models\RecurringInvoice | App\Models\PurchaseOrder | App\Models\Payment $entity */ public $entity = false; public $reminder_template = ''; diff --git a/app/Jobs/PostMark/ProcessPostmarkWebhook.php b/app/Jobs/PostMark/ProcessPostmarkWebhook.php index 18dde45e430c..8e4429ca148d 100644 --- a/app/Jobs/PostMark/ProcessPostmarkWebhook.php +++ b/app/Jobs/PostMark/ProcessPostmarkWebhook.php @@ -56,6 +56,23 @@ class ProcessPostmarkWebhook implements ShouldQueue { } + private function getSystemLog(string $message_id): ?SystemLog + { + return SystemLog::query() + ->where('company_id', $this->invitation->company_id) + ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE) + ->whereJsonContains('log', ['MessageID' => $message_id]) + ->orderBy('id','desc') + ->first(); + + } + + private function updateSystemLog(SystemLog $system_log, array $data): void + { + $system_log->log = $data; + $system_log->save(); + } + /** * Execute the job. * @@ -135,6 +152,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl){ + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger( $data, SystemLog::CATEGORY_MAIL, @@ -166,6 +190,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger( $data, SystemLog::CATEGORY_MAIL, @@ -217,6 +248,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); // if(config('ninja.notification.slack')) @@ -263,6 +301,13 @@ class ProcessPostmarkWebhook implements ShouldQueue $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $this->updateSystemLog($sl, $data); + return; + } + (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); if (config('ninja.notification.slack')) { diff --git a/app/Jobs/Util/PreviewPdf.php b/app/Jobs/Util/PreviewPdf.php index 214201f1a660..0838379e9428 100644 --- a/app/Jobs/Util/PreviewPdf.php +++ b/app/Jobs/Util/PreviewPdf.php @@ -24,25 +24,14 @@ class PreviewPdf implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, PdfMaker, PageNumbering; - public $company; - - private $disk; - - public $design_string; - /** * Create a new job instance. * * @param $design_string * @param Company $company */ - public function __construct($design_string, Company $company) + public function __construct(public string $design_string, public Company $company) { - $this->company = $company; - - $this->design_string = $design_string; - - $this->disk = $disk ?? config('filesystems.default'); } public function handle() diff --git a/app/Listeners/User/UpdateUserLastLogin.php b/app/Listeners/User/UpdateUserLastLogin.php index 9fc383db207f..94e20512eefc 100644 --- a/app/Listeners/User/UpdateUserLastLogin.php +++ b/app/Listeners/User/UpdateUserLastLogin.php @@ -55,7 +55,7 @@ class UpdateUserLastLogin implements ShouldQueue $key = "user_logged_in_{$user->id}{$event->company->db}"; - if ($user->ip != $ip && is_null(Cache::get($key))) { + if ($user->ip != $ip && is_null(Cache::get($key)) && $user->user_logged_in_notification) { $nmo = new NinjaMailerObject; $nmo->mailable = new UserLoggedIn($user, $user->account->companies->first(), $ip); $nmo->company = $user->account->companies->first(); @@ -69,6 +69,7 @@ class UpdateUserLastLogin implements ShouldQueue Cache::put($key, true, 60 * 24); $arr = json_encode(['ip' => $ip]); + $arr = ctrans('texts.new_login_detected'). " {$ip}"; SystemLogger::dispatch( $arr, diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 984029cefe26..63794936e5c8 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -108,12 +108,15 @@ class TemplateEmail extends Mailable if (strlen($settings->bcc_email) > 1) { if (Ninja::isHosted()) { - $bccs = explode(',', str_replace(' ', '', $settings->bcc_email)); - $this->bcc(array_slice($bccs, 0, 2)); - //$this->bcc(reset($bccs)); //remove whitespace if any has been inserted. + + if($company->account->isPaid()) { + $bccs = explode(',', str_replace(' ', '', $settings->bcc_email)); + $this->bcc(array_slice($bccs, 0, 5)); + } + } else { $this->bcc(explode(',', str_replace(' ', '', $settings->bcc_email))); - }//remove whitespace if any has been inserted. + } } $this->subject(str_replace("
", "", $this->build_email->getSubject())) diff --git a/app/Mail/VendorTemplateEmail.php b/app/Mail/VendorTemplateEmail.php index 2ac0987b8045..bdfb818b0091 100644 --- a/app/Mail/VendorTemplateEmail.php +++ b/app/Mail/VendorTemplateEmail.php @@ -11,10 +11,11 @@ namespace App\Mail; +use App\Utils\Ninja; use App\Models\VendorContact; -use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; -use App\Utils\VendorHtmlEngine; use Illuminate\Mail\Mailable; +use App\Utils\VendorHtmlEngine; +use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; class VendorTemplateEmail extends Mailable { @@ -102,8 +103,19 @@ class VendorTemplateEmail extends Mailable $this->from(config('mail.from.address'), $email_from_name); if (strlen($settings->bcc_email) > 1) { - $this->bcc(explode(',', str_replace(' ', '', $settings->bcc_email))); - }//remove whitespace if any has been inserted. + + if (Ninja::isHosted()) { + + if($this->company->account->isPaid()) { + $bccs = explode(',', str_replace(' ', '', $settings->bcc_email)); + $this->bcc(array_slice($bccs, 0, 5)); + } + + } else { + $this->bcc(explode(',', str_replace(' ', '', $settings->bcc_email))); + } + + } $this->subject($this->build_email->getSubject()) ->text('email.template.text', [ diff --git a/app/Models/Presenters/ClientPresenter.php b/app/Models/Presenters/ClientPresenter.php index ac4b184d94e1..aed92c0eb8c9 100644 --- a/app/Models/Presenters/ClientPresenter.php +++ b/app/Models/Presenters/ClientPresenter.php @@ -23,7 +23,7 @@ class ClientPresenter extends EntityPresenter */ public function name() { - if ($this->entity->name) { + if (strlen($this->entity->name) > 1) { return $this->entity->name; } diff --git a/app/Models/User.php b/app/Models/User.php index 22e5130d0df1..aec9b0e787d5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -73,7 +73,8 @@ use Illuminate\Foundation\Auth\User as Authenticatable; * @property int|null $deleted_at * @property string|null $oauth_user_refresh_token * @property string|null $last_confirmed_email_address - * @property int $has_password + * @property bool $has_password + * @property bool $user_logged_in_notification * @property Carbon|null $oauth_user_token_expiry * @property string|null $sms_verification_code * @property bool $verified_phone_number @@ -140,6 +141,7 @@ class User extends Authenticatable implements MustVerifyEmail * */ protected $fillable = [ + 'user_logged_in_notification', 'first_name', 'last_name', 'email', diff --git a/app/PaymentDrivers/Eway/CreditCard.php b/app/PaymentDrivers/Eway/CreditCard.php index 11dbafdcfe2b..d4203d0eab9f 100644 --- a/app/PaymentDrivers/Eway/CreditCard.php +++ b/app/PaymentDrivers/Eway/CreditCard.php @@ -251,7 +251,7 @@ class CreditCard $response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction); - if ($response->TransactionStatus) { + if ($response->TransactionStatus ?? false) { $this->logResponse($response, true); $payment = $this->storePayment($response); } else { diff --git a/app/PaymentDrivers/PayTrace/CreditCard.php b/app/PaymentDrivers/PayTrace/CreditCard.php index 720d5f0b02be..350fc6948b87 100644 --- a/app/PaymentDrivers/PayTrace/CreditCard.php +++ b/app/PaymentDrivers/PayTrace/CreditCard.php @@ -183,7 +183,7 @@ class CreditCard $response = $this->paytrace->gatewayRequest('/v1/transactions/sale/by_customer', $data); - if ($response->success) { + if ($response->success ?? false) { $this->paytrace->logSuccessfulGatewayResponse(['response' => $response, 'data' => $this->paytrace->payment_hash], SystemLog::TYPE_PAYTRACE); return $this->processSuccessfulPayment($response); diff --git a/app/PaymentDrivers/PaytracePaymentDriver.php b/app/PaymentDrivers/PaytracePaymentDriver.php index 5f33ea843e3a..b55e8857c169 100644 --- a/app/PaymentDrivers/PaytracePaymentDriver.php +++ b/app/PaymentDrivers/PaytracePaymentDriver.php @@ -198,7 +198,7 @@ class PaytracePaymentDriver extends BaseDriver $auth_data = json_decode($response); - if (! property_exists($auth_data, 'access_token')) { + if (!isset($auth_data) || ! property_exists($auth_data, 'access_token')) { throw new SystemError('Error authenticating with PayTrace'); } diff --git a/app/PaymentDrivers/Square/SquareWebhook.php b/app/PaymentDrivers/Square/SquareWebhook.php index c83b73fd4765..67d656e8c926 100644 --- a/app/PaymentDrivers/Square/SquareWebhook.php +++ b/app/PaymentDrivers/Square/SquareWebhook.php @@ -134,8 +134,8 @@ class SquareWebhook implements ShouldQueue nlog("Searching by payment hash"); - $payment_hash_id = $apiResponse->getPayment()->getReferenceId() ?? false; - $square_payment = $apiResponse->getPayment()->jsonSerialize(); + $payment_hash_id = $apiResponse->getResult()->getPayment()->getReferenceId() ?? false; + $square_payment = $apiResponse->getResult()->getPayment()->jsonSerialize(); $payment_hash = PaymentHash::query()->where('hash', $payment_hash_id)->firstOrFail(); $payment_hash->data = array_merge((array) $payment_hash->data, (array)$square_payment); diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index cebb8b7b6f79..c539d075d445 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -140,7 +140,7 @@ class Email implements ShouldQueue $this->email_object->client_id ? $this->email_object->settings = $this->email_object->client->getMergedSettings() : $this->email_object->settings = $this->company->settings; - $this->email_object->client_id ? nlog("client settings") : nlog("company settings "); + // $this->email_object->client_id ? nlog("client settings") : nlog("company settings "); $this->email_object->whitelabel = $this->company->account->isPaid() ? true : false; @@ -413,6 +413,14 @@ class Email implements ShouldQueue if ($address_object->address == " ") { return true; } + + if ($address_object->address == "") { + return true; + } + + if($address_object->name == " " || $address_object->name == "") { + return true; + } } diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 401060b6c7b4..1f78facaa1e5 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -255,8 +255,8 @@ class EmailDefaults if (strlen($this->email->email_object->settings->bcc_email) > 1) { if (Ninja::isHosted() && $this->email->company->account->isPaid()) { - $bccs = array_slice(explode(',', str_replace(' ', '', $this->email->email_object->settings->bcc_email)), 0, 2); - } elseif (Ninja::isSelfHost()) { + $bccs = array_slice(explode(',', str_replace(' ', '', $this->email->email_object->settings->bcc_email)), 0, 5); + } else { $bccs = (explode(',', str_replace(' ', '', $this->email->email_object->settings->bcc_email))); } } diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 0585f888bd66..19a366fdc507 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -120,7 +120,7 @@ class AutoBillInvoice extends AbstractService /* Build payment hash */ $payment_hash = PaymentHash::create([ - 'hash' => Str::random(64), + 'hash' => Str::random(32), 'data' => [ 'amount_with_fee' => $amount + $fee, 'invoices' => [ diff --git a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php index 6b7dbe884350..539e78f6e270 100644 --- a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php +++ b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php @@ -75,7 +75,7 @@ class ZugferdEInvoice extends AbstractService } else { $this->xrechnung->setDocumentBuyerReference($client->routing_id); } - if (!empty($client->shipping_address1)){ + if (!empty($client->shipping_address1) && $client->shipping_country->exists()){ $this->xrechnung->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state); } diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index 1e803faf10cd..8884dbdef622 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -109,7 +109,11 @@ class DeletePayment if ($paymentable_invoice->balance == $paymentable_invoice->amount) { $paymentable_invoice->service()->setStatus(Invoice::STATUS_SENT)->save(); - } else { + } + elseif($paymentable_invoice->balance == 0){ + $paymentable_invoice->service()->setStatus(Invoice::STATUS_PAID)->save(); + } + else { $paymentable_invoice->service()->setStatus(Invoice::STATUS_PARTIAL)->save(); } } else { diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index 9598813b80f9..00b540bbc0a4 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -225,6 +225,8 @@ class TaxProvider */ private function configureEuTax(): self { + throw new \Exception("No tax region defined for this country"); + $this->provider = EuTax::class; return $this; diff --git a/app/Transformers/ClientTransformer.php b/app/Transformers/ClientTransformer.php index 711c9cfec5ba..4cfb7742b3e4 100644 --- a/app/Transformers/ClientTransformer.php +++ b/app/Transformers/ClientTransformer.php @@ -17,6 +17,7 @@ use App\Models\ClientContact; use App\Models\ClientGatewayToken; use App\Models\CompanyLedger; use App\Models\Document; +use App\Models\GroupSetting; use App\Models\SystemLog; use App\Utils\Traits\MakesHash; use League\Fractal\Resource\Collection; @@ -42,6 +43,7 @@ class ClientTransformer extends EntityTransformer 'activities', 'ledger', 'system_logs', + 'group_settings', ]; /** @@ -96,6 +98,16 @@ class ClientTransformer extends EntityTransformer return $this->includeCollection($client->system_logs, $transformer, SystemLog::class); } + public function includeGroupSettings(Client $client) + { + if (!$client->group_settings) + return null; + + $transformer = new GroupSettingTransformer($this->serializer); + + return $this->includeItem($client->group_settings, $transformer, GroupSetting::class); + } + /** * @param Client $client * diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php index ffe029fbb0fe..0971c7dc2b02 100644 --- a/app/Transformers/UserTransformer.php +++ b/app/Transformers/UserTransformer.php @@ -64,6 +64,7 @@ class UserTransformer extends EntityTransformer 'oauth_user_token' => empty($user->oauth_user_token) ? '' : '***', 'verified_phone_number' => (bool) $user->verified_phone_number, 'language_id' => (string) $user->language_id ?? '', + 'user_logged_in_notification' => (bool) $user->user_logged_in_notification, ]; } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index f96d37be5df8..01d2d7d6fe43 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -670,16 +670,16 @@ class HtmlEngine $data['$payment.transaction_reference'] = ['value' => '', 'label' => ctrans('texts.transaction_reference')]; - if ($this->entity_string == 'invoice' && $this->entity->payments()->exists()) { + if ($this->entity_string == 'invoice' && $this->entity->net_payments()->exists()) { $payment_list = '

'; - foreach ($this->entity->payments as $payment) { + foreach ($this->entity->net_payments as $payment) { $payment_list .= ctrans('texts.payment_subject') . ": " . $this->formatDate($payment->date, $this->client->date_format()) . " :: " . Number::formatMoney($payment->amount, $this->client) ." :: ". GatewayType::getAlias($payment->gateway_type_id) . "
"; } $data['$payments'] = ['value' => $payment_list, 'label' => ctrans('texts.payments')]; - $payment = $this->entity->payments()->first(); + $payment = $this->entity->net_payments()->first(); $data['$payment.custom1'] = ['value' => $payment->custom_value1, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment1')]; $data['$payment.custom2'] = ['value' => $payment->custom_value2, 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'payment2')]; @@ -1014,17 +1014,17 @@ html { */ protected function generateEntityImagesMarkup() { - // if (!$this->client->getSetting('embed_documents') && !$this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { - // return ''; - // } + if (!$this->client->getSetting('embed_documents') || !$this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { + return ''; + } $dom = new \DOMDocument('1.0', 'UTF-8'); $container = $dom->createElement('div'); $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);justify-items: center;'); - foreach ($this->entity->documents as $document) { - if (!$document->isImage()) { + foreach ($this->entity->documents()->where('is_public',true)->get() as $document) { + if (!$document->isImage()) { continue; } diff --git a/app/Utils/Traits/MakesHash.php b/app/Utils/Traits/MakesHash.php index 63c151b93530..dc8d6d158a40 100644 --- a/app/Utils/Traits/MakesHash.php +++ b/app/Utils/Traits/MakesHash.php @@ -62,8 +62,27 @@ trait MakesHash return $hashids->encode($value); } - public function decodePrimaryKey($value) + public function decodePrimaryKey($value, $return_string_failure = false) { + + try { + $hashids = new Hashids(config('ninja.hash_salt'), 10); + + $decoded_array = $hashids->decode($value); + + if(isset($decoded_array[0]) ?? false) { + return $decoded_array[0]; + } elseif($return_string_failure) { + return "Invalid Primary Key"; + } else { + throw new \Exception('Invalid Primary Key'); + } + + } catch (\Exception $e) { + return response()->json(['error'=>'Invalid primary key'], 400); + } + + /* try { $hashids = new Hashids(config('ninja.hash_salt'), 10); @@ -77,6 +96,7 @@ trait MakesHash } catch (\Exception $e) { return response()->json(['error'=>'Invalid primary key'], 400); } + */ } public function transformKeys($keys) diff --git a/app/Utils/VendorHtmlEngine.php b/app/Utils/VendorHtmlEngine.php index eb1f595584e5..7bd05d75924c 100644 --- a/app/Utils/VendorHtmlEngine.php +++ b/app/Utils/VendorHtmlEngine.php @@ -12,18 +12,19 @@ namespace App\Utils; -use App\Models\Country; -use App\Models\CreditInvitation; -use App\Models\InvoiceInvitation; -use App\Models\PurchaseOrderInvitation; -use App\Models\QuoteInvitation; -use App\Models\RecurringInvoiceInvitation; -use App\Utils\Traits\AppSetup; -use App\Utils\Traits\DesignCalculator; -use App\Utils\Traits\MakesDates; use Exception; +use App\Models\Account; +use App\Models\Country; +use App\Utils\Traits\AppSetup; +use App\Models\QuoteInvitation; +use App\Models\CreditInvitation; +use App\Utils\Traits\MakesDates; +use App\Models\InvoiceInvitation; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; +use App\Utils\Traits\DesignCalculator; +use App\Models\PurchaseOrderInvitation; +use App\Models\RecurringInvoiceInvitation; /** * Note the premise used here is that any currencies will be formatted back to the company currency and not @@ -775,31 +776,37 @@ html { */ protected function generateEntityImagesMarkup() { - if ($this->company->getSetting('embed_documents') === false) { + + if (!$this->vendor->getSetting('embed_documents') || !$this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { return ''; } $dom = new \DOMDocument('1.0', 'UTF-8'); $container = $dom->createElement('div'); - $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, 1fr);'); - - foreach ($this->entity->documents as $document) { + $container->setAttribute('style', 'display:grid; grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);justify-items: center;'); + + foreach ($this->entity->documents()->where('is_public',true)->get() as $document) { if (!$document->isImage()) { continue; } $image = $dom->createElement('img'); - $image->setAttribute('src', $document->generateUrl()); - $image->setAttribute('style', 'max-height: 100px; margin-top: 20px;'); + $image->setAttribute('src', "data:image/png;base64,".base64_encode($document->getFile())); + $image->setAttribute('style', 'max-width: 50%; margin-top: 20px;'); $container->appendChild($image); } $dom->appendChild($container); - return $dom->saveHTML(); + $html = $dom->saveHTML(); + + $dom = null; + + return $html; + } /** diff --git a/config/ninja.php b/config/ninja.php index 7ef548ab7efa..d5f584da4e29 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,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.7.30'), - 'app_tag' => env('APP_TAG','5.7.30'), + 'app_version' => env('APP_VERSION','5.7.33'), + 'app_tag' => env('APP_TAG','5.7.33'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/database/factories/RecurringInvoiceInvitationFactory.php b/database/factories/RecurringInvoiceInvitationFactory.php new file mode 100644 index 000000000000..fe7db151f11d --- /dev/null +++ b/database/factories/RecurringInvoiceInvitationFactory.php @@ -0,0 +1,30 @@ + Str::random(40), + ]; + } +} diff --git a/database/migrations/2023_10_18_061415_add_user_notification_suppression.php b/database/migrations/2023_10_18_061415_add_user_notification_suppression.php new file mode 100644 index 000000000000..4c01624a02ac --- /dev/null +++ b/database/migrations/2023_10_18_061415_add_user_notification_suppression.php @@ -0,0 +1,43 @@ +boolean('user_logged_in_notification')->default(true); + }); + + + $cur = Currency::find(120); + + if(!$cur) { + $cur = new \App\Models\Currency(); + $cur->id = 120; + $cur->code = 'TOP'; + $cur->name = "Tongan Pa'anga"; + $cur->symbol = 'T$'; + $cur->thousand_separator = ','; + $cur->decimal_separator = '.'; + $cur->precision = 2; + $cur->save(); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/seeders/CurrenciesSeeder.php b/database/seeders/CurrenciesSeeder.php index b2c8b35af751..e6976622ee86 100644 --- a/database/seeders/CurrenciesSeeder.php +++ b/database/seeders/CurrenciesSeeder.php @@ -142,6 +142,7 @@ class CurrenciesSeeder extends Seeder ['id' => 117, 'name' => 'Gold Troy Ounce', 'code' => 'XAU', 'symbol' => 'XAU', 'precision' => '3', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 118, 'name' => 'Nicaraguan Córdoba', 'code' => 'NIO', 'symbol' => 'C$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['id' => 119, 'name' => 'Malagasy ariary', 'code' => 'MGA', 'symbol' => 'Ar', 'precision' => '0', 'thousand_separator' => ',', 'decimal_separator' => '.'], + ['id' => 120, 'name' => "Tongan Pa anga", 'code' => 'TOP', 'symbol' => 'T$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ]; foreach ($currencies as $currency) { diff --git a/lang/en/texts.php b/lang/en/texts.php index 49eb412a6202..48cfe30beb3a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -2402,6 +2402,9 @@ $LANG = array( 'currency_libyan_dinar' => 'Libyan Dinar', 'currency_silver_troy_ounce' => 'Silver Troy Ounce', 'currency_gold_troy_ounce' => 'Gold Troy Ounce', + 'currency_nicaraguan_córdoba' => 'Nicaraguan Córdoba', + 'currency_malagasy_ariary' => 'Malagasy ariary', + "currency_tongan_pa_anga" => "Tongan Pa'anga", 'review_app_help' => 'We hope you\'re enjoying using the app.
If you\'d consider :link we\'d greatly appreciate it!', 'writing_a_review' => 'writing a review', @@ -3679,9 +3682,9 @@ $LANG = array( 'send_date' => 'Send Date', 'auto_bill_on' => 'Auto Bill On', 'minimum_under_payment_amount' => 'Minimum Under Payment Amount', - 'allow_over_payment' => 'Allow Over Payment', + 'allow_over_payment' => 'Allow Overpayment', 'allow_over_payment_help' => 'Support paying extra to accept tips', - 'allow_under_payment' => 'Allow Under Payment', + 'allow_under_payment' => 'Allow Underpayment', 'allow_under_payment_help' => 'Support paying at minimum the partial/deposit amount', 'test_mode' => 'Test Mode', 'calculated_rate' => 'Calculated Rate', @@ -3978,8 +3981,8 @@ $LANG = array( 'account_balance' => 'Account Balance', 'thanks' => 'Thanks', 'minimum_required_payment' => 'Minimum required payment is :amount', - 'under_payments_disabled' => 'Company doesn\'t support under payments.', - 'over_payments_disabled' => 'Company doesn\'t support over payments.', + 'under_payments_disabled' => 'Company doesn\'t support underpayments.', + 'over_payments_disabled' => 'Company doesn\'t support overpayments.', 'saved_at' => 'Saved at :time', 'credit_payment' => 'Credit applied to Invoice :invoice_number', 'credit_subject' => 'New credit :number from :account', @@ -4654,8 +4657,8 @@ $LANG = array( 'search_purchase_order' => 'Search Purchase Order', 'search_purchase_orders' => 'Search Purchase Orders', 'login_url' => 'Login URL', - 'enable_applying_payments' => 'Enable Applying Payments', - 'enable_applying_payments_help' => 'Support separately creating and applying payments', + 'enable_applying_payments' => 'Manual Overpayments', + 'enable_applying_payments_help' => 'Support adding an overpayment amount manually on a payment', 'stock_quantity' => 'Stock Quantity', 'notification_threshold' => 'Notification Threshold', 'track_inventory' => 'Track Inventory', @@ -5180,6 +5183,8 @@ $LANG = array( 'upcoming' => 'Upcoming', 'client_contact' => 'Client Contact', 'uncategorized' => 'Uncategorized', + 'login_notification' => 'Login Notification', + 'login_notification_help' => 'Sends an email notifying that a login has taken place.' ); return $LANG; diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php index 8d5df833d326..a0d786ff4610 100644 --- a/resources/views/portal/ninja2020/invoices/payment.blade.php +++ b/resources/views/portal/ninja2020/invoices/payment.blade.php @@ -64,7 +64,7 @@ {{ ctrans('texts.public_notes') }}
- {{ $invoice->public_notes }} + {!! html_entity_decode($invoice->public_notes) !!}
@else
diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php index 0244a3e8227b..89b49d945c6a 100644 --- a/tests/Feature/ClientTest.php +++ b/tests/Feature/ClientTest.php @@ -65,6 +65,34 @@ class ClientTest extends TestCase $this->makeTestData(); } + public function testStoreClientFixes() + { + $data = [ + "contacts" => [ + [ + "email" => "tenda@gmail.com", + "first_name" => "Tenda", + "is_primary" => True, + "last_name" => "Bavuma", + "password" => null, + "send_email" => True + ], + ], + "country_id" => "356", + "display_name" => "Tenda Bavuma", + "name" => "Tenda Bavuma", + "shipping_country_id" => "356", + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/clients', $data); + + $response->assertStatus(200); + } + public function testClientMergeContactDrop() { diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index c7f400db7420..90b12ab96adc 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -20,7 +20,6 @@ use App\Models\Account; use App\Models\Company; use App\Models\Expense; use App\Models\Invoice; -use Tests\MockAccountData; use App\Models\CompanyToken; use App\Models\ClientContact; use App\Export\CSV\TaskExport; @@ -30,8 +29,6 @@ use App\Export\CSV\ProductExport; use App\DataMapper\CompanySettings; use App\Export\CSV\PaymentExport; use App\Factory\CompanyUserFactory; -use App\Factory\InvoiceItemFactory; -use App\Services\Report\ARDetailReport; use Illuminate\Routing\Middleware\ThrottleRequests; /** @@ -262,6 +259,21 @@ class ReportCsvGenerationTest extends TestCase } + public function testForcedInsertionOfMandatoryColumns() + { + $forced = ['client.name']; + + $report_keys = ['invoice.number','client.name', 'invoice.amount']; + $array = array_merge($report_keys, array_diff($forced, $report_keys)); + + $this->assertEquals('client.name', $array[1]); + + $report_keys = ['invoice.number','invoice.amount']; + $array = array_merge($report_keys, array_diff($forced, $report_keys)); + + $this->assertEquals('client.name', $array[2]); + + } public function testVendorCsvGeneration() { @@ -322,7 +334,7 @@ class ReportCsvGenerationTest extends TestCase $data = $export->returnJson(); $this->assertNotNull($data); -// nlog($data); + // nlog($data); // $this->assertEquals(0, $this->traverseJson($data, 'columns.0.identifier')); $this->assertEquals('Vendor Name', $this->traverseJson($data, 'columns.9.display_value')); $this->assertEquals('vendor', $this->traverseJson($data, '0.0.entity')); @@ -1021,6 +1033,44 @@ class ReportCsvGenerationTest extends TestCase 'X-API-TOKEN' => $this->token, ])->post('/api/v1/reports/recurring_invoices', $data)->assertStatus(200); + } + + + public function testRecurringInvoiceColumnsCsvGeneration() + { + + \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'frequency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Recurring Invoice Invoice Number')); + $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'Recurring Invoice How Often')); + $this->assertEquals('Active', $this->getFirstValueByColumn($csv, 'Recurring Invoice Status')); } @@ -1121,7 +1171,7 @@ class ReportCsvGenerationTest extends TestCase public function testQuoteItemsCustomColumnsCsvGeneration() { - \App\Models\Quote::factory()->create([ + $q = \App\Models\Quote::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id, @@ -1167,7 +1217,6 @@ class ReportCsvGenerationTest extends TestCase $csv = $response->streamedContent(); - $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Item Quantity')); diff --git a/tests/Feature/GroupSettingTest.php b/tests/Feature/GroupSettingTest.php index da637d39506c..98bad3daa703 100644 --- a/tests/Feature/GroupSettingTest.php +++ b/tests/Feature/GroupSettingTest.php @@ -11,20 +11,21 @@ namespace Tests\Feature; +use Tests\TestCase; +use Tests\MockAccountData; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; -use Tests\MockAccountData; -use Tests\TestCase; +use App\DataMapper\Settings\SettingsData; +use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; class GroupSettingTest extends TestCase { use MakesHash; - - //use DatabaseTransactions; use MockAccountData; + public $faker; + protected function setUp(): void { parent::setUp(); @@ -38,6 +39,85 @@ class GroupSettingTest extends TestCase $this->makeTestData(); } + public function testCastingMagic() + { + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = ''; + $settings->tax_rate1 = 0; + $s = new SettingsData(); + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("", $settings->tax_name1); + $settings = null; + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = "1"; + $settings->tax_rate1 = 0; + + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("1", $settings->tax_name1); + + $settings = $s->cast($settings)->toArray(); + $this->assertEquals("1", $settings['tax_name1']); + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = []; + $settings->tax_rate1 = 0; + + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("", $settings->tax_name1); + + $settings = $s->cast($settings)->toArray(); + $this->assertEquals("", $settings['tax_name1']); + + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = new \stdClass; + $settings->tax_rate1 = 0; + + $settings = $s->cast($settings)->toObject(); + + $this->assertEquals("", $settings->tax_name1); + + $settings = $s->cast($settings)->toArray(); + $this->assertEquals("", $settings['tax_name1']); + + + + // nlog(json_encode($settings)); + } + + public function testTaxNameInGroupFilters() + { + $settings = new \stdClass; + $settings->currency_id = '1'; + $settings->tax_name1 = ''; + $settings->tax_rate1 = 0; + + $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->assertEquals("", (string)NULL); + $this->assertNotNull($arr['data']['settings']['tax_name1']); + } + public function testAddGroupFilters() { diff --git a/tests/Feature/InvoiceEmailTest.php b/tests/Feature/InvoiceEmailTest.php index a08b306aa44d..23ba03236086 100644 --- a/tests/Feature/InvoiceEmailTest.php +++ b/tests/Feature/InvoiceEmailTest.php @@ -32,6 +32,8 @@ class InvoiceEmailTest extends TestCase use DatabaseTransactions; use GeneratesCounter; + public $faker; + protected function setUp() :void { parent::setUp(); @@ -48,6 +50,14 @@ class InvoiceEmailTest extends TestCase } + public function testInvalidEmailParsing() + { + $email = 'illegal@example.com'; + + $this->assertTrue(strpos($email, '@example.com') !== false); + } + + public function testClientEmailHistory() { $system_log = new SystemLog(); diff --git a/tests/Feature/LiveDesignTest.php b/tests/Feature/LiveDesignTest.php index 03ce04551e54..7dc750f10902 100644 --- a/tests/Feature/LiveDesignTest.php +++ b/tests/Feature/LiveDesignTest.php @@ -13,7 +13,9 @@ namespace Tests\Feature; use Tests\TestCase; use App\Models\Design; +use App\Utils\HtmlEngine; use Tests\MockAccountData; +use App\Models\InvoiceInvitation; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -41,6 +43,30 @@ class LiveDesignTest extends TestCase } } + public function testSyntheticInvitations() + { + $this->assertGreaterThanOrEqual(1, $this->client->contacts->count()); + + $ii = InvoiceInvitation::factory() + ->for($this->invoice) + ->for($this->client->contacts->first(), 'contact') + ->for($this->company) + ->for($this->user) + ->make(); + + $this->assertInstanceOf(InvoiceInvitation::class, $ii); + + $engine = new HtmlEngine($ii); + + $this->assertNotNull($engine); + + $data = $engine->generateLabelsAndValues(); + + $this->assertIsArray($data); + + nlog($data); + } + public function testDesignRoute200() { $data = [ diff --git a/tests/Unit/GroupSettingsTest.php b/tests/Unit/GroupSettingsTest.php index 6dd5b662eba0..4874753e5fa2 100644 --- a/tests/Unit/GroupSettingsTest.php +++ b/tests/Unit/GroupSettingsTest.php @@ -28,6 +28,10 @@ class GroupSettingsTest extends TestCase use DatabaseTransactions; use ClientGroupSettingsSaver; + public $company_settings; + public $client_settings; + public $settings; + protected function setUp() :void { parent::setUp();