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
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
PDF_GENERATOR=hosted_ninja
PDF_GENERATOR=snappdf

View File

@ -38,6 +38,9 @@ jobs:
sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \;
- name: Set current date to variable
id: set_date
run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV
- name: Prepare React FrontEnd
run: |
@ -46,10 +49,11 @@ jobs:
git checkout develop
cp .env.example .env
cp ../vite.config.ts.react ./vite.config.js
sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json
npm i
npm run build
cp -r dist/* ../public/
mv dist/index.html ../resources/views/react/index.blade.php
mv ../public/index.html ../resources/views/react/index.blade.php
- name: Prepare JS/CSS assets
run: |

View File

@ -1,5 +1,5 @@
<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>
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
@ -8,25 +8,30 @@
# Invoice Ninja 5
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org)
Invoice Ninja Version 5 is here! We've taken the best parts of version 4 and added the most requested features to create an invoicing application like no other. Check the [Invoice Ninja YouTube Channel](https://www.youtube.com/@appinvoiceninja) to get up to speed, or try the [Demo](https://react.invoicing.co/demo) now.
Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com)
**Choose your setup**
## Introduction
- [Hosted](https://www.invoiceninja.com): Our hosted version is a Software as a Service (SaaS) solution. You're up and running in under 5 minutes, with no need to worry about hosting or server infrastructure.
- [Self-Hosted](https://www.invoiceninja.org): For those who prefer to manage their own hosting and server infrastructure. This version gives you full control and flexibility.
Version 5 of Invoice Ninja is here!
We took the best parts of version 4 and add the most requested features
to produce a invoicing application like no other.
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client-facing parts of the app.
All Pro and Enterprise features from the hosted app are included in the open code.
We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
#### Get social with us
* [Videos](https://www.youtube.com/@appinvoiceninja)
* [API Documentation](https://api-docs.invoicing.co/)
* [APP Documentation](https://invoiceninja.github.io/)
* [Support Forum](https://forum.invoiceninja.com)
* [Slack](http://slack.invoiceninja.com)
* [Discord](https://discord.gg/ZwEdtfCwXA)
* [Instagram](https://www.instagram.com/appinvoiceninja)
## Setup
#### Documentation
* [Invoice Ninja - API](https://api-docs.invoicing.co/)
* [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/)
* [Invoice Ninja - User Guide](https://invoiceninja.github.io/en/user-guide/)
* [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/)
## Installation Options and Clients
### Mobile Apps
* [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone)
@ -39,16 +44,21 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
* [Linux - Snap](https://snapcraft.io/invoiceninja)
* [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja)
### Installation Options
### Self-Hosted Server Installation
**Note:** The self-hosted options do support the desktop and mobile apps.
* [Server or VM](https://invoiceninja.github.io/en/self-host-installation/)
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html)
* [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html)
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
### Recommended Providers
* [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/)
## Quick Hosting Setup
## [Advanced] Quick Hosting Setup
In addition to the official [Invoice Ninja - Self-Hosted Installation Guide](https://invoiceninja.github.io/en/self-host-installation/) we have a few commands for you.
```sh
git clone --single-branch --branch v5-stable https://github.com/invoiceninja/invoiceninja.git
@ -84,6 +94,7 @@ pass: password
```
## Developers Guide
In addition to the official [Invoice Ninja - Developer Guide](https://invoiceninja.github.io/en/developer-guide/) we've got your back with some insights.
### App Design

View File

@ -1 +1 @@
5.10.13
5.10.16

View File

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

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';
/** @var string $version */
public string $version = 'alpha';
public string $version = 'beta';
/** @var object $regions */
public object $regions;
@ -28,15 +28,38 @@ class TaxModel
* @param TaxModel $model
* @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();
} else {
$this->regions = $model;
//@phpstan-ignore-next-line
foreach($model as $key => $value) {
$this->{$key} = $value;
}
}
$this->migrate();
}
public function migrate(): self
{
if($this->version == 'alpha')
{
$this->regions->EU->subregions->PL = new \stdClass();
$this->regions->EU->subregions->PL->tax_rate = 23;
$this->regions->EU->subregions->PL->tax_name = 'VAT';
$this->regions->EU->subregions->PL->reduced_tax_rate = 8;
$this->regions->EU->subregions->PL->apply_tax = false;
$this->version = 'beta';
}
return $this;
}
/**
@ -474,6 +497,12 @@ class TaxModel
$this->regions->EU->subregions->NL->reduced_tax_rate = 9;
$this->regions->EU->subregions->NL->apply_tax = false;
$this->regions->EU->subregions->PL = new \stdClass();
$this->regions->EU->subregions->PL->tax_rate = 23;
$this->regions->EU->subregions->PL->tax_name = 'VAT';
$this->regions->EU->subregions->PL->reduced_tax_rate = 8;
$this->regions->EU->subregions->PL->apply_tax = false;
$this->regions->EU->subregions->PT = new \stdClass();
$this->regions->EU->subregions->PT->tax_rate = 23;
$this->regions->EU->subregions->PT->tax_name = 'IVA';

View File

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

View File

@ -13,15 +13,21 @@ namespace App\Events\Client;
use App\Models\Client;
use App\Models\Company;
use League\Fractal\Manager;
use League\Fractal\Resource\Item;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use App\Transformers\ArraySerializer;
use Illuminate\Queue\SerializesModels;
use App\Transformers\ClientTransformer;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
/**
* Class ClientWasArchived.
*/
class ClientWasArchived
class ClientWasArchived implements ShouldBroadcast
{
use Dispatchable;
use InteractsWithSockets;
@ -50,13 +56,34 @@ class ClientWasArchived
$this->event_vars = $event_vars;
}
// /**
// * Get the channels the event should broadcast on.
// *
// * @return Channel|array
// */
public function broadcastWith()
{
$manager = new Manager();
$manager->setSerializer(new ArraySerializer());
$class = sprintf('App\\Transformers\\%sTransformer', class_basename($this->client));
$transformer = new $class();
$resource = new Item($this->client, $transformer, $this->client->getEntityType());
$data = $manager->createData($resource)->toArray();
return $data;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn()
{
return [];
return [
new PrivateChannel("company-{$this->company->company_key}"),
];
}
}

View File

