diff --git a/.env.ci b/.env.ci index e4a3a26d6633..60c00fc0eae1 100644 --- a/.env.ci +++ b/.env.ci @@ -24,4 +24,4 @@ PHANTOMJS_PDF_GENERATION=false CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis -PDF_GENERATOR=hosted_ninja \ No newline at end of file +PDF_GENERATOR=snappdf \ No newline at end of file diff --git a/.github/workflows/react_release.yml b/.github/workflows/react_release.yml index c4d6f63994aa..ca3a62ce7f64 100644 --- a/.github/workflows/react_release.yml +++ b/.github/workflows/react_release.yml @@ -38,6 +38,9 @@ jobs: sudo php artisan cache:clear sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./ -type d -exec chmod 755 {} \; + - name: Set current date to variable + id: set_date + run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV - name: Prepare React FrontEnd run: | @@ -46,10 +49,11 @@ jobs: git checkout develop cp .env.example .env cp ../vite.config.ts.react ./vite.config.js + sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json npm i npm run build cp -r dist/* ../public/ - mv dist/index.html ../resources/views/react/index.blade.php + mv ../public/index.html ../resources/views/react/index.blade.php - name: Prepare JS/CSS assets run: | diff --git a/README.md b/README.md index ccbcfb4e4826..190b5eddab4e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
 @@ -8,25 +8,30 @@ # Invoice Ninja 5 -## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) +Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and added the most requested features to create an invoicing application like no other. Check the [Invoice Ninja YouTube Channel](https://www.youtube.com/@appinvoiceninja) to get up to speed, or try the [Demo](https://react.invoicing.co/demo) now. -Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com) +**Choose your setup** -## Introduction +- [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure. +- [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility. -Version 5 of Invoice Ninja is here! -We took the best parts of version 4 and add the most requested features -to produce a invoicing application like no other. +All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app. -All Pro and Enterprise features from the hosted app are included in the open code. -We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app. +#### Get social with us -* [Videos](https://www.youtube.com/@appinvoiceninja) -* [API Documentation](https://api-docs.invoicing.co/) -* [APP Documentation](https://invoiceninja.github.io/) * [Support Forum](https://forum.invoiceninja.com) +* [Slack](http://slack.invoiceninja.com) +* [Discord](https://discord.gg/ZwEdtfCwXA) +* [Instagram](https://www.instagram.com/appinvoiceninja) -## Setup +#### Documentation + +* [Invoice Ninja - API](https://api-docs.invoicing.co/) +* [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) +* [Invoice Ninja - User Guide](https://invoiceninja.github.io/en/user-guide/) +* [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) + +## Installation Options and Clients ### Mobile Apps * [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone) @@ -39,16 +44,21 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding * [Linux - Snap](https://snapcraft.io/invoiceninja) * [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja) -### Installation Options +### Self-Hosted Server Installation +**Note:** The self-hosted options do support the desktop and mobile apps. + +* [Server or VM](https://invoiceninja.github.io/en/self-host-installation/) * [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) -* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html) +* [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html) * [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja) ### Recommended Providers * [Stripe](https://stripe.com/) * [Postmark](https://postmarkapp.com/) -## Quick Hosting Setup +## [Advanced] Quick Hosting Setup + +In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you. ```sh git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git @@ -84,6 +94,7 @@ pass: password ``` ## Developers Guide +In addition to the official [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) we've got your back with some insights. ### App Design diff --git a/VERSION.txt b/VERSION.txt index d315f4df7571..c0204ea277af 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.10.13 \ No newline at end of file +5.10.16 \ No newline at end of file diff --git a/app/Console/Commands/BackupUpdate.php b/app/Console/Commands/BackupUpdate.php index 33b421ea384d..b2a16bff2502 100644 --- a/app/Console/Commands/BackupUpdate.php +++ b/app/Console/Commands/BackupUpdate.php @@ -177,7 +177,6 @@ class BackupUpdate extends Command $doc_bin = $document->getFile(); } catch(\Exception $e) { nlog("Exception:: BackupUpdate::" . $e->getMessage()); - nlog($e->getMessage()); } if ($doc_bin) { diff --git a/app/DataMapper/Tax/PL/Rule.php b/app/DataMapper/Tax/PL/Rule.php new file mode 100644 index 000000000000..7d9c4d957944 --- /dev/null +++ b/app/DataMapper/Tax/PL/Rule.php @@ -0,0 +1,44 @@ +model) { + if(!$model) { $this->regions = $this->init(); } else { - $this->regions = $model; + + //@phpstan-ignore-next-line + foreach($model as $key => $value) { + $this->{$key} = $value; + } + } + $this->migrate(); + } + + public function migrate(): self + { + + if($this->version == 'alpha') + { + $this->regions->EU->subregions->PL = new \stdClass(); + $this->regions->EU->subregions->PL->tax_rate = 23; + $this->regions->EU->subregions->PL->tax_name = 'VAT'; + $this->regions->EU->subregions->PL->reduced_tax_rate = 8; + $this->regions->EU->subregions->PL->apply_tax = false; + + $this->version = 'beta'; + } + + return $this; } /** @@ -474,6 +497,12 @@ class TaxModel $this->regions->EU->subregions->NL->reduced_tax_rate = 9; $this->regions->EU->subregions->NL->apply_tax = false; + $this->regions->EU->subregions->PL = new \stdClass(); + $this->regions->EU->subregions->PL->tax_rate = 23; + $this->regions->EU->subregions->PL->tax_name = 'VAT'; + $this->regions->EU->subregions->PL->reduced_tax_rate = 8; + $this->regions->EU->subregions->PL->apply_tax = false; + $this->regions->EU->subregions->PT = new \stdClass(); $this->regions->EU->subregions->PT->tax_rate = 23; $this->regions->EU->subregions->PT->tax_name = 'IVA'; diff --git a/app/DataMapper/Tax/tax_model.yaml b/app/DataMapper/Tax/tax_model.yaml index 60736e5f5f37..aa664f4efdca 100644 --- a/app/DataMapper/Tax/tax_model.yaml +++ b/app/DataMapper/Tax/tax_model.yaml @@ -197,6 +197,10 @@ region: vat: 21 reduced_vat: 9 apply_tax: false + PL: + vat: 23 + reduced_vat: 8 + apply_tax: false PT: vat: 23 reduced_vat: 6 diff --git a/app/Events/Client/ClientWasArchived.php b/app/Events/Client/ClientWasArchived.php index 2fb1791bbf6c..35699606ad2b 100644 --- a/app/Events/Client/ClientWasArchived.php +++ b/app/Events/Client/ClientWasArchived.php @@ -13,15 +13,21 @@ namespace App\Events\Client; use App\Models\Client; use App\Models\Company; +use League\Fractal\Manager; +use League\Fractal\Resource\Item; use Illuminate\Broadcasting\Channel; -use Illuminate\Broadcasting\InteractsWithSockets; -use Illuminate\Foundation\Events\Dispatchable; +use App\Transformers\ArraySerializer; use Illuminate\Queue\SerializesModels; +use App\Transformers\ClientTransformer; +use Illuminate\Broadcasting\PrivateChannel; +use Illuminate\Foundation\Events\Dispatchable; +use Illuminate\Broadcasting\InteractsWithSockets; +use Illuminate\Contracts\Broadcasting\ShouldBroadcast; /** * Class ClientWasArchived. */ -class ClientWasArchived +class ClientWasArchived implements ShouldBroadcast { use Dispatchable; use InteractsWithSockets; @@ -50,13 +56,34 @@ class ClientWasArchived $this->event_vars = $event_vars; } - // /** - // * Get the channels the event should broadcast on. - // * - // * @return Channel|array - // */ + public function broadcastWith() + { + + $manager = new Manager(); + $manager->setSerializer(new ArraySerializer()); + $class = sprintf('App\\Transformers\\%sTransformer', class_basename($this->client)); + + $transformer = new $class(); + + $resource = new Item($this->client, $transformer, $this->client->getEntityType()); + $data = $manager->createData($resource)->toArray(); + + return $data; + + } + + /** + * Get the channels the event should broadcast on. + * + * @return Channel|array + */ public function broadcastOn() { - return []; + + return [ + new PrivateChannel("company-{$this->company->company_key}"), + ]; + } + } diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 374e7783c3fe..4ea22fe974ee 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -172,6 +172,7 @@ class BaseExport 'tax_rate3' => 'invoice.tax_rate3', 'recurring_invoice' => 'invoice.recurring_id', 'auto_bill' => 'invoice.auto_bill_enabled', + 'project' => 'invoice.project', ]; protected array $recurring_invoice_report_keys = [ @@ -449,6 +450,7 @@ class BaseExport 'status' => 'task.status_id', 'project' => 'task.project_id', 'billable' => 'task.billable', + 'item_notes' => 'task.item_notes', ]; protected array $forced_client_fields = [ @@ -1038,6 +1040,10 @@ class BaseExport $recurring_filters = []; + if($this->company->getSetting('report_include_drafts')){ + $recurring_filters[] = RecurringInvoice::STATUS_DRAFT; + } + if (in_array('active', $status_parameters)) { $recurring_filters[] = RecurringInvoice::STATUS_ACTIVE; } @@ -1252,7 +1258,7 @@ class BaseExport $date_range = $this->input['date_range']; - if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) { + if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key'] ?? '') > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) { $this->date_key = $this->input['date_key']; } @@ -1263,7 +1269,7 @@ class BaseExport $custom_start_date = now()->startOfYear(); $custom_end_date = now(); } - + switch ($date_range) { case 'all': $this->start_date = 'All available data'; diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index d87c49ff943c..39ece67a28a9 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -153,9 +153,9 @@ class InvoiceExport extends BaseExport private function decorateAdvancedFields(Invoice $invoice, array $entity): array { - // if (in_array('invoice.status', $this->input['report_keys'])) { - // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); - // } + if (in_array('invoice.project', $this->input['report_keys'])) { + $entity['invoice.project'] = $invoice->project ? $invoice->project->name : ''; + } if (in_array('invoice.recurring_id', $this->input['report_keys'])) { $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 14a38aebee27..b2b13b523acf 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -265,6 +265,10 @@ class InvoiceItemExport extends BaseExport $entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line } + if (in_array('invoice.project', $this->input['report_keys'])) { + $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';// @phpstan-ignore-line + } + return $entity; } diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 2acd8faa64cc..155d61a88f48 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -29,7 +29,7 @@ class TaskExport extends BaseExport { private $entity_transformer; - public string $date_key = 'created_at'; + public string $date_key = 'calculated_start_date'; private string $date_format = 'Y-m-d'; @@ -156,7 +156,7 @@ class TaskExport extends BaseExport $entity[$key] = $transformed_entity[$parts[1]]; } elseif (array_key_exists($key, $transformed_entity)) { $entity[$key] = $transformed_entity[$key]; - } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration'])) { + } elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) { $entity[$key] = ''; } else { $entity[$key] = $this->decorator->transform($key, $task); @@ -175,7 +175,7 @@ class TaskExport extends BaseExport private function iterateLogs(Task $task, array $entity) { $timezone = Timezone::find($task->company->settings->timezone_id); - $timezone_name = 'US/Eastern'; + $timezone_name = 'America/New_York'; if ($timezone) { $timezone_name = $timezone->name; @@ -209,6 +209,14 @@ class TaskExport extends BaseExport $entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s'); } + if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) { + $entity['task.billable'] = isset($item[3]) && $item[3] == 'true' ? ctrans('texts.yes') : ctrans('texts.no'); + } + + if (in_array('task.item_notes', $this->input['report_keys']) || in_array('item_notes', $this->input['report_keys'])) { + $entity['task.item_notes'] = isset($item[2]) ? (string)$item[2] : ''; + } + $entity = $this->decorateAdvancedFields($task, $entity); $this->storage_array[] = $entity; @@ -219,6 +227,8 @@ class TaskExport extends BaseExport $entity['task.end_time'] = ''; $entity['task.duration'] = ''; $entity['task.duration_words'] = ''; + $entity['task.billable'] = ''; + $entity['task.item_notes'] = ''; } diff --git a/app/Export/Decorators/InvoiceDecorator.php b/app/Export/Decorators/InvoiceDecorator.php index 35985decba66..b6579b7f546d 100644 --- a/app/Export/Decorators/InvoiceDecorator.php +++ b/app/Export/Decorators/InvoiceDecorator.php @@ -92,6 +92,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface { return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : ''; } + public function auto_bill_enabled(Invoice $invoice) { return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no'); diff --git a/app/Export/Decorators/TaskDecorator.php b/app/Export/Decorators/TaskDecorator.php index 05f7c2e69690..d8d908a34c00 100644 --- a/app/Export/Decorators/TaskDecorator.php +++ b/app/Export/Decorators/TaskDecorator.php @@ -18,6 +18,7 @@ use Carbon\Carbon; class TaskDecorator extends Decorator implements DecoratorInterface { + //@todo - we do not handle iterating through the timelog here. public function transform(string $key, mixed $entity): mixed { $task = false; @@ -42,7 +43,7 @@ class TaskDecorator extends Decorator implements DecoratorInterface { $timezone = Timezone::find($task->company->settings->timezone_id); - $timezone_name = 'US/Eastern'; + $timezone_name = 'America/New_York'; if ($timezone) { $timezone_name = $timezone->name; @@ -71,7 +72,7 @@ class TaskDecorator extends Decorator implements DecoratorInterface { $timezone = Timezone::find($task->company->settings->timezone_id); - $timezone_name = 'US/Eastern'; + $timezone_name = 'America/New_York'; if ($timezone) { $timezone_name = $timezone->name; @@ -95,6 +96,26 @@ class TaskDecorator extends Decorator implements DecoratorInterface return ''; } + + /** + * billable + * + * @todo + */ + public function billable(Task $task) + { + return ''; + } + + /** + * items_notes + * @todo + */ + public function items_notes(Task $task) + { + return ''; + } + public function duration(Task $task) { return $task->calcDuration(); diff --git a/app/Filters/BankTransactionFilters.php b/app/Filters/BankTransactionFilters.php index faaeaa7ade4a..f786f49e78ed 100644 --- a/app/Filters/BankTransactionFilters.php +++ b/app/Filters/BankTransactionFilters.php @@ -155,11 +155,13 @@ class BankTransactionFilters extends QueryFilters $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc'; if ($sort_col[0] == 'deposit') { - return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir); + return $this->builder->orderByRaw("(CASE WHEN base_type = 'CREDIT' THEN amount END) $dir")->orderBy('amount', $dir); + // return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir); } if ($sort_col[0] == 'withdrawal') { - return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir); + return $this->builder->orderByRaw("(CASE WHEN base_type = 'DEBIT' THEN amount END) $dir")->orderBy('amount', $dir); + // return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir); } if ($sort_col[0] == 'status') { diff --git a/app/Filters/ClientFilters.php b/app/Filters/ClientFilters.php index ffc3e4fd7717..f1b9f1303d80 100644 --- a/app/Filters/ClientFilters.php +++ b/app/Filters/ClientFilters.php @@ -41,7 +41,7 @@ class ClientFilters extends QueryFilters */ public function balance(string $balance = ''): Builder { - if (strlen($balance) == 0) { + if (strlen($balance) == 0 || count(explode(":", $balance)) < 2) { return $this->builder; } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 2d2ef7cd06b8..de5a932fb76f 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -153,22 +153,22 @@ class InvoiceFilters extends QueryFilters { return $this->builder->where(function ($query) { - $query->whereIn('invoices.status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) - ->where('invoices.is_deleted', 0) - ->where('invoices.balance', '>', 0) - ->orWhere(function ($query) { + $query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) + ->where('is_deleted', 0) + ->where('balance', '>', 0) + ->where(function ($query) { - $query->whereNull('invoices.due_date') + $query->whereNull('due_date') ->orWhere(function ($q) { - $q->where('invoices.due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', 0); + $q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0); }) ->orWhere(function ($q) { - $q->where('invoices.partial_due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', '>', 0); + $q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0); }); }) - ->orderByRaw('ISNULL(invoices.due_date), invoices.due_date ' . 'desc') - ->orderByRaw('ISNULL(invoices.partial_due_date), invoices.partial_due_date ' . 'desc'); + ->orderByRaw('ISNULL(due_date), due_date ' . 'desc') + ->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc'); }); } diff --git a/app/Helpers/Bank/Nordigen/Transformer/TransactionTransformer.php b/app/Helpers/Bank/Nordigen/Transformer/TransactionTransformer.php index 1cf71f50c506..7d3d604a6897 100644 --- a/app/Helpers/Bank/Nordigen/Transformer/TransactionTransformer.php +++ b/app/Helpers/Bank/Nordigen/Transformer/TransactionTransformer.php @@ -171,7 +171,7 @@ class TransactionTransformer implements BankRevenueInterface private function formatDate(string $input) { $timezone = Timezone::find($this->company->settings->timezone_id); - $timezone_name = 'US/Eastern'; + $timezone_name = 'America/New_York'; if ($timezone) { $timezone_name = $timezone->name; diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 76ba72ca5e4f..d1d7dd56cff0 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -11,16 +11,18 @@ namespace App\Helpers\Invoice; -use App\DataMapper\BaseSettings; -use App\DataMapper\InvoiceItem; -use App\DataMapper\Tax\RuleInterface; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; use App\Models\Credit; +use App\Models\Vendor; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\DataMapper\InvoiceItem; +use App\DataMapper\BaseSettings; +use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSum @@ -120,7 +122,7 @@ class InvoiceItemSum private $tax_collection; - private ?Client $client; + private Client | Vendor $client; private bool $calc_tax = false; @@ -131,10 +133,10 @@ class InvoiceItemSum $this->tax_collection = collect([]); $this->invoice = $invoice; + $this->client = $invoice->client ?? $invoice->vendor; if ($this->invoice->client) { $this->currency = $this->invoice->client->currency(); - $this->client = $this->invoice->client; $this->shouldCalculateTax(); } else { $this->currency = $this->invoice->vendor->currency(); @@ -313,7 +315,7 @@ class InvoiceItemSum $key = str_replace(' ', '', $tax_name.$tax_rate); - $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.floatval($tax_rate).'%']; + $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%']; $this->tax_collection->push(collect($group_tax)); } diff --git a/app/Helpers/Invoice/InvoiceItemSumInclusive.php b/app/Helpers/Invoice/InvoiceItemSumInclusive.php index da4c3e1f3aa4..df9a897abefa 100644 --- a/app/Helpers/Invoice/InvoiceItemSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceItemSumInclusive.php @@ -11,14 +11,16 @@ namespace App\Helpers\Invoice; -use App\DataMapper\Tax\RuleInterface; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; use App\Models\Credit; +use App\Models\Vendor; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSumInclusive @@ -109,7 +111,7 @@ class InvoiceItemSumInclusive private bool $calc_tax = false; - private ?Client $client; + private Client | Vendor $client; private RuleInterface $rule; @@ -118,10 +120,10 @@ class InvoiceItemSumInclusive $this->tax_collection = collect([]); $this->invoice = $invoice; + $this->client = $invoice->client ?? $invoice->vendor; if ($this->invoice->client) { $this->currency = $this->invoice->client->currency(); - $this->client = $this->invoice->client; $this->shouldCalculateTax(); } else { $this->currency = $this->invoice->vendor->currency(); @@ -265,7 +267,7 @@ class InvoiceItemSumInclusive $key = str_replace(' ', '', $tax_name.$tax_rate); - $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.$tax_rate.'%']; + $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%']; $this->tax_collection->push(collect($group_tax)); } diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 32a821445d17..e69cf394a6ca 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -11,12 +11,14 @@ namespace App\Helpers\Invoice; +use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Vendor; use App\Utils\Number; use App\Utils\Traits\NumberFormatter; use Illuminate\Support\Collection; @@ -50,6 +52,8 @@ class InvoiceSum private $precision; + private Client | Vendor $client; + public InvoiceItemSum $invoice_items; private $rappen_rounding = false; @@ -60,18 +64,15 @@ class InvoiceSum */ public function __construct($invoice) { + $this->invoice = $invoice; + $this->client = $invoice->client ?? $invoice->vendor; - if ($this->invoice->client) { - $this->precision = $this->invoice->client->currency()->precision; - $this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding'); - } else { - $this->precision = $this->invoice->vendor->currency()->precision; - $this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding'); - - } + $this->precision = $this->client->currency()->precision; + $this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding'); $this->tax_map = new Collection(); + } public function build() @@ -131,7 +132,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) { @@ -139,7 +140,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) >= 2) { @@ -147,7 +148,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax]; } return $this; diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 6df714231f04..07fdf3e86894 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -12,7 +12,10 @@ namespace App\Helpers\Invoice; use App\Models\Quote; +use App\Utils\Number; +use App\Models\Client; use App\Models\Credit; +use App\Models\Vendor; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Models\RecurringQuote; @@ -49,6 +52,8 @@ class InvoiceSumInclusive private $rappen_rounding = false; + private Client | Vendor $client; + public InvoiceItemSumInclusive $invoice_items; /** * Constructs the object with Invoice and Settings object. @@ -58,14 +63,10 @@ class InvoiceSumInclusive public function __construct($invoice) { $this->invoice = $invoice; - - if ($this->invoice->client) { - $this->precision = $this->invoice->client->currency()->precision; - $this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding'); - } else { - $this->precision = $this->invoice->vendor->currency()->precision; - $this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding'); - } + $this->client = $invoice->client ?? $invoice->vendor; + + $this->precision = $this->client->currency()->precision; + $this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding'); $this->tax_map = new Collection(); } @@ -157,19 +158,19 @@ class InvoiceSumInclusive $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) { $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) { $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax]; } return $this; diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php index 394e762d9749..07fc7fc238e4 100644 --- a/app/Http/Controllers/ChartController.php +++ b/app/Http/Controllers/ChartController.php @@ -66,7 +66,7 @@ class ChartController extends BaseController return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); } - public function calculatedField(ShowCalculatedFieldRequest $request) + public function calculatedFields(ShowCalculatedFieldRequest $request) { /** @var \App\Models\User auth()->user() */ diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index f1993693a90f..ec30aacc37b6 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -300,7 +300,9 @@ class InvitationController extends Controller 'signature' => false, 'contact_first_name' => $invitation->contact->first_name ?? '', 'contact_last_name' => $invitation->contact->last_name ?? '', - 'contact_email' => $invitation->contact->email ?? '' + 'contact_email' => $invitation->contact->email ?? '', + 'client_city' => $invitation->client->city ?? '', + 'client_postal_code' => $invitation->client->postal_code ?? '', ]; $request->replace($data); diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 728df5eb1de1..94a46bd5cbe5 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -108,11 +108,11 @@ class PaymentController extends Controller */ public function process(Request $request) { - $request->validate([ - 'contact_first_name' => ['required'], - 'contact_last_name' => ['required'], - 'contact_email' => ['required', 'email'], - ]); + // $request->validate([ + // 'contact_first_name' => ['required'], + // 'contact_last_name' => ['required'], + // 'contact_email' => ['required', 'email'], + // ]); return (new InstantPayment($request))->run(); } diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 66eed43075f6..f0b26ad7d7c7 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -85,7 +85,7 @@ class ImportController extends Controller $contents = $this->convertEncoding($contents); // Store the csv in cache with an expiry of 10 minutes - Cache::put($hash.'-'.$entityType, base64_encode($contents), 600); + Cache::put($hash.'-'.$entityType, base64_encode($contents), 1200); // Parse CSV $csv_array = $this->getCsvData($contents); diff --git a/app/Http/Controllers/MailgunWebhookController.php b/app/Http/Controllers/MailgunWebhookController.php index 0585ebc02d97..88985e136158 100644 --- a/app/Http/Controllers/MailgunWebhookController.php +++ b/app/Http/Controllers/MailgunWebhookController.php @@ -35,7 +35,7 @@ class MailgunWebhookController extends BaseController } if(\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) { - ProcessMailgunWebhook::dispatch($request->all())->delay(10); + ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2,10)); } return response()->json(['message' => 'Success.'], 200); diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index 69f24d28ab19..27b9a4b020cf 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -64,6 +64,9 @@ class StoreCreditRequest extends Request $user = auth()->user(); $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; // $rules['number'] = new UniqueCreditNumberRule($this->all()); $rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', $user->company()->id)]; diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index d7ece562fb6d..0733e3891326 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -65,6 +65,9 @@ class UpdateCreditRequest extends Request $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('credits')->where('company_id', $user->company()->id)->ignore($this->credit->id)]; $rules['client_id'] = ['bail', 'sometimes',Rule::in([$this->credit->client_id])]; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['line_items'] = 'array'; diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 767ac8db722d..34907c6d602f 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -38,11 +38,14 @@ class StoreInvoiceRequest extends Request public function rules() { - $rules = []; /** @var \App\Models\User $user */ $user = auth()->user(); + $rules = []; + + $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)]; + if ($this->file('documents') && is_array($this->file('documents'))) { $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { @@ -57,16 +60,16 @@ class StoreInvoiceRequest extends Request $rules['file'] = $this->fileValidation(); } - $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0'; - - $rules['invitations.*.client_contact_id'] = 'distinct'; - $rules['number'] = ['bail', 'nullable', Rule::unique('invoices')->where('company_id', $user->company()->id)]; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; + $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['is_amount_discount'] = ['boolean']; $rules['date'] = 'bail|sometimes|date:Y-m-d'; + $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; $rules['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric|max:99999999999999'; @@ -79,18 +82,17 @@ class StoreInvoiceRequest extends Request $rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date']; - $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; - $rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999']; - // $rules['amount'] = ['sometimes', 'bail', 'max:99999999999999']; - // $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date']; - return $rules; } public function prepareForValidation() { + + /** @var \App\Models\User $user */ + $user = auth()->user(); + $input = $this->all(); $input = $this->decodePrimaryKeys($input); @@ -102,24 +104,24 @@ class StoreInvoiceRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['amount'] = $this->entityTotalAmount($input['line_items']); } - if(isset($input['partial']) && $input['partial'] == 0) { $input['partial_due_date'] = null; - } - - if (array_key_exists('tax_rate1', $input) && is_null($input['tax_rate1'])) { + } + if (!isset($input['tax_rate1'])) { $input['tax_rate1'] = 0; } - if (array_key_exists('tax_rate2', $input) && is_null($input['tax_rate2'])) { + if (!isset($input['tax_rate2'])) { $input['tax_rate2'] = 0; } - if (array_key_exists('tax_rate3', $input) && is_null($input['tax_rate3'])) { + if (!isset($input['tax_rate3'])) { $input['tax_rate3'] = 0; } if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { $input['exchange_rate'] = 1; } - + if(!isset($input['date'])) { + $input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d'); + } //handles edge case where we need for force set the due date of the invoice. if((isset($input['partial_due_date']) && strlen($input['partial_due_date']) > 1) && (!array_key_exists('due_date', $input) || (empty($input['due_date']) && empty($this->invoice->due_date)))) { $client = \App\Models\Client::withTrashed()->find($input['client_id']); diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 9d960ef379c9..1dee8fac1a1a 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -67,6 +67,9 @@ class UpdateInvoiceRequest extends Request $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])]; $rules['line_items'] = 'array'; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; + $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['tax_rate1'] = 'bail|sometimes|numeric'; diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 3805ed73a9e3..6e2387ce79ae 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -50,6 +50,10 @@ class StorePurchaseOrderRequest extends Request $rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', $user->company()->id)]; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.vendor_contact_id'] = 'bail|required|distinct'; + $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['is_amount_discount'] = ['boolean']; $rules['line_items'] = 'array'; diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 5ae69e8d1598..6403d1fc385f 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -53,6 +53,9 @@ class UpdatePurchaseOrderRequest extends Request $rules['line_items'] = 'array'; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.vendor_contact_id'] = 'bail|required|distinct'; + $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['is_amount_discount'] = ['boolean']; diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 4b2624ee7a6e..afb6226cfe62 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -11,12 +11,13 @@ namespace App\Http\Requests\Quote; -use App\Http\Requests\Request; -use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule; use App\Models\Quote; -use App\Utils\Traits\CleanLineItems; +use App\Http\Requests\Request; use App\Utils\Traits\MakesHash; use Illuminate\Validation\Rule; +use App\Utils\Traits\CleanLineItems; +use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule; +use App\Http\ValidationRules\Project\ValidProjectForClient; class StoreQuoteRequest extends Request { @@ -43,7 +44,7 @@ class StoreQuoteRequest extends Request $rules = []; - $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)]; + $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted',0)]; if ($this->file('documents') && is_array($this->file('documents'))) { $rules['documents.*'] = $this->fileValidation(); @@ -59,15 +60,28 @@ class StoreQuoteRequest extends Request $rules['file'] = $this->fileValidation(); } - $rules['number'] = ['nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)]; + $rules['number'] = ['bail','nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)]; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; + + $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; + $rules['is_amount_discount'] = ['boolean']; + $rules['date'] = 'bail|sometimes|date:Y-m-d'; + $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; + $rules['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric|max:99999999999999'; - $rules['is_amount_discount'] = ['boolean']; + $rules['tax_rate1'] = 'bail|sometimes|numeric'; + $rules['tax_rate2'] = 'bail|sometimes|numeric'; + $rules['tax_rate3'] = 'bail|sometimes|numeric'; + $rules['tax_name1'] = 'bail|sometimes|string|nullable'; + $rules['tax_name2'] = 'bail|sometimes|string|nullable'; + $rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['exchange_rate'] = 'bail|sometimes|numeric'; - $rules['line_items'] = 'array'; - $rules['date'] = 'bail|sometimes|date:Y-m-d'; + + $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date']; - $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; $rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999']; return $rules; @@ -89,19 +103,24 @@ class StoreQuoteRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['amount'] = $this->entityTotalAmount($input['line_items']); } - - if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { - $input['exchange_rate'] = 1; - } - if(isset($input['partial']) && $input['partial'] == 0) { $input['partial_due_date'] = null; } - + if (!isset($input['tax_rate1'])) { + $input['tax_rate1'] = 0; + } + if (!isset($input['tax_rate2'])) { + $input['tax_rate2'] = 0; + } + if (!isset($input['tax_rate3'])) { + $input['tax_rate3'] = 0; + } + if (!isset($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } if(!isset($input['date'])) { $input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d'); } - if(isset($input['partial_due_date']) && (!isset($input['due_date']) || strlen($input['due_date']) <= 1)) { $client = \App\Models\Client::withTrashed()->find($input['client_id']); $valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7; diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index 77ac6f2f4abd..1f5e795dedcf 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -55,6 +55,9 @@ class UpdateQuoteRequest extends Request } elseif ($this->file('file')) { $rules['file'] = $this->fileValidation(); } + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])]; diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 7dc6ef5fdc7e..3bdecfa1e66e 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -61,7 +61,8 @@ class StoreRecurringInvoiceRequest extends Request $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; - $rules['invitations.*.client_contact_id'] = 'distinct'; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['frequency_id'] = 'required|integer|digits_between:1,12'; diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index a89baa8d2cf9..6ea38d30ed5b 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -60,6 +60,8 @@ class UpdateRecurringInvoiceRequest extends Request $rules['number'] = ['bail', 'sometimes', Rule::unique('recurring_invoices')->where('company_id', $user->company()->id)->ignore($this->recurring_invoice->id)]; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->recurring_invoice->client_id])]; diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 1d65de052fa6..0d5e5a13a83f 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -19,8 +19,9 @@ use Illuminate\Contracts\Validation\ValidationRule; */ class BlackListRule implements ValidationRule { - /** Bad domains +/- dispoable email domains */ + /** Bad domains +/- disposable email domains */ private array $blacklist = [ + 'padvn.com', 'anonaddy.me', 'nqmo.com', 'wireconnected.com', diff --git a/app/Http/ViewComposers/Components/Rotessa/AccountComponent.php b/app/Http/ViewComposers/Components/Rotessa/AccountComponent.php index 46e44dc330d1..e161b3a1216f 100644 --- a/app/Http/ViewComposers/Components/Rotessa/AccountComponent.php +++ b/app/Http/ViewComposers/Components/Rotessa/AccountComponent.php @@ -42,6 +42,7 @@ class AccountComponent extends Component public function render() { - return render('gateways.rotessa.components.account', array_merge($this->attributes->getAttributes(), $this->defaults) ); + + return render('gateways.rotessa.components.account', $this->attributes->getAttributes() + $this->defaults); } } diff --git a/app/Http/ViewComposers/Components/Rotessa/AddressComponent.php b/app/Http/ViewComposers/Components/Rotessa/AddressComponent.php index 5b5afc1599e5..b26222b18c53 100644 --- a/app/Http/ViewComposers/Components/Rotessa/AddressComponent.php +++ b/app/Http/ViewComposers/Components/Rotessa/AddressComponent.php @@ -43,6 +43,6 @@ class AddressComponent extends Component public function render() { - return render('gateways.rotessa.components.address',array_merge( $this->defaults, $this->attributes->getAttributes() ) ); + return render('gateways.rotessa.components.address', $this->attributes->getAttributes() + $this->defaults ); } } diff --git a/app/Http/ViewComposers/Components/Rotessa/ContactComponent.php b/app/Http/ViewComposers/Components/Rotessa/ContactComponent.php index b8c001b750e0..3557cd05351f 100644 --- a/app/Http/ViewComposers/Components/Rotessa/ContactComponent.php +++ b/app/Http/ViewComposers/Components/Rotessa/ContactComponent.php @@ -15,11 +15,12 @@ class ContactComponent extends Component { public function __construct(ClientContact $contact) { + $contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([ 'home_phone' =>$contact->client->phone, 'custom_identifier' => $contact->client->number, 'name' =>$contact->client->name, - 'id' => null + 'id' => $contact->client->contact_key, ] )->all(); $this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) ); @@ -37,12 +38,13 @@ class ContactComponent extends Component private $defaults = [ 'customer_type' => "Business", - 'customer_identifier' => null, - 'id' => null + 'custom_identifier' => null, + 'customer_id' => null ]; public function render() { - return render('gateways.rotessa.components.contact', array_merge($this->defaults, $this->attributes->getAttributes() ) ); + \Debugbar::debug($this->attributes->getAttributes() + $this->defaults); + return render('gateways.rotessa.components.contact', $this->attributes->getAttributes() + $this->defaults ); } } diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index b2897352cc4a..fef3ccdd269f 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -98,7 +98,7 @@ class BaseImport } /** @var string $base64_encoded_csv */ - $base64_encoded_csv = Cache::pull($this->hash.'-'.$entity_type); + $base64_encoded_csv = Cache::get($this->hash.'-'.$entity_type); if (empty($base64_encoded_csv)) { return null; @@ -473,6 +473,8 @@ class BaseImport $tasks = $this->groupTasks($tasks, $task_number_key); + nlog($tasks); + foreach ($tasks as $raw_task) { $task_data = []; @@ -702,16 +704,16 @@ class BaseImport ->save(); } - if ($invoice->status_id === Invoice::STATUS_DRAFT) { - } elseif ($invoice->status_id === Invoice::STATUS_SENT) { - $invoice = $invoice - ->service() - ->markSent() - ->save(); - } elseif ( - $invoice->status_id <= Invoice::STATUS_SENT && - $invoice->amount > 0 - ) { + if ($invoice->status_id == Invoice::STATUS_DRAFT) { + return $invoice; + } + + $invoice = $invoice + ->service() + ->markSent() + ->save(); + + if ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0) { if ($invoice->balance <= 0) { $invoice->status_id = Invoice::STATUS_PAID; $invoice->save(); diff --git a/app/Import/Providers/Wave.php b/app/Import/Providers/Wave.php index 89c3ef9931a2..1fbd58b0e0c0 100644 --- a/app/Import/Providers/Wave.php +++ b/app/Import/Providers/Wave.php @@ -172,7 +172,7 @@ class Wave extends BaseImport implements ImportInterface { $entity_type = 'expense'; - $data = $this->getCsvData($entity_type); + $data = $this->getCsvData('invoice'); if (!$data) { $this->entity_count['expense'] = 0; @@ -244,14 +244,17 @@ class Wave extends BaseImport implements ImportInterface if (empty($expense_data['vendor_id'])) { $vendor_data['user_id'] = $this->getUserIDForRecord($expense_data); - $vendor_repository->save( - ['name' => $raw_expense['Vendor Name']], - $vendor = VendorFactory::create( - $this->company->id, - $vendor_data['user_id'] - ) - ); - $expense_data['vendor_id'] = $vendor->id; + if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor'])) + { + $vendor_repository->save( + ['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])], + $vendor = VendorFactory::create( + $this->company->id, + $vendor_data['user_id'] + ) + ); + $expense_data['vendor_id'] = $vendor->id; + } } $validator = Validator::make( diff --git a/app/Import/Transformer/Csv/TaskTransformer.php b/app/Import/Transformer/Csv/TaskTransformer.php index edd8737131cb..54636349f050 100644 --- a/app/Import/Transformer/Csv/TaskTransformer.php +++ b/app/Import/Transformer/Csv/TaskTransformer.php @@ -46,6 +46,7 @@ class TaskTransformer extends BaseTransformer 'company_id' => $this->company->id, 'number' => $this->getString($task_data, 'task.number'), 'user_id' => $this->getString($task_data, 'task.user_id'), + 'rate' => $this->getFloat($task_data, 'task.rate'), 'client_id' => $clientId, 'project_id' => $this->getProjectId($projectId, $clientId), 'description' => $this->getString($task_data, 'task.description'), @@ -87,8 +88,7 @@ class TaskTransformer extends BaseTransformer $is_billable = true; } - if(isset($item['task.start_date']) && - isset($item['task.end_date'])) { + if(isset($item['task.start_date'])) { $start_date = $this->resolveStartDate($item); $end_date = $this->resolveEndDate($item); } elseif(isset($item['task.duration'])) { @@ -136,7 +136,7 @@ class TaskTransformer extends BaseTransformer private function resolveEndDate($item) { - $stub_end_date = $item['task.end_date']; + $stub_end_date = isset($item['task.end_date']) ? $item['task.end_date'] : $item['task.start_date']; $stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : ''; try { diff --git a/app/Import/Transformer/Wave/ExpenseTransformer.php b/app/Import/Transformer/Wave/ExpenseTransformer.php index 8f37c94b788a..afd282b80d63 100644 --- a/app/Import/Transformer/Wave/ExpenseTransformer.php +++ b/app/Import/Transformer/Wave/ExpenseTransformer.php @@ -36,18 +36,26 @@ class ExpenseTransformer extends BaseTransformer $total_tax += floatval($record['Sales Tax Amount']); } - $tax_rate = round(($total_tax / $amount) * 100, 3); + $tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0; + + if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1) + $public_notes = $data['Notes / Memo']; + elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1) + $public_notes = $data['Transaction Description']; + else + $public_notes = ''; + $transformed = [ 'company_id' => $this->company->id, 'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')), 'number' => $this->getString($data, 'Bill Number'), - 'public_notes' => $this->getString($data, 'Notes / Memo'), + 'public_notes' => $public_notes, 'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022 'currency_id' => $this->company->settings->currency_id, 'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']), 'amount' => $amount, - 'tax_name1' => $data['Sales Tax Name'], + 'tax_name1' => isset($data['Sales Tax Name']) ? $data['Sales Tax Name'] : '', 'tax_rate1' => $tax_rate, ]; diff --git a/app/Jobs/EDocument/CreateEDocument.php b/app/Jobs/EDocument/CreateEDocument.php index 103ab05d9d2d..7f96af177a91 100644 --- a/app/Jobs/EDocument/CreateEDocument.php +++ b/app/Jobs/EDocument/CreateEDocument.php @@ -11,7 +11,6 @@ namespace App\Jobs\EDocument; -use App\Services\EDocument\Standards\RoEInvoice; use App\Utils\Ninja; use App\Models\Quote; use App\Models\Credit; @@ -23,10 +22,12 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use App\Services\EDocument\Standards\Peppol; use horstoeko\zugferd\ZugferdDocumentBuilder; +use App\Services\EDocument\Standards\FatturaPA; +use App\Services\EDocument\Standards\RoEInvoice; use App\Services\EDocument\Standards\OrderXDocument; use App\Services\EDocument\Standards\FacturaEInvoice; -use App\Services\EDocument\Standards\FatturaPA; use App\Services\EDocument\Standards\ZugferdEDokument; class CreateEDocument implements ShouldQueue @@ -68,6 +69,8 @@ class CreateEDocument implements ShouldQueue if ($this->document instanceof Invoice) { switch ($e_document_type) { + case "PEPPOL": + return (new Peppol($this->document))->toXml(); case "FACT1": return (new RoEInvoice($this->document))->generateXml(); case "FatturaPA": diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 0326f34effbe..1124fec9c812 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -297,12 +297,13 @@ class NinjaMailerJob implements ShouldQueue $t->replace(Ninja::transformTranslations($this->nmo->settings)); /** Force free/trials onto specific mail driver */ - // if(Ninja::isHosted() && !$this->company->account->isPaid()) - // { - // $this->mailer = 'mailgun'; - // $this->setHostedMailgunMailer(); - // return $this; - // } + + if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) { + $this->mailer = 'mailgun'; + $this->setHostedMailgunMailer(); + return $this; + } + if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') { //check if outlook. @@ -391,7 +392,7 @@ class NinjaMailerJob implements ShouldQueue $smtp_username = $company->smtp_username ?? ''; $smtp_password = $company->smtp_password ?? ''; $smtp_encryption = $company->smtp_encryption ?? 'tls'; - $smtp_local_domain = strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null; + $smtp_local_domain = strlen($company->smtp_local_domain ?? '') > 2 ? $company->smtp_local_domain : null; $smtp_verify_peer = $company->smtp_verify_peer ?? true; if(strlen($smtp_host) <= 1 || diff --git a/app/Jobs/Mailgun/ProcessMailgunWebhook.php b/app/Jobs/Mailgun/ProcessMailgunWebhook.php index 73d32f39e9f7..69be326cd288 100644 --- a/app/Jobs/Mailgun/ProcessMailgunWebhook.php +++ b/app/Jobs/Mailgun/ProcessMailgunWebhook.php @@ -181,7 +181,7 @@ class ProcessMailgunWebhook implements ShouldQueue $sl = $this->getSystemLog($this->request['MessageID']); /** Prevents Gmail tracking from firing inappropriately */ - if($this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) { + if(!$sl || $this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) { return; } diff --git a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php index 6cc5a6440a12..ddf7ce404856 100644 --- a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php +++ b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php @@ -78,7 +78,7 @@ class CleanStaleInvoiceOrder implements ShouldQueue Invoice::query() ->withTrashed() ->where('is_proforma', 1) - ->whereBetween('created_at', [now()->subHours(1), now()->subMinutes(10)]) + ->where('created_at', '<', now()->subHour()) ->cursor() ->each(function ($invoice) use ($repo) { $invoice->is_proforma = false; diff --git a/app/Livewire/BillingPortalPurchase.php b/app/Livewire/BillingPortalPurchase.php index 0a15710018c6..079a36325e80 100644 --- a/app/Livewire/BillingPortalPurchase.php +++ b/app/Livewire/BillingPortalPurchase.php @@ -188,6 +188,10 @@ class BillingPortalPurchase extends Component public ?string $contact_email; + public ?string $client_city; + + public ?string $client_postal_code; + public function mount() { MultiDB::setDb($this->db); @@ -203,7 +207,7 @@ class BillingPortalPurchase extends Component if (request()->query('coupon')) { $this->coupon = request()->query('coupon'); $this->handleCoupon(); - } elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { + } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) { $this->price = $this->subscription->promo_price; } @@ -335,10 +339,6 @@ class BillingPortalPurchase extends Component { $this->contact = $contact; - if ($contact->showRff()) { - return $this->rff(); - } - Auth::guard('contact')->loginUsingId($contact->id, true); if ($this->subscription->trial_enabled) { @@ -351,11 +351,20 @@ class BillingPortalPurchase extends Component if ((int)$this->price == 0) { $this->steps['payment_required'] = false; } else { - $this->steps['fetched_payment_methods'] = true; + // $this->steps['fetched_payment_methods'] = true; } $this->methods = $contact->client->service()->getPaymentMethods($this->price); + foreach($this->methods as $method){ + + if($method['is_paypal'] == '1' && !$this->steps['check_rff']){ + $this->rff(); + break; + } + + } + $this->heading_text = ctrans('texts.payment_methods'); return $this; @@ -366,6 +375,8 @@ class BillingPortalPurchase extends Component $this->contact_first_name = $this->contact->first_name; $this->contact_last_name = $this->contact->last_name; $this->contact_email = $this->contact->email; + $this->client_city = $this->contact->client->city; + $this->client_postal_code = $this->contact->client->postal_code; $this->steps['check_rff'] = true; @@ -377,13 +388,20 @@ class BillingPortalPurchase extends Component $validated = $this->validate([ 'contact_first_name' => ['required'], 'contact_last_name' => ['required'], + 'client_city' => ['required'], + 'client_postal_code' => ['required'], 'contact_email' => ['required', 'email'], ]); $this->contact->first_name = $validated['contact_first_name']; $this->contact->last_name = $validated['contact_last_name']; $this->contact->email = $validated['contact_email']; - $this->contact->save(); + $this->contact->client->postal_code = $validated['client_postal_code']; + $this->contact->client->city = $validated['client_city']; + + $this->contact->pushQuietly(); + + $this->steps['fetched_payment_methods'] = true; return $this->getPaymentMethods($this->contact); } @@ -395,13 +413,13 @@ class BillingPortalPurchase extends Component * @param $company_gateway_id * @param $gateway_type_id */ - public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id) + public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id, $is_paypal = false) { $this->company_gateway_id = $company_gateway_id; $this->payment_method_id = $gateway_type_id; $this->handleBeforePaymentEvents(); - + } /** diff --git a/app/Livewire/BillingPortalPurchasev2.php b/app/Livewire/BillingPortalPurchasev2.php index d707707578b2..5ee7191700ca 100644 --- a/app/Livewire/BillingPortalPurchasev2.php +++ b/app/Livewire/BillingPortalPurchasev2.php @@ -164,6 +164,13 @@ class BillingPortalPurchasev2 extends Component public $payment_confirmed = false; public $is_eligible = true; public $not_eligible_message = ''; + public $check_rff = false; + + public ?string $contact_first_name; + public ?string $contact_last_name; + public ?string $contact_email; + public ?string $client_city; + public ?string $client_postal_code; public function mount() { @@ -472,7 +479,6 @@ class BillingPortalPurchasev2 extends Component */ protected function getPaymentMethods(): self { - nlog("total amount = {$this->float_amount_total}"); if ($this->float_amount_total == 0) { $this->methods = []; @@ -481,10 +487,73 @@ class BillingPortalPurchasev2 extends Component if ($this->contact && $this->float_amount_total >= 1) { $this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total); } + + foreach($this->methods as $method) { + + if($method['is_paypal'] == '1' && !$this->check_rff) { + $this->rff(); + break; + } + + } return $this; } + protected function rff() + { + + $this->contact_first_name = $this->contact->first_name; + $this->contact_last_name = $this->contact->last_name; + $this->contact_email = $this->contact->email; + $this->client_city = $this->contact->client->city; + $this->client_postal_code = $this->contact->client->postal_code; + + if( + strlen($this->contact_first_name ?? '') == 0 || + strlen($this->contact_last_name ?? '') == 0 || + strlen($this->contact_email ?? '') == 0 || + strlen($this->client_city ?? '') == 0 || + strlen($this->client_postal_code ?? '') == 0 + ) + { + $this->check_rff = true; + } + + return $this; + } + + public function handleRff() + { + + $validated = $this->validate([ + 'contact_first_name' => ['required'], + 'contact_last_name' => ['required'], + 'client_city' => ['required'], + 'client_postal_code' => ['required'], + 'contact_email' => ['required', 'email'], + ]); + + $this->check_rff = false; + + $this->contact->first_name = $validated['contact_first_name']; + $this->contact->last_name = $validated['contact_last_name']; + $this->contact->email = $validated['contact_email']; + $this->contact->client->postal_code = $validated['client_postal_code']; + $this->contact->client->city = $validated['client_city']; + + $this->contact->pushQuietly(); + + $this->refreshComponent(); + + return $this; + } + + protected function refreshComponent() + { + $this->dispatch('$refresh'); + } + /** * Middle method between selecting payment method & * submitting the from to the backend. diff --git a/app/Models/Account.php b/app/Models/Account.php index 9ce846a367de..001b05100dcc 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -364,16 +364,19 @@ class Account extends BaseModel return $this->isProClient() && $this->isPaid(); } + public function isNewHostedAccount() + { + return Ninja::isHosted() && Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2; + } + public function isTrial(): bool { if (!Ninja::isNinja()) { return false; } - //@27-01-2024 - updates for logic around trials return !$this->plan_paid && $this->trial_started && Carbon::parse($this->trial_started)->addDays(14)->gte(now()->subHours(12)); - // $plan_details = $this->getPlanDetails(); - // return $plan_details && $plan_details['trial']; + } public function startTrial($plan): void diff --git a/app/Models/Client.php b/app/Models/Client.php index 3aa29455c3ac..e22dc522a324 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -27,7 +27,6 @@ use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Support\Facades\Cache; use Laracasts\Presenter\PresentableTrait; /** @@ -124,7 +123,7 @@ class Client extends BaseModel implements HasLocalePreference use AppSetup; use ClientGroupSettingsSaver; use Excludable; - + protected $presenter = ClientPresenter::class; protected $hidden = [ diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index fd47d0e6feb0..0885db2c6c33 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -351,9 +351,9 @@ class ClientContact extends Authenticatable implements HasLocalePreference public function showRff(): bool { - if (\strlen($this->first_name) === 0 || \strlen($this->last_name) === 0 || \strlen($this->email) === 0) { - return true; - } + // if (\strlen($this->first_name ?? '') === 0 || \strlen($this->last_name ?? '') === 0 || \strlen($this->email ?? '') === 0) { + // return true; + // } return false; } diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 9311699f31e3..62ba38ee8e41 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -159,6 +159,11 @@ class CompanyGateway extends BaseModel protected $touches = []; + public function isPayPal() + { + return in_array($this->gateway_key, ['80af24a6a691230bbec33e930ab40666','80af24a6a691230bbec33e930ab40665']); + } + public function getEntityType() { return self::class; diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index b950e07165e7..43bbfe8fa719 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -105,7 +105,7 @@ class Gateway extends StaticModel $link = 'https://www.forte.net/'; } elseif ($this->id == 62) { $link = 'https://docs.btcpayserver.org/InvoiceNinja/'; - } elseif ($this->id == 4002) { + } elseif ($this->id == 63) { $link = 'https://rotessa.com'; } @@ -141,23 +141,23 @@ class Gateway extends StaticModel case 20: case 56: return [ - GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], - GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']], + GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'charge.refunded', 'payment_intent.payment_failed']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded','charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'charge.refunded', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], - GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], - GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed',]], + GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], + GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed',]], ]; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout @@ -226,7 +226,7 @@ class Gateway extends StaticModel return [ GatewayType::CRYPTO => ['refund' => true, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], ]; //BTCPay - case 4002: + case 63: return [ GatewayType::BANK_TRANSFER => [ 'refund' => false, diff --git a/app/Models/Presenters/UserPresenter.php b/app/Models/Presenters/UserPresenter.php index 917cb43665d9..59211278e16c 100644 --- a/app/Models/Presenters/UserPresenter.php +++ b/app/Models/Presenters/UserPresenter.php @@ -95,4 +95,9 @@ class UserPresenter extends EntityPresenter { return $this->entity->phone ?? ' '; } + + public function email(): string + { + return $this->entity->email ?? ' '; + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index d341a3fb8569..c786dcec0836 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -129,7 +129,7 @@ class Project extends BaseModel public function invoices(): HasMany { - return $this->hasMany(Invoice::class); + return $this->hasMany(Invoice::class)->withTrashed(); } public function quotes(): HasMany diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index f61b6c4fbf45..10eb1f2629a8 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -152,6 +152,8 @@ class SystemLog extends Model public const TYPE_BTC_PAY = 324; + public const TYPE_ROTESSA = 325; + public const TYPE_QUOTA_EXCEEDED = 400; public const TYPE_UPSTREAM_FAILURE = 401; diff --git a/app/PaymentDrivers/Forte/ACH.php b/app/PaymentDrivers/Forte/ACH.php index aef043d50a9e..8ea313e77200 100644 --- a/app/PaymentDrivers/Forte/ACH.php +++ b/app/PaymentDrivers/Forte/ACH.php @@ -170,6 +170,9 @@ class ACH ]; $payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED); - return redirect('client/invoices')->withSuccess('Invoice paid.'); + // return redirect('client/invoices')->withSuccess('Invoice paid.'); + + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); + } } diff --git a/app/PaymentDrivers/Forte/CreditCard.php b/app/PaymentDrivers/Forte/CreditCard.php index 67c404190137..5a317f4ec8a7 100644 --- a/app/PaymentDrivers/Forte/CreditCard.php +++ b/app/PaymentDrivers/Forte/CreditCard.php @@ -187,6 +187,8 @@ class CreditCard 'gateway_type_id' => GatewayType::CREDIT_CARD, ]; $payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED); - return redirect('client/invoices')->withSuccess('Invoice paid.'); + // return redirect('client/invoices')->withSuccess('Invoice paid.'); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); + } } diff --git a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php index 56824c840269..e7f6fe01c145 100644 --- a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php +++ b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php @@ -251,11 +251,11 @@ class PayPalBasePaymentDriver extends BaseDriver [ "address" => [ - "address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1, + "address_line_1" => strlen($this->client->shipping_address1 ?? '') > 1 ? $this->client->shipping_address1 : $this->client->address1, "address_line_2" => $this->client->shipping_address2, - "admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city, - "admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state, - "postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code, + "admin_area_2" => strlen($this->client->shipping_city ?? '') > 1 ? $this->client->shipping_city : $this->client->city, + "admin_area_1" => strlen($this->client->shipping_state ?? '') > 1 ? $this->client->shipping_state : $this->client->state, + "postal_code" => strlen($this->client->shipping_postal_code ?? '') > 1 ? $this->client->shipping_postal_code : $this->client->postal_code, "country_code" => $this->client->present()->shipping_country_code(), ], ] diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php index 4cb22ad728b7..a62b24b6be8b 100644 --- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php +++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php @@ -128,7 +128,7 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver nlog($response); - if($request->has('token') && strlen($request->input('token')) > 2) { + if($request->has('token') && strlen($request->input('token','')) > 2) { return $this->processTokenPayment($request, $response); } @@ -273,14 +273,14 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver ] ]; - - if($shipping = $this->getShippingAddress()) { + if($shipping = $this->getShippingAddress()) $order['purchase_units'][0]["shipping"] = $shipping; - } - if(isset($data['payment_source'])) { + if(isset($data['payment_source'])) $order['payment_source'] = $data['payment_source']; - } + + if(isset($data['payer'])) + $order['payer'] = $data['payer']; $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order); @@ -316,8 +316,17 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver ->firstOrFail(); $orderId = $response['orderID']; + $r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']); + $data["payer"] = [ + "name" => [ + "given_name" => $this->client->present()->first_name(), + "surname" => $this->client->present()->last_name() + ], + "email_address" => $this->client->present()->email(), + ]; + $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; $data["payment_source"] = [ "card" => [ @@ -332,8 +341,6 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver $orderId = $this->createOrder($data); - // $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']); - try { $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']); @@ -395,6 +402,14 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver $data = []; $this->payment_hash = $payment_hash; + $data["payer"] = [ + "name" => [ + "given_name" => $this->client->present()->first_name(), + "surname" => $this->client->present()->last_name() + ], + "email_address" => $this->client->present()->email(), + ]; + $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; $data["payment_source"] = [ "card" => [ diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index ba1cbc62e7f2..dfbf86b75943 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -157,10 +157,6 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver } - - - - public function createOrder(array $data): string { @@ -213,6 +209,10 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver $order['payment_source'] = $data['payment_source']; } + if(isset($data["payer"])){ + $order['payer'] = $data["payer"]; + } + $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order); nlog($r->json()); @@ -274,6 +274,13 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver nlog($r->body()); + $data["payer"] = [ + "name" => [ + "given_name" => $this->client->present()->first_name(), + "surname" => $this->client->present()->last_name() + ], + "email_address" => $this->client->present()->email(), + ]; $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; $data["payment_source"] = [ "card" => [ @@ -349,6 +356,14 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver $data = []; $this->payment_hash = $payment_hash; + $data['payer'] = [ + "name" => [ + "given_name" => $this->client->present()->first_name(), + "surname" => $this->client->present()->last_name() + ], + "email_address" => $this->client->present()->email(), + ]; + $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; $data["payment_source"] = [ "card" => [ diff --git a/app/PaymentDrivers/Rotessa/PaymentMethod.php b/app/PaymentDrivers/Rotessa/PaymentMethod.php index b52c2423bf7e..09717a0da75c 100755 --- a/app/PaymentDrivers/Rotessa/PaymentMethod.php +++ b/app/PaymentDrivers/Rotessa/PaymentMethod.php @@ -60,7 +60,10 @@ class PaymentMethod implements MethodInterface 'id' => null ] )->all(); $data['gateway'] = $this->rotessa; - $data['gateway_type_id'] = $data['client']->country->iso_3166_2 == 'US' ? GatewayType::BANK_TRANSFER : ( $data['client']->country->iso_3166_2 == 'CA' ? GatewayType::ACSS : (int) request('method')); + // Set gateway type according to client country + // $data['gateway_type_id'] = $data['client']->country->iso_3166_2 == 'US' ? GatewayType::BANK_TRANSFER : ( $data['client']->country->iso_3166_2 == 'CA' ? GatewayType::ACSS : (int) request('method')); + // TODO: detect GatewayType based on client country USA vs CAN + $data['gateway_type_id'] = GatewayType::ACSS ; $data['account'] = [ 'routing_number' => $data['client']->routing_id, 'country' => $data['client']->country->iso_3166_2 @@ -145,18 +148,16 @@ class PaymentMethod implements MethodInterface $request->validate([ 'source' => ['required','string','exists:client_gateway_tokens,token'], 'amount' => ['required','numeric'], - 'token_id' => ['required','integer','exists:client_gateway_tokens,id'], 'process_date'=> ['required','date','after_or_equal:today'], ]); $customer = ClientGatewayToken::query() ->where('company_gateway_id', $this->rotessa->company_gateway->id) ->where('client_id', $this->rotessa->client->id) - ->where('id', (int) $request->input('token_id')) ->where('token', $request->input('source')) ->first(); - if(!$customer) throw new \Exception('Client gateway token not found!', 605); + if(!$customer) throw new \Exception('Client gateway token not found!', SystemLog::TYPE_ROTESSA); - $transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date','comment')); + $transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date') + ['comment' => $this->rotessa->getDescription(false) ]); $transaction->additional(['customer_id' => $customer->gateway_customer_reference]); $transaction = array_filter( $transaction->resolve()); $response = $this->rotessa->gateway->capture($transaction)->send(); @@ -182,12 +183,12 @@ class PaymentMethod implements MethodInterface [ 'data' => $data ], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, - 880, + SystemLog::TYPE_ROTESSA, $this->rotessa->client, $this->rotessa->client->company, ); - return redirect()->route('client.payments.show', [ 'payment' => $this->rotessa->encodePrimaryKey($payment->id) ]); + return redirect()->route('client.payments.show', [ 'payment' => $payment->hashed_id ]); } /** @@ -205,7 +206,7 @@ class PaymentMethod implements MethodInterface $exception->getMessage(), SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, - 880, + SystemLog::TYPE_ROTESSA, $this->rotessa->client, $this->rotessa->client->company, ); diff --git a/app/PaymentDrivers/RotessaPaymentDriver.php b/app/PaymentDrivers/RotessaPaymentDriver.php index 6a40750a5ae4..23915c758c05 100644 --- a/app/PaymentDrivers/RotessaPaymentDriver.php +++ b/app/PaymentDrivers/RotessaPaymentDriver.php @@ -24,6 +24,7 @@ use App\Utils\Traits\MakesHash; use App\Jobs\Util\SystemLogger; use App\PaymentDrivers\BaseDriver; use App\Models\ClientGatewayToken; +use Illuminate\Support\Facades\Cache; use Illuminate\Database\Eloquent\Builder; use App\PaymentDrivers\Rotessa\Resources\Customer; use App\PaymentDrivers\Rotessa\PaymentMethod as Acss; @@ -64,13 +65,15 @@ class RotessaPaymentDriver extends BaseDriver { $types = []; - if ($this->client + /* + // TODO: needs to test with US test account + if ($this->client && $this->client->currency() && in_array($this->client->currency()->code, ['USD']) && isset($this->client->country) && in_array($this->client->country->iso_3166_2, ['US'])) { $types[] = GatewayType::BANK_TRANSFER; - } + }*/ if ($this->client && $this->client->currency() @@ -115,39 +118,108 @@ class RotessaPaymentDriver extends BaseDriver public function importCustomers() { $this->init(); try { - $result = $this->gateway->getCustomers()->send(); - if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode()); - - $customers = collect($result->getData())->unique('email'); + if(!$result = Cache::has("rotessa-import_customers-{$this->company_gateway->company->company_key}")) { + $result = $this->gateway->getCustomers()->send(); + if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode()); + // cache results + Cache::put("rotessa-import_customers-{$this->company_gateway->company->company_key}", $result->getData(), 60 * 60 * 24); + } + + $result = Cache::get("rotessa-import_customers-{$this->company_gateway->company->company_key}"); + $customers = collect($result)->unique('email'); $client_emails = $customers->pluck('email')->all(); $company_id = $this->company_gateway->company->id; + // get existing customers $client_contacts = ClientContact::where('company_id', $company_id)->whereIn('email', $client_emails )->whereNull('deleted_at')->get(); $client_contacts = $client_contacts->map(function($item, $key) use ($customers) { - return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number ]); + return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number, 'client_id' => $item->client->id ]); } ); - + // create payment methods $client_contacts->each( function($contact) use ($customers) { - sleep(10); $result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send(); - $this->client = Client::find($contact->custom_identifier); + $this->client = Client::find($contact->client_id); $customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] ); $this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize()); } ); - + + // create new clients from rotessa customers + $client_emails = $client_contacts->pluck('email')->all(); + $client_contacts = $customers->filter(function ($value, $key) use ($client_emails) { + return !in_array(((object) $value)->email, $client_emails); + })->each( function($customer) use ($company_id) { + // create new client contact from rotess customer + $customer = (object) $this->gateway->getCustomersId(['id' => ($customer = (object) $customer)->id])->send()->getData(); + /** + { + "account_number": "11111111" + "active": true, + "address": { + "address_1": "123 Main Street", + "address_2": "Unit 4", + "city": "Birmingham", + "id": 114397, + "postal_code": "36016", + "province_code": "AL" + }, + "authorization_type": "Online", + "bank_account_type": "Checking", + "bank_name": "Scotiabank", + "created_at": "2015-02-10T23:50:45.000-06:00", + "custom_identifier": "Mikey", + "customer_type": "Personal", + "email": "mikesmith@test.com", + "financial_transactions": [], + "home_phone": "(204) 555 5555", + "id": 1, + "identifier": "Mikey", + "institution_number": "", + "name": "Mike Smith", + "phone": "(204) 555 4444", + "routing_number": "111111111", + "transaction_schedules": [], + "transit_number": "", + "updated_at": "2015-02-10T23:50:45.000-06:00" + } + */ + $client = (\App\Factory\ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id))->fill( + [ + 'address1' => $customer->address['address_1'] ?? '', + 'address2' =>$customer->address['address_2'] ?? '', + 'city' => $customer->address['city'] ?? '', + 'postal_code' => $customer->address['postal_code'] ?? '', + 'state' => $customer->address['province_code'] ?? '', + 'country_id' => empty($customer->transit_number) ? 840 : 124, + 'routing_id' => empty(($r = $customer->routing_number))? null : $r, + "number" => str_pad($customer->account_number,3,'0',STR_PAD_LEFT) + ] + ); + $client->saveQuietly(); + $contact = (\App\Factory\ClientContactFactory::create($company_id, $this->company_gateway->user_id))->fill([ + "first_name" => substr($customer->name, 0, stripos($customer->name, " ")), + "last_name" => substr($customer->name, stripos($customer->name, " ")), + "email" => $customer->email, + "phone" => $customer->phone, + "is_primary" => true, + "send_email" => true, + ]); + $client->contacts()->saveMany([$contact]); + $contact = $client->contacts()->first(); + $this->client = $client; + $customer = (new Customer((array) $customer))->additional(['id' => $customer->id, 'custom_identifier' => $customer->custom_identifier ?? $contact->id ] ); + $this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize()); + }); } catch (\Throwable $th) { - $data = [ + $data = [ 'transaction_reference' => null, 'transaction_response' => $th->getMessage(), 'success' => false, 'description' => $th->getMessage(), 'code' =>(int) $th->getCode() ]; - - SystemLogger::dispatch(['server_response' => $th->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client , $this->company_gateway->company); + SystemLogger::dispatch(['server_response' => $th->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_ROTESSA , $this->company_gateway->client , $this->company_gateway->company); - throw $th; } @@ -176,9 +248,11 @@ class RotessaPaymentDriver extends BaseDriver $data = array_filter($customer->resolve()); } - $payment_method_id = Arr::has($data,'address.postal_code') && ((int) $data['address']['postal_code'])? GatewayType::BANK_TRANSFER: GatewayType::ACSS; + // $payment_method_id = Arr::has($data,'address.postal_code') && ((int) $data['address']['postal_code'])? GatewayType::BANK_TRANSFER: GatewayType::ACSS; + // TODO: Check/ Validate postal code between USA vs CAN + $payment_method_id = GatewayType::ACSS; $gateway_token = $this->storeGatewayToken( [ - 'payment_meta' => $data + ['brand' => 'Rotessa'], + 'payment_meta' => $data + ['brand' => 'Rotessa', 'last4' => $data['bank_name'], 'type' => $data['bank_account_type'] ], 'token' => encrypt(join(".", Arr::only($data, 'id','custom_identifier'))), 'payment_method_id' => $payment_method_id , ], ['gateway_customer_reference' => @@ -198,7 +272,7 @@ class RotessaPaymentDriver extends BaseDriver 'code' =>(int) $th->getCode() ]; - SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getData(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->client->company); + SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company); throw $th; } diff --git a/app/PaymentDrivers/Stripe/BrowserPay.php b/app/PaymentDrivers/Stripe/BrowserPay.php index d468d0a2b7c2..09d71fcdb17c 100644 --- a/app/PaymentDrivers/Stripe/BrowserPay.php +++ b/app/PaymentDrivers/Stripe/BrowserPay.php @@ -153,7 +153,7 @@ class BrowserPay implements MethodInterface $this->stripe->client->company, ); - return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); } /** diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php index 3fbed359327d..eab1a98c3262 100644 --- a/app/PaymentDrivers/Stripe/CreditCard.php +++ b/app/PaymentDrivers/Stripe/CreditCard.php @@ -160,7 +160,7 @@ class CreditCard } } - return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); } public function processUnsuccessfulPayment($server_response) diff --git a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php index 36f766dd7303..405f696742e4 100644 --- a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php +++ b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php @@ -11,18 +11,22 @@ namespace App\PaymentDrivers\Stripe\Jobs; -use App\Libraries\MultiDB; use App\Models\Company; -use App\Models\CompanyGateway; use App\Models\Payment; +use App\Libraries\MultiDB; use App\Models\PaymentHash; -use App\PaymentDrivers\Stripe\Utilities; +use App\Services\Email\Email; use Illuminate\Bus\Queueable; +use App\Models\CompanyGateway; +use App\Services\Email\EmailObject; +use Illuminate\Support\Facades\App; +use Illuminate\Mail\Mailables\Address; +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\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; -use Illuminate\Queue\SerializesModels; class ChargeRefunded implements ShouldQueue { @@ -36,19 +40,10 @@ class ChargeRefunded implements ShouldQueue 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) + public function __construct(public array $stripe_request, private string $company_key) { - $this->stripe_request = $stripe_request; - $this->company_key = $company_key; - $this->company_gateway_id = $company_gateway_id; } public function handle() @@ -64,8 +59,8 @@ class ChargeRefunded implements ShouldQueue $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(); + $company_gateway = $payment_hash->payment->company_gateway; $stripe_driver = $company_gateway->driver()->init(); @@ -79,7 +74,7 @@ class ChargeRefunded implements ShouldQueue ->first(); //don't touch if already refunded - if(!$payment || in_array($payment->status_id, [Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])) { + if(!$payment || $payment->status_id == Payment::STATUS_REFUNDED || $payment->is_deleted){ return; } @@ -94,8 +89,19 @@ class ChargeRefunded implements ShouldQueue return; } - if($payment->status_id == Payment::STATUS_COMPLETED) { + usleep(rand(200000,300000)); + $payment = $payment->fresh(); + if($payment->status_id == Payment::STATUS_PARTIALLY_REFUNDED){ + //determine the delta in the refunded amount - how much has already been refunded and only apply the delta. + + if(floatval($payment->refunded) >= floatval($amount_refunded)) + return; + + $amount_refunded -= $payment->refunded; + + } + $invoice_collection = $payment->paymentables ->where('paymentable_type', 'invoices') ->map(function ($pivot) { @@ -117,9 +123,24 @@ class ChargeRefunded implements ShouldQueue ]; }); - } elseif($invoice_collection->sum('amount') != $amount_refunded) { - //too many edges cases at this point, return early + } + elseif($invoice_collection->sum('amount') != $amount_refunded) { + + $refund_text = "A partial refund was processed for Payment #{$payment_hash->payment->number}.- Enter the information for the bank account + {{ ctrans('texts.enter_the_information_for_the_bank_account') }}
- Enter the address information for the account holder + {{ ctrans('texts.enter_information_for_the_account_holder') }}
- Enter the information for the account holder -
-+ {{ ctrans('texts.enter_information_for_the_account_holder') }} +
+