1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-22 01:11:34 +02:00

Merge pull request #8903 from turbo124/v5-stable

Adjustment for missing props
This commit is contained in:
David Bomba 2023-10-25 13:48:47 +11:00 committed by GitHub
commit cce2fb78f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 8455 additions and 6843 deletions

View File

@ -1 +1 @@
5.7.30
5.7.33

View File

@ -11,16 +11,18 @@
namespace App\Console\Commands;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Migration\MaxCompanies;
use Faker\Factory;
use App\Models\User;
use App\Models\Account;
use App\Models\Company;
use App\Models\User;
use Faker\Factory;
use App\Mail\TestMailServer;
use Illuminate\Console\Command;
use App\Jobs\Mail\NinjaMailerJob;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Migration\MaxCompanies;
use Illuminate\Support\Facades\Mail;
class SendTestEmails extends Command
{
@ -55,39 +57,26 @@ class SendTestEmails extends Command
*/
public function handle()
{
$faker = Factory::create();
$account = Account::factory()->create();
$user = User::factory()->create([
'account_id' => $account->id,
'confirmation_code' => '123',
'email' => $faker->safeEmail(),
'first_name' => 'John',
'last_name' => 'Doe',
]);
$company = Company::factory()->create([
'account_id' => $account->id,
]);
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'permissions' => '',
'notifications' => CompanySettings::notificationDefaults(),
//'settings' => DefaultSettings::userSettings(),
'settings' => null,
]);
$to_user = User::first();
$nmo = new NinjaMailerObject;
$nmo->mailable = new MaxCompanies($user->account->companies()->first());
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first()->settings;
$nmo->to_user = $user;
$nmo->mailable = new TestMailServer('Email Server Works!', config('mail.from.address'));
$nmo->company = $to_user->account->companies()->first();
$nmo->settings = $to_user->account->companies()->first()->settings;
$nmo->to_user = $to_user;
(new NinjaMailerJob($nmo))->handle();
try {
Mail::raw("Test Message", function ($message) {
$message->to(config('mail.from.address'))
->from(config('mail.from.address'), config('mail.from.name'))
->subject('Test Email');
});
} catch(\Exception $e) {
$this->info("Error sending email: " . $e->getMessage());
}
}
}

View File