@ -172,6 +172,7 @@ class BaseExport
'tax_rate3' => 'invoice.tax_rate3',
'recurring_invoice' => 'invoice.recurring_id',
'auto_bill' => 'invoice.auto_bill_enabled',
'project' => 'invoice.project',
];
protected array $recurring_invoice_report_keys = [
@ -449,6 +450,7 @@ class BaseExport
'status' => 'task.status_id',
'project' => 'task.project_id',
'billable' => 'task.billable',
'item_notes' => 'task.item_notes',
];
protected array $forced_client_fields = [
@ -1038,6 +1040,10 @@ class BaseExport
$recurring_filters = [];
if($this->company->getSetting('report_include_drafts')){
$recurring_filters[] = RecurringInvoice::STATUS_DRAFT;
}
if (in_array('active', $status_parameters)) {
$recurring_filters[] = RecurringInvoice::STATUS_ACTIVE;
}
@ -1252,7 +1258,7 @@ class BaseExport
$date_range = $this->input['date_range'];
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) {
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key'] ?? '') > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) {
$this->date_key = $this->input['date_key'];
}
@ -1263,7 +1269,7 @@ class BaseExport
$custom_start_date = now()->startOfYear();
$custom_end_date = now();
}
switch ($date_range) {
case 'all':
$this->start_date = 'All available data';

View File

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

View File

@ -265,6 +265,10 @@ class InvoiceItemExport extends BaseExport
$entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line
}
if (in_array('invoice.project', $this->input['report_keys'])) {
$entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';// @phpstan-ignore-line
}
return $entity;
}

View File

@ -29,7 +29,7 @@ class TaskExport extends BaseExport
{
private $entity_transformer;
public string $date_key = 'created_at';
public string $date_key = 'calculated_start_date';
private string $date_format = 'Y-m-d';
@ -156,7 +156,7 @@ class TaskExport extends BaseExport
$entity[$key] = $transformed_entity[$parts[1]];
} elseif (array_key_exists($key, $transformed_entity)) {
$entity[$key] = $transformed_entity[$key];
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration'])) {
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) {
$entity[$key] = '';
} else {
$entity[$key] = $this->decorator->transform($key, $task);
@ -175,7 +175,7 @@ class TaskExport extends BaseExport
private function iterateLogs(Task $task, array $entity)
{
$timezone = Timezone::find($task->company->settings->timezone_id);
$timezone_name = 'US/Eastern';
$timezone_name = 'America/New_York';
if ($timezone) {
$timezone_name = $timezone->name;
@ -209,6 +209,14 @@ class TaskExport extends BaseExport
$entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s');
}
if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) {
$entity['task.billable'] = isset($item[3]) && $item[3] == 'true' ? ctrans('texts.yes') : ctrans('texts.no');
}
if (in_array('task.item_notes', $this->input['report_keys']) || in_array('item_notes', $this->input['report_keys'])) {
$entity['task.item_notes'] = isset($item[2]) ? (string)$item[2] : '';
}
$entity = $this->decorateAdvancedFields($task, $entity);
$this->storage_array[] = $entity;
@ -219,6 +227,8 @@ class TaskExport extends BaseExport
$entity['task.end_time'] = '';
$entity['task.duration'] = '';
$entity['task.duration_words'] = '';
$entity['task.billable'] = '';
$entity['task.item_notes'] = '';
}

View File

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

View File

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

View File

@ -155,11 +155,13 @@ class BankTransactionFilters extends QueryFilters
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'deposit') {
return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir);
return $this->builder->orderByRaw("(CASE WHEN base_type = 'CREDIT' THEN amount END) $dir")->orderBy('amount', $dir);
// return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir);
}
if ($sort_col[0] == 'withdrawal') {
return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir);
return $this->builder->orderByRaw("(CASE WHEN base_type = 'DEBIT' THEN amount END) $dir")->orderBy('amount', $dir);
// return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir);
}
if ($sort_col[0] == 'status') {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,12 +11,14 @@
namespace App\Helpers\Invoice;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\RecurringQuote;
use App\Models\Vendor;
use App\Utils\Number;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Support\Collection;
@ -50,6 +52,8 @@ class InvoiceSum
private $precision;
private Client | Vendor $client;
public InvoiceItemSum $invoice_items;
private $rappen_rounding = false;
@ -60,18 +64,15 @@ class InvoiceSum
*/
public function __construct($invoice)
{
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->precision = $this->invoice->client->currency()->precision;
$this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding');
} else {
$this->precision = $this->invoice->vendor->currency()->precision;
$this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding');
}
$this->precision = $this->client->currency()->precision;
$this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding');
$this->tax_map = new Collection();
}
public function build()
@ -131,7 +132,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax];
}
if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) {
@ -139,7 +140,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax];
}
if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) >= 2) {
@ -147,7 +148,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax];
}
return $this;

View File

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

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);
}
public function calculatedField(ShowCalculatedFieldRequest $request)
public function calculatedFields(ShowCalculatedFieldRequest $request)
{
/** @var \App\Models\User auth()->user() */

View File

@ -300,7 +300,9 @@ class InvitationController extends Controller
'signature' => false,
'contact_first_name' => $invitation->contact->first_name ?? '',
'contact_last_name' => $invitation->contact->last_name ?? '',
'contact_email' => $invitation->contact->email ?? ''
'contact_email' => $invitation->contact->email ?? '',
'client_city' => $invitation->client->city ?? '',
'client_postal_code' => $invitation->client->postal_code ?? '',
];
$request->replace($data);

View File

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

View File

@ -85,7 +85,7 @@ class ImportController extends Controller
$contents = $this->convertEncoding($contents);
// Store the csv in cache with an expiry of 10 minutes
Cache::put($hash.'-'.$entityType, base64_encode($contents), 600);
Cache::put($hash.'-'.$entityType, base64_encode($contents), 1200);
// Parse CSV
$csv_array = $this->getCsvData($contents);

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'])) {
ProcessMailgunWebhook::dispatch($request->all())->delay(10);
ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2,10));
}
return response()->json(['message' => 'Success.'], 200);

View File

@ -64,6 +64,9 @@ class StoreCreditRequest extends Request
$user = auth()->user();
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id;
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
// $rules['number'] = new UniqueCreditNumberRule($this->all());
$rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', $user->company()->id)];

View File

@ -65,6 +65,9 @@ class UpdateCreditRequest extends Request
$rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('credits')->where('company_id', $user->company()->id)->ignore($this->credit->id)];
$rules['client_id'] = ['bail', 'sometimes',Rule::in([$this->credit->client_id])];
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$rules['line_items'] = 'array';

View File

