diff --git a/VERSION.txt b/VERSION.txt index 67fd02cd7eea..2fab05581196 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.11 \ No newline at end of file +5.7.12 \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 9da20b1020c9..c66cdc40e5a9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -11,28 +11,29 @@ namespace App\Console; -use App\Jobs\Cron\AutoBillCron; -use App\Jobs\Cron\RecurringExpensesCron; -use App\Jobs\Cron\RecurringInvoicesCron; -use App\Jobs\Cron\SubscriptionCron; -use App\Jobs\Cron\UpdateCalculatedFields; -use App\Jobs\Invoice\InvoiceCheckLateWebhook; -use App\Jobs\Ninja\AdjustEmailQuota; -use App\Jobs\Ninja\BankTransactionSync; -use App\Jobs\Ninja\CompanySizeCheck; +use App\Utils\Ninja; +use App\Models\Account; use App\Jobs\Ninja\QueueSize; -use App\Jobs\Ninja\SystemMaintenance; -use App\Jobs\Ninja\TaskScheduler; -use App\Jobs\Quote\QuoteCheckExpired; -use App\Jobs\Subscription\CleanStaleInvoiceOrder; use App\Jobs\Util\DiskCleanup; use App\Jobs\Util\ReminderJob; -use App\Jobs\Util\SchedulerCheck; -use App\Jobs\Util\UpdateExchangeRates; +use App\Jobs\Cron\AutoBillCron; use App\Jobs\Util\VersionCheck; -use App\Models\Account; -use App\Utils\Ninja; +use App\Jobs\Ninja\TaskScheduler; +use App\Jobs\Util\SchedulerCheck; +use App\Jobs\Ninja\CheckACHStatus; +use App\Jobs\Cron\SubscriptionCron; +use App\Jobs\Ninja\AdjustEmailQuota; +use App\Jobs\Ninja\CompanySizeCheck; +use App\Jobs\Ninja\SystemMaintenance; +use App\Jobs\Quote\QuoteCheckExpired; +use App\Jobs\Util\UpdateExchangeRates; +use App\Jobs\Ninja\BankTransactionSync; +use App\Jobs\Cron\RecurringExpensesCron; +use App\Jobs\Cron\RecurringInvoicesCron; +use App\Jobs\Cron\UpdateCalculatedFields; use Illuminate\Console\Scheduling\Schedule; +use App\Jobs\Invoice\InvoiceCheckLateWebhook; +use App\Jobs\Subscription\CleanStaleInvoiceOrder; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel @@ -109,6 +110,9 @@ class Kernel extends ConsoleKernel /* Pulls in bank transactions from third party services */ $schedule->job(new BankTransactionSync)->everyFourHours()->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer(); + /* Checks ACH verification status and updates state to authorize when verified */ + $schedule->job(new CheckACHStatus)->everySixHours()->withoutOverlapping()->name('ach-status-job')->onOneServer(); + $schedule->command('ninja:check-data --database=db-ninja-01')->dailyAt('02:10')->withoutOverlapping()->name('check-data-db-1-job')->onOneServer(); $schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:20')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer(); diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 99638a0fa981..e4e8ff3085c5 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -481,8 +481,11 @@ class CompanySettings extends BaseSettings public $enable_e_invoice = false; + public $classification = ''; // individual, company, partnership, trust, charity, government, other + public static $casts = [ 'enable_e_invoice' => 'bool', + 'classification' => 'string', 'default_expense_payment_type_id' => 'string', 'e_invoice_type' => 'string', 'mailgun_endpoint' => 'string', diff --git a/app/Export/CSV/ActivityExport.php b/app/Export/CSV/ActivityExport.php index 4ae02fb63e4e..7775d55dc879 100644 --- a/app/Export/CSV/ActivityExport.php +++ b/app/Export/CSV/ActivityExport.php @@ -57,9 +57,11 @@ class ActivityExport extends BaseExport return ['identifier' => $value, 'display_value' => $headerdisplay[$value]]; })->toArray(); - $report = $query->cursor() - ->map(function ($credit) { - return $this->buildActivityRow($credit); + + $report = $query->cursor() + ->map(function ($resource) { + $row = $this->buildActivityRow($resource); + return $this->processMetaData($row, $resource); })->toArray(); return array_merge(['columns' => $header], $report); @@ -70,6 +72,8 @@ class ActivityExport extends BaseExport return [ Carbon::parse($activity->created_at)->format($this->date_format), ctrans("texts.activity_{$activity->activity_type_id}",[ + 'payment_amount' => $activity->payment ? $activity->payment->amount : '', + 'adjustment' => $activity->payment ? $activity->payment->refunded : '', 'client' => $activity->client ? $activity->client->present()->name() : '', 'contact' => $activity->contact ? $activity->contact->present()->name() : '', 'quote' => $activity->quote ? $activity->quote->number : '', @@ -101,7 +105,7 @@ class ActivityExport extends BaseExport $this->date_format = DateFormat::find($this->company->settings->date_format_id)->format; - ksort($this->entity_keys); + // ksort($this->entity_keys); if (count($this->input['report_keys']) == 0) { $this->input['report_keys'] = array_values($this->entity_keys); @@ -146,4 +150,27 @@ class ActivityExport extends BaseExport { return $entity; } + + + public function processMetaData(array $row, $resource): array + { + + $clean_row = []; + + foreach (array_values($this->input['report_keys']) as $key => $value) { + + nlog("key: {$key}, value: {$value}"); + nlog($row); + $clean_row[$key]['entity'] = 'activity'; + $clean_row[$key]['id'] = $key; + $clean_row[$key]['hashed_id'] = null; + $clean_row[$key]['value'] = $row[$key]; + $clean_row[$key]['identifier'] = $value; + $clean_row[$key]['display_value'] = $row[$key]; + + } + + return $clean_row; + } + } diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 21ccedac0b14..db95dbb354b1 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -11,6 +11,7 @@ namespace App\Export\CSV; +use App\Models\Activity; use App\Models\Quote; use App\Utils\Number; use App\Models\Client; @@ -377,7 +378,7 @@ class BaseExport protected array $expense_report_keys = [ 'amount' => 'expense.amount', 'category' => 'expense.category_id', - 'client' => 'expense.client_id', + // 'client' => 'expense.client_id', 'custom_value1' => 'expense.custom_value1', 'custom_value2' => 'expense.custom_value2', 'custom_value3' => 'expense.custom_value3', @@ -591,31 +592,33 @@ class BaseExport $manager->setSerializer(new ArraySerializer()); $transformed_client = $manager->createData($transformed_client)->toArray(); - if($column == 'name') + if(in_array($column, ['client.name', 'name'])) return $transformed_client['display_name']; - if($column == 'user_id') + if(in_array($column, ['client.user_id', 'user_id'])) return $entity->client->user->present()->name(); - if($column == 'country_id') + if(in_array($column, ['client.assigned_user_id', 'assigned_user_id'])) + return $entity->client->assigned_user->present()->name(); + + if(in_array($column, ['client.country_id', 'country_id'])) return $entity->client->country ? ctrans("texts.country_{$entity->client->country->name}") : ''; - if($column == 'shipping_country_id') + if(in_array($column, ['client.shipping_country_id', 'shipping_country_id'])) return $entity->client->shipping_country ? ctrans("texts.country_{$entity->client->shipping_country->name}") : ''; - if($column == 'size_id') + if(in_array($column, ['client.size_id', 'size_id'])) return $entity->client->size?->name ?? ''; - if($column == 'industry_id') + if(in_array($column, ['client.industry_id', 'industry_id'])) return $entity->client->industry?->name ?? ''; - if ($column == 'currency_id') { + if (in_array($column, ['client.currency_id', 'currency_id'])) return $entity->client->currency() ? $entity->client->currency()->code : $entity->company->currency()->code; - } - - if($column == 'client.payment_terms') { + + if(in_array($column, ['payment_terms', 'client.payment_terms'])) return $entity->client->getSetting('payment_terms'); - } + if(array_key_exists($column, $transformed_client)) return $transformed_client[$column]; @@ -913,7 +916,7 @@ class BaseExport $helper = new Helpers(); $header = []; - + // nlog("header"); foreach ($this->input['report_keys'] as $value) { $key = array_search($value, $this->entity_keys); @@ -960,6 +963,9 @@ class BaseExport if(!$key) { $prefix = ctrans('texts.expense')." "; $key = array_search($value, $this->expense_report_keys); + + if(!$key && $value == 'expense.category') + $key = 'category'; } if(!$key) { @@ -986,6 +992,8 @@ class BaseExport $prefix = ''; } + // nlog("key => {$key}"); + $key = str_replace('item.', '', $key); $key = str_replace('recurring_invoice.', '', $key); $key = str_replace('purchase_order.', '', $key); @@ -1018,10 +1026,13 @@ class BaseExport } } - elseif(count($parts) == 2 && stripos($parts[0], 'contact') !== false) { + elseif(count($parts) == 2 && (stripos($parts[0], 'vendor_contact') !== false || stripos($parts[0], 'contact') !== false)) { + $parts[0] = str_replace('vendor_contact', 'contact', $parts[0]); + $entity = "contact".substr($parts[1], -1); $custom_field_string = strlen($helper->makeCustomField($this->company->custom_fields, $entity)) > 1 ? $helper->makeCustomField($this->company->custom_fields, $entity) : ctrans("texts.{$parts[1]}"); $header[] = ctrans("texts.{$parts[0]}") . " " . $custom_field_string; + } elseif(count($parts) == 2 && in_array(substr($original_key, 0, -1), ['credit','quote','invoice','purchase_order','recurring_invoice','task'])){ $custom_field_string = strlen($helper->makeCustomField($this->company->custom_fields, "product".substr($original_key,-1))) > 1 ? $helper->makeCustomField($this->company->custom_fields, "product".substr($original_key,-1)) : ctrans("texts.{$parts[1]}"); @@ -1092,8 +1103,6 @@ class BaseExport } - nlog($clean_row); - return $clean_row; } diff --git a/app/Export/CSV/ExpenseExport.php b/app/Export/CSV/ExpenseExport.php index 32eaf9b6ca60..6de23398163f 100644 --- a/app/Export/CSV/ExpenseExport.php +++ b/app/Export/CSV/ExpenseExport.php @@ -159,6 +159,10 @@ class ExpenseExport extends BaseExport $entity['expense.assigned_user'] = $expense->assigned_user ? $expense->assigned_user->present()->name() : ''; } + if (in_array('expense.category_id', $this->input['report_keys'])) { + $entity['expense.category_id'] = $expense->category ? $expense->category->name : ''; + } + return $entity; } } diff --git a/app/Export/CSV/VendorExport.php b/app/Export/CSV/VendorExport.php index 7f35f96e5425..74e9785b2f40 100644 --- a/app/Export/CSV/VendorExport.php +++ b/app/Export/CSV/VendorExport.php @@ -55,7 +55,7 @@ class VendorExport extends BaseExport if (count($this->input['report_keys']) == 0) { $this->input['report_keys'] = array_values($this->vendor_report_keys); } - + $query = Vendor::query()->with('contacts') ->withTrashed() ->where('company_id', $this->company->id) diff --git a/app/Factory/ClientFactory.php b/app/Factory/ClientFactory.php index 37d23311add5..fbb6f36ab528 100644 --- a/app/Factory/ClientFactory.php +++ b/app/Factory/ClientFactory.php @@ -32,7 +32,8 @@ class ClientFactory $client->is_deleted = 0; $client->client_hash = Str::random(40); $client->settings = ClientSettings::defaults(); - + $client->classification = ''; + return $client; } } diff --git a/app/Factory/RecurringExpenseFactory.php b/app/Factory/RecurringExpenseFactory.php index cc92a45811e3..15d000334c17 100644 --- a/app/Factory/RecurringExpenseFactory.php +++ b/app/Factory/RecurringExpenseFactory.php @@ -34,7 +34,8 @@ class RecurringExpenseFactory $recurring_expense->tax_amount1 = 0; $recurring_expense->tax_amount2 = 0; $recurring_expense->tax_amount3 = 0; - $recurring_expense->date = null; + $recurring_expense->date = now()->format('Y-m-d'); + $recurring_expense->next_send_date = now()->format('Y-m-d'); $recurring_expense->payment_date = null; $recurring_expense->amount = 0; $recurring_expense->foreign_amount = 0; @@ -47,6 +48,7 @@ class RecurringExpenseFactory $recurring_expense->custom_value4 = ''; $recurring_expense->uses_inclusive_taxes = true; $recurring_expense->calculate_tax_by_amount = true; + $recurring_expense->remaining_cycles = -1; return $recurring_expense; } diff --git a/app/Factory/VendorFactory.php b/app/Factory/VendorFactory.php index 021d91053c6e..04c009267b21 100644 --- a/app/Factory/VendorFactory.php +++ b/app/Factory/VendorFactory.php @@ -28,6 +28,7 @@ class VendorFactory $vendor->country_id = 4; $vendor->is_deleted = 0; $vendor->vendor_hash = Str::random(40); + $vendor->classification = ''; return $vendor; } diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index bc368e5f7fa5..74625c98416c 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -391,8 +391,6 @@ class InvoiceItemSum { $this->setGroupedTaxes(collect([])); - - foreach ($this->line_items as $key => $this->item) { if ($this->item->line_total == 0) { continue; diff --git a/app/Helpers/Invoice/InvoiceItemSumInclusive.php b/app/Helpers/Invoice/InvoiceItemSumInclusive.php index b2a4b40337e6..ad1a92600a08 100644 --- a/app/Helpers/Invoice/InvoiceItemSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceItemSumInclusive.php @@ -349,14 +349,16 @@ class InvoiceItemSumInclusive { $this->setGroupedTaxes(collect([])); - $item_tax = 0; foreach ($this->line_items as $this->item) { if ($this->sub_total == 0) { $amount = $this->item->line_total; } else { - $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)); + $amount = ($this->sub_total > 0) ? $this->item->line_total - ($this->invoice->discount * ($this->item->line_total / $this->sub_total)) : 0; + // $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)); } + + $item_tax = 0; $item_tax_rate1_total = $this->calcInclusiveLineTax($this->item->tax_rate1, $amount); @@ -381,9 +383,17 @@ class InvoiceItemSumInclusive if ($item_tax_rate3_total != 0) { $this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total); } + + $this->setTotalTaxes($this->getTotalTaxes() + $item_tax); + $this->item->gross_line_total = $this->getLineTotal(); + + $this->item->tax_amount = $item_tax; + } - $this->setTotalTaxes($item_tax); + return $this; + + // $this->setTotalTaxes($item_tax); } diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index e57237bf3d05..69410f54db9e 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -315,8 +315,9 @@ class InvoiceSumInclusive public function setTaxMap() { - if ($this->invoice->is_amount_discount == true) { + if ($this->invoice->is_amount_discount) { $this->invoice_items->calcTaxesWithAmountDiscount(); + $this->invoice->line_items = $this->invoice_items->getLineItems(); } $this->tax_map = collect(); diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 016485535bc0..ede5c1b92d21 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -697,4 +697,19 @@ class CompanyController extends BaseController return $this->itemResponse($company->fresh()); } + + public function logo() + { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $company = $user->company(); + $logo = strlen($company->settings->company_logo) > 5 ? $company->settings->company_logo : 'https://pdf.invoicing.co/favicon-v2.png'; + $headers = ['Content-Disposition' => 'inline']; + + return response()->streamDownload(function () use ($logo){ + echo @file_get_contents($logo); + }, 'logo.png', $headers); + + } } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index fba7c9529439..8e7559894b1e 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -14,12 +14,11 @@ namespace App\Http\Controllers; use App\Models\User; use App\Models\Client; use App\Models\ClientContact; -use App\Http\Requests\Search\GenericSearchRequest; use App\Models\Invoice; class SearchController extends Controller { - public function __invoke(GenericSearchRequest $request) + public function __invoke() { /** @var \App\Models\User $user */ $user = auth()->user(); @@ -86,7 +85,7 @@ class SearchController extends Controller 'name' => $invoice->client->present()->name() . ' - ' . $invoice->number, 'type' => '/invoice', 'id' => $invoice->hashed_id, - 'path' => "/clients/{$invoice->hashed_id}/edit", + 'path' => "/invoices/{$invoice->hashed_id}/edit", 'heading' => ctrans('texts.invoices') ]; }); @@ -104,7 +103,7 @@ class SearchController extends Controller 'custom_fields' => '/settings/user_details/custom_fields', 'preferences' => '/settings/user_details/preferences', 'company_details' => '/settings/company_details', - 'company_details,details' => '/settings/company_details/details', + 'company_details,details' => '/settings/company_details/', 'company_details,address' => '/settings/company_details/address', 'company_details,logo' => '/settings/company_details/logo', 'company_details,defaults' => '/settings/company_details/defaults', diff --git a/app/Http/Requests/Client/StoreClientRequest.php b/app/Http/Requests/Client/StoreClientRequest.php index c06305dd8e77..3a1d3841bfc4 100644 --- a/app/Http/Requests/Client/StoreClientRequest.php +++ b/app/Http/Requests/Client/StoreClientRequest.php @@ -93,7 +93,8 @@ class StoreClientRequest extends Request $rules['number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)]; $rules['id_number'] = ['bail', 'nullable', Rule::unique('clients')->where('company_id', $user->company()->id)]; - + $rules['classification'] = 'bail|sometimes|nullable|in:individual,company,partnership,trust,charity,government,other'; + return $rules; } diff --git a/app/Http/Requests/Client/UpdateClientRequest.php b/app/Http/Requests/Client/UpdateClientRequest.php index 0344d7aaf205..0c2f3628874f 100644 --- a/app/Http/Requests/Client/UpdateClientRequest.php +++ b/app/Http/Requests/Client/UpdateClientRequest.php @@ -60,6 +60,7 @@ class UpdateClientRequest extends Request $rules['size_id'] = 'integer|nullable'; $rules['country_id'] = 'integer|nullable'; $rules['shipping_country_id'] = 'integer|nullable'; + $rules['classification'] = 'bail|sometimes|nullable|in:individual,company,partnership,trust,charity,government,other'; if ($this->id_number) { $rules['id_number'] = Rule::unique('clients')->where('company_id', $user->company()->id)->ignore($this->client->id); diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index 1cd8d8b4c850..02b465638efe 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -30,18 +30,25 @@ class UpdatePaymentRequest extends Request */ public function authorize() : bool { - return auth()->user()->can('edit', $this->payment); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->can('edit', $this->payment); } public function rules() { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $rules = [ 'invoices' => ['array', new PaymentAppliedValidAmount($this->all()), new ValidCreditsPresentRule($this->all())], 'invoices.*.invoice_id' => 'distinct', ]; if ($this->number) { - $rules['number'] = Rule::unique('payments')->where('company_id', auth()->user()->company()->id)->ignore($this->payment->id); + $rules['number'] = Rule::unique('payments')->where('company_id', $user->company()->id)->ignore($this->payment->id); } if ($this->file('documents') && is_array($this->file('documents'))) { @@ -75,7 +82,8 @@ class UpdatePaymentRequest extends Request if (isset($input['invoices']) && is_array($input['invoices']) !== false) { foreach ($input['invoices'] as $key => $value) { - if (array_key_exists('invoice_id', $input['invoices'][$key])) { + if(isset($input['invoices'][$key]['invoice_id'])){ + // if (array_key_exists('invoice_id', $input['invoices'][$key])) { $input['invoices'][$key]['invoice_id'] = $this->decodePrimaryKey($value['invoice_id']); } } @@ -83,7 +91,8 @@ class UpdatePaymentRequest extends Request if (isset($input['credits']) && is_array($input['credits']) !== false) { foreach ($input['credits'] as $key => $value) { - if (array_key_exists('credits', $input['credits'][$key])) { + // if (array_key_exists('credits', $input['credits'][$key])) { + if (isset($input['credits'][$key]['credit_id'])) { $input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($value['credit_id']); } } diff --git a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php index 76c61120971e..e45f66e80099 100644 --- a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php @@ -75,6 +75,9 @@ class UpdateRecurringExpenseRequest extends Request public function prepareForValidation() { + /** @var \App\Models\User $user*/ + $user = auth()->user(); + $input = $this->all(); $input = $this->decodePrimaryKeys($input); @@ -88,7 +91,7 @@ class UpdateRecurringExpenseRequest extends Request } if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { - $input['currency_id'] = (string) auth()->user()->company()->settings->currency_id; + $input['currency_id'] = (string) $user->company()->settings->currency_id; } $this->replace($input); diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php index 2de5edeca00c..3ce8ba472198 100644 --- a/app/Http/Requests/Vendor/StoreVendorRequest.php +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -60,6 +60,7 @@ class StoreVendorRequest extends Request } $rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id'; + $rules['classification'] = 'bail|sometimes|nullable|in:individual,company,partnership,trust,charity,government,other'; return $rules; } diff --git a/app/Http/Requests/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php index 867b4541c0bf..b50e8cf79229 100644 --- a/app/Http/Requests/Vendor/UpdateVendorRequest.php +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -61,6 +61,7 @@ class UpdateVendorRequest extends Request } $rules['language_id'] = 'bail|nullable|sometimes|exists:languages,id'; + $rules['classification'] = 'bail|sometimes|nullable|in:individual,company,partnership,trust,charity,government,other'; return $rules; } diff --git a/app/Jobs/Cron/RecurringExpensesCron.php b/app/Jobs/Cron/RecurringExpensesCron.php index 1b51f2fc70f4..55eee07e5bb5 100644 --- a/app/Jobs/Cron/RecurringExpensesCron.php +++ b/app/Jobs/Cron/RecurringExpensesCron.php @@ -11,12 +11,14 @@ namespace App\Jobs\Cron; +use App\Utils\Ninja; use App\Libraries\MultiDB; use Illuminate\Support\Carbon; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use Illuminate\Support\Facades\Auth; use App\Utils\Traits\GeneratesCounter; +use App\Events\Expense\ExpenseWasCreated; use Illuminate\Foundation\Bus\Dispatchable; use App\Factory\RecurringExpenseToExpenseFactory; @@ -109,6 +111,9 @@ class RecurringExpensesCron $expense->number = $this->getNextExpenseNumber($expense); $expense->saveQuietly(); + event(new ExpenseWasCreated($expense, $expense->company, Ninja::eventVars(null))); + event('eloquent.created: App\Models\Expense', $expense); + $recurring_expense->next_send_date = $recurring_expense->nextSendDate(); $recurring_expense->next_send_date_client = $recurring_expense->next_send_date; $recurring_expense->last_sent_date = now(); diff --git a/app/Jobs/Ninja/CheckACHStatus.php b/app/Jobs/Ninja/CheckACHStatus.php new file mode 100644 index 000000000000..a7f61b25520c --- /dev/null +++ b/app/Jobs/Ninja/CheckACHStatus.php @@ -0,0 +1,79 @@ +where('created_at', '>', now()->subMonths(2)) + ->where('gateway_type_id', 2) + ->whereHas('gateway', function ($q) { + $q->whereIn('gateway_key', ['d14dd26a37cecc30fdd65700bfb55b23','d14dd26a47cecc30fdd65700bfb67b34']); + }) + ->whereJsonContains('meta', ['state' => 'unauthorized']) + ->cursor() + ->each(function ($token) { + + try { + $stripe = $token->gateway->driver($token->client)->init(); + $pm = $stripe->getStripePaymentMethod($token->token); + + if($pm) { + + $meta = $token->meta; + $meta->state = 'authorized'; + $token->meta = $meta; + $token->save(); + + } + + } catch (\Exception $e) { + } + + }); + } + } +} \ No newline at end of file diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index ad6074309f80..3c98c00a4387 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -11,22 +11,24 @@ namespace App\Jobs\RecurringInvoice; -use App\DataMapper\Analytics\SendRecurringFailure; -use App\Factory\InvoiceInvitationFactory; -use App\Factory\RecurringInvoiceToInvoiceFactory; -use App\Jobs\Cron\AutoBill; -use App\Jobs\Entity\EmailEntity; +use Carbon\Carbon; +use App\Utils\Ninja; use App\Models\Invoice; +use App\Jobs\Cron\AutoBill; +use Illuminate\Bus\Queueable; +use App\Utils\Traits\MakesHash; +use App\Jobs\Entity\EmailEntity; use App\Models\RecurringInvoice; use App\Utils\Traits\GeneratesCounter; -use App\Utils\Traits\MakesHash; -use Carbon\Carbon; -use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Turbo124\Beacon\Facades\LightLogs; +use Illuminate\Queue\InteractsWithQueue; +use App\Events\Invoice\InvoiceWasCreated; +use App\Factory\InvoiceInvitationFactory; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use App\Factory\RecurringInvoiceToInvoiceFactory; +use App\DataMapper\Analytics\SendRecurringFailure; class SendRecurring implements ShouldQueue { @@ -105,6 +107,7 @@ class SendRecurring implements ShouldQueue $this->recurring_invoice->save(); event('eloquent.created: App\Models\Invoice', $invoice); + event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars())); //auto bill, BUT NOT DRAFTS!! if ($invoice->auto_bill_enabled && $invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $invoice->client->getSetting('auto_email_invoice')) { diff --git a/app/Jobs/Report/PreviewReport.php b/app/Jobs/Report/PreviewReport.php index ce07cbfc1c34..c52cd1a3e110 100644 --- a/app/Jobs/Report/PreviewReport.php +++ b/app/Jobs/Report/PreviewReport.php @@ -39,8 +39,8 @@ class PreviewReport implements ShouldQueue /** @var \App\Export\CSV\CreditExport $export */ $export = new $this->report_class($this->company, $this->request); $report = $export->returnJson(); - nlog($report); - nlog($this->report_class); + // nlog($report); + // nlog($this->report_class); // nlog($report); Cache::put($this->hash, $report, 60 * 60); } diff --git a/app/Listeners/Activity/CreatedExpenseActivity.php b/app/Listeners/Activity/CreatedExpenseActivity.php index 9309db70e921..c6e46122cd5f 100644 --- a/app/Listeners/Activity/CreatedExpenseActivity.php +++ b/app/Listeners/Activity/CreatedExpenseActivity.php @@ -49,7 +49,8 @@ class CreatedExpenseActivity implements ShouldQueue $fields->user_id = $user_id; $fields->company_id = $event->expense->company_id; $fields->activity_type_id = Activity::CREATE_EXPENSE; - + $fields->recurring_expense_id = $event->expense->recurring_expense_id ?? null; + $this->activity_repo->save($fields, $event->expense, $event->event_vars); } } diff --git a/app/Listeners/Invoice/CreateInvoiceActivity.php b/app/Listeners/Invoice/CreateInvoiceActivity.php index 42a5ccb34a19..f5baea05f0ea 100644 --- a/app/Listeners/Invoice/CreateInvoiceActivity.php +++ b/app/Listeners/Invoice/CreateInvoiceActivity.php @@ -52,7 +52,8 @@ class CreateInvoiceActivity implements ShouldQueue $fields->client_id = $event->invoice->client_id; $fields->company_id = $event->invoice->company_id; $fields->activity_type_id = Activity::CREATE_INVOICE; - + $fields->recurring_invoice_id = $event->invoice->recurring_id; + $this->activity_repo->save($fields, $event->invoice, $event->event_vars); } } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 2677a149d5c5..1b9d89ce59d7 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -416,6 +416,9 @@ class Activity extends StaticModel if($this->vendor) $replacements['vendor'] = ['label' => $this?->vendor?->present()->name() ?? '', 'hashed_id' => $this->vendor->hashed_id ?? '']; + if($this->activity_type_id == 4 && $this->recurring_invoice) + $replacements['recurring_invoice'] = ['label' => $this?->recurring_invoice?->number ?? '', 'hashed_id' => $this->recurring_invoice->hashed_id ?? '']; + $replacements['activity_type_id'] = $this->activity_type_id; $replacements['id'] = $this->id; $replacements['hashed_id'] = $this->hashed_id; diff --git a/app/Models/Client.php b/app/Models/Client.php index 2087a27186da..5abb007ad357 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -169,6 +169,7 @@ class Client extends BaseModel implements HasLocalePreference 'routing_id', 'is_tax_exempt', 'has_valid_vat_number', + 'classification', ]; protected $with = [ diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 3773e8237d99..6df4fbc0dbaf 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -361,6 +361,24 @@ class Payment extends BaseModel return new PaymentService($this); } + /** + * $data = [ + 'id' => $payment->id, + 'amount' => 10, + 'invoices' => [ + [ + 'invoice_id' => $invoice->id, + 'amount' => 10, + ], + ], + 'date' => '2020/12/12', + 'gateway_refund' => false, + 'email_receipt' => false, + ]; + * + * @param array $data + * @return self + */ public function refund(array $data) :self { return $this->service()->refundPayment($data); diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 25ebbc4d9e29..243a38691370 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -113,6 +113,7 @@ class Vendor extends BaseModel 'custom_value4', 'number', 'language_id', + 'classification', ]; protected $casts = [ diff --git a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php new file mode 100644 index 000000000000..d0393dd2e752 --- /dev/null +++ b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php @@ -0,0 +1,147 @@ +stripe_request = $stripe_request; + $this->company_key = $company_key; + $this->company_gateway_id = $company_gateway_id; + } + + public function handle() + { + MultiDB::findAndSetDbByCompanyKey($this->company_key); + nlog($this->stripe_request); + + $company = Company::query()->where('company_key', $this->company_key)->first(); + + $source = $this->stripe_request['object']; + $charge_id = $source['id']; + $amount_refunded = $source['amount_refunded'] ?? 0; + + $payment_hash_key = $source['metadata']['payment_hash'] ?? null; + + $company_gateway = CompanyGateway::query()->find($this->company_gateway_id); + $payment_hash = PaymentHash::query()->where('hash', $payment_hash_key)->first(); + + $stripe_driver = $company_gateway->driver()->init(); + + $stripe_driver->payment_hash = $payment_hash; + + /** @var \App\Models\Payment $payment **/ + $payment = Payment::query() + ->withTrashed() + ->where('company_id', $company->id) + ->where('transaction_reference', $charge_id) + ->first(); + + //don't touch if already refunded + if(!$payment || in_array($payment->status_id, [Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])) { + return; + } + + $stripe_driver->client = $payment->client; + + $amount_refunded = $stripe_driver->convertFromStripeAmount($amount_refunded, $payment->client->currency()->precision, $payment->client->currency()); + + if ($payment->status_id == Payment::STATUS_PENDING) { + $payment->service()->deletePayment(); + $payment->status_id = Payment::STATUS_FAILED; + $payment->save(); + return; + } + + if($payment->status_id == Payment::STATUS_COMPLETED) { + + $invoice_collection = $payment->paymentables + ->where('paymentable_type','invoices') + ->map(function ($pivot){ + return [ + 'invoice_id' => $pivot->paymentable_id, + 'amount' => $pivot->amount - $pivot->refunded + ]; + }); + + if($invoice_collection->count() == 1 && $invoice_collection->first()['amount'] >= $amount_refunded) { + //If there is only one invoice- and we are refunding _less_ than the amount of the invoice, we can just refund the payment + + $invoice_collection = $payment->paymentables + ->where('paymentable_type', 'invoices') + ->map(function ($pivot) use ($amount_refunded){ + return [ + 'invoice_id' => $pivot->paymentable_id, + 'amount' => $amount_refunded + ]; + }); + + } + elseif($invoice_collection->sum('amount') != $amount_refunded) { + //too many edges cases at this point, return early + return; + } + + $invoices = $invoice_collection->toArray(); + + $data = [ + 'id' => $payment->id, + 'amount' => $amount_refunded, + 'invoices' => $invoices, + 'date' => now()->format('Y-m-d'), + 'gateway_refund' => false, + 'email_receipt' => false, + ]; + + nlog($data); + + $payment->refund($data); + + $payment->private_notes .= 'Refunded via Stripe'; + return; + } + + } + + public function middleware() + { + return [new WithoutOverlapping($this->company_gateway_id)]; + } +} diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index a80c09789dcd..de4f2ac93e14 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -39,6 +39,7 @@ use App\PaymentDrivers\Stripe\FPX; use App\PaymentDrivers\Stripe\GIROPAY; use App\PaymentDrivers\Stripe\iDeal; use App\PaymentDrivers\Stripe\ImportCustomers; +use App\PaymentDrivers\Stripe\Jobs\ChargeRefunded; use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook; use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook; @@ -790,6 +791,12 @@ class StripePaymentDriver extends BaseDriver } elseif ($request->data['object']['status'] == "pending") { return response()->json([], 200); } + } elseif ($request->type === "charge.refunded") { + + ChargeRefunded::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); + + return response()->json([], 200); + } return response()->json([], 200); diff --git a/app/Services/Quote/ConvertQuoteToProject.php b/app/Services/Quote/ConvertQuoteToProject.php index 6b1b642cd16a..59227fcfc566 100644 --- a/app/Services/Quote/ConvertQuoteToProject.php +++ b/app/Services/Quote/ConvertQuoteToProject.php @@ -36,7 +36,7 @@ class ConvertQuoteToProject }); $project = ProjectFactory::create($this->quote->company_id, $this->quote->user_id); - $project->name = ctrans('texts.quote_number_short'). " " . $this->quote->number . "[{$this->quote->client->present()->name()}]"; + $project->name = ctrans('texts.quote_number_short'). " " . $this->quote->number . " [{$this->quote->client->present()->name()}]"; $project->client_id = $this->quote->client_id; $project->public_notes = $this->quote->public_notes; $project->private_notes = $this->quote->private_notes; diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 7f5e914a3be6..15797cff2f01 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -91,6 +91,7 @@ class AccountTransformer extends EntityTransformer 'trial_days_left' => Ninja::isHosted() ? (int) $account->getTrialDays() : 0, 'account_sms_verified' => (bool) $account->account_sms_verified, 'has_iap_plan' => (bool)$account->inapp_transaction_id, + 'tax_api_enabled' => (bool) config('services.tax.zip_tax.key') ? true : false ]; } diff --git a/app/Transformers/ClientTransformer.php b/app/Transformers/ClientTransformer.php index f34b805ff581..711c9cfec5ba 100644 --- a/app/Transformers/ClientTransformer.php +++ b/app/Transformers/ClientTransformer.php @@ -151,6 +151,7 @@ class ClientTransformer extends EntityTransformer 'is_tax_exempt' => (bool) $client->is_tax_exempt, 'routing_id' => (string) $client->routing_id, 'tax_info' => $client->tax_data ?: new \stdClass, + 'classification' => $client->classification ?: '', ]; } } diff --git a/app/Transformers/VendorTransformer.php b/app/Transformers/VendorTransformer.php index 051e8fb2f497..29aeafa703cd 100644 --- a/app/Transformers/VendorTransformer.php +++ b/app/Transformers/VendorTransformer.php @@ -104,6 +104,7 @@ class VendorTransformer extends EntityTransformer 'created_at' => (int) $vendor->created_at, 'number' => (string) $vendor->number ?: '', 'language_id' => (string) $vendor->language_id ?: '', + 'classification' => (string) $vendor->classification ?: '', ]; } } diff --git a/composer.lock b/composer.lock index ba232aa837fc..49dc0b294acd 100644 --- a/composer.lock +++ b/composer.lock @@ -525,16 +525,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.281.4", + "version": "3.281.7", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c37035bcfb67a9d54f91dae303b3fe8f98ea59f4" + "reference": "926cea9a41a545ca9801ac304f2a9ffd23ac68c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c37035bcfb67a9d54f91dae303b3fe8f98ea59f4", - "reference": "c37035bcfb67a9d54f91dae303b3fe8f98ea59f4", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/926cea9a41a545ca9801ac304f2a9ffd23ac68c9", + "reference": "926cea9a41a545ca9801ac304f2a9ffd23ac68c9", "shasum": "" }, "require": { @@ -614,9 +614,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.281.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.281.7" }, - "time": "2023-09-11T18:07:49+00:00" + "time": "2023-09-14T18:05:11+00:00" }, { "name": "bacon/bacon-qr-code", @@ -2517,16 +2517,16 @@ }, { "name": "google/apiclient", - "version": "v2.15.0", + "version": "v2.15.1", "source": { "type": "git", "url": "https://github.com/googleapis/google-api-php-client.git", - "reference": "49787fa30b8d8313146a61efbf77ed1fede723c2" + "reference": "7a95ed29e4b6c6859d2d22300c5455a92e2622ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/49787fa30b8d8313146a61efbf77ed1fede723c2", - "reference": "49787fa30b8d8313146a61efbf77ed1fede723c2", + "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/7a95ed29e4b6c6859d2d22300c5455a92e2622ad", + "reference": "7a95ed29e4b6c6859d2d22300c5455a92e2622ad", "shasum": "" }, "require": { @@ -2537,7 +2537,7 @@ "guzzlehttp/psr7": "^1.8.4||^2.2.1", "monolog/monolog": "^2.9||^3.0", "php": "^7.4|^8.0", - "phpseclib/phpseclib": "^3.0.2" + "phpseclib/phpseclib": "^3.0.19" }, "require-dev": { "cache/filesystem-adapter": "^1.1", @@ -2580,9 +2580,9 @@ ], "support": { "issues": "https://github.com/googleapis/google-api-php-client/issues", - "source": "https://github.com/googleapis/google-api-php-client/tree/v2.15.0" + "source": "https://github.com/googleapis/google-api-php-client/tree/v2.15.1" }, - "time": "2023-05-18T13:51:33+00:00" + "time": "2023-09-13T21:46:39+00:00" }, { "name": "google/apiclient-services", @@ -3441,16 +3441,16 @@ }, { "name": "horstoeko/zugferd", - "version": "v1.0.26", + "version": "v1.0.28", "source": { "type": "git", "url": "https://github.com/horstoeko/zugferd.git", - "reference": "2a7541a35f00499c206391273f30159dc2c7072a" + "reference": "be78b1b53a46e94a69b92dcff1e909180170583c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/2a7541a35f00499c206391273f30159dc2c7072a", - "reference": "2a7541a35f00499c206391273f30159dc2c7072a", + "url": "https://api.github.com/repos/horstoeko/zugferd/zipball/be78b1b53a46e94a69b92dcff1e909180170583c", + "reference": "be78b1b53a46e94a69b92dcff1e909180170583c", "shasum": "" }, "require": { @@ -3508,9 +3508,9 @@ ], "support": { "issues": "https://github.com/horstoeko/zugferd/issues", - "source": "https://github.com/horstoeko/zugferd/tree/v1.0.26" + "source": "https://github.com/horstoeko/zugferd/tree/v1.0.28" }, - "time": "2023-08-18T03:05:43+00:00" + "time": "2023-09-12T14:54:01+00:00" }, { "name": "http-interop/http-factory-guzzle", @@ -4331,16 +4331,16 @@ }, { "name": "laravel/framework", - "version": "v10.22.0", + "version": "v10.23.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "9234388a895206d4e1df37342b61adc67e5c5d31" + "reference": "dbfd495557678759153e8d71cc2f6027686ca51e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9234388a895206d4e1df37342b61adc67e5c5d31", - "reference": "9234388a895206d4e1df37342b61adc67e5c5d31", + "url": "https://api.github.com/repos/laravel/framework/zipball/dbfd495557678759153e8d71cc2f6027686ca51e", + "reference": "dbfd495557678759153e8d71cc2f6027686ca51e", "shasum": "" }, "require": { @@ -4440,7 +4440,7 @@ "league/flysystem-read-only": "^3.3", "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", - "orchestra/testbench-core": "^8.4", + "orchestra/testbench-core": "^8.10", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.0.7", @@ -4527,20 +4527,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-09-05T13:20:01+00:00" + "time": "2023-09-13T14:51:46+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.6", + "version": "v0.1.7", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "b514c5620e1b3b61221b0024dc88def26d9654f4" + "reference": "554e7d855a22e87942753d68e23b327ad79b2070" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/b514c5620e1b3b61221b0024dc88def26d9654f4", - "reference": "b514c5620e1b3b61221b0024dc88def26d9654f4", + "url": "https://api.github.com/repos/laravel/prompts/zipball/554e7d855a22e87942753d68e23b327ad79b2070", + "reference": "554e7d855a22e87942753d68e23b327ad79b2070", "shasum": "" }, "require": { @@ -4573,9 +4573,9 @@ ], "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.6" + "source": "https://github.com/laravel/prompts/tree/v0.1.7" }, - "time": "2023-08-18T13:32:23+00:00" + "time": "2023-09-12T11:09:22+00:00" }, { "name": "laravel/serializable-closure", @@ -4700,16 +4700,16 @@ }, { "name": "laravel/socialite", - "version": "v5.9.0", + "version": "v5.9.1", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "14acfa3262875f180fba51efe3c7aaa089a9ef24" + "reference": "49ecc4c907ed88c1254bae991c6b2948945645c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/14acfa3262875f180fba51efe3c7aaa089a9ef24", - "reference": "14acfa3262875f180fba51efe3c7aaa089a9ef24", + "url": "https://api.github.com/repos/laravel/socialite/zipball/49ecc4c907ed88c1254bae991c6b2948945645c2", + "reference": "49ecc4c907ed88c1254bae991c6b2948945645c2", "shasum": "" }, "require": { @@ -4766,7 +4766,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2023-09-05T15:20:21+00:00" + "time": "2023-09-07T16:13:53+00:00" }, { "name": "laravel/tinker", @@ -8271,16 +8271,16 @@ }, { "name": "predis/predis", - "version": "v2.2.1", + "version": "v2.2.2", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "5f2b410a74afaff296a87a494e4c5488cf9fab57" + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/5f2b410a74afaff296a87a494e4c5488cf9fab57", - "reference": "5f2b410a74afaff296a87a494e4c5488cf9fab57", + "url": "https://api.github.com/repos/predis/predis/zipball/b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", "shasum": "" }, "require": { @@ -8320,7 +8320,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.2.1" + "source": "https://github.com/predis/predis/tree/v2.2.2" }, "funding": [ { @@ -8328,7 +8328,7 @@ "type": "github" } ], - "time": "2023-08-15T23:01:46+00:00" + "time": "2023-09-13T16:42:03+00:00" }, { "name": "psr/cache", @@ -14560,16 +14560,16 @@ }, { "name": "brianium/paratest", - "version": "v7.2.6", + "version": "v7.2.7", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173" + "reference": "1526eb4fd195f65075456dee394d14742ae0a66c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/7f372b5bb59b4271adedc67d3129df29b84c4173", - "reference": "7f372b5bb59b4271adedc67d3129df29b84c4173", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/1526eb4fd195f65075456dee394d14742ae0a66c", + "reference": "1526eb4fd195f65075456dee394d14742ae0a66c", "shasum": "" }, "require": { @@ -14639,7 +14639,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.2.6" + "source": "https://github.com/paratestphp/paratest/tree/v7.2.7" }, "funding": [ { @@ -14651,7 +14651,7 @@ "type": "paypal" } ], - "time": "2023-08-29T07:47:39+00:00" + "time": "2023-09-14T14:10:09+00:00" }, { "name": "composer/class-map-generator", @@ -15880,16 +15880,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.33", + "version": "1.10.34", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1" + "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1", - "reference": "03b1cf9f814ba0863c4e9affea49a4d1ed9a2ed1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7f806b6f1403e6914c778140e2ba07c293cb4901", + "reference": "7f806b6f1403e6914c778140e2ba07c293cb4901", "shasum": "" }, "require": { @@ -15938,20 +15938,20 @@ "type": "tidelift" } ], - "time": "2023-09-04T12:20:53+00:00" + "time": "2023-09-13T09:49:47+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "10.1.4", + "version": "10.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "cd59bb34756a16ca8253ce9b2909039c227fff71" + "reference": "1df504e42a88044c27a90136910f0b3fe9e91939" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/cd59bb34756a16ca8253ce9b2909039c227fff71", - "reference": "cd59bb34756a16ca8253ce9b2909039c227fff71", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1df504e42a88044c27a90136910f0b3fe9e91939", + "reference": "1df504e42a88044c27a90136910f0b3fe9e91939", "shasum": "" }, "require": { @@ -16008,7 +16008,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.4" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.5" }, "funding": [ { @@ -16016,7 +16016,7 @@ "type": "github" } ], - "time": "2023-08-31T14:04:38+00:00" + "time": "2023-09-12T14:37:22+00:00" }, { "name": "phpunit/php-file-iterator", @@ -16263,16 +16263,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.3.3", + "version": "10.3.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "241ed4dd0db1c096984e62d414c4e1ac8d5dbff4" + "reference": "b8d59476f19115c9774b3b447f78131781c6c32b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/241ed4dd0db1c096984e62d414c4e1ac8d5dbff4", - "reference": "241ed4dd0db1c096984e62d414c4e1ac8d5dbff4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b8d59476f19115c9774b3b447f78131781c6c32b", + "reference": "b8d59476f19115c9774b3b447f78131781c6c32b", "shasum": "" }, "require": { @@ -16286,7 +16286,7 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=8.1", - "phpunit/php-code-coverage": "^10.1.1", + "phpunit/php-code-coverage": "^10.1.5", "phpunit/php-file-iterator": "^4.0", "phpunit/php-invoker": "^4.0", "phpunit/php-text-template": "^3.0", @@ -16344,7 +16344,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.3" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.3.4" }, "funding": [ { @@ -16360,7 +16360,7 @@ "type": "tidelift" } ], - "time": "2023-09-05T04:34:51+00:00" + "time": "2023-09-12T14:42:28+00:00" }, { "name": "sebastian/cli-parser", diff --git a/config/ninja.php b/config/ninja.php index 1c509fec85e7..fcb68333ee1a 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.11'), - 'app_tag' => env('APP_TAG','5.7.11'), + 'app_version' => env('APP_VERSION','5.7.12'), + 'app_tag' => env('APP_TAG','5.7.12'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/database/migrations/2023_09_11_003230_add_client_and_company_classifications.php b/database/migrations/2023_09_11_003230_add_client_and_company_classifications.php new file mode 100644 index 000000000000..ba41f214219f --- /dev/null +++ b/database/migrations/2023_09_11_003230_add_client_and_company_classifications.php @@ -0,0 +1,31 @@ +string('classification')->nullable(); + }); + + Schema::table('vendors', function (Blueprint $table) { + $table->string('classification')->nullable(); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index b67f6f745b16..90bc248ee1c1 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5158,7 +5158,15 @@ $LANG = array( 'click_or_drop_files_here' => 'Click or drop files here', 'set_public' => 'Set public', 'set_private' => 'Set private', + 'individual' => 'Individual', + 'business' => 'Business', + 'partnership' => 'partnership', + 'trust' => 'Trust', + 'charity' => 'Charity', + 'government' => 'Government', 'in_stock_quantity' => 'Stock quantity', + 'vendor_contact' => 'Vendor Contact', + ); return $LANG; diff --git a/routes/api.php b/routes/api.php index 3ce1fdc1737e..61e44f833902 100644 --- a/routes/api.php +++ b/routes/api.php @@ -177,6 +177,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('companies/purge_save_settings/{company}', [MigrationController::class, 'purgeCompanySaveSettings'])->middleware('password_protected'); Route::resource('companies', CompanyController::class); // name = (companies. index / create / show / update / destroy / edit + Route::get('companies/{company}/logo', [CompanyController::class, 'logo']); Route::put('companies/{company}/upload', [CompanyController::class, 'upload']); Route::post('companies/{company}/default', [CompanyController::class, 'default']); Route::post('companies/updateOriginTaxData/{company}', [CompanyController::class, 'updateOriginTaxData'])->middleware('throttle:3,1'); diff --git a/tests/Feature/ClassificationTest.php b/tests/Feature/ClassificationTest.php new file mode 100644 index 000000000000..fe9df9600756 --- /dev/null +++ b/tests/Feature/ClassificationTest.php @@ -0,0 +1,220 @@ +faker = \Faker\Factory::create(); + + $this->makeTestData(); + + + } + + public function testClientClassification() + { + $data = [ + 'name' => 'Personal Company', + 'classification' => 'individual' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/clients', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('individual', $arr['data']['classification']); + } + + public function testValidationClassification() + { + $data = [ + 'name' => 'Personal Company', + 'classification' => 'this_is_not_validated' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/clients', $data); + + $response->assertStatus(422); + + } + + public function testValidation2Classification() + { + $this->client->classification = 'company'; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/clients/'.$this->client->hashed_id, $this->client->toArray()); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('company', $arr['data']['classification']); + } + + public function testValidation3Classification() + { + $this->client->classification = 'this_is_not_validated'; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/clients/'.$this->client->hashed_id, $this->client->toArray()); + + $response->assertStatus(422); + + } + + public function testVendorClassification() + { + $data = [ + 'name' => 'Personal Company', + 'classification' => 'individual' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/vendors', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('individual', $arr['data']['classification']); + } + + public function testVendorValidationClassification() + { + $data = [ + 'name' => 'Personal Company', + 'classification' => 'this_is_not_validated' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/vendors', $data); + + $response->assertStatus(422); + + } + + public function testVendorValidation2Classification() + { + $this->vendor->classification = 'company'; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/vendors/'.$this->vendor->hashed_id, $this->vendor->toArray()); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('company', $arr['data']['classification']); + } + + public function testVendorValidation3Classification() + { + $this->vendor->classification = 'this_is_not_validated'; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/vendors/'.$this->vendor->hashed_id, $this->vendor->toArray()); + + $response->assertStatus(422); + + } + + public function testCompanyClassification() + { + $settings = $this->company->settings; + $settings->classification = 'company'; + + $this->company->settings = $settings; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/companies/'.$this->company->hashed_id, $this->company->toArray()); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('company', $arr['data']['settings']['classification']); + } + + public function testCompanyValidationClassification() + { + $settings = $this->company->settings; + $settings->classification = 545454; + + $this->company->settings = $settings; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/companies/'.$this->company->hashed_id, $this->company->toArray()); + + $response->assertStatus(422); + + } + + public function testCompanyValidation2Classification() + { + $settings = $this->company->settings; + $settings->classification = null; + + $this->company->settings = $settings; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/companies/'.$this->company->hashed_id, $this->company->toArray()); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('', $arr['data']['settings']['classification']); + } +} \ No newline at end of file diff --git a/tests/Feature/ClientPortal/CreditsTest.php b/tests/Feature/ClientPortal/CreditsTest.php index e555dcf8f0d6..ce401c2986bc 100644 --- a/tests/Feature/ClientPortal/CreditsTest.php +++ b/tests/Feature/ClientPortal/CreditsTest.php @@ -41,6 +41,7 @@ class CreditsTest extends TestCase $this->faker = Factory::create(); $this->buildCache(true); + } public function testShowingOnlyCreditsWithDueDateLessOrEqualToNow() @@ -106,6 +107,8 @@ class CreditsTest extends TestCase ->assertDontSee('testing-number-01') ->assertSee('testing-number-02') ->assertSee('testing-number-03'); + + $user->forceDelete(); } public function testShowingCreditsWithNullDueDate() @@ -173,5 +176,8 @@ class CreditsTest extends TestCase ->assertSee('testing-number-01') ->assertSee('testing-number-02') ->assertSee('testing-number-03'); + + $account->delete(); + } } diff --git a/tests/Feature/ClientPortal/InvoicesTest.php b/tests/Feature/ClientPortal/InvoicesTest.php index 11cb71a7b4f9..e56d4b76a161 100644 --- a/tests/Feature/ClientPortal/InvoicesTest.php +++ b/tests/Feature/ClientPortal/InvoicesTest.php @@ -30,6 +30,8 @@ class InvoicesTest extends TestCase use DatabaseTransactions; use AppSetup; + public $faker; + protected function setUp(): void { parent::setUp(); @@ -62,28 +64,37 @@ class InvoicesTest extends TestCase 'company_id' => $company->id, ]); - $sent = Invoice::factory()->for($client)->create([ + $sent = Invoice::factory()->create([ 'user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, + 'number' => 'testing-number-02', + 'due_date' => now()->addMonth(), 'status_id' => Invoice::STATUS_SENT, ]); - $paid = Invoice::factory()->for($client)->create([ + $paid = Invoice::factory()->create([ 'user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, + 'number' => 'testing-number-03', 'status_id' => Invoice::STATUS_PAID, ]); - $unpaid = Invoice::factory()->for($client)->create([ + $unpaid = Invoice::factory()->create([ 'user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, + 'number' => 'testing-number-04', + 'due_date' => '', 'status_id' => Invoice::STATUS_UNPAID, ]); - $this->actingAs($client->contacts->first(), 'contact'); + $sent->load('client'); + $paid->load('client'); + $unpaid->load('client'); + + $this->actingAs($client->contacts()->first(), 'contact'); Livewire::test(InvoicesTable::class, ['company_id' => $company->id, 'db' => $company->db]) ->assertSee($sent->number) @@ -94,5 +105,8 @@ class InvoicesTest extends TestCase ->set('status', ['paid']) ->assertSee($paid->number) ->assertDontSee($unpaid->number); + + $account->delete(); + } } diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index 6d0fefbc2bf1..6cfc427d9271 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -34,6 +34,8 @@ class CompanyTest extends TestCase use MockAccountData; use DatabaseTransactions; + public $faker; + protected function setUp() :void { parent::setUp(); @@ -47,6 +49,19 @@ class CompanyTest extends TestCase $this->makeTestData(); } + + public function testCompanyLogoInline() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get("/api/v1/companies/{$this->company->hashed_id}/logo"); + + $response->assertStatus(200); + $response->streamedContent(); + + } + public function testUpdateCompanyPropertyInvoiceTaskHours() { $company_update = [ @@ -56,9 +71,9 @@ class CompanyTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update) - ->assertStatus(200); + ])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update); + $response->assertStatus(200); $arr = $response->json(); diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index f7224d9aa43a..be93e9f74947 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -18,15 +18,19 @@ use App\Models\Credit; use League\Csv\Reader; 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; use App\Utils\Traits\MakesHash; +use App\Export\CSV\VendorExport; +use App\Export\CSV\ProductExport; use App\DataMapper\CompanySettings; +use App\Export\CSV\PaymentExport; use App\Factory\CompanyUserFactory; use App\Factory\InvoiceItemFactory; -use App\Models\Expense; use App\Services\Report\ARDetailReport; use Illuminate\Routing\Middleware\ThrottleRequests; @@ -293,7 +297,7 @@ class ReportCsvGenerationTest extends TestCase ])->post('/api/v1/reports/vendors', $data); $csv = $response->streamedContent(); -nlog($csv); + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Vendor Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Vendor Number')); $this->assertEquals('city', $this->getFirstValueByColumn($csv, 'Vendor City')); @@ -305,6 +309,28 @@ nlog($csv); $this->assertEquals('public_notes', $this->getFirstValueByColumn($csv, 'Vendor Public Notes')); $this->assertEquals('website', $this->getFirstValueByColumn($csv, 'Vendor Website')); + $data = [ + 'date_range' => 'all', + // 'end_date' => 'bail|required_if:date_range,custom|nullable|date', + // 'start_date' => 'bail|required_if:date_range,custom|nullable|date', + 'report_keys' => [], + 'send_email' => false, + // 'status' => 'sometimes|string|nullable|in:all,draft,sent,viewed,paid,unpaid,overdue', + ]; + + $export = new VendorExport($this->company, $data); + $data = $export->returnJson(); + + $this->assertNotNull($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')); + $this->assertEquals('address1', $this->traverseJson($data, '0.0.id')); + $this->assertNull($this->traverseJson($data, '0.0.hashed_id')); + $this->assertEquals('address1', $this->traverseJson($data, '0.0.value')); + $this->assertEquals('vendor.address1', $this->traverseJson($data, '0.0.identifier')); + $this->assertEquals('address1', $this->traverseJson($data, '0.0.display_value')); } public function testVendorCustomColumnCsvGeneration() @@ -348,6 +374,22 @@ nlog($csv); $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Vendor Name')); $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Vendor Number')); $this->assertEquals('city', $this->getFirstValueByColumn($csv, 'Vendor City')); + + $export = new VendorExport($this->company, $data); + $data = $export->returnJson(); + + $this->assertNotNull($data); + + $this->assertEquals(0, $this->traverseJson($data, 'columns.0.identifier')); + $this->assertEquals('Vendor Name', $this->traverseJson($data, 'columns.0.display_value')); + $this->assertEquals('vendor', $this->traverseJson($data, '0.0.entity')); + $this->assertEquals('name', $this->traverseJson($data, '0.0.id')); + $this->assertNull($this->traverseJson($data, '0.0.hashed_id')); + $this->assertEquals('Vendor 1', $this->traverseJson($data, '0.0.value')); + $this->assertEquals('vendor.name', $this->traverseJson($data, '0.0.identifier')); + $this->assertEquals('Vendor 1', $this->traverseJson($data, '0.0.display_value')); + $this->assertEquals('number', $this->traverseJson($data, '0.2.id')); + } @@ -423,6 +465,19 @@ nlog($csv); $this->assertEquals('123456', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); $this->assertEquals(1000, $this->getFirstValueByColumn($csv, 'Invoice Amount')); + $export = new TaskExport($this->company, $data); + $data = $export->returnJson(); + + $this->assertNotNull($data); + + $this->assertEquals(0, $this->traverseJson($data, 'columns.0.identifier')); + $this->assertEquals('Client Name', $this->traverseJson($data, 'columns.0.display_value')); + $this->assertEquals('client', $this->traverseJson($data, '0.0.entity')); + $this->assertEquals('name', $this->traverseJson($data, '0.0.id')); + $this->assertNotNull($this->traverseJson($data, '0.0.hashed_id')); + $this->assertEquals('bob', $this->traverseJson($data, '0.0.value')); + $this->assertEquals('client.name', $this->traverseJson($data, '0.0.identifier')); + $this->assertEquals('bob', $this->traverseJson($data, '0.0.display_value')); $data = [ 'date_range' => 'all', @@ -527,7 +582,7 @@ nlog($csv); ])->post('/api/v1/reports/products', $data); $csv = $response->streamedContent(); -// nlog($csv); + $this->assertEquals('product_key', $this->getFirstValueByColumn($csv, 'Product')); $this->assertEquals('notes', $this->getFirstValueByColumn($csv, 'Notes')); $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Cost')); @@ -535,8 +590,22 @@ nlog($csv); $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); - $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); - + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + + $export = new ProductExport($this->company, $data); + $data = $export->returnJson(); + + $this->assertNotNull($data); + + $this->assertEquals(0, $this->traverseJson($data, 'columns.0.identifier')); + $this->assertEquals('Custom Value 1', $this->traverseJson($data, 'columns.0.display_value')); + $this->assertEquals('custom_value1', $this->traverseJson($data, '0.0.entity')); + $this->assertEquals('custom_value1', $this->traverseJson($data, '0.0.id')); + $this->assertNull($this->traverseJson($data, '0.0.hashed_id')); + $this->assertEquals('Custom 1', $this->traverseJson($data, '0.0.value')); + $this->assertEquals('custom_value1', $this->traverseJson($data, '0.0.identifier')); + $this->assertEquals('Custom 1', $this->traverseJson($data, '0.0.display_value')); + } @@ -593,7 +662,26 @@ nlog($csv); $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Client Balance')); $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Client Paid to Date')); - + + $export = new PaymentExport($this->company, $data); + $data = $export->returnJson(); + + $this->assertNotNull($data); + + $this->assertEquals(0, $this->traverseJson($data, 'columns.0.identifier')); + $this->assertEquals('Payment Date', $this->traverseJson($data, 'columns.0.display_value')); + $this->assertEquals(1, $this->traverseJson($data, 'columns.1.identifier')); + $this->assertEquals('Payment Amount', $this->traverseJson($data, 'columns.1.display_value')); + $this->assertEquals(2, $this->traverseJson($data, 'columns.2.identifier')); + $this->assertEquals('Invoice Invoice Number', $this->traverseJson($data, 'columns.2.display_value')); + $this->assertEquals(4, $this->traverseJson($data, 'columns.4.identifier')); + $this->assertEquals('Client Name', $this->traverseJson($data, 'columns.4.display_value')); + + + $this->assertEquals('payment', $this->traverseJson($data, '0.0.entity')); + $this->assertEquals('date', $this->traverseJson($data, '0.0.id')); + $this->assertNull($this->traverseJson($data, '0.0.hashed_id')); + $this->assertEquals('payment.date', $this->traverseJson($data, '0.0.identifier')); $data = [ @@ -1364,6 +1452,13 @@ nlog($csv); } + private function traverseJson($array, $keys) + { + $value = data_get($array, $keys, false); + + return $value; + } + private function getFirstValueByColumn($csv, $column) { $reader = Reader::createFromString($csv); diff --git a/tests/Feature/QuoteTest.php b/tests/Feature/QuoteTest.php index 57ebde27b3e6..35605558207c 100644 --- a/tests/Feature/QuoteTest.php +++ b/tests/Feature/QuoteTest.php @@ -171,7 +171,7 @@ class QuoteTest extends TestCase $project = Project::find($this->decodePrimaryKey($res['data'][0]['project_id'])); - $this->assertEquals($project->name, ctrans('texts.quote_number_short') . " " . $this->quote->number."[{$this->quote->client->present()->name()}]"); + $this->assertEquals($project->name, ctrans('texts.quote_number_short') . " " . $this->quote->number." [{$this->quote->client->present()->name()}]"); } public function testQuoteList() diff --git a/tests/MockUnitData.php b/tests/MockUnitData.php index 4ca402d23a6c..fcf7c3e1841f 100644 --- a/tests/MockUnitData.php +++ b/tests/MockUnitData.php @@ -11,15 +11,16 @@ namespace Tests; +use App\Models\User; +use App\Models\Client; +use App\Models\Vendor; +use App\Models\Account; +use App\Models\Company; +use App\Models\CompanyToken; +use App\Models\ClientContact; use App\DataMapper\CompanySettings; use App\DataMapper\DefaultSettings; use App\Factory\InvoiceItemFactory; -use App\Models\Account; -use App\Models\Client; -use App\Models\ClientContact; -use App\Models\Company; -use App\Models\CompanyToken; -use App\Models\User; /** * Class MockUnitData. @@ -34,6 +35,8 @@ trait MockUnitData public $client; + public $vendor; + public $faker; public $primary_contact; @@ -92,6 +95,11 @@ trait MockUnitData 'company_id' => $this->company->id, ]); + $this->vendor = Vendor::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + $this->primary_contact = ClientContact::factory()->create([ 'user_id' => $this->user->id, 'client_id' => $this->client->id, diff --git a/tests/Unit/ClientSettingsTest.php b/tests/Unit/ClientSettingsTest.php index 01e707a70506..e0200d021628 100644 --- a/tests/Unit/ClientSettingsTest.php +++ b/tests/Unit/ClientSettingsTest.php @@ -24,6 +24,8 @@ class ClientSettingsTest extends TestCase use MockAccountData; use DatabaseTransactions; + public $faker; + protected function setUp() :void { parent::setUp(); @@ -33,6 +35,122 @@ class ClientSettingsTest extends TestCase $this->faker = \Faker\Factory::create(); } + + public function testClientValidSettingsWithBadProps() + { + $data = [ + 'name' => $this->faker->firstName(), + 'id_number' => 'Coolio', + 'settings' => [ + 'currency_id' => '43', + 'language_id' => '1', + 'website' => null, + 'address1' => null, + 'address2' => null, + 'city' => null, + 'state' => null, + 'postal_code' => null, + 'phone' => null, + 'email' => null, + 'vat_number' => null, + 'id_number' => null, + 'purchase_order_terms' => null, + 'purchase_order_footer' => null, + 'besr_id' => null, + 'qr_iban' => null, + 'name' => 'frank', + 'custom_value1' => null, + 'custom_value2' => null, + 'custom_value3' => null, + 'custom_value4' => null, + 'invoice_terms' => null, + 'quote_terms' =>null, + 'quote_footer' => null, + 'credit_terms' => null, + 'credit_footer' => null, + ], + ]; + + $response = false; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/clients/', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('frank', $arr['data']['settings']['name']); + + $client_id = $arr['data']['id']; + + $data = [ + 'name' => $this->faker->firstName(), + 'id_number' => 'Coolio', + 'settings' => [ + 'currency_id' => '43', + 'language_id' => '1', + 'name' => 'white', + ], + ]; + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/clients/'.$client_id, $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $this->assertEquals('white', $arr['data']['settings']['name']); + + $data = [ + 'name' => $this->faker->firstName(), + 'id_number' => 'Coolio', + 'settings' => [ + 'currency_id' => '43', + 'language_id' => '1', + 'website' => null, + 'address1' => null, + 'besr_id' => null, + 'qr_iban' => null, + 'name' => 'white', + 'custom_value1' => null, + 'custom_value2' => null, + 'custom_value3' => null, + 'custom_value4' => null, + 'invoice_terms' => null, + 'quote_terms' =>null, + 'quote_footer' => null, + 'credit_terms' => null, + 'credit_footer' => null, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/clients/'.$client_id, $data); + + $response->assertStatus(200); + + $arr = $response->json(); + $this->assertEquals('white', $arr['data']['settings']['name']); + + // $this->assertEquals('1', $arr['data']['settings']['currency_id']); + // $this->assertEquals('1', $arr['data']['settings']['language_id']); + // $this->assertEquals('1', $arr['data']['settings']['payment_terms']); + // $this->assertEquals(10, $arr['data']['settings']['default_task_rate']); + // $this->assertEquals(true, $arr['data']['settings']['send_reminders']); + // $this->assertEquals('1', $arr['data']['settings']['valid_until']); + } + + + public function testClientBaseline() { $data = [ @@ -49,7 +167,6 @@ class ClientSettingsTest extends TestCase ])->post('/api/v1/clients/', $data); } catch (ValidationException $e) { $message = json_decode($e->validator->getMessageBag(), 1); - nlog($message); } $response->assertStatus(200); @@ -83,7 +200,6 @@ class ClientSettingsTest extends TestCase ])->post('/api/v1/clients/', $data); } catch (ValidationException $e) { $message = json_decode($e->validator->getMessageBag(), 1); - nlog($message); } $response->assertStatus(200); @@ -115,17 +231,14 @@ class ClientSettingsTest extends TestCase $response = false; - try { + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->post('/api/v1/clients/', $data); - } catch (ValidationException $e) { - $message = json_decode($e->validator->getMessageBag(), 1); - nlog($message); - } + ])->postJson('/api/v1/clients/', $data); + - $response->assertStatus(302); + $response->assertStatus(422); } public function testClientIllegalLanguage()