@ -0,0 +1,515 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Settings;
class SettingsData
{
public bool $auto_archive_invoice = false; // @implemented
public string $qr_iban = ''; //@implemented
public string $besr_id = ''; //@implemented
public string $lock_invoices = 'off'; // off, when_sent, when_paid //@implemented
public bool $enable_client_portal_tasks = false; //@ben to implement
public string $show_all_tasks_client_portal = 'invoiced'; // all, uninvoiced, invoiced
public bool $enable_client_portal_password = false; //@implemented
public bool $enable_client_portal = true; //@implemented
public bool $enable_client_portal_dashboard = false; // @TODO There currently is no dashboard, so this is pending
public bool $signature_on_pdf = false; //@implemented
public bool $document_email_attachment = false; //@TODO I assume this is 3rd party attachments on the entity to be included
public string $portal_design_id = '1'; //? @deprecated
public string $timezone_id = ''; //@implemented
public string $date_format_id = ''; //@implemented
public bool $military_time = false; // @TODO Implemented in Tasks only?
public string $language_id = ''; //@implemented
public bool $show_currency_code = false; //@implemented
public string $company_gateway_ids = ''; //@implemented
public string $currency_id = '1'; //@implemented
public string $custom_value1 = ''; //@implemented
public string $custom_value2 = ''; //@implemented
public string $custom_value3 = ''; //@implemented
public string $custom_value4 = ''; //@implemented
public float $default_task_rate = 0; // @TODO Where do we inject this?
public string $payment_terms = ''; //@implemented
public bool $send_reminders = true; //@TODO
public string $custom_message_dashboard = ''; // @TODO There currently is no dashboard, so this is pending
public string $custom_message_unpaid_invoice = '';
public string $custom_message_paid_invoice = '';
public string $custom_message_unapproved_quote = '';
public bool $auto_archive_quote = false; //@implemented
public bool $auto_convert_quote = true; //@implemented
public bool $auto_email_invoice = true; //@only used for Recurring Invoices, if set to false, we never send?
public int $entity_send_time = 6;
public bool $inclusive_taxes = false; //@implemented
public string $quote_footer = ''; //@implemented
public object $translations;
public string $counter_number_applied = 'when_saved'; // when_saved, when_sent //@implemented
public string $quote_number_applied = 'when_saved'; // when_saved, when_sent //@implemented
public string $invoice_number_pattern = ''; //@implemented
public int $invoice_number_counter = 1; //@implemented
public string $recurring_invoice_number_pattern = ''; //@implemented
public int $recurring_invoice_number_counter = 1; //@implemented
public string $quote_number_pattern = ''; //@implemented
public int $quote_number_counter = 1; //@implemented
public string $client_number_pattern = ''; //@implemented
public int $client_number_counter = 1; //@implemented
public string $credit_number_pattern = ''; //@implemented
public int $credit_number_counter = 1; //@implemented
public string $task_number_pattern = ''; //@implemented
public int $task_number_counter = 1; //@implemented
public string $expense_number_pattern = ''; //@implemented
public int $expense_number_counter = 1; //@implemented
public string $recurring_expense_number_pattern = '';
public int $recurring_expense_number_counter = 1;
public string $recurring_quote_number_pattern = '';
public int $recurring_quote_number_counter = 1;
public string $vendor_number_pattern = ''; //@implemented
public int $vendor_number_counter = 1; //@implemented
public string $ticket_number_pattern = ''; //@implemented
public int $ticket_number_counter = 1; //@implemented
public string $payment_number_pattern = ''; //@implemented
public int $payment_number_counter = 1; //@implemented
public string $project_number_pattern = ''; //@implemented
public int $project_number_counter = 1; //@implemented
public string $purchase_order_number_pattern = ''; //@implemented
public int $purchase_order_number_counter = 1; //@implemented
public bool $shared_invoice_quote_counter = false; //@implemented
public bool $shared_invoice_credit_counter = false; //@implemented
public string $recurring_number_prefix = ''; //@implemented
public string $reset_counter_frequency_id = '0'; //@implemented
public string $reset_counter_date = ''; //@implemented
public int $counter_padding = 4; //@implemented
public string $auto_bill = 'off'; // off, always, opt-in, opt-out //@implemented
public string $auto_bill_date = 'on_due_date'; // on_due_date, on_send_date //@implemented
public string $invoice_terms = ''; //@implemented
public string $quote_terms = ''; //@implemented
public int $invoice_taxes = 0; // ? used in AP only?
public string $invoice_design_id = 'Wpmbk5ezJn'; //@implemented
public string $quote_design_id = 'Wpmbk5ezJn'; //@implemented
public string $credit_design_id = 'Wpmbk5ezJn'; //@implemented
public string $purchase_order_design_id = 'Wpmbk5ezJn';
public string $purchase_order_footer = ''; //@implemented
public string $purchase_order_terms = ''; //@implemented
public string $purchase_order_public_notes = ''; //@implemented
public bool $require_purchase_order_signature = false; //@TODO ben to confirm
public string $invoice_footer = ''; //@implemented
public string $credit_footer = ''; //@implemented
public string $credit_terms = ''; //@implemented
public string $invoice_labels = ''; //@TODO used in AP only?
public string $tax_name1 = ''; //@TODO where do we use this?
public float $tax_rate1 = 0; //@TODO where do we use this?
public string $tax_name2 = ''; //@TODO where do we use this?
public float $tax_rate2 = 0; //@TODO where do we use this?
public string $tax_name3 = ''; //@TODO where do we use this?
public float $tax_rate3 = 0; //@TODO where do we use this?
public string $payment_type_id = '0'; //@TODO where do we use this?
public string $valid_until = ''; //@implemented
public bool $show_accept_invoice_terms = false; //@TODO ben to confirm
public bool $show_accept_quote_terms = false; //@TODO ben to confirm
public string $email_sending_method = 'default'; // enum 'default', 'gmail', 'office365', 'client_postmark', 'client_mailgun' //@implemented
public string $gmail_sending_user_id = '0'; //@implemented
public string $reply_to_email = ''; //@implemented
public string $reply_to_name = ''; //@implemented
public string $bcc_email = ''; //@TODO
public bool $pdf_email_attachment = false; //@implemented
public bool $ubl_email_attachment = false; //@implemented
public string $email_style = 'light'; // plain, light, dark, custom //@implemented
public string $email_style_custom = ''; // the template itself //@implemented
public string $email_subject_invoice = ''; //@implemented
public string $email_subject_quote = ''; //@implemented
public string $email_subject_credit = ''; //@implemented
public string $email_subject_payment = ''; //@implemented
public string $email_subject_payment_partial = ''; //@implemented
public string $email_subject_statement = ''; //@implemented
public string $email_subject_purchase_order = ''; //@implemented
public string $email_template_purchase_order = ''; //@implemented
public string $email_template_invoice = ''; //@implemented
public string $email_template_credit = ''; //@implemented
public string $email_template_quote = ''; //@implemented
public string $email_template_payment = ''; //@implemented
public string $email_template_payment_partial = ''; //@implemented
public string $email_template_statement = ''; //@implemented
public string $email_subject_reminder1 = ''; //@implemented
public string $email_subject_reminder2 = ''; //@implemented
public string $email_subject_reminder3 = ''; //@implemented
public string $email_subject_reminder_endless = ''; //@implemented
public string $email_template_reminder1 = ''; //@implemented
public string $email_template_reminder2 = ''; //@implemented
public string $email_template_reminder3 = ''; //@implemented
public string $email_template_reminder_endless = ''; //@implemented
public string $email_signature = ''; //@implemented
public bool $enable_email_markup = true; //@TODO -
public string $email_subject_custom1 = ''; //@TODO
public string $email_subject_custom2 = ''; //@TODO
public string $email_subject_custom3 = ''; //@TODO
public string $email_template_custom1 = ''; //@TODO
public string $email_template_custom2 = ''; //@TODO
public string $email_template_custom3 = ''; //@TODO
public bool $enable_reminder1 = false; //@implmemented
public bool $enable_reminder2 = false; //@implmemented
public bool $enable_reminder3 = false; //@implmemented
public bool $enable_reminder_endless = false; //@implmemented
public int $num_days_reminder1 = 0; //@implmemented
public int $num_days_reminder2 = 0; //@implmemented
public int $num_days_reminder3 = 0; //@implmemented
public string $schedule_reminder1 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) implmemented
public string $schedule_reminder2 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) implmemented
public string $schedule_reminder3 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) implmemented
public int $reminder_send_time = 0; // number of seconds from UTC +0 to send reminders @TODO
public float $late_fee_amount1 = 0; //@implemented
public float $late_fee_amount2 = 0; //@implemented
public float $late_fee_amount3 = 0; //@implemented
public float $late_fee_percent1 = 0; //@implemented
public float $late_fee_percent2 = 0; //@implemented
public float $late_fee_percent3 = 0; //@implemented
public string $endless_reminder_frequency_id = '0'; //@implemented
public float $late_fee_endless_amount = 0; //@implemented
public float $late_fee_endless_percent = 0; //@implemented
public bool $client_online_payment_notification = true; //@todo implement in notifications check this bool prior to sending payment notification to client
public bool $client_manual_payment_notification = true; //@todo implement in notifications check this bool prior to sending manual payment notification to client
public string $name = ''; //@implemented
public string $company_logo = ''; //@implemented
public string $website = ''; //@implemented
public string $address1 = ''; //@implemented
public string $address2 = ''; //@implemented
public string $city = ''; //@implemented
public string $state = ''; //@implemented
public string $postal_code = ''; //@implemented
public string $phone = ''; //@implemented
public string $email = ''; //@implemented
public string $country_id; //@implemented
public string $vat_number = ''; //@implemented
public string $id_number = ''; //@implemented
public string $page_size = 'A4'; // Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6
public string $page_layout = 'portrait';
public int $font_size = 16; //@implemented
public string $primary_font = 'Roboto';
public string $secondary_font = 'Roboto';
public string $primary_color = '#298AAB';
public string $secondary_color = '#7081e0';
public bool $page_numbering = false;
public string $page_numbering_alignment = 'C'; // C, R, L
public bool $hide_paid_to_date = false; //@TODO where?
public bool $embed_documents = false; //@TODO where?
public bool $all_pages_header = false; //@deprecated 31-05-2021
public bool $all_pages_footer = false; //@deprecated 31-05-2021
public string $pdf_variables = ''; //@implemented
public string $portal_custom_head = ''; //@TODO @BEN
public string $portal_custom_css = ''; //@TODO @BEN
public string $portal_custom_footer = ''; //@TODO @BEN
public string $portal_custom_js = ''; //@TODO @BEN
public bool $client_can_register = false; //@deprecated 04/06/2021
public string $client_portal_terms = ''; //@TODO @BEN
public string $client_portal_privacy_policy = ''; //@TODO @BEN
public bool $client_portal_enable_uploads = false; //@implemented
public bool $client_portal_allow_under_payment = false; //@implemented
public float $client_portal_under_payment_minimum = 0; //@implemented
public bool $client_portal_allow_over_payment = false; //@implemented
public string $use_credits_payment = 'off'; // always, option, off //@implemented
public bool $hide_empty_columns_on_pdf = false;
public string $email_from_name = '';
public bool $auto_archive_invoice_cancelled = false;
public bool $vendor_portal_enable_uploads = false;
public bool $send_email_on_mark_paid = false;
public string $postmark_secret = '';
public string $custom_sending_email = '';
public string $mailgun_secret = '';
public string $mailgun_domain = '';
public string $mailgun_endpoint = 'api.mailgun.net'; // api.eu.mailgun.net
public bool $auto_bill_standard_invoices = false;
public string $email_alignment = 'center'; // center, left, right
public bool $show_email_footer = true;
public string $company_logo_size = '';
public bool $show_paid_stamp = false;
public bool $show_shipping_address = false;
public bool $accept_client_input_quote_approval = false;
public bool $allow_billable_task_items = true;
public bool $show_task_item_description = false;
public bool $client_initiated_payments = false;
public float $client_initiated_payments_minimum = 0;
public bool $sync_invoice_quote_columns = true;
public string $e_invoice_type = 'EN16931';
public string $default_expense_payment_type_id = '0';
public bool $enable_e_invoice = false;
public string $classification = '';
private mixed $object;
public function cast(mixed $object)
{
if(is_array($object))
$object = (object)$object;
if (is_object($object)) {
foreach ($object as $key => $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;
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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];
}

View File

@ -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);