@ -38,11 +38,14 @@ class StoreInvoiceRequest extends Request
public function rules()
{
$rules = [];
/** @var \App\Models\User $user */
$user = auth()->user();
$rules = [];
$rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) {
@ -57,16 +60,16 @@ class StoreInvoiceRequest extends Request
$rules['file'] = $this->fileValidation();
}
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['invitations.*.client_contact_id'] = 'distinct';
$rules['number'] = ['bail', 'nullable', Rule::unique('invoices')->where('company_id', $user->company()->id)];
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['is_amount_discount'] = ['boolean'];
$rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric|max:99999999999999';
@ -79,18 +82,17 @@ class StoreInvoiceRequest extends Request
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0';
$rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
// $rules['amount'] = ['sometimes', 'bail', 'max:99999999999999'];
// $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules;
}
public function prepareForValidation()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
@ -102,24 +104,24 @@ class StoreInvoiceRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = $this->entityTotalAmount($input['line_items']);
}
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (array_key_exists('tax_rate1', $input) && is_null($input['tax_rate1'])) {
}
if (!isset($input['tax_rate1'])) {
$input['tax_rate1'] = 0;
}
if (array_key_exists('tax_rate2', $input) && is_null($input['tax_rate2'])) {
if (!isset($input['tax_rate2'])) {
$input['tax_rate2'] = 0;
}
if (array_key_exists('tax_rate3', $input) && is_null($input['tax_rate3'])) {
if (!isset($input['tax_rate3'])) {
$input['tax_rate3'] = 0;
}
if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) {
$input['exchange_rate'] = 1;
}
if(!isset($input['date'])) {
$input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
}
//handles edge case where we need for force set the due date of the invoice.
if((isset($input['partial_due_date']) && strlen($input['partial_due_date']) > 1) && (!array_key_exists('due_date', $input) || (empty($input['due_date']) && empty($this->invoice->due_date)))) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']);

View File

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

View File

@ -50,6 +50,10 @@ class StorePurchaseOrderRequest extends Request
$rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', $user->company()->id)];
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.vendor_contact_id'] = 'bail|required|distinct';
$rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['is_amount_discount'] = ['boolean'];
$rules['line_items'] = 'array';

View File

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

View File

@ -11,12 +11,13 @@
namespace App\Http\Requests\Quote;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule;
use App\Models\Quote;
use App\Utils\Traits\CleanLineItems;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
use App\Utils\Traits\CleanLineItems;
use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule;
use App\Http\ValidationRules\Project\ValidProjectForClient;
class StoreQuoteRequest extends Request
{
@ -43,7 +44,7 @@ class StoreQuoteRequest extends Request
$rules = [];
$rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)];
$rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted',0)];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
@ -59,15 +60,28 @@ class StoreQuoteRequest extends Request
$rules['file'] = $this->fileValidation();
}
$rules['number'] = ['nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)];
$rules['number'] = ['bail','nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)];
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['is_amount_discount'] = ['boolean'];
$rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['is_amount_discount'] = ['boolean'];
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['line_items'] = 'array';
$rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0';
$rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date'];
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
return $rules;
@ -89,19 +103,24 @@ class StoreQuoteRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
$input['amount'] = $this->entityTotalAmount($input['line_items']);
}
if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) {
$input['exchange_rate'] = 1;
}
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (!isset($input['tax_rate1'])) {
$input['tax_rate1'] = 0;
}
if (!isset($input['tax_rate2'])) {
$input['tax_rate2'] = 0;
}
if (!isset($input['tax_rate3'])) {
$input['tax_rate3'] = 0;
}
if (!isset($input['exchange_rate'])) {
$input['exchange_rate'] = 1;
}
if(!isset($input['date'])) {
$input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
}
if(isset($input['partial_due_date']) && (!isset($input['due_date']) || strlen($input['due_date']) <= 1)) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']);
$valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7;

View File

@ -55,6 +55,9 @@ class UpdateQuoteRequest extends Request
} elseif ($this->file('file')) {
$rules['file'] = $this->fileValidation();
}
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)];
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])];

View File

@ -61,7 +61,8 @@ class StoreRecurringInvoiceRequest extends Request
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id;
$rules['invitations.*.client_contact_id'] = 'distinct';
$rules['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$rules['frequency_id'] = 'required|integer|digits_between:1,12';

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['invitations'] = 'sometimes|bail|array';
$rules['invitations.*.client_contact_id'] = 'bail|required|distinct';
$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
{
/** Bad domains +/- dispoable email domains */
/** Bad domains +/- disposable email domains */
private array $blacklist = [
'padvn.com',
'anonaddy.me',
'nqmo.com',
'wireconnected.com',

View File

@ -42,6 +42,7 @@ class AccountComponent extends Component
public function render()
{
return render('gateways.rotessa.components.account', array_merge($this->attributes->getAttributes(), $this->defaults) );
return render('gateways.rotessa.components.account', $this->attributes->getAttributes() + $this->defaults);
}
}

View File

@ -43,6 +43,6 @@ class AddressComponent extends Component
public function render()
{
return render('gateways.rotessa.components.address',array_merge( $this->defaults, $this->attributes->getAttributes() ) );
return render('gateways.rotessa.components.address', $this->attributes->getAttributes() + $this->defaults );
}
}

View File

@ -15,11 +15,12 @@ class ContactComponent extends Component
{
public function __construct(ClientContact $contact) {
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' =>$contact->client->phone,
'custom_identifier' => $contact->client->number,
'name' =>$contact->client->name,
'id' => null
'id' => $contact->client->contact_key,
] )->all();
$this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields) );
@ -37,12 +38,13 @@ class ContactComponent extends Component
private $defaults = [
'customer_type' => "Business",
'customer_identifier' => null,
'id' => null
'custom_identifier' => null,
'customer_id' => null
];
public function render()
{
return render('gateways.rotessa.components.contact', array_merge($this->defaults, $this->attributes->getAttributes() ) );
\Debugbar::debug($this->attributes->getAttributes() + $this->defaults);
return render('gateways.rotessa.components.contact', $this->attributes->getAttributes() + $this->defaults );
}
}

View File

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

View File

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

View File

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

View File

@ -36,18 +36,26 @@ class ExpenseTransformer extends BaseTransformer
$total_tax += floatval($record['Sales Tax Amount']);
}
$tax_rate = round(($total_tax / $amount) * 100, 3);
$tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0;
if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1)
$public_notes = $data['Notes / Memo'];
elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1)
$public_notes = $data['Transaction Description'];
else
$public_notes = '';
$transformed = [
'company_id' => $this->company->id,
'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')),
'number' => $this->getString($data, 'Bill Number'),
'public_notes' => $this->getString($data, 'Notes / Memo'),
'public_notes' => $public_notes,
'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022
'currency_id' => $this->company->settings->currency_id,
'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']),
'amount' => $amount,
'tax_name1' => $data['Sales Tax Name'],
'tax_name1' => isset($data['Sales Tax Name']) ? $data['Sales Tax Name'] : '',
'tax_rate1' => $tax_rate,
];

View File

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

View File

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

View File

