Merge pull request #20 from M-E-Development-Design/v5-develop

V5 develop
This commit is contained in:
Kendall Arneaud 2024-08-02 10:02:59 -04:00 committed by GitHub
commit 08f33ac3e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
122 changed files with 2214 additions and 1082 deletions

View File

@ -24,4 +24,4 @@ PHANTOMJS_PDF_GENERATION=false
CACHE_DRIVER=redis CACHE_DRIVER=redis
QUEUE_CONNECTION=redis QUEUE_CONNECTION=redis
SESSION_DRIVER=redis SESSION_DRIVER=redis
PDF_GENERATOR=hosted_ninja PDF_GENERATOR=snappdf

View File

@ -38,6 +38,9 @@ jobs:
sudo php artisan cache:clear sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \; 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 - name: Prepare React FrontEnd
run: | run: |
@ -46,10 +49,11 @@ jobs:
git checkout develop git checkout develop
cp .env.example .env cp .env.example .env
cp ../vite.config.ts.react ./vite.config.js cp ../vite.config.ts.react ./vite.config.js
sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json
npm i npm i
npm run build npm run build
cp -r dist/* ../public/ 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 - name: Prepare JS/CSS assets
run: | run: |

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/> <a href ="https://www.youtube.com/watch?v=CxGxXiotv0I" target="_blank" title="Invoice Ninja Overview Video"><img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/></a>
</p> </p>
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
@ -8,25 +8,30 @@
# Invoice Ninja 5 # 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! 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.
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 code. #### Get social with us
We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
* [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) * [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 ### Mobile Apps
* [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone) * [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 - Snap](https://snapcraft.io/invoiceninja)
* [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.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/) * [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) * [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
### Recommended Providers ### Recommended Providers
* [Stripe](https://stripe.com/) * [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.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 ```sh
git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git
@ -84,6 +94,7 @@ pass: password
``` ```
## Developers Guide ## 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 ### App Design

View File

@ -1 +1 @@
5.10.13 5.10.16

View File

@ -177,7 +177,6 @@ class BackupUpdate extends Command
$doc_bin = $document->getFile(); $doc_bin = $document->getFile();
} catch(\Exception $e) { } catch(\Exception $e) {
nlog("Exception:: BackupUpdate::" . $e->getMessage()); nlog("Exception:: BackupUpdate::" . $e->getMessage());
nlog($e->getMessage());
} }
if ($doc_bin) { if ($doc_bin) {

View File

@ -0,0 +1,44 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Tax\PL;
use App\DataMapper\Tax\DE\Rule as DERule;
class Rule extends DERule
{
/** @var string $seller_region */
public string $seller_region = 'EU';
/** @var bool $consumer_tax_exempt */
public bool $consumer_tax_exempt = false;
/** @var bool $business_tax_exempt */
public bool $business_tax_exempt = false;
/** @var bool $eu_business_tax_exempt */
public bool $eu_business_tax_exempt = true;
/** @var bool $foreign_business_tax_exempt */
public bool $foreign_business_tax_exempt = false;
/** @var bool $foreign_consumer_tax_exempt */
public bool $foreign_consumer_tax_exempt = false;
/** @var float $tax_rate */
public float $tax_rate = 0;
/** @var float $reduced_tax_rate */
public float $reduced_tax_rate = 0;
public string $tax_name1 = 'VAT';
}

View File

@ -17,7 +17,7 @@ class TaxModel
public string $seller_subregion = 'CA'; public string $seller_subregion = 'CA';
/** @var string $version */ /** @var string $version */
public string $version = 'alpha'; public string $version = 'beta';
/** @var object $regions */ /** @var object $regions */
public object $regions; public object $regions;
@ -28,17 +28,40 @@ class TaxModel
* @param TaxModel $model * @param TaxModel $model
* @return void * @return void
*/ */
public function __construct(public ?TaxModel $model = null) public function __construct(public mixed $model = null)
{ {
if(!$this->model) { if(!$model) {
$this->regions = $this->init(); $this->regions = $this->init();
} else { } 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;
}
/** /**
* Initializes the rules and builds any required data. * Initializes the rules and builds any required data.
* *
@ -474,6 +497,12 @@ class TaxModel
$this->regions->EU->subregions->NL->reduced_tax_rate = 9; $this->regions->EU->subregions->NL->reduced_tax_rate = 9;
$this->regions->EU->subregions->NL->apply_tax = false; $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 = new \stdClass();
$this->regions->EU->subregions->PT->tax_rate = 23; $this->regions->EU->subregions->PT->tax_rate = 23;
$this->regions->EU->subregions->PT->tax_name = 'IVA'; $this->regions->EU->subregions->PT->tax_name = 'IVA';

View File

@ -197,6 +197,10 @@ region:
vat: 21 vat: 21
reduced_vat: 9 reduced_vat: 9
apply_tax: false apply_tax: false
PL:
vat: 23
reduced_vat: 8
apply_tax: false
PT: PT:
vat: 23 vat: 23
reduced_vat: 6 reduced_vat: 6

View File

@ -13,15 +13,21 @@ namespace App\Events\Client;
use App\Models\Client; use App\Models\Client;
use App\Models\Company; use App\Models\Company;
use League\Fractal\Manager;
use League\Fractal\Resource\Item;
use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets; use App\Transformers\ArraySerializer;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; 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 class ClientWasArchived implements ShouldBroadcast
{ {
use Dispatchable; use Dispatchable;
use InteractsWithSockets; use InteractsWithSockets;
@ -50,13 +56,34 @@ class ClientWasArchived
$this->event_vars = $event_vars; $this->event_vars = $event_vars;
} }
// /** public function broadcastWith()
// * Get the channels the event should broadcast on. {
// *
// * @return Channel|array $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() public function broadcastOn()
{ {
return [];
return [
new PrivateChannel("company-{$this->company->company_key}"),
];
} }
} }

View File

@ -172,6 +172,7 @@ class BaseExport
'tax_rate3' => 'invoice.tax_rate3', 'tax_rate3' => 'invoice.tax_rate3',
'recurring_invoice' => 'invoice.recurring_id', 'recurring_invoice' => 'invoice.recurring_id',
'auto_bill' => 'invoice.auto_bill_enabled', 'auto_bill' => 'invoice.auto_bill_enabled',
'project' => 'invoice.project',
]; ];
protected array $recurring_invoice_report_keys = [ protected array $recurring_invoice_report_keys = [
@ -449,6 +450,7 @@ class BaseExport
'status' => 'task.status_id', 'status' => 'task.status_id',
'project' => 'task.project_id', 'project' => 'task.project_id',
'billable' => 'task.billable', 'billable' => 'task.billable',
'item_notes' => 'task.item_notes',
]; ];
protected array $forced_client_fields = [ protected array $forced_client_fields = [
@ -1038,6 +1040,10 @@ class BaseExport
$recurring_filters = []; $recurring_filters = [];
if($this->company->getSetting('report_include_drafts')){
$recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
}
if (in_array('active', $status_parameters)) { if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE; $recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
} }
@ -1252,7 +1258,7 @@ class BaseExport
$date_range = $this->input['date_range']; $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']; $this->date_key = $this->input['date_key'];
} }

View File

@ -153,9 +153,9 @@ class InvoiceExport extends BaseExport
private function decorateAdvancedFields(Invoice $invoice, array $entity): array private function decorateAdvancedFields(Invoice $invoice, array $entity): array
{ {
// if (in_array('invoice.status', $this->input['report_keys'])) { if (in_array('invoice.project', $this->input['report_keys'])) {
// $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';
// } }
if (in_array('invoice.recurring_id', $this->input['report_keys'])) { if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';

View File

@ -265,6 +265,10 @@ class InvoiceItemExport extends BaseExport
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line $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; return $entity;
} }

View File

@ -29,7 +29,7 @@ class TaskExport extends BaseExport
{ {
private $entity_transformer; private $entity_transformer;
public string $date_key = 'created_at'; public string $date_key = 'calculated_start_date';
private string $date_format = 'Y-m-d'; private string $date_format = 'Y-m-d';
@ -156,7 +156,7 @@ class TaskExport extends BaseExport
$entity[$key] = $transformed_entity[$parts[1]]; $entity[$key] = $transformed_entity[$parts[1]];
} elseif (array_key_exists($key, $transformed_entity)) { } elseif (array_key_exists($key, $transformed_entity)) {
$entity[$key] = $transformed_entity[$key]; $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] = ''; $entity[$key] = '';
} else { } else {
$entity[$key] = $this->decorator->transform($key, $task); $entity[$key] = $this->decorator->transform($key, $task);
@ -175,7 +175,7 @@ class TaskExport extends BaseExport
private function iterateLogs(Task $task, array $entity) private function iterateLogs(Task $task, array $entity)
{ {
$timezone = Timezone::find($task->company->settings->timezone_id); $timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $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'); $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); $entity = $this->decorateAdvancedFields($task, $entity);
$this->storage_array[] = $entity; $this->storage_array[] = $entity;
@ -219,6 +227,8 @@ class TaskExport extends BaseExport
$entity['task.end_time'] = ''; $entity['task.end_time'] = '';
$entity['task.duration'] = ''; $entity['task.duration'] = '';
$entity['task.duration_words'] = ''; $entity['task.duration_words'] = '';
$entity['task.billable'] = '';
$entity['task.item_notes'] = '';
} }

View File

@ -92,6 +92,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface
{ {
return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : ''; return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : '';
} }
public function auto_bill_enabled(Invoice $invoice) public function auto_bill_enabled(Invoice $invoice)
{ {
return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no'); return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no');

View File

@ -18,6 +18,7 @@ use Carbon\Carbon;
class TaskDecorator extends Decorator implements DecoratorInterface class TaskDecorator extends Decorator implements DecoratorInterface
{ {
//@todo - we do not handle iterating through the timelog here.
public function transform(string $key, mixed $entity): mixed public function transform(string $key, mixed $entity): mixed
{ {
$task = false; $task = false;
@ -42,7 +43,7 @@ class TaskDecorator extends Decorator implements DecoratorInterface
{ {
$timezone = Timezone::find($task->company->settings->timezone_id); $timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;
@ -71,7 +72,7 @@ class TaskDecorator extends Decorator implements DecoratorInterface
{ {
$timezone = Timezone::find($task->company->settings->timezone_id); $timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;
@ -95,6 +96,26 @@ class TaskDecorator extends Decorator implements DecoratorInterface
return ''; return '';
} }
/**
* billable
*
* @todo
*/
public function billable(Task $task)
{
return '';
}
/**
* items_notes
* @todo
*/
public function items_notes(Task $task)
{
return '';
}
public function duration(Task $task) public function duration(Task $task)
{ {
return $task->calcDuration(); return $task->calcDuration();

View File

@ -155,11 +155,13 @@ class BankTransactionFilters extends QueryFilters
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc'; $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'deposit') { 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') { 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') { if ($sort_col[0] == 'status') {

View File

@ -41,7 +41,7 @@ class ClientFilters extends QueryFilters
*/ */
public function balance(string $balance = ''): Builder public function balance(string $balance = ''): Builder
{ {
if (strlen($balance) == 0) { if (strlen($balance) == 0 || count(explode(":", $balance)) < 2) {
return $this->builder; return $this->builder;
} }

View File

@ -153,22 +153,22 @@ class InvoiceFilters extends QueryFilters
{ {
return $this->builder->where(function ($query) { return $this->builder->where(function ($query) {
$query->whereIn('invoices.status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) $query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->where('invoices.is_deleted', 0) ->where('is_deleted', 0)
->where('invoices.balance', '>', 0) ->where('balance', '>', 0)
->orWhere(function ($query) { ->where(function ($query) {
$query->whereNull('invoices.due_date') $query->whereNull('due_date')
->orWhere(function ($q) { ->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) { ->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(due_date), due_date ' . 'desc')
->orderByRaw('ISNULL(invoices.partial_due_date), invoices.partial_due_date ' . 'desc'); ->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc');
}); });
} }

View File

@ -171,7 +171,7 @@ class TransactionTransformer implements BankRevenueInterface
private function formatDate(string $input) private function formatDate(string $input)
{ {
$timezone = Timezone::find($this->company->settings->timezone_id); $timezone = Timezone::find($this->company->settings->timezone_id);
$timezone_name = 'US/Eastern'; $timezone_name = 'America/New_York';
if ($timezone) { if ($timezone) {
$timezone_name = $timezone->name; $timezone_name = $timezone->name;

View File

@ -11,16 +11,18 @@
namespace App\Helpers\Invoice; namespace App\Helpers\Invoice;
use App\DataMapper\BaseSettings; use App\Models\Quote;
use App\DataMapper\InvoiceItem; use App\Utils\Number;
use App\DataMapper\Tax\RuleInterface;
use App\Models\Client; use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\RecurringQuote; 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; use App\Utils\Traits\NumberFormatter;
class InvoiceItemSum class InvoiceItemSum
@ -120,7 +122,7 @@ class InvoiceItemSum
private $tax_collection; private $tax_collection;
private ?Client $client; private Client | Vendor $client;
private bool $calc_tax = false; private bool $calc_tax = false;
@ -131,10 +133,10 @@ class InvoiceItemSum
$this->tax_collection = collect([]); $this->tax_collection = collect([]);
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) { if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency(); $this->currency = $this->invoice->client->currency();
$this->client = $this->invoice->client;
$this->shouldCalculateTax(); $this->shouldCalculateTax();
} else { } else {
$this->currency = $this->invoice->vendor->currency(); $this->currency = $this->invoice->vendor->currency();
@ -313,7 +315,7 @@ class InvoiceItemSum
$key = str_replace(' ', '', $tax_name.$tax_rate); $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)); $this->tax_collection->push(collect($group_tax));
} }

View File

@ -11,14 +11,16 @@
namespace App\Helpers\Invoice; namespace App\Helpers\Invoice;
use App\DataMapper\Tax\RuleInterface; use App\Models\Quote;
use App\Utils\Number;
use App\Models\Client; use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\RecurringQuote; use App\Models\RecurringQuote;
use App\Models\RecurringInvoice;
use App\DataMapper\Tax\RuleInterface;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
class InvoiceItemSumInclusive class InvoiceItemSumInclusive
@ -109,7 +111,7 @@ class InvoiceItemSumInclusive
private bool $calc_tax = false; private bool $calc_tax = false;
private ?Client $client; private Client | Vendor $client;
private RuleInterface $rule; private RuleInterface $rule;
@ -118,10 +120,10 @@ class InvoiceItemSumInclusive
$this->tax_collection = collect([]); $this->tax_collection = collect([]);
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) { if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency(); $this->currency = $this->invoice->client->currency();
$this->client = $this->invoice->client;
$this->shouldCalculateTax(); $this->shouldCalculateTax();
} else { } else {
$this->currency = $this->invoice->vendor->currency(); $this->currency = $this->invoice->vendor->currency();
@ -265,7 +267,7 @@ class InvoiceItemSumInclusive
$key = str_replace(' ', '', $tax_name.$tax_rate); $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)); $this->tax_collection->push(collect($group_tax));
} }

View File

@ -11,12 +11,14 @@
namespace App\Helpers\Invoice; namespace App\Helpers\Invoice;
use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\RecurringQuote; use App\Models\RecurringQuote;
use App\Models\Vendor;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\NumberFormatter; use App\Utils\Traits\NumberFormatter;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -50,6 +52,8 @@ class InvoiceSum
private $precision; private $precision;
private Client | Vendor $client;
public InvoiceItemSum $invoice_items; public InvoiceItemSum $invoice_items;
private $rappen_rounding = false; private $rappen_rounding = false;
@ -60,18 +64,15 @@ class InvoiceSum
*/ */
public function __construct($invoice) public function __construct($invoice)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) { $this->precision = $this->client->currency()->precision;
$this->precision = $this->invoice->client->currency()->precision; $this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding');
$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->tax_map = new Collection(); $this->tax_map = new Collection();
} }
public function build() public function build()
@ -131,7 +132,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1); $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1);
$this->total_taxes += $tax; $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) { 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); $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2);
$this->total_taxes += $tax; $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) { 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); $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3);
$this->total_taxes += $tax; $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; return $this;

View File

@ -12,7 +12,10 @@
namespace App\Helpers\Invoice; namespace App\Helpers\Invoice;
use App\Models\Quote; use App\Models\Quote;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\RecurringQuote; use App\Models\RecurringQuote;
@ -49,6 +52,8 @@ class InvoiceSumInclusive
private $rappen_rounding = false; private $rappen_rounding = false;
private Client | Vendor $client;
public InvoiceItemSumInclusive $invoice_items; public InvoiceItemSumInclusive $invoice_items;
/** /**
* Constructs the object with Invoice and Settings object. * Constructs the object with Invoice and Settings object.
@ -58,14 +63,10 @@ class InvoiceSumInclusive
public function __construct($invoice) public function __construct($invoice)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) { $this->precision = $this->client->currency()->precision;
$this->precision = $this->invoice->client->currency()->precision; $this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding');
$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->tax_map = new Collection(); $this->tax_map = new Collection();
} }
@ -157,19 +158,19 @@ class InvoiceSumInclusive
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount);
$this->total_taxes += $tax; $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) { if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount);
$this->total_taxes += $tax; $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) { if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount); $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount);
$this->total_taxes += $tax; $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; return $this;

View File

@ -66,7 +66,7 @@ class ChartController extends BaseController
return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); 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() */ /** @var \App\Models\User auth()->user() */

View File

@ -300,7 +300,9 @@ class InvitationController extends Controller
'signature' => false, 'signature' => false,
'contact_first_name' => $invitation->contact->first_name ?? '', 'contact_first_name' => $invitation->contact->first_name ?? '',
'contact_last_name' => $invitation->contact->last_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); $request->replace($data);