View File

@ -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')

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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')

View File

@ -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;
}

View File

@ -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'])) {

View File

@ -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)

View File

@ -30,6 +30,7 @@ class BankIntegrationFactory
$bank_integration->bank_account_type = '';
$bank_integration->balance = 0;
$bank_integration->currency = '';
$bank_integration->auto_sync = 1;
return $bank_integration;
}

View File

@ -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;
}

View File

@ -136,8 +136,13 @@ class TaskFilters extends QueryFilters
$status_parameters = explode(',', $value);
if(count($status_parameters) >= 1)
$this->builder->whereIn('status_id', $this->transformKeys($status_parameters));
if(count($status_parameters) >= 1){
$this->builder->where(function ($query) use ($status_parameters) {
$query->whereIn('status_id', $this->transformKeys($status_parameters))->whereNull('invoice_id');
});
}
return $this->builder;
}

View File

@ -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,

View File

@ -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
<rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>";
} 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)

View File

@ -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());

View File

@ -87,7 +87,8 @@ class YodleeController extends BaseController
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->from_date = now()->subYear();
$bank_integration->auto_sync = true;
$bank_integration->save();
}
}
@ -166,8 +167,8 @@ class YodleeController extends BaseController
public function refreshWebhook(Request $request)
{
//we should ignore this one
nlog("yodlee refresh");
nlog($request->all());
// nlog("yodlee refresh");
// nlog($request->all());
return response()->json(['message' => 'Success'], 200);
@ -236,8 +237,8 @@ class YodleeController extends BaseController
public function refreshUpdatesWebhook(Request $request)
{
//notifies a user if there are problems with yodlee accessing the data
nlog("update refresh");
nlog($request->all());
// nlog("update refresh");
// nlog($request->all());
return response()->json(['message' => 'Success'], 200);

View File

@ -230,6 +230,8 @@ class BankIntegrationController extends BaseController
$bank_integration->nickname = $account['nickname'];
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->auto_sync = true;
$bank_integration->save();
}
}