@ -181,7 +181,7 @@ class ProcessMailgunWebhook implements ShouldQueue
$sl = $this->getSystemLog($this->request['MessageID']);
/** Prevents Gmail tracking from firing inappropriately */
if($this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) {
if(!$sl || $this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) {
return;
}

View File

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

View File

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

View File

@ -164,6 +164,13 @@ class BillingPortalPurchasev2 extends Component
public $payment_confirmed = false;
public $is_eligible = true;
public $not_eligible_message = '';
public $check_rff = false;
public ?string $contact_first_name;
public ?string $contact_last_name;
public ?string $contact_email;
public ?string $client_city;
public ?string $client_postal_code;
public function mount()
{
@ -472,7 +479,6 @@ class BillingPortalPurchasev2 extends Component
*/
protected function getPaymentMethods(): self
{
nlog("total amount = {$this->float_amount_total}");
if ($this->float_amount_total == 0) {
$this->methods = [];
@ -481,10 +487,73 @@ class BillingPortalPurchasev2 extends Component
if ($this->contact && $this->float_amount_total >= 1) {
$this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total);
}
foreach($this->methods as $method) {
if($method['is_paypal'] == '1' && !$this->check_rff) {
$this->rff();
break;
}
}
return $this;
}
protected function rff()
{
$this->contact_first_name = $this->contact->first_name;
$this->contact_last_name = $this->contact->last_name;
$this->contact_email = $this->contact->email;
$this->client_city = $this->contact->client->city;
$this->client_postal_code = $this->contact->client->postal_code;
if(
strlen($this->contact_first_name ?? '') == 0 ||
strlen($this->contact_last_name ?? '') == 0 ||
strlen($this->contact_email ?? '') == 0 ||
strlen($this->client_city ?? '') == 0 ||
strlen($this->client_postal_code ?? '') == 0
)
{
$this->check_rff = true;
}
return $this;
}
public function handleRff()
{
$validated = $this->validate([
'contact_first_name' => ['required'],
'contact_last_name' => ['required'],
'client_city' => ['required'],
'client_postal_code' => ['required'],
'contact_email' => ['required', 'email'],
]);
$this->check_rff = false;
$this->contact->first_name = $validated['contact_first_name'];
$this->contact->last_name = $validated['contact_last_name'];
$this->contact->email = $validated['contact_email'];
$this->contact->client->postal_code = $validated['client_postal_code'];
$this->contact->client->city = $validated['client_city'];
$this->contact->pushQuietly();
$this->refreshComponent();
return $this;
}
protected function refreshComponent()
{
$this->dispatch('$refresh');
}
/**
* Middle method between selecting payment method &
* submitting the from to the backend.

View File

@ -364,16 +364,19 @@ class Account extends BaseModel
return $this->isProClient() && $this->isPaid();
}
public function isNewHostedAccount()
{
return Ninja::isHosted() && Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2;
}
public function isTrial(): bool
{
if (!Ninja::isNinja()) {
return false;
}
//@27-01-2024 - updates for logic around trials
return !$this->plan_paid && $this->trial_started && Carbon::parse($this->trial_started)->addDays(14)->gte(now()->subHours(12));
// $plan_details = $this->getPlanDetails();
// return $plan_details && $plan_details['trial'];
}
public function startTrial($plan): void

View File

@ -27,7 +27,6 @@ use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait;
/**
@ -124,7 +123,7 @@ class Client extends BaseModel implements HasLocalePreference
use AppSetup;
use ClientGroupSettingsSaver;
use Excludable;
protected $presenter = ClientPresenter::class;
protected $hidden = [

View File

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

View File

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

View File

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

View File

@ -95,4 +95,9 @@ class UserPresenter extends EntityPresenter
{
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
{
return $this->hasMany(Invoice::class);
return $this->hasMany(Invoice::class)->withTrashed();
}
public function quotes(): HasMany

View File

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

View File

@ -170,6 +170,9 @@ class ACH
];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
return redirect('client/invoices')->withSuccess('Invoice paid.');
// return redirect('client/invoices')->withSuccess('Invoice paid.');
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
}

View File

@ -187,6 +187,8 @@ class CreditCard
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
return redirect('client/invoices')->withSuccess('Invoice paid.');
// return redirect('client/invoices')->withSuccess('Invoice paid.');
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
}

View File

@ -251,11 +251,11 @@ class PayPalBasePaymentDriver extends BaseDriver
[
"address" =>
[
"address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1,
"address_line_1" => strlen($this->client->shipping_address1 ?? '') > 1 ? $this->client->shipping_address1 : $this->client->address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city,
"admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state,
"postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
"admin_area_2" => strlen($this->client->shipping_city ?? '') > 1 ? $this->client->shipping_city : $this->client->city,
"admin_area_1" => strlen($this->client->shipping_state ?? '') > 1 ? $this->client->shipping_state : $this->client->state,
"postal_code" => strlen($this->client->shipping_postal_code ?? '') > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
],
]

View File

@ -128,7 +128,7 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
nlog($response);
if($request->has('token') && strlen($request->input('token')) > 2) {
if($request->has('token') && strlen($request->input('token','')) > 2) {
return $this->processTokenPayment($request, $response);
}
@ -273,14 +273,14 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
]
];
if($shipping = $this->getShippingAddress()) {
if($shipping = $this->getShippingAddress())
$order['purchase_units'][0]["shipping"] = $shipping;
}
if(isset($data['payment_source'])) {
if(isset($data['payment_source']))
$order['payment_source'] = $data['payment_source'];
}
if(isset($data['payer']))
$order['payer'] = $data['payer'];
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
@ -316,8 +316,17 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
->firstOrFail();
$orderId = $response['orderID'];
$r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']);
$data["payer"] = [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name()
],
"email_address" => $this->client->present()->email(),
];
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [
@ -332,8 +341,6 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$orderId = $this->createOrder($data);
// $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
try {
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']);
@ -395,6 +402,14 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$data = [];
$this->payment_hash = $payment_hash;
$data["payer"] = [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name()
],
"email_address" => $this->client->present()->email(),
];
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [

View File

@ -157,10 +157,6 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
}
public function createOrder(array $data): string
{
@ -213,6 +209,10 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
$order['payment_source'] = $data['payment_source'];
}
if(isset($data["payer"])){
$order['payer'] = $data["payer"];
}
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
nlog($r->json());
@ -274,6 +274,13 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
nlog($r->body());
$data["payer"] = [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name()
],
"email_address" => $this->client->present()->email(),
];
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [
@ -349,6 +356,14 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
$data = [];
$this->payment_hash = $payment_hash;
$data['payer'] = [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name()
],
"email_address" => $this->client->present()->email(),
];
$data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee;
$data["payment_source"] = [
"card" => [

View File

@ -60,7 +60,10 @@ class PaymentMethod implements MethodInterface
'id' => null
] )->all();
$data['gateway'] = $this->rotessa;
$data['gateway_type_id'] = $data['client']->country->iso_3166_2 == 'US' ? GatewayType::BANK_TRANSFER : ( $data['client']->country->iso_3166_2 == 'CA' ? GatewayType::ACSS : (int) request('method'));
// Set gateway type according to client country
// $data['gateway_type_id'] = $data['client']->country->iso_3166_2 == 'US' ? GatewayType::BANK_TRANSFER : ( $data['client']->country->iso_3166_2 == 'CA' ? GatewayType::ACSS : (int) request('method'));
// TODO: detect GatewayType based on client country USA vs CAN
$data['gateway_type_id'] = GatewayType::ACSS ;
$data['account'] = [
'routing_number' => $data['client']->routing_id,
'country' => $data['client']->country->iso_3166_2
@ -145,18 +148,16 @@ class PaymentMethod implements MethodInterface
$request->validate([
'source' => ['required','string','exists:client_gateway_tokens,token'],
'amount' => ['required','numeric'],
'token_id' => ['required','integer','exists:client_gateway_tokens,id'],
'process_date'=> ['required','date','after_or_equal:today'],
]);
$customer = ClientGatewayToken::query()
->where('company_gateway_id', $this->rotessa->company_gateway->id)
->where('client_id', $this->rotessa->client->id)
->where('id', (int) $request->input('token_id'))
->where('token', $request->input('source'))
->first();
if(!$customer) throw new \Exception('Client gateway token not found!', 605);
if(!$customer) throw new \Exception('Client gateway token not found!', SystemLog::TYPE_ROTESSA);
$transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date','comment'));
$transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date') + ['comment' => $this->rotessa->getDescription(false) ]);
$transaction->additional(['customer_id' => $customer->gateway_customer_reference]);
$transaction = array_filter( $transaction->resolve());
$response = $this->rotessa->gateway->capture($transaction)->send();
@ -182,12 +183,12 @@ class PaymentMethod implements MethodInterface
[ 'data' => $data ],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
880,
SystemLog::TYPE_ROTESSA,
$this->rotessa->client,
$this->rotessa->client->company,
);
return redirect()->route('client.payments.show', [ 'payment' => $this->rotessa->encodePrimaryKey($payment->id) ]);
return redirect()->route('client.payments.show', [ 'payment' => $payment->hashed_id ]);
}
/**
@ -205,7 +206,7 @@ class PaymentMethod implements MethodInterface
$exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
880,
SystemLog::TYPE_ROTESSA,
$this->rotessa->client,
$this->rotessa->client->company,
);

View File

@ -24,6 +24,7 @@ use App\Utils\Traits\MakesHash;
use App\Jobs\Util\SystemLogger;
use App\PaymentDrivers\BaseDriver;
use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Builder;
use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\PaymentMethod as Acss;
@ -64,13 +65,15 @@ class RotessaPaymentDriver extends BaseDriver
{
$types = [];
if ($this->client
/*
// TODO: needs to test with US test account
if ($this->client
&& $this->client->currency()
&& in_array($this->client->currency()->code, ['USD'])
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_2, ['US'])) {
$types[] = GatewayType::BANK_TRANSFER;
}
}*/
if ($this->client
&& $this->client->currency()
@ -115,39 +118,108 @@ class RotessaPaymentDriver extends BaseDriver
public function importCustomers() {
$this->init();
try {
$result = $this->gateway->getCustomers()->send();
if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
$customers = collect($result->getData())->unique('email');
if(!$result = Cache::has("rotessa-import_customers-{$this->company_gateway->company->company_key}")) {
$result = $this->gateway->getCustomers()->send();
if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
// cache results
Cache::put("rotessa-import_customers-{$this->company_gateway->company->company_key}", $result->getData(), 60 * 60 * 24);
}
$result = Cache::get("rotessa-import_customers-{$this->company_gateway->company->company_key}");
$customers = collect($result)->unique('email');
$client_emails = $customers->pluck('email')->all();
$company_id = $this->company_gateway->company->id;
// get existing customers
$client_contacts = ClientContact::where('company_id', $company_id)->whereIn('email', $client_emails )->whereNull('deleted_at')->get();
$client_contacts = $client_contacts->map(function($item, $key) use ($customers) {
return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number ]);
return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number, 'client_id' => $item->client->id ]);
} );
// create payment methods
$client_contacts->each(
function($contact) use ($customers) {
sleep(10);
$result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send();
$this->client = Client::find($contact->custom_identifier);
$this->client = Client::find($contact->client_id);
$customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
}
);
// create new clients from rotessa customers
$client_emails = $client_contacts->pluck('email')->all();
$client_contacts = $customers->filter(function ($value, $key) use ($client_emails) {
return !in_array(((object) $value)->email, $client_emails);
})->each( function($customer) use ($company_id) {
// create new client contact from rotess customer
$customer = (object) $this->gateway->getCustomersId(['id' => ($customer = (object) $customer)->id])->send()->getData();
/**
{
"account_number": "11111111"
"active": true,
"address": {
"address_1": "123 Main Street",
"address_2": "Unit 4",
"city": "Birmingham",
"id": 114397,
"postal_code": "36016",
"province_code": "AL"
},
"authorization_type": "Online",
"bank_account_type": "Checking",
"bank_name": "Scotiabank",
"created_at": "2015-02-10T23:50:45.000-06:00",
"custom_identifier": "Mikey",
"customer_type": "Personal",
"email": "mikesmith@test.com",
"financial_transactions": [],
"home_phone": "(204) 555 5555",
"id": 1,
"identifier": "Mikey",
"institution_number": "",
"name": "Mike Smith",
"phone": "(204) 555 4444",
"routing_number": "111111111",
"transaction_schedules": [],
"transit_number": "",
"updated_at": "2015-02-10T23:50:45.000-06:00"
}
*/
$client = (\App\Factory\ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id))->fill(
[
'address1' => $customer->address['address_1'] ?? '',
'address2' =>$customer->address['address_2'] ?? '',
'city' => $customer->address['city'] ?? '',
'postal_code' => $customer->address['postal_code'] ?? '',
'state' => $customer->address['province_code'] ?? '',
'country_id' => empty($customer->transit_number) ? 840 : 124,
'routing_id' => empty(($r = $customer->routing_number))? null : $r,
"number" => str_pad($customer->account_number,3,'0',STR_PAD_LEFT)
]
);
$client->saveQuietly();
$contact = (\App\Factory\ClientContactFactory::create($company_id, $this->company_gateway->user_id))->fill([
"first_name" => substr($customer->name, 0, stripos($customer->name, " ")),
"last_name" => substr($customer->name, stripos($customer->name, " ")),
"email" => $customer->email,
"phone" => $customer->phone,
"is_primary" => true,
"send_email" => true,
]);
$client->contacts()->saveMany([$contact]);
$contact = $client->contacts()->first();
$this->client = $client;
$customer = (new Customer((array) $customer))->additional(['id' => $customer->id, 'custom_identifier' => $customer->custom_identifier ?? $contact->id ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
});
} catch (\Throwable $th) {
$data = [
$data = [
'transaction_reference' => null,
'transaction_response' => $th->getMessage(),
'success' => false,
'description' => $th->getMessage(),
'code' =>(int) $th->getCode()
];
SystemLogger::dispatch(['server_response' => $th->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client , $this->company_gateway->company);
SystemLogger::dispatch(['server_response' => $th->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_ROTESSA , $this->company_gateway->client , $this->company_gateway->company);
throw $th;
}
@ -176,9 +248,11 @@ class RotessaPaymentDriver extends BaseDriver
$data = array_filter($customer->resolve());
}
$payment_method_id = Arr::has($data,'address.postal_code') && ((int) $data['address']['postal_code'])? GatewayType::BANK_TRANSFER: GatewayType::ACSS;
// $payment_method_id = Arr::has($data,'address.postal_code') && ((int) $data['address']['postal_code'])? GatewayType::BANK_TRANSFER: GatewayType::ACSS;
// TODO: Check/ Validate postal code between USA vs CAN
$payment_method_id = GatewayType::ACSS;
$gateway_token = $this->storeGatewayToken( [
'payment_meta' => $data + ['brand' => 'Rotessa'],
'payment_meta' => $data + ['brand' => 'Rotessa', 'last4' => $data['bank_name'], 'type' => $data['bank_account_type'] ],
'token' => encrypt(join(".", Arr::only($data, 'id','custom_identifier'))),
'payment_method_id' => $payment_method_id ,
], ['gateway_customer_reference' =>
@ -198,7 +272,7 @@ class RotessaPaymentDriver extends BaseDriver
'code' =>(int) $th->getCode()
];
SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getData(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->client->company);
SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company);
throw $th;
}

View File

@ -153,7 +153,7 @@ class BrowserPay implements MethodInterface
$this->stripe->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
/**

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)

View File

@ -11,18 +11,22 @@
namespace App\PaymentDrivers\Stripe\Jobs;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\Payment;
use App\Libraries\MultiDB;
use App\Models\PaymentHash;
use App\PaymentDrivers\Stripe\Utilities;
use App\Services\Email\Email;
use Illuminate\Bus\Queueable;
use App\Models\CompanyGateway;
use App\Services\Email\EmailObject;
use Illuminate\Support\Facades\App;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
use App\PaymentDrivers\Stripe\Utilities;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ChargeRefunded implements ShouldQueue
{
@ -36,19 +40,10 @@ class ChargeRefunded implements ShouldQueue
public $deleteWhenMissingModels = true;
public $stripe_request;
public $company_key;
private $company_gateway_id;
public $payment_completed = false;
public function __construct($stripe_request, $company_key, $company_gateway_id)
public function __construct(public array $stripe_request, private string $company_key)
{
$this->stripe_request = $stripe_request;
$this->company_key = $company_key;
$this->company_gateway_id = $company_gateway_id;
}
public function handle()
@ -64,8 +59,8 @@ class ChargeRefunded implements ShouldQueue
$payment_hash_key = $source['metadata']['payment_hash'] ?? null;
$company_gateway = CompanyGateway::query()->find($this->company_gateway_id);
$payment_hash = PaymentHash::query()->where('hash', $payment_hash_key)->first();
$company_gateway = $payment_hash->payment->company_gateway;
$stripe_driver = $company_gateway->driver()->init();
@ -79,7 +74,7 @@ class ChargeRefunded implements ShouldQueue
->first();
//don't touch if already refunded
if(!$payment || in_array($payment->status_id, [Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])) {
if(!$payment || $payment->status_id == Payment::STATUS_REFUNDED || $payment->is_deleted){
return;
}
@ -94,8 +89,19 @@ class ChargeRefunded implements ShouldQueue
return;
}
if($payment->status_id == Payment::STATUS_COMPLETED) {
usleep(rand(200000,300000));
$payment = $payment->fresh();
if($payment->status_id == Payment::STATUS_PARTIALLY_REFUNDED){
//determine the delta in the refunded amount - how much has already been refunded and only apply the delta.
if(floatval($payment->refunded) >= floatval($amount_refunded))
return;
$amount_refunded -= $payment->refunded;
}
$invoice_collection = $payment->paymentables
->where('paymentable_type', 'invoices')
->map(function ($pivot) {
@ -117,9 +123,24 @@ class ChargeRefunded implements ShouldQueue
];
});
} elseif($invoice_collection->sum('amount') != $amount_refunded) {
//too many edges cases at this point, return early
}
elseif($invoice_collection->sum('amount') != $amount_refunded) {
$refund_text = "A partial refund was processed for Payment #{$payment_hash->payment->number}. <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;
}
$invoices = $invoice_collection->toArray();
@ -131,20 +152,21 @@ class ChargeRefunded implements ShouldQueue
'date' => now()->format('Y-m-d'),
'gateway_refund' => false,
'email_receipt' => false,
'via_webhook' => true,
];
nlog($data);
$payment->refund($data);
$payment->private_notes .= 'Refunded via Stripe';
return;
}
$payment->private_notes .= 'Refunded via Stripe ';
$payment->saveQuietly();
}
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;
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 Illuminate\Http\RedirectResponse;
use Laracasts\Presenter\Exceptions\PresenterException;
use Stripe\Stripe;
use Stripe\Account;
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\PaymentMethod;
use Stripe\SetupIntent;
use Stripe\Stripe;
use Stripe\StripeClient;
use App\Models\GatewayType;
use App\Models\PaymentHash;
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
{
@ -670,31 +671,39 @@ class StripePaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request)
{
nlog($request->all());
if ($request->type === 'customer.source.updated') {
$ach = new ACH($this);
$ach->updateBankAccount($request->all());
}
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);
}
//payment_intent.succeeded - this will confirm or cancel the payment
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);
}
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);
}
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);
}
@ -702,7 +711,6 @@ class StripePaymentDriver extends BaseDriver
if ($request->type === 'charge.succeeded') {
foreach ($request->data as $transaction) {
$payment = Payment::query()
->where('company_id', $this->company_gateway->company_id)
->where(function ($query) use ($transaction) {

View File

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

View File

@ -44,17 +44,16 @@ class InstantPayment
public function run()
{
nlog($this->request->all());
/** @var \App\Models\ClientContact $cc */
$cc = auth()->guard('contact')->user();
$cc->first_name = $this->request->contact_first_name;
$cc->last_name = $this->request->contact_last_name;
$cc->email = $this->request->contact_email;
$cc->save();
$cc->client->postal_code = strlen($cc->client->postal_code ?? '') > 1 ? $cc->client->postal_code : $this->request->client_postal_code;
$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;
@ -73,8 +72,6 @@ class InstantPayment
*/
$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->each(function ($invoice) {

View File

@ -95,6 +95,48 @@ class Storecove {
// 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)
{
@ -256,8 +298,6 @@ class Storecove {
}
public function addIdentifier(int $legal_entity_id, string $identifier, string $scheme)
{
$uri = "legal_entities/{$legal_entity_id}/peppol_identifiers";
@ -278,7 +318,6 @@ class Storecove {
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private function getHeaders(array $headers = [])
{
@ -300,5 +339,4 @@ class Storecove {
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\FinancialAccountType\PayeeFinancialAccount;
use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID;
use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty;
use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification;
class Peppol extends AbstractService
@ -150,14 +151,36 @@ class Peppol extends AbstractService
private InvoiceSum | InvoiceSumInclusive $calc;
private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice;
/**
* @param Invoice $invoice
*/
public function __construct(public Invoice $invoice, public ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice = null)
* @param Invoice $invoice
*/
public function __construct(public Invoice $invoice)
{
$this->p_invoice = $p_invoice ?? new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$this->company = $invoice->company;
$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
@ -170,7 +193,31 @@ class Peppol extends AbstractService
public function toXml(): string
{
$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()
@ -185,31 +232,12 @@ class Peppol extends AbstractService
$this->p_invoice->TaxTotal = $this->getTotalTaxes();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
// $this->p_invoice->PaymentMeans = $this->getPaymentMeans();
// $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);
$this->countryLevelMutators();
return $this;
}
// private function getPaymentMeans(): PaymentMeans
// {
// $payeeFinancialAccount = new PayeeFinancialAccount()
// $payeeFinancialAccount->
// $ppm = new PaymentMeans();
// $ppm->PayeeFinancialAccount = $payeeFinancialAccount;
// return $ppm;
// }
private function getLegalMonetaryTotal(): LegalMonetaryTotal
{
$taxable = $this->getTaxable();
@ -482,7 +510,7 @@ class Peppol extends AbstractService
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate2, $item->line_total) : $this->calcAmountLineTax($item->tax_rate2, $item->line_total);
$tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate2, $item->line_total) : $this->calcAmountLineTax($item->tax_rate2, $item->line_total);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
@ -516,7 +544,7 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate3, $item->line_total) : $this->calcAmountLineTax($item->tax_rate3, $item->line_total);
$tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate3, $item->line_total) : $this->calcAmountLineTax($item->tax_rate3, $item->line_total);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
@ -572,7 +600,9 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv
$party->PhysicalLocation = $address;
$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;
@ -695,16 +725,283 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv
'PaymentTerms' => 7,
];
//only scans for top level props
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;
elseif($prop_value = PropertyResolver::resolve($this->invoice->company->e_invoice, $prop)) {
$this->p_invoice->{$prop} = $prop_value;
}
}
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 */
// if(Ninja::isHosted() && !$this->company->account->isPaid()) {
// $this->mailer = 'mailgun';
// $this->setHostedMailgunMailer();
// return $this;
// }
if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) {
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
return $this;
}
if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') {
@ -619,7 +619,7 @@ class Email implements ShouldQueue
$smtp_username = $company->smtp_username ?? '';
$smtp_password = $company->smtp_password ?? '';
$smtp_encryption = $company->smtp_encryption ?? 'tls';
$smtp_local_domain = strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null;
$smtp_local_domain = strlen($company->smtp_local_domain ?? '') > 2 ? $company->smtp_local_domain : null;
$smtp_verify_peer = $company->smtp_verify_peer ?? true;
if(strlen($smtp_host) <= 1 ||

View File

@ -42,6 +42,9 @@ class AutoBillInvoice extends AbstractService
public function __construct(private Invoice $invoice, protected string $db)
{
$this->client = $this->invoice->client;
}
public function run()
@ -49,8 +52,7 @@ class AutoBillInvoice extends AbstractService
MultiDB::setDb($this->db);
/* @var \App\Modesl\Client $client */
$this->client = $this->invoice->client;
$is_partial = false;
/* Is the invoice payable? */
@ -272,7 +274,7 @@ class AutoBillInvoice extends AbstractService
*
* @return self
*/
private function applyUnappliedPayment(): self
public function applyUnappliedPayment(): self
{
$unapplied_payments = Payment::query()
->where('client_id', $this->client->id)
@ -284,6 +286,11 @@ class AutoBillInvoice extends AbstractService
->get();
$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}");
@ -347,7 +354,7 @@ class AutoBillInvoice extends AbstractService
*
* @return $this
*/
private function applyCreditPayment(): self
public function applyCreditPayment(): self
{
$available_credits = Credit::query()->where('client_id', $this->client->id)
->where('is_deleted', false)

View File

@ -44,7 +44,6 @@ class RefundPayment
->setStatus() //sets status of payment
->updatePaymentables() //update the paymentable items
->adjustInvoices()
->finalize()
->save();
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);
}
$notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : ";
$notes .= $this->refund_data['gateway_refund'] !== false ? ctrans('texts.yes') : ctrans('texts.no');
$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 = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : " . $is_gateway_refund;
$this->createActivity($notes);
$this->finalize();
return $this->payment;
}
@ -178,7 +178,7 @@ class RefundPayment
*/
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;
} else {
$this->payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;

View File

@ -739,7 +739,7 @@ class PdfBuilder
if ($item->is_amount_discount) {
$data[$key][$table_type.'.discount'] = $this->service->config->formatMoney($item->discount);
} else {
$data[$key][$table_type.'.discount'] = floatval($item->discount).'%';
$data[$key][$table_type.'.discount'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->discount)).'%';
}
} else {
$data[$key][$table_type.'.discount'] = '';
@ -749,17 +749,17 @@ class PdfBuilder
// but that's no longer necessary.
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'];
}
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'];
}
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'];
}