View File

@ -108,11 +108,11 @@ class PaymentController extends Controller
*/ */
public function process(Request $request) public function process(Request $request)
{ {
$request->validate([ // $request->validate([
'contact_first_name' => ['required'], // 'contact_first_name' => ['required'],
'contact_last_name' => ['required'], // 'contact_last_name' => ['required'],
'contact_email' => ['required', 'email'], // 'contact_email' => ['required', 'email'],
]); // ]);
return (new InstantPayment($request))->run(); return (new InstantPayment($request))->run();
} }

View File

@ -85,7 +85,7 @@ class ImportController extends Controller
$contents = $this->convertEncoding($contents); $contents = $this->convertEncoding($contents);
// Store the csv in cache with an expiry of 10 minutes // 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 // Parse CSV
$csv_array = $this->getCsvData($contents); $csv_array = $this->getCsvData($contents);

View File

@ -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'])) { 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); return response()->json(['message' => 'Success.'], 200);

View File

@ -65,6 +65,9 @@ class StoreCreditRequest extends Request
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; $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'] = new UniqueCreditNumberRule($this->all());
$rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', $user->company()->id)]; $rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', $user->company()->id)];
$rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['discount'] = 'sometimes|numeric|max:99999999999999';

View File

@ -66,6 +66,9 @@ class UpdateCreditRequest extends Request
$rules['client_id'] = ['bail', 'sometimes',Rule::in([$this->credit->client_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'; $rules['line_items'] = 'array';
$rules['date'] = 'bail|sometimes|date:Y-m-d'; $rules['date'] = 'bail|sometimes|date:Y-m-d';

View File

@ -38,11 +38,14 @@ class StoreInvoiceRequest extends Request
public function rules() public function rules()
{ {
$rules = [];
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->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'))) { if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation(); $rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) { } elseif ($this->file('documents')) {
@ -57,16 +60,16 @@ class StoreInvoiceRequest extends Request
$rules['file'] = $this->fileValidation(); $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['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['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['date'] = 'bail|sometimes|date:Y-m-d'; $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['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['discount'] = 'sometimes|numeric|max:99999999999999';
@ -79,18 +82,17 @@ class StoreInvoiceRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $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['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', '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; return $rules;
} }
public function prepareForValidation() public function prepareForValidation()
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$input = $this->all(); $input = $this->all();
$input = $this->decodePrimaryKeys($input); $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['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = $this->entityTotalAmount($input['line_items']); $input['amount'] = $this->entityTotalAmount($input['line_items']);
} }
if(isset($input['partial']) && $input['partial'] == 0) { if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null; $input['partial_due_date'] = null;
} }
if (!isset($input['tax_rate1'])) {
if (array_key_exists('tax_rate1', $input) && is_null($input['tax_rate1'])) {
$input['tax_rate1'] = 0; $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; $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; $input['tax_rate3'] = 0;
} }
if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) {
$input['exchange_rate'] = 1; $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. //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)))) { 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']); $client = \App\Models\Client::withTrashed()->find($input['client_id']);

View File

@ -67,6 +67,9 @@ class UpdateInvoiceRequest extends Request
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['tax_rate1'] = 'bail|sometimes|numeric'; $rules['tax_rate1'] = 'bail|sometimes|numeric';

View File

@ -50,6 +50,10 @@ class StorePurchaseOrderRequest extends Request
$rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', $user->company()->id)]; $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['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';

View File

@ -53,6 +53,9 @@ class UpdatePurchaseOrderRequest extends Request
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.vendor_contact_id'] = 'bail|required|distinct';
$rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];

View File

@ -11,12 +11,13 @@
namespace App\Http\Requests\Quote; namespace App\Http\Requests\Quote;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule;
use App\Models\Quote; use App\Models\Quote;
use App\Utils\Traits\CleanLineItems; use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule; 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 class StoreQuoteRequest extends Request
{ {
@ -43,7 +44,7 @@ class StoreQuoteRequest extends Request
$rules = []; $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'))) { if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation(); $rules['documents.*'] = $this->fileValidation();
@ -59,15 +60,28 @@ class StoreQuoteRequest extends Request
$rules['file'] = $this->fileValidation(); $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['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['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['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', 'numeric', 'max:99999999999999'];
return $rules; return $rules;
@ -89,19 +103,24 @@ class StoreQuoteRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = $this->entityTotalAmount($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) { if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null; $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'])) { if(!isset($input['date'])) {
$input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d'); $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)) { if(isset($input['partial_due_date']) && (!isset($input['due_date']) || strlen($input['due_date']) <= 1)) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']); $client = \App\Models\Client::withTrashed()->find($input['client_id']);
$valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7; $valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7;

View File

@ -56,6 +56,9 @@ class UpdateQuoteRequest extends Request
$rules['file'] = $this->fileValidation(); $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['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])]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';

View File

@ -61,7 +61,8 @@ class StoreRecurringInvoiceRequest extends Request
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; $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'; $rules['frequency_id'] = 'required|integer|digits_between:1,12';

View File

@ -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['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])]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->recurring_invoice->client_id])];

View File

@ -19,8 +19,9 @@ use Illuminate\Contracts\Validation\ValidationRule;
*/ */
class BlackListRule implements ValidationRule class BlackListRule implements ValidationRule
{ {
/** Bad domains +/- dispoable email domains */ /** Bad domains +/- disposable email domains */
private array $blacklist = [ private array $blacklist = [
'padvn.com',
'anonaddy.me', 'anonaddy.me',
'nqmo.com', 'nqmo.com',
'wireconnected.com', 'wireconnected.com',

View File

@ -42,6 +42,7 @@ class AccountComponent extends Component
public function render() 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);
} }
} }

View File

@ -43,6 +43,6 @@ class AddressComponent extends Component
public function render() 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 );
} }
} }

View File

@ -15,11 +15,12 @@ class ContactComponent extends Component
{ {
public function __construct(ClientContact $contact) { public function __construct(ClientContact $contact) {
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([ $contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' =>$contact->client->phone, 'home_phone' =>$contact->client->phone,
'custom_identifier' => $contact->client->number, 'custom_identifier' => $contact->client->number,
'name' =>$contact->client->name, 'name' =>$contact->client->name,
'id' => null 'id' => $contact->client->contact_key,
] )->all(); ] )->all();
$this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) ); $this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) );
@ -37,12 +38,13 @@ class ContactComponent extends Component
private $defaults = [ private $defaults = [
'customer_type' => "Business", 'customer_type' => "Business",
'customer_identifier' => null, 'custom_identifier' => null,
'id' => null 'customer_id' => null
]; ];
public function render() 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 );
} }
} }

View File