View File

@ -14,6 +14,9 @@ namespace App\Http\Controllers;
use App\Utils\Ninja;
use App\Models\Client;
use App\Models\Account;
use App\Models\Company;
use App\Models\SystemLog;
use Postmark\PostmarkClient;
use Illuminate\Http\Response;
use App\Factory\ClientFactory;
use App\Filters\ClientFilters;
@ -36,6 +39,8 @@ use App\Http\Requests\Client\CreateClientRequest;
use App\Http\Requests\Client\UpdateClientRequest;
use App\Http\Requests\Client\UploadClientRequest;
use App\Http\Requests\Client\DestroyClientRequest;
use App\Http\Requests\Client\ReactivateClientEmailRequest;
use App\Jobs\PostMark\ProcessPostmarkWebhook;
/**
* Class ClientController.
@ -219,7 +224,7 @@ class ClientController extends BaseController
}
});
return $this->listResponse(Client::withTrashed()->company()->whereIn('id', $request->ids));
return $this->listResponse(Client::query()->withTrashed()->company()->whereIn('id', $request->ids));
}
/**
@ -308,8 +313,66 @@ 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());
}
/**
* Reactivate a client email
*
* @param ReactivateClientEmailRequest $request
* @param string $bounce_id //could also be the invitationId
* @return \Illuminate\Http\JsonResponse
*/
public function reactivateEmail(ReactivateClientEmailRequest $request, string $bounce_id)
{
/** @var \App\Models\User $user */
$user = auth()->user();
if(stripos($bounce_id, '-') !== false){
$log =
SystemLog::query()
->where('company_id', $user->company()->id)
->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE)
->where('category_id', SystemLog::CATEGORY_MAIL)
->whereJsonContains('log', ['MessageID' => $bounce_id])
->orderBy('id', 'desc')
->first();
$resolved_bounce_id = false;
if($log && ($log?->log['ID'] ?? false)){
$resolved_bounce_id = $log->log['ID'] ?? false;
}
if(!$resolved_bounce_id){
$ppwebhook = new ProcessPostmarkWebhook([]);
$resolved_bounce_id = $ppwebhook->getBounceId($bounce_id);
}
if(!$resolved_bounce_id){
return response()->json(['message' => 'Bounce ID not found'], 400);
}
$bounce_id = $resolved_bounce_id;
}
$postmark = new PostmarkClient(config('services.postmark.token'));
try {
$response = $postmark->activateBounce((int)$bounce_id);
return response()->json(['message' => 'Success'], 200);
}
catch(\Exception $e){
return response()->json(['message' => $e->getMessage(), 400]);
}
}
}