View File

@ -88,7 +88,7 @@ class SubscriptionService
// 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)) {
$recurring_invoice = $this->convertInvoiceToRecurringBundle($payment_hash->payment->client_id, $payment_hash->data->billing_context->bundle);
} else {
@ -1024,10 +1024,10 @@ class SubscriptionService
$invoice->subscription_id = $this->subscription->id;
$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->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->is_amount_discount = $this->subscription->is_amount_discount;
}
@ -1118,7 +1118,7 @@ class SubscriptionService
*/
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];
}
@ -1436,7 +1436,7 @@ class SubscriptionService
*/
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) {
return (object) $bund;
}));
@ -1492,7 +1492,7 @@ class SubscriptionService
*/
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']);
}

View File

@ -557,15 +557,7 @@ class TemplateService
'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),
'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled,
'client' => [
'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),
],
'client' => $this->getClient($invoice),
'payments' => $payments,
'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
'line_tax_map' => $invoice->calc()->getTaxMap(),
@ -680,14 +672,7 @@ class TemplateService
'custom_value4' => $payment->custom_value4 ?? '',
'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()),
'client' => [
'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',
],
'client' => $this->getClient($payment),
'paymentables' => $pivot,
'refund_activity' => $this->getPaymentRefundActivity($payment),
];
@ -760,14 +745,7 @@ class TemplateService
'amount' => Number::formatMoney($quote->amount, $quote->client),
'balance' => Number::formatMoney($quote->balance, $quote->client),
'balance_raw' => (float) $quote->balance,
'client' => [
'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',
],
'client' => $this->getClient($quote),
'status_id' => $quote->status_id,
'status' => Quote::stringStatus($quote->status_id),
'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()),
'paid_to_date' => Number::formatMoney($credit->paid_to_date, $credit->client),
'auto_bill_enabled' => (bool) $credit->auto_bill_enabled,
'client' => [
'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',
],
'client' => $this->getClient($credit),
'payments' => $payments,
'total_tax_map' => $credit->calc()->getTotalTaxMap(),
'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
*
@ -953,14 +943,7 @@ class TemplateService
'custom_value4' => $task->custom_value4 ?: '',
'status' => $task->status ? $task->status->name : '',
'user' => $this->userInfo($task->user),
'client' => $task->client ? [
'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',
] : [],
'client' => $this->getClient($task),
];
@ -1015,15 +998,9 @@ class TemplateService
'color' => (string) $project->color ?: '',
'current_hours' => (int) $project->current_hours ?: 0,
'tasks' => ($project->tasks && !$nested) ? $this->processTasks($project->tasks, true) : [], //@phpstan-ignore-line
'client' => $project->client ? [
'name' => $project->client->present()->name(),
'balance' => $project->client->balance,
'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)
'client' => $this->getClient($project),
'user' => $this->userInfo($project->user),
'invoices' => $this->processInvoices($project->invoices)
];
}
@ -1046,16 +1023,7 @@ class TemplateService
] : [],
'amount' => (float)$purchase_order->amount,
'balance' => (float)$purchase_order->balance,
'client' => $purchase_order->client ? [
'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',
] : [],
'client' => $this->getClient($purchase_order),
'status_id' => (string)($purchase_order->status_id ?: 1),
'status' => PurchaseOrder::stringStatus($purchase_order->status_id ?? 1),
'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\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ComposerServiceProvider::class,

