mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
commit
50b2fffac2
@ -1 +1 @@
|
||||
5.7.11
|
||||
5.7.12
|
@ -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();
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
->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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ class ClientFactory
|
||||
$client->is_deleted = 0;
|
||||
$client->client_hash = Str::random(40);
|
||||
$client->settings = ClientSettings::defaults();
|
||||
$client->classification = '';
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ class VendorFactory
|
||||
$vendor->country_id = 4;
|
||||
$vendor->is_deleted = 0;
|
||||
$vendor->vendor_hash = Str::random(40);
|
||||
$vendor->classification = '';
|
||||
|
||||
return $vendor;
|
||||
}
|
||||
|
@ -391,8 +391,6 @@ class InvoiceItemSum
|
||||
{
|
||||
$this->setGroupedTaxes(collect([]));
|
||||
|
||||
|
||||
|
||||
foreach ($this->line_items as $key => $this->item) {
|
||||
if ($this->item->line_total == 0) {
|
||||
continue;
|
||||
|
@ -349,15 +349,17 @@ 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);
|
||||
|
||||
$item_tax += $item_tax_rate1_total;
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -93,6 +93,7 @@ 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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
79
app/Jobs/Ninja/CheckACHStatus.php
Normal file
79
app/Jobs/Ninja/CheckACHStatus.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?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\Jobs\Ninja;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Models\ClientGatewayToken;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class CheckACHStatus implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//multiDB environment, need to
|
||||
foreach (MultiDB::$dbs as $db) {
|
||||
MultiDB::setDB($db);
|
||||
|
||||
nlog("Checking ACH status");
|
||||
|
||||
ClientGatewayToken::query()
|
||||
->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) {
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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')) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ 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);
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ 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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -169,6 +169,7 @@ class Client extends BaseModel implements HasLocalePreference
|
||||
'routing_id',
|
||||
'is_tax_exempt',
|
||||
'has_valid_vat_number',
|
||||
'classification',
|
||||
];
|
||||
|
||||
protected $with = [
|
||||
|
@ -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);
|
||||
|
@ -113,6 +113,7 @@ class Vendor extends BaseModel
|
||||
'custom_value4',
|
||||
'number',
|
||||
'language_id',
|
||||
'classification',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
147
app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php
Normal file
147
app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php
Normal file
@ -0,0 +1,147 @@
|
||||
<?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\PaymentDrivers\Stripe\Jobs;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Payment;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\PaymentHash;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Models\CompanyGateway;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\PaymentDrivers\Stripe\Utilities;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
|
||||
class ChargeRefunded implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Utilities;
|
||||
|
||||
public $tries = 1; //number of retries
|
||||
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public $stripe_request;
|
||||
|
||||
public $company_key;
|
||||
|
||||
private $company_gateway_id;
|
||||
|
||||
public $payment_completed = false;
|
||||
|
||||
public function __construct($stripe_request, $company_key, $company_gateway_id)
|
||||
{
|
||||
$this->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)];
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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
|
||||
|
||||
];
|
||||
}
|
||||
|
@ -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 ?: '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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 ?: '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
132
composer.lock
generated
132
composer.lock
generated
@ -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",
|
||||
|
@ -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', ''),
|
||||
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('clients', function (Blueprint $table) {
|
||||
$table->string('classification')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('vendors', function (Blueprint $table) {
|
||||
$table->string('classification')->nullable();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
@ -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');
|
||||
|
220
tests/Feature/ClassificationTest.php
Normal file
220
tests/Feature/ClassificationTest.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?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 Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
use Tests\MockUnitData;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
class ClassificationTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use DatabaseTransactions;
|
||||
use MockUnitData;
|
||||
|
||||
protected function setUp() :void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->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']);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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'));
|
||||
@ -537,6 +592,20 @@ nlog($csv);
|
||||
$this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3'));
|
||||
$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'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -594,6 +663,25 @@ nlog($csv);
|
||||
$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);
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user