View File

@ -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);

View File

@ -62,7 +62,7 @@ class PostMarkController extends BaseController
public function webhook(Request $request)
{
if ($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('services.postmark.token')) {
ProcessPostmarkWebhook::dispatch($request->all());
ProcessPostmarkWebhook::dispatch($request->all())->delay(10);
return response()->json(['message' => 'Success'], 200);
}

View File

@ -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,181 @@ 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 = empty($entity_obj->footer) ? $settings->{$entity_prop."_footer"} : $entity_obj->footer;
$entity_obj->terms = empty($entity_obj->term) ? $settings->{$entity_prop."_terms"} : $entity_obj->terms;
$entity_obj->public_notes = empty($entity_obj->public_notes) ? $request->getClient()->public_notes : $entity_obj->public_notes;
$invitation->{$request->entity} = $entity_obj;
}
if(empty($entity_obj->design_id))
$entity_obj->design_id = intval($this->decodePrimaryKey($settings->{$entity_prop."_design_id"}));
/** 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 +239,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 +296,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 +457,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 +476,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 +494,7 @@ class PreviewController extends BaseController
if ($numbered_pdf) {
$pdf = $numbered_pdf;
}
$this->purgeCache();
return $pdf;
}
@ -335,7 +510,7 @@ class PreviewController extends BaseController
$response->header('Content-Type', 'application/pdf');
$response->header('Server-Timing', microtime(true)-$start);
$this->purgeCache();
return $response;
}
@ -523,7 +698,6 @@ class PreviewController extends BaseController
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
}

View File

@ -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) {

View File

@ -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);
}

View File

@ -117,6 +117,7 @@ class StripeConnectController extends BaseController
'refresh_token' => $response->refresh_token,
'access_token' => $response->access_token,
'appleDomainVerification' => '',
// "statementDescriptor" => "",
];
$company_gateway->setConfig($payload);

View File

@ -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');
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Client;
use App\Http\Requests\Request;
class ReactivateClientEmailRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
}

View File

@ -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',
];

View File

@ -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);
}
}

View File

@ -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'];
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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'])) {

View File

@ -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()

View File

@ -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;
}
}
}

View File

@ -20,7 +20,7 @@ class Request extends FormRequest
use MakesHash;
use RuntimeFormRequest;
protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml,zip,csv|max:100000';
protected $file_validation = 'sometimes|file|mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml,zip,csv,ods,odt,odp|max:100000';
/**
* Get the validation rules that apply to the request.
*
@ -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 {

View File

@ -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()

View File

@ -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 = '';

View File

@ -83,11 +83,21 @@ class EmailPayment implements ShouldQueue
$invitation = null;
$nmo = new NinjaMailerObject;
if ($this->payment->invoices && $this->payment->invoices->count() >= 1) {
$invitation = $this->payment->invoices->first()->invitations()->first();
if($this->contact){
$invitation = $this->payment->invoices->first()->invitations()->where('client_contact_id', $this->contact->id)->first();
}
else
$invitation = $this->payment->invoices->first()->invitations()->first();
if($invitation)
$nmo->invitation = $invitation;
}
$nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($email_builder, $this->contact, $invitation);
$nmo->to_user = $this->contact;
$nmo->settings = $this->settings;

View File

@ -88,11 +88,22 @@ class EmailRefundPayment implements ShouldQueue
$invitation = null;
$nmo = new NinjaMailerObject;
if ($this->payment->invoices && $this->payment->invoices->count() >= 1) {
$invitation = $this->payment->invoices->first()->invitations()->first();
if($this->contact) {
$invitation = $this->payment->invoices->first()->invitations()->where('client_contact_id', $this->contact->id)->first();
} else {
$invitation = $this->payment->invoices->first()->invitations()->first();
}
if($invitation)
$nmo->invitation = $invitation;
}
$nmo = new NinjaMailerObject;
$nmo->mailable = new TemplateEmail($email_builder, $this->contact, $invitation);
$nmo->to_user = $this->contact;
$nmo->settings = $this->settings;

View File

@ -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')) {
@ -294,6 +339,32 @@ class ProcessPostmarkWebhook implements ShouldQueue
}
}
public function getRawMessage(string $message_id)
{
$postmark = new PostmarkClient(config('services.postmark.token'));
$messageDetail = $postmark->getOutboundMessageDetails($message_id);
return $messageDetail;
}
public function getBounceId(string $message_id): ?int
{
$messageDetail = $this->getRawMessage($message_id);
$event = collect($messageDetail->messageevents)->first(function ($event) {
return $event?->Details?->BounceID ?? false;
});
return $event?->Details?->BounceID ?? null;
}
private function fetchMessage(): array
{
if(strlen($this->request['MessageID']) < 1){
@ -311,6 +382,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
$events = collect($messageDetail->messageevents)->map(function ($event) {
return [
'bounce_id' => $event?->Details?->BounceID ?? '',
'recipient' => $event->Recipient ?? '',
'status' => $event->Type ?? '',
'delivery_message' => $event->Details->DeliveryMessage ?? $event->Details->Summary ?? '',

View File

@ -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()

View File

@ -41,6 +41,7 @@ class MailSentListener implements ShouldQueue
*/
public function handle(MessageSent $event)
{
if (!Ninja::isHosted()) {
return;
}

View File

@ -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,

View File

@ -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("<br>", "", $this->build_email->getSubject()))