View File

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

View File

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

View File

@ -15,42 +15,23 @@ return new class extends Migration
public function up(): void
{
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->apiKey = '';
$configuration->testMode = true;
$configuration = new \stdClass;
$configuration->api_key = '';
$configuration->test_mode = true;
if (!$record) {
$gateway = new Gateway;
} else {
$gateway = $record;
$gateway = new Gateway();
$gateway->id = 63;
$gateway->name = 'Rotessa';
$gateway->key = '91be24c7b792230bced33e930ac61676';
$gateway->provider = 'Rotessa';
$gateway->is_offsite = true;
$gateway->fields = \json_encode($configuration);
$gateway->visible = 1;
$gateway->site_url = "https://rotessa.com";
$gateway->default_gateway_type_id = 2;
$gateway->save();
}
$gateway->id = 4002;
$gateway->name = 'Rotessa';
$gateway->key = Str::lower(Str::random(32));
$gateway->provider = 'Rotessa';
$gateway->is_offsite = true;
$gateway->fields = \json_encode($configuration);
$gateway->visible = 1;
$gateway->site_url = "https://rotessa.com";
$gateway->default_gateway_type_id = 2;
$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' => 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' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":""}'],
];
foreach ($gateways as $gateway) {
@ -104,7 +105,7 @@ class PaymentLibrariesSeeder extends Seeder
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()) {
Gateway::whereIn('id', [20, 49])->update(['visible' => 0]);

View File

@ -5300,7 +5300,7 @@ $lang = array(
'merge_to_pdf' => 'Merge to PDF',
'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_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;

View File

@ -5124,7 +5124,7 @@ $lang = array(
'all_contacts' => 'All Contacts',
'insert_below' => 'Insert Below',
'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_heading_token_invalid' => 'Invalid Token',
'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',
'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.',
'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;

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',
'auto_expand_product_table_notes' => 'Développer automatiquement les notes du tableau de produits',
'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;

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

View File

@ -9,39 +9,47 @@
*/
class Payment {
constructor(displayTerms, displaySignature, displayRff) {
constructor(displayTerms, displaySignature) {
this.shouldDisplayTerms = displayTerms;
this.shouldDisplaySignature = displaySignature;
this.shouldDisplayRff = displayRff;
this.submitting = false;
this.steps = new Map()
if (this.shouldDisplayRff) {
this.steps.set("rff", {
element: document.getElementById('displayRequiredFieldsModal'),
nextButton: document.getElementById('rff-next-step'),
callback: () => {
const fields = {
firstName: document.querySelector('input[name="rff_first_name"]'),
lastName: document.querySelector('input[name="rff_last_name"]'),
email: document.querySelector('input[name="rff_email"]'),
}
if (fields.firstName) {
document.querySelector('input[name="contact_first_name"]').value = fields.firstName.value;
}
if (fields.lastName) {
document.querySelector('input[name="contact_last_name"]').value = fields.lastName.value;
}
if (fields.email) {
document.querySelector('input[name="contact_email"]').value = fields.email.value;
}
this.steps.set("rff", {
element: document.getElementById('displayRequiredFieldsModal'),
nextButton: document.getElementById('rff-next-step'),
callback: () => {
const fields = {
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"]'),
}
});
}
if (fields.firstName) {
document.querySelector('input[name="contact_first_name"]').value = fields.firstName.value;
}
if (fields.lastName) {
document.querySelector('input[name="contact_last_name"]').value = fields.lastName.value;
}
if (fields.email) {
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) {
this.steps.set("signature", {
@ -71,7 +79,17 @@ class Payment {
element.dataset.companyGatewayId;
document.getElementById("payment_method_id").value =
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) {
return this.submitForm();
}
@ -124,6 +142,5 @@ const signature = document.querySelector(
).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_last_name" value="{{ $contact->last_name }}">
<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>
@if($steps['started_payment'] == false)
@foreach($this->methods as $method)
<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">
{{ $method['label'] }}
</button>
@ -189,27 +192,41 @@
<form wire:submit="handleRff">
@csrf
@if(strlen($contact->first_name) === 0)
@if(strlen($contact->first_name ?? '') === 0)
<div class="col-auto mt-3">
<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" />
</div>
@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">
<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" />
</div>
@endif
@if(strlen($contact->email) === 0)
@if(strlen($contact->email ?? '') === 0)
<div class="col-auto mt-3 @if($contact->email) !== 0) hidden @endif">
<label for="email" class="input-label">{{ ctrans('texts.email') }}</label>
<input id="email" class="input w-full" wire:model="contact_email" />
</div>
@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
type="submit"
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