@ -98,7 +98,7 @@ class BaseImport
} }
/** @var string $base64_encoded_csv */ /** @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)) { if (empty($base64_encoded_csv)) {
return null; return null;
@ -473,6 +473,8 @@ class BaseImport
$tasks = $this->groupTasks($tasks, $task_number_key); $tasks = $this->groupTasks($tasks, $task_number_key);
nlog($tasks);
foreach ($tasks as $raw_task) { foreach ($tasks as $raw_task) {
$task_data = []; $task_data = [];
@ -702,16 +704,16 @@ class BaseImport
->save(); ->save();
} }
if ($invoice->status_id === Invoice::STATUS_DRAFT) { if ($invoice->status_id == Invoice::STATUS_DRAFT) {
} elseif ($invoice->status_id === Invoice::STATUS_SENT) { return $invoice;
}
$invoice = $invoice $invoice = $invoice
->service() ->service()
->markSent() ->markSent()
->save(); ->save();
} elseif (
$invoice->status_id <= Invoice::STATUS_SENT && if ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0) {
$invoice->amount > 0
) {
if ($invoice->balance <= 0) { if ($invoice->balance <= 0) {
$invoice->status_id = Invoice::STATUS_PAID; $invoice->status_id = Invoice::STATUS_PAID;
$invoice->save(); $invoice->save();

View File

@ -172,7 +172,7 @@ class Wave extends BaseImport implements ImportInterface
{ {
$entity_type = 'expense'; $entity_type = 'expense';
$data = $this->getCsvData($entity_type); $data = $this->getCsvData('invoice');
if (!$data) { if (!$data) {
$this->entity_count['expense'] = 0; $this->entity_count['expense'] = 0;
@ -244,8 +244,10 @@ class Wave extends BaseImport implements ImportInterface
if (empty($expense_data['vendor_id'])) { if (empty($expense_data['vendor_id'])) {
$vendor_data['user_id'] = $this->getUserIDForRecord($expense_data); $vendor_data['user_id'] = $this->getUserIDForRecord($expense_data);
if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor']))
{
$vendor_repository->save( $vendor_repository->save(
['name' => $raw_expense['Vendor Name']], ['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])],
$vendor = VendorFactory::create( $vendor = VendorFactory::create(
$this->company->id, $this->company->id,
$vendor_data['user_id'] $vendor_data['user_id']
@ -253,6 +255,7 @@ class Wave extends BaseImport implements ImportInterface
); );
$expense_data['vendor_id'] = $vendor->id; $expense_data['vendor_id'] = $vendor->id;
} }
}
$validator = Validator::make( $validator = Validator::make(
$expense_data, $expense_data,

View File

@ -46,6 +46,7 @@ class TaskTransformer extends BaseTransformer
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'number' => $this->getString($task_data, 'task.number'), 'number' => $this->getString($task_data, 'task.number'),
'user_id' => $this->getString($task_data, 'task.user_id'), 'user_id' => $this->getString($task_data, 'task.user_id'),
'rate' => $this->getFloat($task_data, 'task.rate'),
'client_id' => $clientId, 'client_id' => $clientId,
'project_id' => $this->getProjectId($projectId, $clientId), 'project_id' => $this->getProjectId($projectId, $clientId),
'description' => $this->getString($task_data, 'task.description'), 'description' => $this->getString($task_data, 'task.description'),
@ -87,8 +88,7 @@ class TaskTransformer extends BaseTransformer
$is_billable = true; $is_billable = true;
} }
if(isset($item['task.start_date']) && if(isset($item['task.start_date'])) {
isset($item['task.end_date'])) {
$start_date = $this->resolveStartDate($item); $start_date = $this->resolveStartDate($item);
$end_date = $this->resolveEndDate($item); $end_date = $this->resolveEndDate($item);
} elseif(isset($item['task.duration'])) { } elseif(isset($item['task.duration'])) {
@ -136,7 +136,7 @@ class TaskTransformer extends BaseTransformer
private function resolveEndDate($item) 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'] : ''; $stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : '';
try { try {

View File

@ -36,18 +36,26 @@ class ExpenseTransformer extends BaseTransformer
$total_tax += floatval($record['Sales Tax Amount']); $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 = [ $transformed = [
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')), 'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')),
'number' => $this->getString($data, 'Bill Number'), '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 'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022
'currency_id' => $this->company->settings->currency_id, 'currency_id' => $this->company->settings->currency_id,
'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']), 'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']),
'amount' => $amount, 'amount' => $amount,
'tax_name1' => $data['Sales Tax Name'], 'tax_name1' => isset($data['Sales Tax Name']) ? $data['Sales Tax Name'] : '',
'tax_rate1' => $tax_rate, 'tax_rate1' => $tax_rate,
]; ];

View File

@ -11,7 +11,6 @@
namespace App\Jobs\EDocument; namespace App\Jobs\EDocument;
use App\Services\EDocument\Standards\RoEInvoice;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Models\Quote; use App\Models\Quote;
use App\Models\Credit; use App\Models\Credit;
@ -23,10 +22,12 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use App\Services\EDocument\Standards\Peppol;
use horstoeko\zugferd\ZugferdDocumentBuilder; 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\OrderXDocument;
use App\Services\EDocument\Standards\FacturaEInvoice; use App\Services\EDocument\Standards\FacturaEInvoice;
use App\Services\EDocument\Standards\FatturaPA;
use App\Services\EDocument\Standards\ZugferdEDokument; use App\Services\EDocument\Standards\ZugferdEDokument;
class CreateEDocument implements ShouldQueue class CreateEDocument implements ShouldQueue
@ -68,6 +69,8 @@ class CreateEDocument implements ShouldQueue
if ($this->document instanceof Invoice) { if ($this->document instanceof Invoice) {
switch ($e_document_type) { switch ($e_document_type) {
case "PEPPOL":
return (new Peppol($this->document))->toXml();
case "FACT1": case "FACT1":
return (new RoEInvoice($this->document))->generateXml(); return (new RoEInvoice($this->document))->generateXml();
case "FatturaPA": case "FatturaPA":

View File

@ -297,12 +297,13 @@ class NinjaMailerJob implements ShouldQueue
$t->replace(Ninja::transformTranslations($this->nmo->settings)); $t->replace(Ninja::transformTranslations($this->nmo->settings));
/** Force free/trials onto specific mail driver */ /** Force free/trials onto specific mail driver */
// if(Ninja::isHosted() && !$this->company->account->isPaid())
// { if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) {
// $this->mailer = 'mailgun'; $this->mailer = 'mailgun';
// $this->setHostedMailgunMailer(); $this->setHostedMailgunMailer();
// return $this; return $this;
// } }
if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') { if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') {
//check if outlook. //check if outlook.
@ -391,7 +392,7 @@ class NinjaMailerJob implements ShouldQueue
$smtp_username = $company->smtp_username ?? ''; $smtp_username = $company->smtp_username ?? '';
$smtp_password = $company->smtp_password ?? ''; $smtp_password = $company->smtp_password ?? '';
$smtp_encryption = $company->smtp_encryption ?? 'tls'; $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; $smtp_verify_peer = $company->smtp_verify_peer ?? true;
if(strlen($smtp_host) <= 1 || if(strlen($smtp_host) <= 1 ||

View File

@ -181,7 +181,7 @@ class ProcessMailgunWebhook implements ShouldQueue
$sl = $this->getSystemLog($this->request['MessageID']); $sl = $this->getSystemLog($this->request['MessageID']);
/** Prevents Gmail tracking from firing inappropriately */ /** 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; return;
} }

View File

@ -78,7 +78,7 @@ class CleanStaleInvoiceOrder implements ShouldQueue
Invoice::query() Invoice::query()
->withTrashed() ->withTrashed()
->where('is_proforma', 1) ->where('is_proforma', 1)
->whereBetween('created_at', [now()->subHours(1), now()->subMinutes(10)]) ->where('created_at', '<', now()->subHour())
->cursor() ->cursor()
->each(function ($invoice) use ($repo) { ->each(function ($invoice) use ($repo) {
$invoice->is_proforma = false; $invoice->is_proforma = false;

View File

@ -188,6 +188,10 @@ class BillingPortalPurchase extends Component
public ?string $contact_email; public ?string $contact_email;
public ?string $client_city;
public ?string $client_postal_code;
public function mount() public function mount()
{ {
MultiDB::setDb($this->db); MultiDB::setDb($this->db);
@ -203,7 +207,7 @@ class BillingPortalPurchase extends Component
if (request()->query('coupon')) { if (request()->query('coupon')) {
$this->coupon = request()->query('coupon'); $this->coupon = request()->query('coupon');
$this->handleCoupon(); $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; $this->price = $this->subscription->promo_price;
} }
@ -335,10 +339,6 @@ class BillingPortalPurchase extends Component
{ {
$this->contact = $contact; $this->contact = $contact;
if ($contact->showRff()) {
return $this->rff();
}
Auth::guard('contact')->loginUsingId($contact->id, true); Auth::guard('contact')->loginUsingId($contact->id, true);
if ($this->subscription->trial_enabled) { if ($this->subscription->trial_enabled) {
@ -351,11 +351,20 @@ class BillingPortalPurchase extends Component
if ((int)$this->price == 0) { if ((int)$this->price == 0) {
$this->steps['payment_required'] = false; $this->steps['payment_required'] = false;
} else { } else {
$this->steps['fetched_payment_methods'] = true; // $this->steps['fetched_payment_methods'] = true;
} }
$this->methods = $contact->client->service()->getPaymentMethods($this->price); $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'); $this->heading_text = ctrans('texts.payment_methods');
return $this; return $this;
@ -366,6 +375,8 @@ class BillingPortalPurchase extends Component
$this->contact_first_name = $this->contact->first_name; $this->contact_first_name = $this->contact->first_name;
$this->contact_last_name = $this->contact->last_name; $this->contact_last_name = $this->contact->last_name;
$this->contact_email = $this->contact->email; $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; $this->steps['check_rff'] = true;
@ -377,13 +388,20 @@ class BillingPortalPurchase extends Component
$validated = $this->validate([ $validated = $this->validate([
'contact_first_name' => ['required'], 'contact_first_name' => ['required'],
'contact_last_name' => ['required'], 'contact_last_name' => ['required'],
'client_city' => ['required'],
'client_postal_code' => ['required'],
'contact_email' => ['required', 'email'], 'contact_email' => ['required', 'email'],
]); ]);
$this->contact->first_name = $validated['contact_first_name']; $this->contact->first_name = $validated['contact_first_name'];
$this->contact->last_name = $validated['contact_last_name']; $this->contact->last_name = $validated['contact_last_name'];
$this->contact->email = $validated['contact_email']; $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); return $this->getPaymentMethods($this->contact);
} }
@ -395,7 +413,7 @@ class BillingPortalPurchase extends Component
* @param $company_gateway_id * @param $company_gateway_id
* @param $gateway_type_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->company_gateway_id = $company_gateway_id;
$this->payment_method_id = $gateway_type_id; $this->payment_method_id = $gateway_type_id;

View File

@ -164,6 +164,13 @@ class BillingPortalPurchasev2 extends Component
public $payment_confirmed = false; public $payment_confirmed = false;
public $is_eligible = true; public $is_eligible = true;
public $not_eligible_message = ''; 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() public function mount()
{ {
@ -472,7 +479,6 @@ class BillingPortalPurchasev2 extends Component
*/ */
protected function getPaymentMethods(): self protected function getPaymentMethods(): self
{ {
nlog("total amount = {$this->float_amount_total}");
if ($this->float_amount_total == 0) { if ($this->float_amount_total == 0) {
$this->methods = []; $this->methods = [];
@ -482,9 +488,72 @@ class BillingPortalPurchasev2 extends Component
$this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total); $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; 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 & * Middle method between selecting payment method &
* submitting the from to the backend. * submitting the from to the backend.

View File

@ -364,16 +364,19 @@ class Account extends BaseModel
return $this->isProClient() && $this->isPaid(); return $this->isProClient() && $this->isPaid();
} }
public function isNewHostedAccount()
{
return Ninja::isHosted() && Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2;
}
public function isTrial(): bool public function isTrial(): bool
{ {
if (!Ninja::isNinja()) { if (!Ninja::isNinja()) {
return false; 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)); 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 public function startTrial($plan): void

View File

@ -27,7 +27,6 @@ use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
/** /**

View File

@ -351,9 +351,9 @@ class ClientContact extends Authenticatable implements HasLocalePreference
public function showRff(): bool public function showRff(): bool
{ {
if (\strlen($this->first_name) === 0 || \strlen($this->last_name) === 0 || \strlen($this->email) === 0) { // if (\strlen($this->first_name ?? '') === 0 || \strlen($this->last_name ?? '') === 0 || \strlen($this->email ?? '') === 0) {
return true; // return true;
} // }
return false; return false;
} }

View File

@ -159,6 +159,11 @@ class CompanyGateway extends BaseModel
protected $touches = []; protected $touches = [];
public function isPayPal()
{
return in_array($this->gateway_key, ['80af24a6a691230bbec33e930ab40666','80af24a6a691230bbec33e930ab40665']);
}
public function getEntityType() public function getEntityType()
{ {
return self::class; return self::class;

View File

@ -105,7 +105,7 @@ class Gateway extends StaticModel
$link = 'https://www.forte.net/'; $link = 'https://www.forte.net/';
} elseif ($this->id == 62) { } elseif ($this->id == 62) {
$link = 'https://docs.btcpayserver.org/InvoiceNinja/'; $link = 'https://docs.btcpayserver.org/InvoiceNinja/';
} elseif ($this->id == 4002) { } elseif ($this->id == 63) {
$link = 'https://rotessa.com'; $link = 'https://rotessa.com';
} }
@ -141,23 +141,23 @@ class Gateway extends StaticModel
case 20: case 20:
case 56: case 56:
return [ return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', '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.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.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', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_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::ALIPAY => ['refund' => false, 'token_billing' => false],
GatewayType::APPLE_PAY => ['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::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.failed', 'payment_intent.succeeded', '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.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.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.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.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.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.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.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.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.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.failed',]], GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed',]],
]; ];
case 39: case 39:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout
@ -226,7 +226,7 @@ class Gateway extends StaticModel
return [ return [
GatewayType::CRYPTO => ['refund' => true, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], GatewayType::CRYPTO => ['refund' => true, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']],
]; //BTCPay ]; //BTCPay
case 4002: case 63:
return [ return [
GatewayType::BANK_TRANSFER => [ GatewayType::BANK_TRANSFER => [
'refund' => false, 'refund' => false,

View File

@ -95,4 +95,9 @@ class UserPresenter extends EntityPresenter
{ {
return $this->entity->phone ?? ' '; return $this->entity->phone ?? ' ';
} }
public function email(): string
{
return $this->entity->email ?? ' ';
}
} }

View File

@ -129,7 +129,7 @@ class Project extends BaseModel
public function invoices(): HasMany public function invoices(): HasMany
{ {
return $this->hasMany(Invoice::class); return $this->hasMany(Invoice::class)->withTrashed();
} }
public function quotes(): HasMany public function quotes(): HasMany

View File

@ -152,6 +152,8 @@ class SystemLog extends Model
public const TYPE_BTC_PAY = 324; public const TYPE_BTC_PAY = 324;
public const TYPE_ROTESSA = 325;
public const TYPE_QUOTA_EXCEEDED = 400; public const TYPE_QUOTA_EXCEEDED = 400;
public const TYPE_UPSTREAM_FAILURE = 401; public const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -170,6 +170,9 @@ class ACH
]; ];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED); $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]);
} }
} }

View File

@ -187,6 +187,8 @@ class CreditCard
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
]; ];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED); $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]);
} }
} }

View File

@ -251,11 +251,11 @@ class PayPalBasePaymentDriver extends BaseDriver
[ [
"address" => "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, "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_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, "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, "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(), "country_code" => $this->client->present()->shipping_country_code(),
], ],
] ]

View File

@ -128,7 +128,7 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
nlog($response); 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); 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; $order['purchase_units'][0]["shipping"] = $shipping;
}
if(isset($data['payment_source'])) { if(isset($data['payment_source']))
$order['payment_source'] = $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); $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
@ -316,8 +316,17 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
->firstOrFail(); ->firstOrFail();
$orderId = $response['orderID']; $orderId = $response['orderID'];
$r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']); $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['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [ $data["payment_source"] = [
"card" => [ "card" => [
@ -332,8 +341,6 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$orderId = $this->createOrder($data); $orderId = $this->createOrder($data);
// $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
try { try {
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']); $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
@ -395,6 +402,14 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$data = []; $data = [];
$this->payment_hash = $payment_hash; $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['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [ $data["payment_source"] = [
"card" => [ "card" => [

View File

@ -157,10 +157,6 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
} }
public function createOrder(array $data): string public function createOrder(array $data): string
{ {
@ -213,6 +209,10 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
$order['payment_source'] = $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); $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
nlog($r->json()); nlog($r->json());
@ -274,6 +274,13 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
nlog($r->body()); 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['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [ $data["payment_source"] = [
"card" => [ "card" => [
@ -349,6 +356,14 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
$data = []; $data = [];
$this->payment_hash = $payment_hash; $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['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [ $data["payment_source"] = [
"card" => [ "card" => [

View File

@ -60,7 +60,10 @@ class PaymentMethod implements MethodInterface
'id' => null 'id' => null
] )->all(); ] )->all();
$data['gateway'] = $this->rotessa; $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'] = [ $data['account'] = [
'routing_number' => $data['client']->routing_id, 'routing_number' => $data['client']->routing_id,
'country' => $data['client']->country->iso_3166_2 'country' => $data['client']->country->iso_3166_2
@ -145,18 +148,16 @@ class PaymentMethod implements MethodInterface
$request->validate([ $request->validate([
'source' => ['required','string','exists:client_gateway_tokens,token'], 'source' => ['required','string','exists:client_gateway_tokens,token'],
'amount' => ['required','numeric'], 'amount' => ['required','numeric'],
'token_id' => ['required','integer','exists:client_gateway_tokens,id'],
'process_date'=> ['required','date','after_or_equal:today'], 'process_date'=> ['required','date','after_or_equal:today'],
]); ]);
$customer = ClientGatewayToken::query() $customer = ClientGatewayToken::query()
->where('company_gateway_id', $this->rotessa->company_gateway->id) ->where('company_gateway_id', $this->rotessa->company_gateway->id)
->where('client_id', $this->rotessa->client->id) ->where('client_id', $this->rotessa->client->id)
->where('id', (int) $request->input('token_id'))
->where('token', $request->input('source')) ->where('token', $request->input('source'))
->first(); ->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->additional(['customer_id' => $customer->gateway_customer_reference]);
$transaction = array_filter( $transaction->resolve()); $transaction = array_filter( $transaction->resolve());
$response = $this->rotessa->gateway->capture($transaction)->send(); $response = $this->rotessa->gateway->capture($transaction)->send();
@ -182,12 +183,12 @@ class PaymentMethod implements MethodInterface
[ 'data' => $data ], [ 'data' => $data ],
SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::EVENT_GATEWAY_SUCCESS,
880, SystemLog::TYPE_ROTESSA,
$this->rotessa->client, $this->rotessa->client,
$this->rotessa->client->company, $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(), $exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::EVENT_GATEWAY_FAILURE,
880, SystemLog::TYPE_ROTESSA,
$this->rotessa->client, $this->rotessa->client,
$this->rotessa->client->company, $this->rotessa->client->company,
); );

View File

@ -24,6 +24,7 @@ use App\Utils\Traits\MakesHash;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\PaymentDrivers\BaseDriver; use App\PaymentDrivers\BaseDriver;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use App\PaymentDrivers\Rotessa\Resources\Customer; use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\PaymentMethod as Acss; use App\PaymentDrivers\Rotessa\PaymentMethod as Acss;
@ -64,13 +65,15 @@ class RotessaPaymentDriver extends BaseDriver
{ {
$types = []; $types = [];
/*
// TODO: needs to test with US test account
if ($this->client if ($this->client
&& $this->client->currency() && $this->client->currency()
&& in_array($this->client->currency()->code, ['USD']) && in_array($this->client->currency()->code, ['USD'])
&& isset($this->client->country) && isset($this->client->country)
&& in_array($this->client->country->iso_3166_2, ['US'])) { && in_array($this->client->country->iso_3166_2, ['US'])) {
$types[] = GatewayType::BANK_TRANSFER; $types[] = GatewayType::BANK_TRANSFER;
} }*/
if ($this->client if ($this->client
&& $this->client->currency() && $this->client->currency()
@ -115,27 +118,98 @@ class RotessaPaymentDriver extends BaseDriver
public function importCustomers() { public function importCustomers() {
$this->init(); $this->init();
try { try {
if(!$result = Cache::has("rotessa-import_customers-{$this->company_gateway->company->company_key}")) {
$result = $this->gateway->getCustomers()->send(); $result = $this->gateway->getCustomers()->send();
if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode()); 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);
}
$customers = collect($result->getData())->unique('email'); $result = Cache::get("rotessa-import_customers-{$this->company_gateway->company->company_key}");
$customers = collect($result)->unique('email');
$client_emails = $customers->pluck('email')->all(); $client_emails = $customers->pluck('email')->all();
$company_id = $this->company_gateway->company->id; $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 = ClientContact::where('company_id', $company_id)->whereIn('email', $client_emails )->whereNull('deleted_at')->get();
$client_contacts = $client_contacts->map(function($item, $key) use ($customers) { $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( $client_contacts->each(
function($contact) use ($customers) { function($contact) use ($customers) {
sleep(10);
$result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send(); $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 ] ); $customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize()); $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) { } catch (\Throwable $th) {
$data = [ $data = [
'transaction_reference' => null, 'transaction_reference' => null,
@ -144,9 +218,7 @@ class RotessaPaymentDriver extends BaseDriver
'description' => $th->getMessage(), 'description' => $th->getMessage(),
'code' =>(int) $th->getCode() 'code' =>(int) $th->getCode()
]; ];
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);
SystemLogger::dispatch(['server_response' => $th->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client , $this->company_gateway->company);
throw $th; throw $th;
} }
@ -176,9 +248,11 @@ class RotessaPaymentDriver extends BaseDriver
$data = array_filter($customer->resolve()); $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( [ $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'))), 'token' => encrypt(join(".", Arr::only($data, 'id','custom_identifier'))),
'payment_method_id' => $payment_method_id , 'payment_method_id' => $payment_method_id ,
], ['gateway_customer_reference' => ], ['gateway_customer_reference' =>
@ -198,7 +272,7 @@ class RotessaPaymentDriver extends BaseDriver
'code' =>(int) $th->getCode() '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; throw $th;
} }

View File

@ -153,7 +153,7 @@ class BrowserPay implements MethodInterface
$this->stripe->client->company, $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]);
} }
/** /**

View File

@ -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) public function processUnsuccessfulPayment($server_response)

View File

@ -11,18 +11,22 @@
namespace App\PaymentDrivers\Stripe\Jobs; namespace App\PaymentDrivers\Stripe\Jobs;
use App\Libraries\MultiDB;
use App\Models\Company; use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\Payment; use App\Models\Payment;
use App\Libraries\MultiDB;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\PaymentDrivers\Stripe\Utilities; use App\Services\Email\Email;
use Illuminate\Bus\Queueable; 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\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ChargeRefunded implements ShouldQueue class ChargeRefunded implements ShouldQueue
{ {
@ -36,19 +40,10 @@ class ChargeRefunded implements ShouldQueue
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
public $stripe_request;
public $company_key;
private $company_gateway_id;
public $payment_completed = false; 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() public function handle()
@ -64,8 +59,8 @@ class ChargeRefunded implements ShouldQueue
$payment_hash_key = $source['metadata']['payment_hash'] ?? null; $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(); $payment_hash = PaymentHash::query()->where('hash', $payment_hash_key)->first();
$company_gateway = $payment_hash->payment->company_gateway;
$stripe_driver = $company_gateway->driver()->init(); $stripe_driver = $company_gateway->driver()->init();
@ -79,7 +74,7 @@ class ChargeRefunded implements ShouldQueue
->first(); ->first();
//don't touch if already refunded //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; return;
} }
@ -94,7 +89,18 @@ class ChargeRefunded implements ShouldQueue
return; 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 $invoice_collection = $payment->paymentables
->where('paymentable_type', 'invoices') ->where('paymentable_type', 'invoices')
@ -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}. <br><br> This payment is associated with multiple invoices, so you will need to manually apply the refund to the correct invoice/s.";
App::setLocale($payment_hash->payment->company->getLocale());
$mo = new EmailObject();
$mo->subject = "Refund processed in Stripe for multiple invoices, action required.";
$mo->body = $refund_text;
$mo->text_body = $refund_text;
$mo->company_key = $payment_hash->payment->company->company_key;
$mo->html_template = 'email.template.generic';
$mo->to = [new Address($payment_hash->payment->company->owner()->email, $payment_hash->payment->company->owner()->present()->name())];
Email::dispatch($mo, $payment_hash->payment->company);
return; return;
} }
$invoices = $invoice_collection->toArray(); $invoices = $invoice_collection->toArray();
@ -131,6 +152,7 @@ class ChargeRefunded implements ShouldQueue
'date' => now()->format('Y-m-d'), 'date' => now()->format('Y-m-d'),
'gateway_refund' => false, 'gateway_refund' => false,
'email_receipt' => false, 'email_receipt' => false,
'via_webhook' => true,
]; ];
nlog($data); nlog($data);
@ -138,13 +160,13 @@ class ChargeRefunded implements ShouldQueue
$payment->refund($data); $payment->refund($data);
$payment->private_notes .= 'Refunded via Stripe '; $payment->private_notes .= 'Refunded via Stripe ';
return;
} $payment->saveQuietly();
} }
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping($this->company_gateway_id)]; return [new WithoutOverlapping($this->company_key)];
} }
} }

View File

@ -12,54 +12,55 @@
namespace App\PaymentDrivers; namespace App\PaymentDrivers;
use App\Exceptions\PaymentFailed;
use App\Exceptions\StripeConnectFailure;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\Client;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\PaymentDrivers\Stripe\ACH;
use App\PaymentDrivers\Stripe\ACSS;
use App\PaymentDrivers\Stripe\Alipay;
use App\PaymentDrivers\Stripe\BACS;
use App\PaymentDrivers\Stripe\Bancontact;
use App\PaymentDrivers\Stripe\BankTransfer;
use App\PaymentDrivers\Stripe\BECS;
use App\PaymentDrivers\Stripe\BrowserPay;
use App\PaymentDrivers\Stripe\Charge;
use App\PaymentDrivers\Stripe\Connect\Verify;
use App\PaymentDrivers\Stripe\CreditCard;
use App\PaymentDrivers\Stripe\EPS;
use App\PaymentDrivers\Stripe\FPX;
use App\PaymentDrivers\Stripe\GIROPAY;
use App\PaymentDrivers\Stripe\iDeal;
use App\PaymentDrivers\Stripe\ImportCustomers;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook;
use App\PaymentDrivers\Stripe\Klarna;
use App\PaymentDrivers\Stripe\PRZELEWY24;
use App\PaymentDrivers\Stripe\SEPA;
use App\PaymentDrivers\Stripe\SOFORT;
use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash;
use Exception; use Exception;
use Illuminate\Http\RedirectResponse; use Stripe\Stripe;
use Laracasts\Presenter\Exceptions\PresenterException;
use Stripe\Account; use Stripe\Account;
use Stripe\Customer; use Stripe\Customer;
use Stripe\Exception\ApiErrorException; use App\Models\Client;
use App\Models\Payment;
use Stripe\SetupIntent;
use Stripe\StripeClient;
use App\Models\SystemLog;
use Stripe\PaymentIntent; use Stripe\PaymentIntent;
use Stripe\PaymentMethod; use Stripe\PaymentMethod;
use Stripe\SetupIntent; use App\Models\GatewayType;
use Stripe\Stripe; use App\Models\PaymentHash;
use Stripe\StripeClient; use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\Stripe\ACH;
use App\PaymentDrivers\Stripe\EPS;
use App\PaymentDrivers\Stripe\FPX;
use App\PaymentDrivers\Stripe\ACSS;
use App\PaymentDrivers\Stripe\BACS;
use App\PaymentDrivers\Stripe\BECS;
use App\PaymentDrivers\Stripe\SEPA;
use App\PaymentDrivers\Stripe\iDeal;
use App\PaymentDrivers\Stripe\Alipay;
use App\PaymentDrivers\Stripe\Charge;
use App\PaymentDrivers\Stripe\Klarna;
use App\PaymentDrivers\Stripe\SOFORT;
use Illuminate\Http\RedirectResponse;
use App\PaymentDrivers\Stripe\GIROPAY;
use Stripe\Exception\ApiErrorException;
use App\Exceptions\StripeConnectFailure;
use App\PaymentDrivers\Stripe\Utilities;
use App\PaymentDrivers\Stripe\Bancontact;
use App\PaymentDrivers\Stripe\BrowserPay;
use App\PaymentDrivers\Stripe\CreditCard;
use App\PaymentDrivers\Stripe\PRZELEWY24;
use App\PaymentDrivers\Stripe\BankTransfer;
use App\PaymentDrivers\Stripe\Connect\Verify;
use App\PaymentDrivers\Stripe\ImportCustomers;
use App\PaymentDrivers\Stripe\Jobs\ChargeRefunded;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use Laracasts\Presenter\Exceptions\PresenterException;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook;
use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook;
class StripePaymentDriver extends BaseDriver class StripePaymentDriver extends BaseDriver
{ {
@ -670,31 +671,39 @@ class StripePaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request) public function processWebhookRequest(PaymentWebhookRequest $request)
{ {
nlog($request->all());
if ($request->type === 'customer.source.updated') { if ($request->type === 'customer.source.updated') {
$ach = new ACH($this); $ach = new ACH($this);
$ach->updateBankAccount($request->all()); $ach->updateBankAccount($request->all());
} }
if ($request->type === 'payment_intent.processing') { if ($request->type === 'payment_intent.processing') {
PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 12))); PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
return response()->json([], 200); return response()->json([], 200);
} }
//payment_intent.succeeded - this will confirm or cancel the payment //payment_intent.succeeded - this will confirm or cancel the payment
if ($request->type === 'payment_intent.succeeded') { if ($request->type === 'payment_intent.succeeded') {
PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 15))); PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
return response()->json([], 200); return response()->json([], 200);
} }
if ($request->type === 'payment_intent.partially_funded') { if ($request->type === 'payment_intent.partially_funded') {
PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 15))); PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5));
return response()->json([], 200); return response()->json([], 200);
} }
if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) { if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) {
PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(2));
return response()->json([], 200);
}
if ($request->type === 'charge.refunded' && $request->data['object']['status'] == 'succeeded') {
ChargeRefunded::dispatch($request->data, $request->company_key)->delay(now()->addSeconds(5));
return response()->json([], 200); return response()->json([], 200);
} }
@ -702,7 +711,6 @@ class StripePaymentDriver extends BaseDriver
if ($request->type === 'charge.succeeded') { if ($request->type === 'charge.succeeded') {
foreach ($request->data as $transaction) { foreach ($request->data as $transaction) {
$payment = Payment::query() $payment = Payment::query()
->where('company_id', $this->company_gateway->company_id) ->where('company_id', $this->company_gateway->company_id)
->where(function ($query) use ($transaction) { ->where(function ($query) use ($transaction) {

View File

@ -192,6 +192,7 @@ class PaymentMethod
'label' => ctrans('texts.apply_credit'), 'label' => ctrans('texts.apply_credit'),
'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT,
'gateway_type_id' => GatewayType::CREDIT, 'gateway_type_id' => GatewayType::CREDIT,
'is_paypal' => false,
]; ];
} }
@ -210,12 +211,14 @@ class PaymentMethod
'label' => $gateway->getConfigField('name').$fee_label, 'label' => $gateway->getConfigField('name').$fee_label,
'company_gateway_id' => $gateway->id, 'company_gateway_id' => $gateway->id,
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
'is_paypal' => $gateway->isPayPal(),
]; ];
} else { } else {
$this->payment_urls[] = [ $this->payment_urls[] = [
'label' => $gateway->getTypeAlias($type).$fee_label, 'label' => $gateway->getTypeAlias($type).$fee_label,
'company_gateway_id' => $gateway->id, 'company_gateway_id' => $gateway->id,
'gateway_type_id' => $type, 'gateway_type_id' => $type,
'is_paypal' => $gateway->isPayPal(),
]; ];
} }
@ -236,12 +239,14 @@ class PaymentMethod
'label' => $gateway->getConfigField('name').$fee_label, 'label' => $gateway->getConfigField('name').$fee_label,
'company_gateway_id' => $gateway_id, 'company_gateway_id' => $gateway_id,
'gateway_type_id' => GatewayType::CREDIT_CARD, 'gateway_type_id' => GatewayType::CREDIT_CARD,
'is_paypal' => $gateway->isPayPal(),
]; ];
} else { } else {
$this->payment_urls[] = [ $this->payment_urls[] = [
'label' => $gateway->getTypeAlias($gateway_type_id).$fee_label, 'label' => $gateway->getTypeAlias($gateway_type_id).$fee_label,
'company_gateway_id' => $gateway_id, 'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id, 'gateway_type_id' => $gateway_type_id,
'is_paypal' => $gateway->isPayPal(),
]; ];
} }
} }
@ -259,6 +264,7 @@ class PaymentMethod
'label' => ctrans('texts.apply_credit'), 'label' => ctrans('texts.apply_credit'),
'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT,
'gateway_type_id' => GatewayType::CREDIT, 'gateway_type_id' => GatewayType::CREDIT,
'is_paypal' => false,
]; ];
} }

View File

@ -44,17 +44,16 @@ class InstantPayment
public function run() public function run()
{ {
nlog($this->request->all());
/** @var \App\Models\ClientContact $cc */ /** @var \App\Models\ClientContact $cc */
$cc = auth()->guard('contact')->user(); $cc = auth()->guard('contact')->user();
$cc->first_name = $this->request->contact_first_name; $cc->first_name = $this->request->contact_first_name;
$cc->last_name = $this->request->contact_last_name; $cc->last_name = $this->request->contact_last_name;
$cc->email = $this->request->contact_email; $cc->email = $this->request->contact_email;
$cc->client->postal_code = strlen($cc->client->postal_code ?? '') > 1 ? $cc->client->postal_code : $this->request->client_postal_code;
$cc->save(); $cc->client->city = strlen($cc->client->city ?? '') > 1 ? $cc->client->city : $this->request->client_city;
$cc->client->shipping_postal_code = strlen($cc->client->shipping_postal_code ?? '') > 1 ? $cc->client->shipping_postal_code : $cc->client->postal_code;
$cc->client->shipping_city = strlen($cc->client->shipping_city ?? '') > 1 ? $cc->client->shipping_city : $cc->client->city;
$cc->pushQuietly();
$is_credit_payment = false; $is_credit_payment = false;
@ -73,8 +72,6 @@ class InstantPayment
*/ */
$payable_invoices = collect($this->request->payable_invoices); $payable_invoices = collect($this->request->payable_invoices);
nlog($payable_invoices);
$invoices = Invoice::query()->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get(); $invoices = Invoice::query()->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get();
$invoices->each(function ($invoice) { $invoices->each(function ($invoice) {

View File

@ -95,6 +95,48 @@ class Storecove {
// parseStrategy: ubl // parseStrategy: ubl
// } // }
*/ */
public function sendJsonDocument($document)
{
$payload = [
"legalEntityId" => 290868,
"idempotencyGuid" => \Illuminate\Support\Str::uuid(),
"routing" => [
"eIdentifiers" => [],
"emails" => ["david@invoiceninja.com"]
],
// "document" => [
// 'documentType' => 'invoice',
// "rawDocumentData" => [
// "document" => base64_encode($document),
// "parse" => true,
// "parseStrategy" => "ubl",
// ],
// ],
"document"=> [
"documentType" => "invoice",
"invoice" => $document,
],
];
$uri = "document_submissions";
nlog($payload);
$r = $this->httpClient($uri, (HttpVerb::POST)->value, $payload, $this->getHeaders());
nlog($r->body());
nlog($r->json());
if($r->successful()) {
return $r->json()['guid'];
}
return false;
}
public function sendDocument($document) public function sendDocument($document)
{ {
@ -256,8 +298,6 @@ class Storecove {
} }
public function addIdentifier(int $legal_entity_id, string $identifier, string $scheme) public function addIdentifier(int $legal_entity_id, string $identifier, string $scheme)
{ {
$uri = "legal_entities/{$legal_entity_id}/peppol_identifiers"; $uri = "legal_entities/{$legal_entity_id}/peppol_identifiers";
@ -278,7 +318,6 @@ class Storecove {
} }
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function getHeaders(array $headers = []) private function getHeaders(array $headers = [])
{ {
@ -300,5 +339,4 @@ class Storecove {
return $r; return $r;
} }
} }

View File

@ -48,6 +48,7 @@ use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerPart
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID;
use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty;
use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification;
class Peppol extends AbstractService class Peppol extends AbstractService
@ -150,14 +151,36 @@ class Peppol extends AbstractService
private InvoiceSum | InvoiceSumInclusive $calc; private InvoiceSum | InvoiceSumInclusive $calc;
private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice;
/** /**
* @param Invoice $invoice * @param Invoice $invoice
*/ */
public function __construct(public Invoice $invoice, public ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice = null) public function __construct(public Invoice $invoice)
{ {
$this->p_invoice = $p_invoice ?? new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$this->company = $invoice->company; $this->company = $invoice->company;
$this->calc = $this->invoice->calc(); $this->calc = $this->invoice->calc();
$this->setInvoice();
}
private function setInvoice(): self
{
if($this->invoice->e_invoice){
$e = new EInvoice();
$this->p_invoice = $e->decode('Peppol', json_encode($this->invoice->e_invoice->Invoice), 'json');
return $this;
}
$this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$this->setInvoiceDefaults();
return $this;
} }
public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice
@ -170,7 +193,31 @@ class Peppol extends AbstractService
public function toXml(): string public function toXml(): string
{ {
$e = new EInvoice(); $e = new EInvoice();
return $e->encode($this->p_invoice, 'xml'); $xml = $e->encode($this->p_invoice, 'xml');
$prefix = '<?xml version="1.0" encoding="utf-8"?>
<Invoice
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">';
return str_ireplace(['\n','<?xml version="1.0"?>'], ['', $prefix], $xml);
}
public function toJson(): string
{
$e = new EInvoice();
$json = $e->encode($this->p_invoice, 'json');
return $json;
// $prefixes = str_ireplace(["cac:","cbc:"], "", $json);
// return str_ireplace(["InvoiceLine", "PostalAddress", "PartyName"], ["invoiceLines","address", "companyName"], $prefixes);
}
public function toArray(): array
{
return json_decode($this->toJson(), true);
} }
public function run() public function run()
@ -185,31 +232,12 @@ class Peppol extends AbstractService
$this->p_invoice->TaxTotal = $this->getTotalTaxes(); $this->p_invoice->TaxTotal = $this->getTotalTaxes();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal(); $this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
// $this->p_invoice->PaymentMeans = $this->getPaymentMeans(); $this->countryLevelMutators();
// $payeeFinancialAccount = (new PayeeFinancialAccount())
// ->setBankId($company->settings->custom_value1)
// ->setBankName($company->settings->custom_value2);
// $paymentMeans = (new PaymentMeans())
// ->setPaymentMeansCode($invoice->custom_value1)
// ->setPayeeFinancialAccount($payeeFinancialAccount);
// $ubl_invoice->setPaymentMeans($paymentMeans);
return $this; return $this;
} }
// private function getPaymentMeans(): PaymentMeans
// {
// $payeeFinancialAccount = new PayeeFinancialAccount()
// $payeeFinancialAccount->
// $ppm = new PaymentMeans();
// $ppm->PayeeFinancialAccount = $payeeFinancialAccount;
// return $ppm;
// }
private function getLegalMonetaryTotal(): LegalMonetaryTotal private function getLegalMonetaryTotal(): LegalMonetaryTotal
{ {
$taxable = $this->getTaxable(); $taxable = $this->getTaxable();
@ -572,7 +600,9 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv
$party->PhysicalLocation = $address; $party->PhysicalLocation = $address;
$contact = new Contact(); $contact = new Contact();
$contact->ElectronicMail = $this->invoice->company->owner()->email ?? 'owner@gmail.com'; $contact->ElectronicMail = $this->getSetting('Invoice.AccountingSupplierParty.Party.Contact') ?? $this->invoice->company->owner()->present()->email();
$contact->Telephone = $this->getSetting('Invoice.AccountingSupplierParty.Party.Telephone') ?? $this->invoice->company->getSetting('phone');
$contact->Name = $this->getSetting('Invoice.AccountingSupplierParty.Party.Name') ?? $this->invoice->company->owner()->present()->name();
$party->Contact = $contact; $party->Contact = $contact;
@ -695,16 +725,283 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv
'PaymentTerms' => 7, 'PaymentTerms' => 7,
]; ];
//only scans for top level props
foreach($settings as $prop => $visibility){ foreach($settings as $prop => $visibility){
if($prop_value = PropertyResolver::resolve($this->invoice->client->e_invoice, $prop)) if($prop_value = $this->getSetting($prop))
$this->p_invoice->{$prop} = $prop_value; $this->p_invoice->{$prop} = $prop_value;
elseif($prop_value = PropertyResolver::resolve($this->invoice->company->e_invoice, $prop)) {
$this->p_invoice->{$prop} = $prop_value;
}
} }
return $this; return $this;
} }
public function getSetting(string $property_path): mixed
{
if($prop_value = PropertyResolver::resolve($this->invoice->e_invoice, $property_path))
return $prop_value;
elseif($prop_value = PropertyResolver::resolve($this->invoice->client->e_invoice, $property_path))
return $prop_value;
elseif($prop_value = PropertyResolver::resolve($this->invoice->company->e_invoice, $property_path))
return $prop_value;
return null;
}
public function countryLevelMutators():self
{
if(method_exists($this, $this->invoice->company->country()->iso_3166_2))
$this->{$this->invoice->company->country()->iso_3166_2}();
return $this;
}
private function setPaymentMeans(bool $required = false): self
{
if($this->p_invoice->PaymentMeans)
return $this;
elseif(!isset($this->p_invoice->PaymentMeans) && $paymentMeans = $this->getSetting('Invoice.PaymentMeans')){
$this->p_invoice->PaymentMeans = is_array($paymentMeans) ? $paymentMeans : [$paymentMeans];
return $this;
}
if($required)
throw new \Exception('e-invoice generation halted:: Payment Means required');
return $this;
}
private function DE(): self
{
// accountingsupplierparty.party.contact MUST be set - Name / Telephone / Electronic Mail
// this is forced by default.
$this->setPaymentMeans(true);
return $this;
}
private function CH(): self
{
//if QR-Bill support required - then special flow required.... optional.
return $this;
}
private function AT(): self
{
//special fields for sending to AT:GOV
return $this;
}
private function AU(): self
{
//if payment means are included, they must be the same `type`
return $this;
}
private function ES(): self
{
// For B2B, provide an ES:DIRE routing identifier and an ES:VAT tax identifier.
// both sender and receiver must be an ES company;
// you must have a "credit_transfer" PaymentMean;
// the "dueDate" property is mandatory.
// For B2G, provide three ES:FACE identifiers in the routing object,
// as well as the ES:VAT tax identifier in the accountingCustomerParty.publicIdentifiers.
// The invoice will then be routed through the FACe network. The three required ES:FACE identifiers are as follows:
// "routing": {
// "eIdentifiers":[
// {
// "scheme": "ES:FACE",
// "id": "L01234567",
// "role": "ES-01-FISCAL"
// },
// {
// "scheme": "ES:FACE",
// "id": "L01234567",
// "role": "ES-02-RECEPTOR"
// },
// {
// "scheme": "ES:FACE",
// "id": "L01234567",
// "role": "ES-03-PAGADOR"
// }
// ]
// }
return $this;
}
private function FI(): self
{
// For Finvoice, provide an FI:OPID routing identifier and an FI:OVT legal identifier.
// An FI:VAT is recommended. In many cases (depending on the sender/receiver country and the type of service/goods)
// an FI:VAT is required. So we recommend always including this.
return $this;
}
private function FR(): self
{
// When sending invoices to the French government (Chorus Pro):
// All invoices have to be routed to SIRET 0009:11000201100044. There is no test environment for sending to public entities.
// The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array.
// The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference)
// The commitment number must be sent in the invoice.orderReference (deprecated) or the invoice.references array (documentType purchase_order).
// Invoices to companies (SIRET / 0009 or SIRENE / 0002) are routed directly to that identifier.
return $this;
}
private function IT(): self
{
// IT Sender, IT Receiver, B2B/B2G
// Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario)
// IT Sender, IT Receiver, B2C
// Provide the receiver IT:CF and the receiver IT:CUUO (codice destinatario)
// IT Sender, non-IT Receiver
// Provide the receiver tax identifier and any routing identifier applicable to the receiving country (see Receiver Identifiers).
// non-IT Sender, IT Receiver, B2B/B2G
// Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario)
// non-IT Sender, IT Receiver, B2C
// Provide the receiver IT:CF and an optional email. The invoice will be eReported and sent via email. Note that this cannot be a PEC email address.
return $this;
}
private function MY(): self
{
//way too much to digest here, delayed.
return $this;
}
private function NL(): self
{
// When sending to public entities, the invoice.accountingSupplierParty.party.contact.email is mandatory.
// Dutch senders and receivers require a legal identifier. For companies, this is NL:KVK, for public entities this is NL:OINO.
return $this;
}
private function NZ(): self
{
// New Zealand uses a GLN to identify businesses. In addition, when sending invoices to a New Zealand customer, make sure you include the pseudo identifier NZ:GST as their tax identifier.
return $this;
}
private function PL(): self
{
// Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "PL:VAT",
// "id": "PL0101010101"
// }
// ],
// "networks": [
// {
// "application": "pl-ksef",
// "settings": {
// "enabled": true
// }
// }
// ]
// }
// Note this will only work if your LegalEntity has been setup for this network.
return $this;
}
private function RO(): self
{
// Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "RO:VAT",
// "id": "RO010101010"
// }
// ],
// "networks": [
// {
// "application": "ro-anaf",
// "settings": {
// "enabled": true
// }
// }
// ]
// }
// Note this will only work if your LegalEntity has been setup for this network.
// The county field for a Romania address must use the ISO3166-2:RO codes, e.g. "RO-AB, RO-AR". Dont omit the country prefix!
// The city field for county RO-B must be SECTOR1 - SECTOR6.
return $this;
}
private function SG(): self
{
//delayed - stage 2
return $this;
}
//Sweden
private function SE(): self
{
// Deliver invoices to the "Svefaktura" co-operation of local Swedish service providers.
// Routing is through the SE:ORGNR together with a network specification:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "SE:ORGNR",
// "id": "0012345678"
// }
// ],
// "networks": [
// {
// "application": "svefaktura",
// "settings": {
// "enabled": true
// }
// }
// ]
// }
// Use of the "Svefaktura" co-operation can also be induced by specifying an operator id, as follows:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "SE:ORGNR",
// "id": "0012345678"
// },
// {
// "scheme": "SE:OPID",
// "id": "1234567890"
// }
// ]
// }
return $this;
}
} }

View File

@ -526,11 +526,11 @@ class Email implements ShouldQueue
{ {
/** Force free/trials onto specific mail driver */ /** Force free/trials onto specific mail driver */
// if(Ninja::isHosted() && !$this->company->account->isPaid()) { if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) {
// $this->mailer = 'mailgun'; $this->mailer = 'mailgun';
// $this->setHostedMailgunMailer(); $this->setHostedMailgunMailer();
// return $this; return $this;
// } }
if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') { if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') {
@ -619,7 +619,7 @@ class Email implements ShouldQueue
$smtp_username = $company->smtp_username ?? ''; $smtp_username = $company->smtp_username ?? '';
$smtp_password = $company->smtp_password ?? ''; $smtp_password = $company->smtp_password ?? '';
$smtp_encryption = $company->smtp_encryption ?? 'tls'; $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; $smtp_verify_peer = $company->smtp_verify_peer ?? true;
if(strlen($smtp_host) <= 1 || if(strlen($smtp_host) <= 1 ||

View File

@ -42,6 +42,9 @@ class AutoBillInvoice extends AbstractService
public function __construct(private Invoice $invoice, protected string $db) public function __construct(private Invoice $invoice, protected string $db)
{ {
$this->client = $this->invoice->client;
} }
public function run() public function run()
@ -49,7 +52,6 @@ class AutoBillInvoice extends AbstractService
MultiDB::setDb($this->db); MultiDB::setDb($this->db);
/* @var \App\Modesl\Client $client */ /* @var \App\Modesl\Client $client */
$this->client = $this->invoice->client;
$is_partial = false; $is_partial = false;
@ -272,7 +274,7 @@ class AutoBillInvoice extends AbstractService
* *
* @return self * @return self
*/ */
private function applyUnappliedPayment(): self public function applyUnappliedPayment(): self
{ {
$unapplied_payments = Payment::query() $unapplied_payments = Payment::query()
->where('client_id', $this->client->id) ->where('client_id', $this->client->id)
@ -285,6 +287,11 @@ class AutoBillInvoice extends AbstractService
$available_unapplied_balance = $unapplied_payments->sum('amount') - $unapplied_payments->sum('applied'); $available_unapplied_balance = $unapplied_payments->sum('amount') - $unapplied_payments->sum('applied');
nlog($this->client->id);
nlog($this->invoice->id);
nlog($unapplied_payments->sum('amount'));
nlog($unapplied_payments->sum('applied'));
nlog("available unapplied balance = {$available_unapplied_balance}"); nlog("available unapplied balance = {$available_unapplied_balance}");
if ((int) $available_unapplied_balance == 0) { if ((int) $available_unapplied_balance == 0) {
@ -347,7 +354,7 @@ class AutoBillInvoice extends AbstractService
* *
* @return $this * @return $this
*/ */
private function applyCreditPayment(): self public function applyCreditPayment(): self
{ {
$available_credits = Credit::query()->where('client_id', $this->client->id) $available_credits = Credit::query()->where('client_id', $this->client->id)
->where('is_deleted', false) ->where('is_deleted', false)

View File

@ -44,7 +44,6 @@ class RefundPayment
->setStatus() //sets status of payment ->setStatus() //sets status of payment
->updatePaymentables() //update the paymentable items ->updatePaymentables() //update the paymentable items
->adjustInvoices() ->adjustInvoices()
->finalize()
->save(); ->save();
if (array_key_exists('email_receipt', $this->refund_data) && $this->refund_data['email_receipt'] == 'true') { if (array_key_exists('email_receipt', $this->refund_data) && $this->refund_data['email_receipt'] == 'true') {
@ -52,10 +51,11 @@ class RefundPayment
EmailRefundPayment::dispatch($this->payment, $this->payment->company, $contact); EmailRefundPayment::dispatch($this->payment, $this->payment->company, $contact);
} }
$notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : "; $is_gateway_refund = ($this->refund_data['gateway_refund'] !== false || $this->refund_failed || (isset($this->refund_data['via_webhook']) && $this->refund_data['via_webhook'] !== false)) ? ctrans('texts.yes') : ctrans('texts.no');
$notes .= $this->refund_data['gateway_refund'] !== false ? ctrans('texts.yes') : ctrans('texts.no'); $notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : " . $is_gateway_refund;
$this->createActivity($notes); $this->createActivity($notes);
$this->finalize();
return $this->payment; return $this->payment;
} }
@ -178,7 +178,7 @@ class RefundPayment
*/ */
private function setStatus() private function setStatus()
{ {
if ($this->total_refund == $this->payment->amount) { if ($this->total_refund == $this->payment->amount || floatval($this->payment->amount) == floatval($this->payment->refunded)) {
$this->payment->status_id = Payment::STATUS_REFUNDED; $this->payment->status_id = Payment::STATUS_REFUNDED;
} else { } else {
$this->payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED; $this->payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;

View File

@ -739,7 +739,7 @@ class PdfBuilder
if ($item->is_amount_discount) { if ($item->is_amount_discount) {
$data[$key][$table_type.'.discount'] = $this->service->config->formatMoney($item->discount); $data[$key][$table_type.'.discount'] = $this->service->config->formatMoney($item->discount);
} else { } else {
$data[$key][$table_type.'.discount'] = floatval($item->discount).'%'; $data[$key][$table_type.'.discount'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->discount)).'%';
} }
} else { } else {
$data[$key][$table_type.'.discount'] = ''; $data[$key][$table_type.'.discount'] = '';
@ -749,17 +749,17 @@ class PdfBuilder
// but that's no longer necessary. // but that's no longer necessary.
if (isset($item->tax_rate1)) { if (isset($item->tax_rate1)) {
$data[$key][$table_type.'.tax_rate1'] = floatval($item->tax_rate1).'%'; $data[$key][$table_type.'.tax_rate1'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate1)).'%';
$data[$key][$table_type.'.tax1'] = &$data[$key][$table_type.'.tax_rate1']; $data[$key][$table_type.'.tax1'] = &$data[$key][$table_type.'.tax_rate1'];
} }
if (isset($item->tax_rate2)) { if (isset($item->tax_rate2)) {
$data[$key][$table_type.'.tax_rate2'] = floatval($item->tax_rate2).'%'; $data[$key][$table_type.'.tax_rate2'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate2)).'%';
$data[$key][$table_type.'.tax2'] = &$data[$key][$table_type.'.tax_rate2']; $data[$key][$table_type.'.tax2'] = &$data[$key][$table_type.'.tax_rate2'];
} }
if (isset($item->tax_rate3)) { if (isset($item->tax_rate3)) {
$data[$key][$table_type.'.tax_rate3'] = floatval($item->tax_rate3).'%'; $data[$key][$table_type.'.tax_rate3'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate3)).'%';
$data[$key][$table_type.'.tax3'] = &$data[$key][$table_type.'.tax_rate3']; $data[$key][$table_type.'.tax3'] = &$data[$key][$table_type.'.tax_rate3'];
} }

View File

@ -88,7 +88,7 @@ class SubscriptionService
// if we have a recurring product - then generate a recurring invoice // if we have a recurring product - then generate a recurring invoice
if (strlen($this->subscription->recurring_product_ids) >= 1) { if (strlen($this->subscription->recurring_product_ids ?? '') >= 1) {
if (isset($payment_hash->data->billing_context->bundle)) { if (isset($payment_hash->data->billing_context->bundle)) {
$recurring_invoice = $this->convertInvoiceToRecurringBundle($payment_hash->payment->client_id, $payment_hash->data->billing_context->bundle); $recurring_invoice = $this->convertInvoiceToRecurringBundle($payment_hash->payment->client_id, $payment_hash->data->billing_context->bundle);
} else { } else {
@ -1024,10 +1024,10 @@ class SubscriptionService
$invoice->subscription_id = $this->subscription->id; $invoice->subscription_id = $this->subscription->id;
$invoice->is_proforma = true; $invoice->is_proforma = true;
if (strlen($data['coupon']) >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { if (strlen($data['coupon'] ?? '') >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) {
$invoice->discount = $this->subscription->promo_discount; $invoice->discount = $this->subscription->promo_discount;
$invoice->is_amount_discount = $this->subscription->is_amount_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount;
} elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) {
$invoice->discount = $this->subscription->promo_discount; $invoice->discount = $this->subscription->promo_discount;
$invoice->is_amount_discount = $this->subscription->is_amount_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount;
} }
@ -1118,7 +1118,7 @@ class SubscriptionService
*/ */
public function triggerWebhook($context) public function triggerWebhook($context)
{ {
if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { //@phpstan-ignore-line if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url'] ?? '') < 1) { //@phpstan-ignore-line
return ["message" => "Success", "status_code" => 200]; return ["message" => "Success", "status_code" => 200];
} }
@ -1436,7 +1436,7 @@ class SubscriptionService
*/ */
public function handleNoPaymentFlow(Invoice $invoice, $bundle, ClientContact $contact) public function handleNoPaymentFlow(Invoice $invoice, $bundle, ClientContact $contact)
{ {
if (strlen($this->subscription->recurring_product_ids) >= 1) { if (strlen($this->subscription->recurring_product_ids ?? '') >= 1) {
$recurring_invoice = $this->convertInvoiceToRecurringBundle($contact->client_id, collect($bundle)->map(function ($bund) { $recurring_invoice = $this->convertInvoiceToRecurringBundle($contact->client_id, collect($bundle)->map(function ($bund) {
return (object) $bund; return (object) $bund;
})); }));
@ -1492,7 +1492,7 @@ class SubscriptionService
*/ */
private function handleRedirect($default_redirect) private function handleRedirect($default_redirect)
{ {
if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >= 1) { if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url'] ?? '') >= 1) {
return method_exists(redirect(), "send") ? redirect($this->subscription->webhook_configuration['return_url'])->send() : redirect($this->subscription->webhook_configuration['return_url']); return method_exists(redirect(), "send") ? redirect($this->subscription->webhook_configuration['return_url'])->send() : redirect($this->subscription->webhook_configuration['return_url']);
} }

View File

@ -557,15 +557,7 @@ class TemplateService
'reminder_last_sent' => $this->translateDate($invoice->reminder_last_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder_last_sent' => $this->translateDate($invoice->reminder_last_sent, $invoice->client->date_format(), $invoice->client->locale()),
'paid_to_date' => Number::formatMoney($invoice->paid_to_date, $invoice->client), 'paid_to_date' => Number::formatMoney($invoice->paid_to_date, $invoice->client),
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled, 'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'client' => [ 'client' => $this->getClient($invoice),
'name' => $invoice->client->present()->name(),
'balance' => $invoice->client->balance,
'payment_balance' => $invoice->client->payment_balance,
'credit_balance' => $invoice->client->credit_balance,
'vat_number' => $invoice->client->vat_number ?? '',
'currency' => $invoice->client->currency()->code ?? 'USD',
'locale' => substr($invoice->client->locale(), 0, 2),
],
'payments' => $payments, 'payments' => $payments,
'total_tax_map' => $invoice->calc()->getTotalTaxMap(), 'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
'line_tax_map' => $invoice->calc()->getTaxMap(), 'line_tax_map' => $invoice->calc()->getTaxMap(),
@ -680,14 +672,7 @@ class TemplateService
'custom_value4' => $payment->custom_value4 ?? '', 'custom_value4' => $payment->custom_value4 ?? '',
'created_at' => $this->translateDate($payment->created_at, $payment->client->date_format(), $payment->client->locale()), 'created_at' => $this->translateDate($payment->created_at, $payment->client->date_format(), $payment->client->locale()),
'updated_at' => $this->translateDate($payment->updated_at, $payment->client->date_format(), $payment->client->locale()), 'updated_at' => $this->translateDate($payment->updated_at, $payment->client->date_format(), $payment->client->locale()),
'client' => [ 'client' => $this->getClient($payment),
'name' => $payment->client->present()->name(),
'balance' => $payment->client->balance,
'payment_balance' => $payment->client->payment_balance,
'credit_balance' => $payment->client->credit_balance,
'vat_number' => $payment->client->vat_number ?? '',
'currency' => $payment->client->currency()->code ?? 'USD',
],
'paymentables' => $pivot, 'paymentables' => $pivot,
'refund_activity' => $this->getPaymentRefundActivity($payment), 'refund_activity' => $this->getPaymentRefundActivity($payment),
]; ];
@ -760,14 +745,7 @@ class TemplateService
'amount' => Number::formatMoney($quote->amount, $quote->client), 'amount' => Number::formatMoney($quote->amount, $quote->client),
'balance' => Number::formatMoney($quote->balance, $quote->client), 'balance' => Number::formatMoney($quote->balance, $quote->client),
'balance_raw' => (float) $quote->balance, 'balance_raw' => (float) $quote->balance,
'client' => [ 'client' => $this->getClient($quote),
'name' => $quote->client->present()->name(),
'balance' => $quote->client->balance,
'payment_balance' => $quote->client->payment_balance,
'credit_balance' => $quote->client->credit_balance,
'vat_number' => $quote->client->vat_number ?? '',
'currency' => $quote->client->currency()->code ?? 'USD',
],
'status_id' => $quote->status_id, 'status_id' => $quote->status_id,
'status' => Quote::stringStatus($quote->status_id), 'status' => Quote::stringStatus($quote->status_id),
'number' => $quote->number ?: '', 'number' => $quote->number ?: '',
@ -888,14 +866,7 @@ class TemplateService
'reminder_last_sent' => $this->translateDate($credit->reminder_last_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder_last_sent' => $this->translateDate($credit->reminder_last_sent, $credit->client->date_format(), $credit->client->locale()),
'paid_to_date' => Number::formatMoney($credit->paid_to_date, $credit->client), 'paid_to_date' => Number::formatMoney($credit->paid_to_date, $credit->client),
'auto_bill_enabled' => (bool) $credit->auto_bill_enabled, 'auto_bill_enabled' => (bool) $credit->auto_bill_enabled,
'client' => [ 'client' => $this->getClient($credit),
'name' => $credit->client->present()->name(),
'balance' => $credit->client->balance,
'payment_balance' => $credit->client->payment_balance,
'credit_balance' => $credit->client->credit_balance,
'vat_number' => $credit->client->vat_number ?? '',
'currency' => $credit->client->currency()->code ?? 'USD',
],
'payments' => $payments, 'payments' => $payments,
'total_tax_map' => $credit->calc()->getTotalTaxMap(), 'total_tax_map' => $credit->calc()->getTotalTaxMap(),
'line_tax_map' => $credit->calc()->getTaxMap(), 'line_tax_map' => $credit->calc()->getTaxMap(),
@ -924,6 +895,25 @@ class TemplateService
} }
private function getClient($entity): array
{
return $entity->client ? [
'name' => $entity->client->present()->name(),
'balance' => $entity->client->balance,
'payment_balance' => $entity->client->payment_balance,
'credit_balance' => $entity->client->credit_balance,
'vat_number' => $entity->client->vat_number ?? '',
'currency' => $entity->client->currency()->code ?? 'USD',
'custom_value1' => $entity->client->custom_value1 ?? '',
'custom_value2' => $entity->client->custom_value2 ?? '',
'custom_value3' => $entity->client->custom_value3 ?? '',
'custom_value4' => $entity->client->custom_value4 ?? '',
'address' => $entity->client->present()->address(),
'shipping_address' => $entity->client->present()->shipping_address(),
'locale' => substr($entity->client->locale(), 0, 2),
] : [];
}
/** /**
* @todo refactor * @todo refactor
* *
@ -953,14 +943,7 @@ class TemplateService
'custom_value4' => $task->custom_value4 ?: '', 'custom_value4' => $task->custom_value4 ?: '',
'status' => $task->status ? $task->status->name : '', 'status' => $task->status ? $task->status->name : '',
'user' => $this->userInfo($task->user), 'user' => $this->userInfo($task->user),
'client' => $task->client ? [ 'client' => $this->getClient($task),
'name' => $task->client->present()->name(),
'balance' => $task->client->balance,
'payment_balance' => $task->client->payment_balance,
'credit_balance' => $task->client->credit_balance,
'vat_number' => $task->client->vat_number ?? '',
'currency' => $task->client->currency()->code ?? 'USD',
] : [],
]; ];
@ -1015,15 +998,9 @@ class TemplateService
'color' => (string) $project->color ?: '', 'color' => (string) $project->color ?: '',
'current_hours' => (int) $project->current_hours ?: 0, 'current_hours' => (int) $project->current_hours ?: 0,
'tasks' => ($project->tasks && !$nested) ? $this->processTasks($project->tasks, true) : [], //@phpstan-ignore-line 'tasks' => ($project->tasks && !$nested) ? $this->processTasks($project->tasks, true) : [], //@phpstan-ignore-line
'client' => $project->client ? [ 'client' => $this->getClient($project),
'name' => $project->client->present()->name(), 'user' => $this->userInfo($project->user),
'balance' => $project->client->balance, 'invoices' => $this->processInvoices($project->invoices)
'payment_balance' => $project->client->payment_balance,
'credit_balance' => $project->client->credit_balance,
'vat_number' => $project->client->vat_number ?? '',
'currency' => $project->client->currency()->code ?? 'USD',
] : [],
'user' => $this->userInfo($project->user)
]; ];
} }
@ -1046,16 +1023,7 @@ class TemplateService
] : [], ] : [],
'amount' => (float)$purchase_order->amount, 'amount' => (float)$purchase_order->amount,
'balance' => (float)$purchase_order->balance, 'balance' => (float)$purchase_order->balance,
'client' => $purchase_order->client ? [ 'client' => $this->getClient($purchase_order),
'name' => $purchase_order->client->present()->name(),
'balance' => $purchase_order->client->balance,
'payment_balance' => $purchase_order->client->payment_balance,
'credit_balance' => $purchase_order->client->credit_balance,
'vat_number' => $purchase_order->client->vat_number ?? '',
'address' => $purchase_order->client->present()->address(),
'shipping_address' => $purchase_order->client->present()->shipping_address(),
'currency' => $purchase_order->client->currency()->code ?? 'USD',
] : [],
'status_id' => (string)($purchase_order->status_id ?: 1), 'status_id' => (string)($purchase_order->status_id ?: 1),
'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1), 'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1),
'is_deleted' => (bool)$purchase_order->is_deleted, 'is_deleted' => (bool)$purchase_order->is_deleted,

436
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -193,7 +193,7 @@ return [
*/ */
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class, App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class, App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class, App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class, App\Providers\RouteServiceProvider::class,
App\Providers\ComposerServiceProvider::class, App\Providers\ComposerServiceProvider::class,

View File

@ -39,7 +39,7 @@ return [
'host' => env('PUSHER_HOST', 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com'), 'host' => env('PUSHER_HOST', 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com'),
'port' => env('PUSHER_PORT', 443), 'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'), 'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true, 'encrypted' => false,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
], ],
'client_options' => [ 'client_options' => [

View File

@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.10.13'), 'app_version' => env('APP_VERSION', '5.10.16'),
'app_tag' => env('APP_TAG', '5.10.13'), 'app_tag' => env('APP_TAG', '5.10.16'),
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false), 'api_secret' => env('API_SECRET', false),

View File

@ -15,24 +15,16 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Model::unguard(); Model::unguard();
\DB::statement('SET FOREIGN_KEY_CHECKS=0;');
$record = Gateway::where('name', '=', 'Rotessa')->first();
$count = (int) Gateway::count();
if(!Gateway::find(63)) {
$configuration = new \stdClass; $configuration = new \stdClass;
$configuration->api_key = ''; $configuration->apiKey = '';
$configuration->test_mode = true; $configuration->testMode = true;
if (!$record) { $gateway = new Gateway();
$gateway = new Gateway; $gateway->id = 63;
} else {
$gateway = $record;
}
$gateway->id = 4002;
$gateway->name = 'Rotessa'; $gateway->name = 'Rotessa';
$gateway->key = Str::lower(Str::random(32)); $gateway->key = '91be24c7b792230bced33e930ac61676';
$gateway->provider = 'Rotessa'; $gateway->provider = 'Rotessa';
$gateway->is_offsite = true; $gateway->is_offsite = true;
$gateway->fields = \json_encode($configuration); $gateway->fields = \json_encode($configuration);
@ -40,17 +32,6 @@ return new class extends Migration
$gateway->site_url = "https://rotessa.com"; $gateway->site_url = "https://rotessa.com";
$gateway->default_gateway_type_id = 2; $gateway->default_gateway_type_id = 2;
$gateway->save(); $gateway->save();
Gateway::query()->where('name','=', 'Rotessa')->update(['visible' => 1]);
\DB::statement('SET FOREIGN_KEY_CHECKS=1;');
} }
/**
* Reverse the migrations.
*/
public function down(): void
{
Gateway::where('name', '=', 'Rotessa')->delete();
} }
}; };

View File

@ -0,0 +1,36 @@
<?php
use App\Models\Company;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Company::whereNotNull('tax_data')
->cursor()
->each(function($company){
if($company->tax_data?->version == 'alpha')
{
$company->update(['tax_data' => new \App\DataMapper\Tax\TaxModel($company->tax_data)]);
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -88,6 +88,7 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 60, 'name' => 'PayPal REST', 'provider' => 'PayPal_Rest', 'key' => '80af24a6a691230bbec33e930ab40665', 'fields' => '{"clientId":"","secret":"","signature":"","testMode":false}'], ['id' => 60, 'name' => 'PayPal REST', 'provider' => 'PayPal_Rest', 'key' => '80af24a6a691230bbec33e930ab40665', 'fields' => '{"clientId":"","secret":"","signature":"","testMode":false}'],
['id' => 61, 'name' => 'PayPal Platform', 'provider' => 'PayPal_PPCP', 'key' => '80af24a6a691230bbec33e930ab40666', 'fields' => '{"testMode":false}'], ['id' => 61, 'name' => 'PayPal Platform', 'provider' => 'PayPal_PPCP', 'key' => '80af24a6a691230bbec33e930ab40666', 'fields' => '{"testMode":false}'],
['id' => 62, 'name' => 'BTCPay', 'provider' => 'BTCPay', 'key' => 'vpyfbmdrkqcicpkjqdusgjfluebftuva', 'fields' => '{"btcpayUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'], ['id' => 62, 'name' => 'BTCPay', 'provider' => 'BTCPay', 'key' => 'vpyfbmdrkqcicpkjqdusgjfluebftuva', 'fields' => '{"btcpayUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'],
['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":""}'],
]; ];
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {
@ -104,7 +105,7 @@ class PaymentLibrariesSeeder extends Seeder
Gateway::query()->update(['visible' => 0]); Gateway::query()->update(['visible' => 0]);
Gateway::whereIn('id', [1, 3, 7, 11, 15, 20, 39, 46, 55, 50, 57, 52, 58, 59, 60, 62])->update(['visible' => 1]); Gateway::whereIn('id', [1, 3, 7, 11, 15, 20, 39, 46, 55, 50, 57, 52, 58, 59, 60, 62, 63])->update(['visible' => 1]);
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
Gateway::whereIn('id', [20, 49])->update(['visible' => 0]); Gateway::whereIn('id', [20, 49])->update(['visible' => 0]);

View File

@ -5300,7 +5300,7 @@ $lang = array(
'merge_to_pdf' => 'Merge to PDF', 'merge_to_pdf' => 'Merge to PDF',
'latest_requires_php_version' => 'Note: the latest version requires PHP :version', 'latest_requires_php_version' => 'Note: the latest version requires PHP :version',
'auto_expand_product_table_notes' => 'Automatically expand products table notes', 'auto_expand_product_table_notes' => 'Automatically expand products table notes',
'auto_expand_product_table_notes_help' => 'Automatically expands the notes section within the products table to display more lines.', 'auto_expand_product_table_notes_help' => 'Automatically expands the notes section within the products table to display more lines.'
); );
return $lang; return $lang;

View File

@ -5124,7 +5124,7 @@ $lang = array(
'all_contacts' => 'All Contacts', 'all_contacts' => 'All Contacts',
'insert_below' => 'Insert Below', 'insert_below' => 'Insert Below',
'nordigen_handler_subtitle' => 'Bank account authentication. Selecting your institution to complete the request with your account credentials.', 'nordigen_handler_subtitle' => 'Bank account authentication. Selecting your institution to complete the request with your account credentials.',
'nordigen_handler_error_heading_unknown' => 'An error has occured', 'nordigen_handler_error_heading_unknown' => 'An error has occurred',
'nordigen_handler_error_contents_unknown' => 'An unknown error has occurred! Reason:', 'nordigen_handler_error_contents_unknown' => 'An unknown error has occurred! Reason:',
'nordigen_handler_error_heading_token_invalid' => 'Invalid Token', 'nordigen_handler_error_heading_token_invalid' => 'Invalid Token',
'nordigen_handler_error_contents_token_invalid' => 'The provided token was invalid. Contact support for help, if this issue persists.', 'nordigen_handler_error_contents_token_invalid' => 'The provided token was invalid. Contact support for help, if this issue persists.',
@ -5301,6 +5301,15 @@ $lang = array(
'latest_requires_php_version' => 'Note: the latest version requires PHP :version', 'latest_requires_php_version' => 'Note: the latest version requires PHP :version',
'auto_expand_product_table_notes' => 'Automatically expand products table notes', 'auto_expand_product_table_notes' => 'Automatically expand products table notes',
'auto_expand_product_table_notes_help' => 'Automatically expands the notes section within the products table to display more lines.', 'auto_expand_product_table_notes_help' => 'Automatically expands the notes section within the products table to display more lines.',
'institution_number' => 'Institution Number',
'transit_number' => 'Transit Number',
'personal' => 'Personal',
'address_information' => 'Address Information',
'enter_the_information_for_the_bank_account' => 'Enter the Information for the Bank Account',
'account_holder_information' => 'Account Holder Information',
'enter_information_for_the_account_holder' => 'Enter Information for the Account Holder',
'customer_type' => 'Customer Type',
'process_date' => 'Process Date'
); );
return $lang; return $lang;

View File

@ -5298,7 +5298,7 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'latest_requires_php_version' => 'Note: La dernière version requiert PHP :version', 'latest_requires_php_version' => 'Note: La dernière version requiert PHP :version',
'auto_expand_product_table_notes' => 'Développer automatiquement les notes du tableau de produits', 'auto_expand_product_table_notes' => 'Développer automatiquement les notes du tableau de produits',
'auto_expand_product_table_notes_help' => '  'auto_expand_product_table_notes_help' => ' 
Développe automatiquement la section des notes dans le tableau de produits pour afficher plus de lignes.', Développe automatiquement la section des notes dans le tableau de produits pour afficher plus de lignes.'
); );
return $lang; return $lang;

File diff suppressed because one or more lines are too long

1
public/build/assets/app-039bd735.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

109
public/build/assets/app-e0713224.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,9 +0,0 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/class s{constructor(t,e,a){this.shouldDisplayTerms=t,this.shouldDisplaySignature=e,this.shouldDisplayRff=a,this.submitting=!1,this.steps=new Map,this.shouldDisplayRff&&this.steps.set("rff",{element:document.getElementById("displayRequiredFieldsModal"),nextButton:document.getElementById("rff-next-step"),callback:()=>{const n={firstName:document.querySelector('input[name="rff_first_name"]'),lastName:document.querySelector('input[name="rff_last_name"]'),email:document.querySelector('input[name="rff_email"]')};n.firstName&&(document.querySelector('input[name="contact_first_name"]').value=n.firstName.value),n.lastName&&(document.querySelector('input[name="contact_last_name"]').value=n.lastName.value),n.email&&(document.querySelector('input[name="contact_email"]').value=n.email.value)}}),this.shouldDisplaySignature&&this.steps.set("signature",{element:document.getElementById("displaySignatureModal"),nextButton:document.getElementById("signature-next-step"),boot:()=>this.signaturePad=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"}),callback:()=>document.querySelector('input[name="signature"').value=this.signaturePad.toDataURL()}),this.shouldDisplayTerms&&this.steps.set("terms",{element:document.getElementById("displayTermsModal"),nextButton:document.getElementById("accept-terms-button")})}handleMethodSelect(t){if(document.getElementById("company_gateway_id").value=t.dataset.companyGatewayId,document.getElementById("payment_method_id").value=t.dataset.gatewayTypeId,this.steps.size===0)return this.submitForm();const e=this.steps.values().next().value;e.element.removeAttribute("style"),e.boot&&e.boot(),console.log(e),e.nextButton.addEventListener("click",()=>{e.element.setAttribute("style","display: none;"),this.steps=new Map(Array.from(this.steps.entries()).slice(1)),e.callback&&e.callback(),this.handleMethodSelect(t)})}submitForm(){this.submitting=!0,document.getElementById("payment-form").submit()}handle(){document.querySelectorAll(".dropdown-gateway-button").forEach(t=>{t.addEventListener("click",()=>{this.submitting||this.handleMethodSelect(t)})})}}const i=document.querySelector('meta[name="require-invoice-signature"]').content,o=document.querySelector('meta[name="show-invoice-terms"]').content,l=document.querySelector('meta[name="show-required-fields-form"]').content;new s(!!+o,!!+i,!!+l).handle();

View File

@ -0,0 +1,9 @@
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/class a{constructor(t,n){this.shouldDisplayTerms=t,this.shouldDisplaySignature=n,this.submitting=!1,this.steps=new Map,this.steps.set("rff",{element:document.getElementById("displayRequiredFieldsModal"),nextButton:document.getElementById("rff-next-step"),callback:()=>{const e={firstName:document.querySelector('input[name="rff_first_name"]'),lastName:document.querySelector('input[name="rff_last_name"]'),email:document.querySelector('input[name="rff_email"]'),city:document.querySelector('input[name="rff_city"]'),postalCode:document.querySelector('input[name="rff_postal_code"]')};e.firstName&&(document.querySelector('input[name="contact_first_name"]').value=e.firstName.value),e.lastName&&(document.querySelector('input[name="contact_last_name"]').value=e.lastName.value),e.email&&(document.querySelector('input[name="contact_email"]').value=e.email.value),e.city&&(document.querySelector('input[name="client_city"]').value=e.city.value),e.postalCode&&(document.querySelector('input[name="client_postal_code"]').value=e.postalCode.value)}}),this.shouldDisplaySignature&&this.steps.set("signature",{element:document.getElementById("displaySignatureModal"),nextButton:document.getElementById("signature-next-step"),boot:()=>this.signaturePad=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"}),callback:()=>document.querySelector('input[name="signature"').value=this.signaturePad.toDataURL()}),this.shouldDisplayTerms&&this.steps.set("terms",{element:document.getElementById("displayTermsModal"),nextButton:document.getElementById("accept-terms-button")})}handleMethodSelect(t){document.getElementById("company_gateway_id").value=t.dataset.companyGatewayId,document.getElementById("payment_method_id").value=t.dataset.gatewayTypeId;const n=document.querySelector('input[name="contact_first_name"').value.length>=1&&document.querySelector('input[name="contact_last_name"').value.length>=1&&document.querySelector('input[name="contact_email"').value.length>=1&&document.querySelector('input[name="client_city"').value.length>=1&&document.querySelector('input[name="client_postal_code"').value.length>=1;if((t.dataset.isPaypal!="1"||n)&&this.steps.delete("rff"),this.steps.size===0)return this.submitForm();const e=this.steps.values().next().value;e.element.removeAttribute("style"),e.boot&&e.boot(),console.log(e),e.nextButton.addEventListener("click",()=>{e.element.setAttribute("style","display: none;"),this.steps=new Map(Array.from(this.steps.entries()).slice(1)),e.callback&&e.callback(),this.handleMethodSelect(t)})}submitForm(){this.submitting=!0,document.getElementById("payment-form").submit()}handle(){document.querySelectorAll(".dropdown-gateway-button").forEach(t=>{t.addEventListener("click",()=>{this.submitting||this.handleMethodSelect(t)})})}}const l=document.querySelector('meta[name="require-invoice-signature"]').content,o=document.querySelector('meta[name="show-invoice-terms"]').content;new a(!!+o,!!+l).handle();

View File

@ -9,7 +9,7 @@
] ]
}, },
"resources/js/app.js": { "resources/js/app.js": {
"file": "assets/app-234e3402.js", "file": "assets/app-e0713224.js",
"imports": [ "imports": [
"_index-08e160a7.js", "_index-08e160a7.js",
"__commonjsHelpers-725317a4.js" "__commonjsHelpers-725317a4.js"
@ -23,7 +23,7 @@
"src": "resources/js/clients/invoices/action-selectors.js" "src": "resources/js/clients/invoices/action-selectors.js"
}, },
"resources/js/clients/invoices/payment.js": { "resources/js/clients/invoices/payment.js": {
"file": "assets/payment-1bdbd169.js", "file": "assets/payment-292ee4d0.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/clients/invoices/payment.js" "src": "resources/js/clients/invoices/payment.js"
}, },
@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js" "src": "resources/js/setup/setup.js"
}, },
"resources/sass/app.scss": { "resources/sass/app.scss": {
"file": "assets/app-f3b33400.css", "file": "assets/app-039bd735.css",
"isEntry": true, "isEntry": true,
"src": "resources/sass/app.scss" "src": "resources/sass/app.scss"
} }

View File

@ -9,15 +9,13 @@
*/ */
class Payment { class Payment {
constructor(displayTerms, displaySignature, displayRff) { constructor(displayTerms, displaySignature) {
this.shouldDisplayTerms = displayTerms; this.shouldDisplayTerms = displayTerms;
this.shouldDisplaySignature = displaySignature; this.shouldDisplaySignature = displaySignature;
this.shouldDisplayRff = displayRff;
this.submitting = false; this.submitting = false;
this.steps = new Map() this.steps = new Map()
if (this.shouldDisplayRff) {
this.steps.set("rff", { this.steps.set("rff", {
element: document.getElementById('displayRequiredFieldsModal'), element: document.getElementById('displayRequiredFieldsModal'),
nextButton: document.getElementById('rff-next-step'), nextButton: document.getElementById('rff-next-step'),
@ -26,6 +24,8 @@ class Payment {
firstName: document.querySelector('input[name="rff_first_name"]'), firstName: document.querySelector('input[name="rff_first_name"]'),
lastName: document.querySelector('input[name="rff_last_name"]'), lastName: document.querySelector('input[name="rff_last_name"]'),
email: document.querySelector('input[name="rff_email"]'), email: document.querySelector('input[name="rff_email"]'),
city: document.querySelector('input[name="rff_city"]'),
postalCode: document.querySelector('input[name="rff_postal_code"]'),
} }
if (fields.firstName) { if (fields.firstName) {
@ -39,9 +39,17 @@ class Payment {
if (fields.email) { if (fields.email) {
document.querySelector('input[name="contact_email"]').value = fields.email.value; document.querySelector('input[name="contact_email"]').value = fields.email.value;
} }
if (fields.city) {
document.querySelector('input[name="client_city"]').value = fields.city.value;
}
if (fields.postalCode) {
document.querySelector('input[name="client_postal_code"]').value = fields.postalCode.value;
}
} }
}); });
}
if (this.shouldDisplaySignature) { if (this.shouldDisplaySignature) {
this.steps.set("signature", { this.steps.set("signature", {
@ -72,6 +80,16 @@ class Payment {
document.getElementById("payment_method_id").value = document.getElementById("payment_method_id").value =
element.dataset.gatewayTypeId; element.dataset.gatewayTypeId;
const filledRff = document.querySelector('input[name="contact_first_name"').value.length >=1 &&
document.querySelector('input[name="contact_last_name"').value.length >= 1 &&
document.querySelector('input[name="contact_email"').value.length >= 1 &&
document.querySelector('input[name="client_city"').value.length >= 1 &&
document.querySelector('input[name="client_postal_code"').value.length >= 1;
if (element.dataset.isPaypal != '1' || filledRff) {
this.steps.delete("rff");
}
if (this.steps.size === 0) { if (this.steps.size === 0) {
return this.submitForm(); return this.submitForm();
} }
@ -124,6 +142,5 @@ const signature = document.querySelector(
).content; ).content;
const terms = document.querySelector('meta[name="show-invoice-terms"]').content; const terms = document.querySelector('meta[name="show-invoice-terms"]').content;
const rff = document.querySelector('meta[name="show-required-fields-form"]').content;
new Payment(Boolean(+terms), Boolean(+signature), Boolean(+rff)).handle(); new Payment(Boolean(+terms), Boolean(+signature)).handle();

View File

@ -141,12 +141,15 @@
<input type="hidden" name="contact_first_name" value="{{ $contact->first_name }}"> <input type="hidden" name="contact_first_name" value="{{ $contact->first_name }}">
<input type="hidden" name="contact_last_name" value="{{ $contact->last_name }}"> <input type="hidden" name="contact_last_name" value="{{ $contact->last_name }}">
<input type="hidden" name="contact_email" value="{{ $contact->email }}"> <input type="hidden" name="contact_email" value="{{ $contact->email }}">
<input type="hidden" name="client_city" value="{{ $contact->client->city }}">
<input type="hidden" name="client_postal_code" value="{{ $contact->client->postal_code }}">
</form> </form>
@if($steps['started_payment'] == false) @if($steps['started_payment'] == false)
@foreach($this->methods as $method) @foreach($this->methods as $method)
<button <button
wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}'); $wire.$refresh(); " wire:click="handleMethodSelectingEvent('{{ $method['company_gateway_id'] }}', '{{ $method['gateway_type_id'] }}', '{{ $method['is_paypal'] }}'); $wire.$refresh(); "
class="px-3 py-2 border rounded mr-4 hover:border-blue-600"> class="px-3 py-2 border rounded mr-4 hover:border-blue-600">
{{ $method['label'] }} {{ $method['label'] }}
</button> </button>
@ -189,27 +192,41 @@
<form wire:submit="handleRff"> <form wire:submit="handleRff">
@csrf @csrf
@if(strlen($contact->first_name) === 0) @if(strlen($contact->first_name ?? '') === 0)
<div class="col-auto mt-3"> <div class="col-auto mt-3">
<label for="first_name" class="input-label">{{ ctrans('texts.first_name') }}</label> <label for="first_name" class="input-label">{{ ctrans('texts.first_name') }}</label>
<input id="first_name" class="input w-full" wire:model="contact_first_name" /> <input id="first_name" class="input w-full" wire:model="contact_first_name" />
</div> </div>
@endif @endif
@if(strlen($contact->last_name) === 0) @if(strlen($contact->last_name ?? '') === 0)
<div class="col-auto mt-3 @if($contact->last_name) !== 0) hidden @endif"> <div class="col-auto mt-3 @if($contact->last_name) !== 0) hidden @endif">
<label for="last_name" class="input-label">{{ ctrans('texts.last_name') }}</label> <label for="last_name" class="input-label">{{ ctrans('texts.last_name') }}</label>
<input id="last_name" class="input w-full" wire:model="contact_last_name" /> <input id="last_name" class="input w-full" wire:model="contact_last_name" />
</div> </div>
@endif @endif
@if(strlen($contact->email) === 0) @if(strlen($contact->email ?? '') === 0)
<div class="col-auto mt-3 @if($contact->email) !== 0) hidden @endif"> <div class="col-auto mt-3 @if($contact->email) !== 0) hidden @endif">
<label for="email" class="input-label">{{ ctrans('texts.email') }}</label> <label for="email" class="input-label">{{ ctrans('texts.email') }}</label>
<input id="email" class="input w-full" wire:model="contact_email" /> <input id="email" class="input w-full" wire:model="contact_email" />
</div> </div>
@endif @endif
@if(strlen($client_postal_code ?? '') === 0)
<div class="col-auto mt-3 @if($client_postal_code) !== 0) hidden @endif">
<label for="postal_code" class="input-label">{{ ctrans('texts.postal_code') }}</label>
<input id="postal_code" class="input w-full" wire:model="client_postal_code" />
</div>
@endif
@if(strlen($client_city ?? '') === 0)
<div class="col-auto mt-3 @if($client_city) !== 0) hidden @endif">
<label for="city" class="input-label">{{ ctrans('texts.city') }}</label>
<input id="city" class="input w-full" wire:model="client_city" />
</div>
@endif
<button <button
type="submit" type="submit"
class="button button-block bg-primary text-white mt-4"> class="button button-block bg-primary text-white mt-4">

Some files were not shown because too many files have changed in this diff Show More