View File

@ -17,7 +17,6 @@ use Illuminate\Queue\SerializesModels;
class TestMailServer extends Mailable
{
// use Queueable, SerializesModels;
public $support_messages;

View File

@ -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', [

View File

@ -306,7 +306,7 @@ class Client extends BaseModel implements HasLocalePreference
return $this->hasMany(ClientContact::class)->where('is_primary', true);
}
public function company() :BelongsTo
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}

View File

@ -786,6 +786,26 @@ class Company extends BaseModel
return $this->hasMany(CompanyUser::class)->withTrashed();
}
public function invoice_invitations(): HasMany
{
return $this->hasMany(InvoiceInvitation::class);
}
public function quote_invitations(): HasMany
{
return $this->hasMany(QuoteInvitation::class);
}
public function credit_invitations(): HasMany
{
return $this->hasMany(CreditInvitation::class);
}
public function purchase_order_invitations(): HasMany
{
return $this->hasMany(PurchaseOrderInvitation::class);
}
/**
* @return \App\Models\User|null
*/

View File

@ -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;
}

View File

@ -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',

View File

@ -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 {

View File

@ -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);

View File

@ -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');
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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)));
}
}

View File

@ -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' => [

View File

@ -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);
}

View File

@ -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 {

View File

@ -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;

View File

@ -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
*

View File

@ -33,6 +33,7 @@ class CreditInvitationTransformer extends EntityTransformer
'created_at' => (int) $invitation->created_at,
'email_status' => $invitation->email_status ?: '',
'email_error' => (string) $invitation->email_error,
'message_id' => (string) $invitation->message_id ?: '',
];
}
}

View File

@ -33,6 +33,7 @@ class InvoiceInvitationTransformer extends EntityTransformer
'created_at' => (int) $invitation->created_at,
'email_status' => $invitation->email_status ?: '',
'email_error' => (string) $invitation->email_error,
'message_id' => (string) $invitation->message_id ?: '',
];
}
}

View File

@ -24,6 +24,7 @@ class PurchaseOrderInvitationTransformer extends EntityTransformer
'created_at' => (int) $invitation->created_at,
'email_status' => $invitation->email_status ?: '',
'email_error' => (string) $invitation->email_error,
'message_id' => (string) $invitation->message_id ?: '',
];
}
}

View File

@ -33,6 +33,7 @@ class QuoteInvitationTransformer extends EntityTransformer
'created_at' => (int) $invitation->created_at,
'email_status' => $invitation->email_status ?: '',
'email_error' => (string) $invitation->email_error,
'message_id' => (string) $invitation->message_id ?: '',
];
}
}

View File

@ -33,6 +33,7 @@ class RecurringInvoiceInvitationTransformer extends EntityTransformer
'created_at' => (int) $invitation->created_at,
'email_status' => $invitation->email_status ?: '',
'email_error' => (string) $invitation->email_error,
'message_id' => (string) $invitation->message_id ?: '',
];
}
}

View File

@ -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,
];
}

View File

@ -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 = '<br><br>';
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) . "<br>";
}
$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;
}

View File

@ -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)

View File

@ -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;
}
/**

View File

@ -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', ''),

View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class RecurringInvoiceInvitationFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'key' => Str::random(40),
];
}
}

View File

@ -0,0 +1,43 @@
<?php
use App\Models\Currency;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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
{
//
}
};

View File

@ -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) {

View File

@ -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.<br/>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;

File diff suppressed because it is too large Load Diff

View File

@ -653,5 +653,91 @@
$ref: '#/components/responses/429'
5XX:
description: 'Server error'
default:
$ref: '#/components/responses/default'
/api/v1/reactivate_email/{bounce_id}:
post:
tags:
- clients
summary: 'Removes email suppression of a user in the system'
description: 'Emails are suppressed by PostMark, when they receive a Hard bounce / Spam Complaint. This endpoint allows you to remove the suppression and send emails to the user again.'
operationId: reactivateEmail
parameters:
- $ref: '#/components/parameters/X-API-TOKEN'
- $ref: '#/components/parameters/X-Requested-With'
- $ref: '#/components/parameters/include'
- name: bounce_id
in: path
description: 'The postmark Bounce ID reference'
required: true
schema:
type: string
format: string
example: 123243
responses:
200:
description: 'Success'
headers:
X-MINIMUM-CLIENT-VERSION:
$ref: '#/components/headers/X-MINIMUM-CLIENT-VERSION'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
400:
description: 'Postmark exception - generated if the suppression cannot be removed for any reason'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
422:
$ref: '#/components/responses/422'
429:
$ref: '#/components/responses/429'
5XX:
description: 'Server error'
default:
$ref: '#/components/responses/default'
/api/v1/clients/{client}/updateTaxData:
post:
tags:
- clients
summary: 'Update tax data'
description: 'Updates the clients tax data - if their address has changed'
operationId: updateClientTaxData
parameters:
- $ref: '#/components/parameters/X-API-TOKEN'
- $ref: '#/components/parameters/X-Requested-With'
- $ref: '#/components/parameters/include'
- name: client
in: path
description: 'The Client Hashed ID reference'
required: true
schema:
type: string
format: string
example: V2J234DFA
responses:
200:
description: 'Success'
headers:
X-MINIMUM-CLIENT-VERSION:
$ref: '#/components/headers/X-MINIMUM-CLIENT-VERSION'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
400:
description: 'Postmark exception - generated if the suppression cannot be removed for any reason'
401:
$ref: '#/components/responses/401'
403:
$ref: '#/components/responses/403'
422:
$ref: '#/components/responses/422'
429:
$ref: '#/components/responses/429'
5XX:
description: 'Server error'
default:
$ref: '#/components/responses/default'

View File

@ -1,4 +1,4 @@
@if ($entity->documents->count() > 0 || $entity->company->documents->count() > 0 || ($entity->expense && $entity->expense->invoice_documents) || ($entity->task && $entity->company->invoice_task_documents))
@if ($entity->documents()->where('is_public',1)->count() > 0 || $entity->company->documents()->where('is_public',1)->count() > 0 || ($entity->expense && $entity->expense->invoice_documents) || ($entity->task && $entity->company->invoice_task_documents))
<div class="bg-white shadow sm:rounded-lg my-4">
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">

View File

@ -64,7 +64,7 @@
{{ ctrans('texts.public_notes') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $invoice->public_notes }}
{!! html_entity_decode($invoice->public_notes) !!}
</dd>
@else
<dt class="text-sm font-medium leading-5 text-gray-500">

View File

@ -164,6 +164,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('clients/{client}/{mergeable_client}/merge', [ClientController::class, 'merge'])->name('clients.merge')->middleware('password_protected');
Route::post('clients/bulk', [ClientController::class, 'bulk'])->name('clients.bulk');
Route::post('reactivate_email/{bounce_id}', [ClientController::class, 'reactivateEmail'])->name('clients.reactivate_email');
Route::post('filters/{entity}', [FilterController::class, 'index'])->name('filters');
Route::resource('client_gateway_tokens', ClientGatewayTokenController::class);

View File

@ -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()
{

View File

@ -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'));

View File

@ -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()
{

View File

@ -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();

View File

@ -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 = [

View File

@ -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();