mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
commit
f184f38819
41
app/Casts/ClientSyncCast.php
Normal file
41
app/Casts/ClientSyncCast.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?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\Casts;
|
||||
|
||||
use App\DataMapper\ClientSync;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class ClientSyncCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
$data = json_decode($value, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$is = new ClientSync();
|
||||
$is->qb_id = $data['qb_id'];
|
||||
|
||||
return $is;
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return [
|
||||
$key => json_encode([
|
||||
'qb_id' => $value->qb_id,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
41
app/Casts/InvoiceSyncCast.php
Normal file
41
app/Casts/InvoiceSyncCast.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?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\Casts;
|
||||
|
||||
use App\DataMapper\InvoiceSync;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class InvoiceSyncCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
$data = json_decode($value, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$is = new InvoiceSync();
|
||||
$is->qb_id = $data['qb_id'];
|
||||
|
||||
return $is;
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return [
|
||||
$key => json_encode([
|
||||
'qb_id' => $value->qb_id,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
41
app/Casts/ProductSyncCast.php
Normal file
41
app/Casts/ProductSyncCast.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?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\Casts;
|
||||
|
||||
use App\DataMapper\ProductSync;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class ProductSyncCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
$data = json_decode($value, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ps = new ProductSync();
|
||||
$ps->qb_id = $data['qb_id'];
|
||||
|
||||
return $ps;
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return [
|
||||
$key => json_encode([
|
||||
'qb_id' => $value->qb_id,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
@ -18,33 +18,19 @@ class QuickbooksSettingsCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
if (is_null($value))
|
||||
return new QuickbooksSettings();
|
||||
|
||||
$data = json_decode($value, true);
|
||||
|
||||
if(!is_array($data))
|
||||
return null;
|
||||
|
||||
$qb = new QuickbooksSettings();
|
||||
$qb->accessTokenKey = $data['accessTokenKey'];
|
||||
$qb->refresh_token = $data['refresh_token'];
|
||||
$qb->realmID = $data['realmID'];
|
||||
$qb->accessTokenExpiresAt = $data['accessTokenExpiresAt'];
|
||||
$qb->refreshTokenExpiresAt = $data['refreshTokenExpiresAt'];
|
||||
$qb->settings = $data['settings'] ?? [];
|
||||
|
||||
return $qb;
|
||||
return QuickbooksSettings::fromArray($data);
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return [
|
||||
$key => json_encode([
|
||||
'accessTokenKey' => $value->accessTokenKey,
|
||||
'refresh_token' => $value->refresh_token,
|
||||
'realmID' => $value->realmID,
|
||||
'accessTokenExpiresAt' => $value->accessTokenExpiresAt,
|
||||
'refreshTokenExpiresAt' => $value->refreshTokenExpiresAt,
|
||||
'settings' => $value->settings,
|
||||
])
|
||||
];
|
||||
if ($value instanceof QuickbooksSettings) {
|
||||
return json_encode(get_object_vars($value));
|
||||
}
|
||||
|
||||
return json_encode($value);
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ use App\Factory\ClientContactFactory;
|
||||
use App\Factory\VendorContactFactory;
|
||||
use App\Jobs\Company\CreateCompanyToken;
|
||||
use App\Models\RecurringInvoiceInvitation;
|
||||
use App\Utils\Traits\CleanLineItems;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/*
|
||||
@ -80,10 +81,12 @@ Options:
|
||||
*/
|
||||
class CheckData extends Command
|
||||
{
|
||||
use CleanLineItems;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ninja:check-data {--database=} {--fix=} {--portal_url=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=} {--bank_transaction=}';
|
||||
protected $signature = 'ninja:check-data {--database=} {--fix=} {--portal_url=} {--client_id=} {--vendor_id=} {--paid_to_date=} {--client_balance=} {--ledger_balance=} {--balance_status=} {--bank_transaction=} {--line_items=}';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
@ -146,6 +149,10 @@ class CheckData extends Command
|
||||
$this->fixBankTransactions();
|
||||
}
|
||||
|
||||
if($this->option('line_items')) {
|
||||
$this->cleanInvoiceLineItems();
|
||||
}
|
||||
|
||||
$this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE));
|
||||
$this->logMessage('Total execution time in seconds: ' . (microtime(true) - $time_start));
|
||||
|
||||
@ -1177,4 +1184,21 @@ class CheckData extends Command
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function cleanInvoiceLineItems()
|
||||
{
|
||||
Invoice::withTrashed()->cursor()->each(function ($invoice) {
|
||||
$invoice->line_items = $this->cleanItems($invoice->line_items);
|
||||
$invoice->saveQuietly();
|
||||
});
|
||||
|
||||
Credit::withTrashed()->cursor()->each(function ($invoice) {
|
||||
$invoice->line_items = $this->cleanItems($invoice->line_items);
|
||||
$invoice->saveQuietly();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
42
app/DataMapper/ClientSync.php
Normal file
42
app/DataMapper/ClientSync.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?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;
|
||||
|
||||
use App\Casts\ClientSyncCast;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
|
||||
/**
|
||||
* ClientSync.
|
||||
*/
|
||||
class ClientSync implements Castable
|
||||
{
|
||||
public string $qb_id;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->qb_id = $attributes['qb_id'] ?? '';
|
||||
}
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function castUsing(array $arguments): string
|
||||
{
|
||||
return ClientSyncCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
}
|
43
app/DataMapper/InvoiceSync.php
Normal file
43
app/DataMapper/InvoiceSync.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?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;
|
||||
|
||||
use App\Casts\InvoiceSyncCast;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
|
||||
/**
|
||||
* InvoiceSync.
|
||||
*/
|
||||
class InvoiceSync implements Castable
|
||||
{
|
||||
public string $qb_id;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->qb_id = $attributes['qb_id'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function castUsing(array $arguments): string
|
||||
{
|
||||
return InvoiceSyncCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
}
|
43
app/DataMapper/ProductSync.php
Normal file
43
app/DataMapper/ProductSync.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?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;
|
||||
|
||||
use App\Casts\ProductSyncCast;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
|
||||
/**
|
||||
* ProductSync.
|
||||
*/
|
||||
class ProductSync implements Castable
|
||||
{
|
||||
public string $qb_id;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->qb_id = $attributes['qb_id'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function castUsing(array $arguments): string
|
||||
{
|
||||
return ProductSyncCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
}
|
@ -30,34 +30,28 @@ class QuickbooksSettings implements Castable
|
||||
public int $refreshTokenExpiresAt;
|
||||
|
||||
public string $baseURL;
|
||||
/**
|
||||
* entity client,invoice,quote,purchase_order,vendor,payment
|
||||
* sync true/false
|
||||
* update_record true/false
|
||||
* direction push/pull/birectional
|
||||
* */
|
||||
public array $settings = [
|
||||
'client' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'invoice' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'sales' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'quote' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'purchase_order' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'product' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'payment' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
'expense' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
|
||||
];
|
||||
|
||||
public QuickbooksSync $settings;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->accessTokenKey = $attributes['accessTokenKey'] ?? '';
|
||||
$this->refresh_token = $attributes['refresh_token'] ?? '';
|
||||
$this->realmID = $attributes['realmID'] ?? '';
|
||||
$this->accessTokenExpiresAt = $attributes['accessTokenExpiresAt'] ?? 0;
|
||||
$this->refreshTokenExpiresAt = $attributes['refreshTokenExpiresAt'] ?? 0;
|
||||
$this->baseURL = $attributes['baseURL'] ?? '';
|
||||
$this->settings = new QuickbooksSync($attributes['settings'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the caster class to use when casting from / to this cast target.
|
||||
*
|
||||
* @param array<string, mixed> $arguments
|
||||
*/
|
||||
public static function castUsing(array $arguments): string
|
||||
{
|
||||
return QuickbooksSettingsCast::class;
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
}
|
||||
|
55
app/DataMapper/QuickbooksSync.php
Normal file
55
app/DataMapper/QuickbooksSync.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* QuickbooksSync.
|
||||
*/
|
||||
class QuickbooksSync
|
||||
{
|
||||
public QuickbooksSyncMap $client;
|
||||
|
||||
public QuickbooksSyncMap $vendor;
|
||||
|
||||
public QuickbooksSyncMap $invoice;
|
||||
|
||||
public QuickbooksSyncMap $sales;
|
||||
|
||||
public QuickbooksSyncMap $quote;
|
||||
|
||||
public QuickbooksSyncMap $purchase_order;
|
||||
|
||||
public QuickbooksSyncMap $product;
|
||||
|
||||
public QuickbooksSyncMap $payment;
|
||||
|
||||
public QuickbooksSyncMap $expense;
|
||||
|
||||
public string $default_income_account = '';
|
||||
|
||||
public string $default_expense_account = '';
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->client = new QuickbooksSyncMap($attributes['client'] ?? []);
|
||||
$this->vendor = new QuickbooksSyncMap($attributes['vendor'] ?? []);
|
||||
$this->invoice = new QuickbooksSyncMap($attributes['invoice'] ?? []);
|
||||
$this->sales = new QuickbooksSyncMap($attributes['sales'] ?? []);
|
||||
$this->quote = new QuickbooksSyncMap($attributes['quote'] ?? []);
|
||||
$this->purchase_order = new QuickbooksSyncMap($attributes['purchase_order'] ?? []);
|
||||
$this->product = new QuickbooksSyncMap($attributes['product'] ?? []);
|
||||
$this->payment = new QuickbooksSyncMap($attributes['payment'] ?? []);
|
||||
$this->expense = new QuickbooksSyncMap($attributes['expense'] ?? []);
|
||||
$this->default_income_account = $attributes['default_income_account'] ?? '';
|
||||
$this->default_expense_account = $attributes['default_expense_account'] ?? '';
|
||||
}
|
||||
}
|
30
app/DataMapper/QuickbooksSyncMap.php
Normal file
30
app/DataMapper/QuickbooksSyncMap.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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;
|
||||
|
||||
use App\Enum\SyncDirection;
|
||||
|
||||
/**
|
||||
* QuickbooksSyncMap.
|
||||
*/
|
||||
class QuickbooksSyncMap
|
||||
{
|
||||
public SyncDirection $direction = SyncDirection::BIDIRECTIONAL;
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
$this->direction = isset($attributes['direction'])
|
||||
? SyncDirection::from($attributes['direction'])
|
||||
: SyncDirection::BIDIRECTIONAL;
|
||||
|
||||
}
|
||||
}
|
@ -35,6 +35,10 @@ class TaxModel
|
||||
$this->regions = $this->init();
|
||||
} else {
|
||||
|
||||
if(!$model->seller_subregion) {
|
||||
$this->seller_subregion = '';
|
||||
}
|
||||
|
||||
//@phpstan-ignore-next-line
|
||||
foreach($model as $key => $value) {
|
||||
$this->{$key} = $value;
|
||||
|
19
app/Enum/SyncDirection.php
Normal file
19
app/Enum/SyncDirection.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?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\Enum;
|
||||
|
||||
enum SyncDirection: string
|
||||
{
|
||||
case PUSH = 'push'; // only creates and updates records created by Invoice Ninja.
|
||||
case PULL = 'pull'; // creates and updates record from QB.
|
||||
case BIDIRECTIONAL = 'bidirectional'; // creates and updates records created by Invoice Ninja and from QB.
|
||||
}
|
@ -26,29 +26,15 @@ class InvoiceWasPaid implements ShouldBroadcast
|
||||
{
|
||||
use SerializesModels, DefaultInvoiceBroadcast;
|
||||
|
||||
/**
|
||||
* @var Invoice
|
||||
*/
|
||||
public $invoice;
|
||||
|
||||
public $payment;
|
||||
|
||||
public $company;
|
||||
|
||||
public $event_vars;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @param Company $company
|
||||
* @param Payment $payment
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(Invoice $invoice, Payment $payment, Company $company, array $event_vars)
|
||||
public function __construct(public Invoice $invoice, public Payment $payment, public Company $company, public array $event_vars)
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
$this->payment = $payment;
|
||||
$this->company = $company;
|
||||
$this->event_vars = $event_vars;
|
||||
}
|
||||
}
|
||||
|
@ -11,32 +11,33 @@
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use Aws\Exception\CredentialsException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
|
||||
use Illuminate\Database\Eloquent\RelationNotFoundException;
|
||||
use Illuminate\Encryption\MissingAppKeyException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Queue\MaxAttemptsExceededException;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use InvalidArgumentException;
|
||||
use League\Flysystem\UnableToCreateDirectory;
|
||||
use PDOException;
|
||||
use Sentry\Laravel\Integration;
|
||||
use Sentry\State\Scope;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Process\Exception\RuntimeException;
|
||||
use Throwable;
|
||||
use PDOException;
|
||||
use App\Utils\Ninja;
|
||||
use Sentry\State\Scope;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Http\Request;
|
||||
use InvalidArgumentException;
|
||||
use Sentry\Laravel\Integration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Aws\Exception\CredentialsException;
|
||||
use Illuminate\Database\QueryException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use League\Flysystem\UnableToCreateDirectory;
|
||||
use Illuminate\Session\TokenMismatchException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Encryption\MissingAppKeyException;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Queue\MaxAttemptsExceededException;
|
||||
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
||||
use Symfony\Component\Process\Exception\RuntimeException;
|
||||
use Illuminate\Database\Eloquent\RelationNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@ -52,6 +53,7 @@ class Handler extends ExceptionHandler
|
||||
ValidationException::class,
|
||||
// ModelNotFoundException::class,
|
||||
NotFoundHttpException::class,
|
||||
RelationNotFoundException::class,
|
||||
];
|
||||
|
||||
protected $selfHostDontReport = [
|
||||
@ -65,6 +67,8 @@ class Handler extends ExceptionHandler
|
||||
RuntimeException::class,
|
||||
InvalidArgumentException::class,
|
||||
CredentialsException::class,
|
||||
RelationNotFoundException::class,
|
||||
QueryException::class,
|
||||
];
|
||||
|
||||
protected $hostedDontReport = [
|
||||
@ -73,6 +77,7 @@ class Handler extends ExceptionHandler
|
||||
ValidationException::class,
|
||||
ModelNotFoundException::class,
|
||||
NotFoundHttpException::class,
|
||||
RelationNotFoundException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -68,7 +68,7 @@ class BankTransactionFilters extends QueryFilters
|
||||
*/
|
||||
public function client_status(string $value = ''): Builder
|
||||
{
|
||||
if (strlen($value ?? '') == 0) {
|
||||
if (strlen($value) == 0) {
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
@ -57,6 +57,7 @@ class CompanyGatewayController extends BaseController
|
||||
|
||||
private string $forte_key = 'kivcvjexxvdiyqtj3mju5d6yhpeht2xs';
|
||||
|
||||
private string $cbapowerboard_key = 'b67581d804dbad1743b61c57285142ad';
|
||||
|
||||
/**
|
||||
* CompanyGatewayController constructor.
|
||||
@ -227,16 +228,34 @@ class CompanyGatewayController extends BaseController
|
||||
|
||||
ApplePayDomain::dispatch($company_gateway, $company_gateway->company->db);
|
||||
|
||||
if (in_array($company_gateway->gateway_key, $this->stripe_keys)) {
|
||||
StripeWebhook::dispatch($company_gateway->company->company_key, $company_gateway->id);
|
||||
} elseif($company_gateway->gateway_key == $this->checkout_key) {
|
||||
CheckoutSetupWebhook::dispatch($company_gateway->company->company_key, $company_gateway->id);
|
||||
} elseif($company_gateway->gateway_key == $this->forte_key) {
|
||||
switch ($company_gateway->gateway_key) {
|
||||
case in_array($company_gateway->gateway_key, $this->stripe_keys):
|
||||
StripeWebhook::dispatch($company_gateway->company->company_key, $company_gateway->id);
|
||||
break;
|
||||
|
||||
dispatch(function () use ($company_gateway) {
|
||||
MultiDB::setDb($company_gateway->company->db);
|
||||
$company_gateway->driver()->updateFees();
|
||||
})->afterResponse();
|
||||
case $this->checkout_key:
|
||||
CheckoutSetupWebhook::dispatch($company_gateway->company->company_key, $company_gateway->id);
|
||||
break;
|
||||
|
||||
case $this->forte_key:
|
||||
dispatch(function () use ($company_gateway) {
|
||||
MultiDB::setDb($company_gateway->company->db);
|
||||
$company_gateway->driver()->updateFees();
|
||||
})->afterResponse();
|
||||
|
||||
break;
|
||||
|
||||
case $this->cbapowerboard_key:
|
||||
dispatch(function () use ($company_gateway) {
|
||||
MultiDB::setDb($company_gateway->company->db);
|
||||
$company_gateway->driver()->init()->settings()->updateSettings();
|
||||
})->afterResponse();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
# code...
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
|
@ -19,28 +19,6 @@ use App\Services\Quickbooks\QuickbooksService;
|
||||
|
||||
class ImportQuickbooksController extends BaseController
|
||||
{
|
||||
// private array $import_entities = [
|
||||
// 'client' => 'Customer',
|
||||
// 'invoice' => 'Invoice',
|
||||
// 'product' => 'Item',
|
||||
// 'payment' => 'Payment'
|
||||
// ];
|
||||
|
||||
public function onAuthorized(AuthorizedQuickbooksRequest $request)
|
||||
{
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||
$company = $request->getCompany();
|
||||
$qb = new QuickbooksService($company);
|
||||
|
||||
$realm = $request->query('realmId');
|
||||
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
|
||||
$qb->sdk()->saveOAuthToken($access_token_object);
|
||||
|
||||
return redirect(config('ninja.react_url'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
@ -60,4 +38,21 @@ class ImportQuickbooksController extends BaseController
|
||||
return redirect()->to($authorizationUrl);
|
||||
}
|
||||
|
||||
public function onAuthorized(AuthorizedQuickbooksRequest $request)
|
||||
{
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||
$company = $request->getCompany();
|
||||
$qb = new QuickbooksService($company);
|
||||
|
||||
$realm = $request->query('realmId');
|
||||
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
|
||||
$qb->sdk()->saveOAuthToken($access_token_object);
|
||||
|
||||
return redirect(config('ninja.react_url'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -11,10 +11,12 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Search\GenericSearchRequest;
|
||||
use App\Models\User;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Client;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\User;
|
||||
use Elastic\Elasticsearch\ClientBuilder;
|
||||
use App\Http\Requests\Search\GenericSearchRequest;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
@ -26,6 +28,14 @@ class SearchController extends Controller
|
||||
|
||||
public function __invoke(GenericSearchRequest $request)
|
||||
{
|
||||
if(config('scount.driver') == 'elastic' && $request->has('search') && $request->input('search') !== '') {
|
||||
try{
|
||||
return $this->search($request->input('search', ''));
|
||||
} catch(\Exception $e) {
|
||||
nlog("elk down?");
|
||||
}
|
||||
}
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
@ -41,6 +51,96 @@ class SearchController extends Controller
|
||||
|
||||
}
|
||||
|
||||
public function search(string $search)
|
||||
{
|
||||
$user = auth()->user();
|
||||
$company = $user->company();
|
||||
|
||||
$elastic = ClientBuilder::fromConfig(config('elastic.client.connections.default'));
|
||||
|
||||
$params = [
|
||||
'index' => 'clients,invoices,client_contacts',
|
||||
'body' => [
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
'multi_match' => [
|
||||
'query' => $search,
|
||||
'fields' => ['*'],
|
||||
'fuzziness' => 'AUTO',
|
||||
],
|
||||
],
|
||||
'filter' => [
|
||||
'match' => [
|
||||
'company_key' => $company->company_key,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'size' => 1000,
|
||||
],
|
||||
];
|
||||
|
||||
$results = $elastic->search($params);
|
||||
|
||||
$this->mapResults($results['hits']['hits'] ?? []);
|
||||
|
||||
return response()->json([
|
||||
'clients' => $this->clients,
|
||||
'client_contacts' => $this->client_contacts,
|
||||
'invoices' => $this->invoices,
|
||||
'settings' => $this->settingsMap(),
|
||||
], 200);
|
||||
|
||||
}
|
||||
|
||||
private function mapResults(array $results)
|
||||
{
|
||||
|
||||
foreach($results as $result) {
|
||||
switch($result['_index']) {
|
||||
case 'clients':
|
||||
|
||||
if($result['_source']['is_deleted']) //do not return deleted results
|
||||
break;
|
||||
|
||||
$this->clients[] = [
|
||||
'name' => $result['_source']['name'],
|
||||
'type' => '/client',
|
||||
'id' => $result['_source']['hashed_id'],
|
||||
'path' => "/clients/{$result['_source']['hashed_id']}"
|
||||
];
|
||||
|
||||
break;
|
||||
case 'invoices':
|
||||
|
||||
if ($result['_source']['is_deleted']) //do not return deleted invoices
|
||||
break;
|
||||
|
||||
|
||||
$this->invoices[] = [
|
||||
'name' => $result['_source']['name'],
|
||||
'type' => '/invoice',
|
||||
'id' => $result['_source']['hashed_id'],
|
||||
'path' => "/invoices/{$result['_source']['hashed_id']}/edit"
|
||||
];
|
||||
break;
|
||||
case 'client_contacts':
|
||||
|
||||
if($result['_source']['__soft_deleted']) // do not return deleted contacts
|
||||
break;
|
||||
|
||||
$this->client_contacts[] = [
|
||||
'name' => $result['_source']['name'],
|
||||
'type' => '/client',
|
||||
'id' => $result['_source']['hashed_id'],
|
||||
'path' => "/clients/{$result['_source']['hashed_id']}"
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function clientMap(User $user)
|
||||
{
|
||||
|
||||
@ -81,20 +181,14 @@ class SearchController extends Controller
|
||||
$invoices = Invoice::query()
|
||||
->company()
|
||||
->with('client')
|
||||
->where('invoices.is_deleted', 0)
|
||||
// ->whereHas('client', function ($q) {
|
||||
// $q->where('is_deleted', 0);
|
||||
// })
|
||||
|
||||
->leftJoin('clients', function ($join) {
|
||||
$join->on('invoices.client_id', '=', 'clients.id')
|
||||
->where('clients.is_deleted', 0);
|
||||
})
|
||||
|
||||
->where('is_deleted', 0)
|
||||
->whereHas('client', function ($q) {
|
||||
$q->where('is_deleted', 0);
|
||||
})
|
||||
->when(!$user->hasPermission('view_all') || !$user->hasPermission('view_invoice'), function ($query) use ($user) {
|
||||
$query->where('invoices.user_id', $user->id);
|
||||
})
|
||||
->orderBy('invoices.id', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->take(3000)
|
||||
->get();
|
||||
|
||||
|
@ -37,19 +37,6 @@ class StripeConnectController extends BaseController
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||
|
||||
// $company_gateway = CompanyGateway::query()
|
||||
// ->where('gateway_key', 'd14dd26a47cecc30fdd65700bfb67b34')
|
||||
// ->where('company_id', $request->getCompany()->id)
|
||||
// ->first();
|
||||
|
||||
// if ($company_gateway) {
|
||||
// $config = $company_gateway->getConfig();
|
||||
|
||||
// if (property_exists($config, 'account_id') && strlen($config->account_id) > 5) {
|
||||
// return view('auth.connect.existing');
|
||||
// }
|
||||
// }
|
||||
|
||||
$stripe_client_id = config('ninja.ninja_stripe_client_id');
|
||||
$redirect_uri = config('ninja.app_url').'/stripe/completed';
|
||||
$endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_client_id}&redirect_uri={$redirect_uri}&scope=read_write&state={$token}";
|
||||
@ -98,6 +85,10 @@ class StripeConnectController extends BaseController
|
||||
return view('auth.connect.access_denied');
|
||||
}
|
||||
|
||||
if(!$request->getTokenContent()) {
|
||||
return view('auth.connect.session_expired');
|
||||
}
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
|
||||
|
||||
$company = Company::query()->where('company_key', $request->getTokenContent()['company_key'])->first();
|
||||
|
@ -37,7 +37,7 @@ class ShowCalculatedFieldRequest extends Request
|
||||
'date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom',
|
||||
'start_date' => 'bail|sometimes|date',
|
||||
'end_date' => 'bail|sometimes|date',
|
||||
'field' => 'required|bail|in:active_invoices, outstanding_invoices, completed_payments, refunded_payments, active_quotes, unapproved_quotes, logged_tasks, invoiced_tasks, paid_tasks, logged_expenses, pending_expenses, invoiced_expenses, invoice_paid_expenses',
|
||||
'field' => 'required|bail|in:active_invoices,outstanding_invoices,completed_payments,refunded_payments,active_quotes,unapproved_quotes logged_tasks,invoiced_tasks,paid_tasks,logged_expenses,pending_expenses,invoiced_expenses,invoice_paid_expenses',
|
||||
'calculation' => 'required|bail|in:sum,avg,count',
|
||||
'period' => 'required|bail|in:current,previous,total',
|
||||
'format' => 'sometimes|bail|in:time,money',
|
||||
|
@ -390,6 +390,7 @@ class BaseImport
|
||||
|
||||
try {
|
||||
$invoice_data = $invoice_transformer->transform($raw_invoice);
|
||||
$invoice_data['user_id'] = $this->company->owner()->id;
|
||||
|
||||
$invoice_data['line_items'] = $this->cleanItems(
|
||||
$invoice_data['line_items'] ?? []
|
||||
|
21
app/Interfaces/SyncInterface.php
Normal file
21
app/Interfaces/SyncInterface.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?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\Interfaces;
|
||||
|
||||
interface SyncInterface
|
||||
{
|
||||
public function find(string $id): mixed;
|
||||
|
||||
public function syncToNinja(array $records): void;
|
||||
|
||||
public function syncToForeign(array $records): void;
|
||||
}
|
@ -354,9 +354,9 @@ class CompanyImport implements ShouldQueue
|
||||
unlink($tmp_file);
|
||||
}
|
||||
|
||||
if(Storage::exists($this->file_location)) {
|
||||
unlink(Storage::path($this->file_location));
|
||||
}
|
||||
if(Storage::exists($this->file_location))
|
||||
Storage::delete($this->file_location);
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -154,6 +154,20 @@ class NinjaMailerJob implements ShouldQueue
|
||||
LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject))
|
||||
->send();
|
||||
|
||||
} catch(\Symfony\Component\Mailer\Exception\TransportException $e){
|
||||
nlog("Mailer failed with a Transport Exception {$e->getMessage()}");
|
||||
|
||||
if(Ninja::isHosted() && $this->mailer == 'smtp'){
|
||||
$settings = $this->nmo->settings;
|
||||
$settings->email_sending_method = 'default';
|
||||
$this->company->settings = $settings;
|
||||
$this->company->save();
|
||||
}
|
||||
|
||||
$this->fail();
|
||||
$this->cleanUpMailers();
|
||||
$this->logMailError($e->getMessage(), $this->company->clients()->first());
|
||||
|
||||
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
|
||||
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
|
||||
$this->fail();
|
||||
|
@ -89,7 +89,7 @@ class ProcessMailgunWebhook implements ShouldQueue
|
||||
{
|
||||
nlog($this->request);
|
||||
|
||||
if(!$this->request['event-data']['tags'][0]) {
|
||||
if(!$this->request['event-data']['tags'][0] ?? false) { //@phpstan-ignore-line
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -431,31 +431,31 @@ class BillingPortalPurchasev2 extends Component
|
||||
* @throws PresenterException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function createClientContact()
|
||||
{
|
||||
$company = $this->subscription->company;
|
||||
$user = $this->subscription->user;
|
||||
$user->setCompany($company);
|
||||
// private function createClientContact()
|
||||
// {
|
||||
// $company = $this->subscription->company;
|
||||
// $user = $this->subscription->user;
|
||||
// $user->setCompany($company);
|
||||
|
||||
$client_repo = new ClientRepository(new ClientContactRepository());
|
||||
$data = [
|
||||
'name' => '',
|
||||
'group_settings_id' => $this->subscription->group_id,
|
||||
'contacts' => [
|
||||
['email' => $this->email],
|
||||
],
|
||||
'client_hash' => Str::random(40),
|
||||
'settings' => ClientSettings::defaults(),
|
||||
];
|
||||
// $client_repo = new ClientRepository(new ClientContactRepository());
|
||||
// $data = [
|
||||
// 'name' => '',
|
||||
// 'group_settings_id' => $this->subscription->group_id,
|
||||
// 'contacts' => [
|
||||
// ['email' => $this->email],
|
||||
// ],
|
||||
// 'client_hash' => Str::random(40),
|
||||
// 'settings' => ClientSettings::defaults(),
|
||||
// ];
|
||||
|
||||
$client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
|
||||
// $client = $client_repo->save($data, ClientFactory::create($company->id, $user->id));
|
||||
|
||||
$this->contact = $client->fresh()->contacts()->first();
|
||||
// $this->contact = $client->fresh()->contacts()->first();
|
||||
|
||||
Auth::guard('contact')->loginUsingId($this->contact->id, true);
|
||||
// Auth::guard('contact')->loginUsingId($this->contact->id, true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
// return $this;
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
|
@ -80,7 +80,6 @@ class PaymentMethod extends Component
|
||||
{
|
||||
|
||||
nlog($e->getMessage());
|
||||
|
||||
$stopPropagation();
|
||||
|
||||
}
|
||||
|
@ -91,7 +91,6 @@ class ProcessPayment extends Component
|
||||
|
||||
$bag = new \Illuminate\Support\MessageBag();
|
||||
$bag->add('gateway_error', $e->getMessage());
|
||||
|
||||
session()->put('errors', $errors->put('default', $bag));
|
||||
|
||||
$invoice_id = $this->getContext()['payable_invoices'][0]['invoice_id'];
|
||||
|
@ -136,4 +136,12 @@ class RequiredFields extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
public function exception($e, $stopPropagation)
|
||||
{
|
||||
|
||||
nlog($e->getMessage());
|
||||
|
||||
$stopPropagation();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -13,13 +13,14 @@ namespace App\Mail\Admin;
|
||||
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Company;
|
||||
use App\Models\VendorContact;
|
||||
use App\Utils\Ninja;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class ClientUnsubscribedObject
|
||||
{
|
||||
public function __construct(
|
||||
public ClientContact $contact,
|
||||
public ClientContact | VendorContact$contact,
|
||||
public Company $company,
|
||||
private bool $use_react_link = false
|
||||
) {
|
||||
|
@ -11,23 +11,25 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\DataMapper\ClientSettings;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\DataMapper\FeesAndLimits;
|
||||
use App\Libraries\Currency\Conversion\CurrencyApi;
|
||||
use App\Models\Presenters\ClientPresenter;
|
||||
use App\Models\Traits\Excludable;
|
||||
use App\Services\Client\ClientService;
|
||||
use Laravel\Scout\Searchable;
|
||||
use App\Utils\Traits\AppSetup;
|
||||
use App\Utils\Traits\ClientGroupSettingsSaver;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Contracts\Translation\HasLocalePreference;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\DataMapper\FeesAndLimits;
|
||||
use App\Models\Traits\Excludable;
|
||||
use App\DataMapper\ClientSettings;
|
||||
use App\DataMapper\ClientSync;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Services\Client\ClientService;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
use App\Models\Presenters\ClientPresenter;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Utils\Traits\ClientGroupSettingsSaver;
|
||||
use App\Libraries\Currency\Conversion\CurrencyApi;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Contracts\Translation\HasLocalePreference;
|
||||
|
||||
/**
|
||||
* App\Models\Client
|
||||
@ -69,6 +71,7 @@ use Laracasts\Presenter\PresentableTrait;
|
||||
* @property int|null $shipping_country_id
|
||||
* @property object|null $settings
|
||||
* @property object|null $group_settings
|
||||
* @property object|null $sync
|
||||
* @property bool $is_deleted
|
||||
* @property int|null $group_settings_id
|
||||
* @property string|null $vat_number
|
||||
@ -124,6 +127,9 @@ class Client extends BaseModel implements HasLocalePreference
|
||||
use ClientGroupSettingsSaver;
|
||||
use Excludable;
|
||||
|
||||
|
||||
use Searchable;
|
||||
|
||||
protected $presenter = ClientPresenter::class;
|
||||
|
||||
protected $hidden = [
|
||||
@ -186,6 +192,7 @@ class Client extends BaseModel implements HasLocalePreference
|
||||
'last_login' => 'timestamp',
|
||||
'tax_data' => 'object',
|
||||
'e_invoice' => 'object',
|
||||
'sync' => ClientSync::class,
|
||||
];
|
||||
|
||||
protected $touches = [];
|
||||
@ -232,6 +239,38 @@ class Client extends BaseModel implements HasLocalePreference
|
||||
'custom_value4',
|
||||
];
|
||||
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'name' => $this->present()->name(),
|
||||
'is_deleted' => $this->is_deleted,
|
||||
'hashed_id' => $this->hashed_id,
|
||||
'number' => $this->number,
|
||||
'id_number' => $this->id_number,
|
||||
'vat_number' => $this->vat_number,
|
||||
'balance' => $this->balance,
|
||||
'paid_to_date' => $this->paid_to_date,
|
||||
'phone' => $this->phone,
|
||||
'address1' => $this->address1,
|
||||
'address2' => $this->address2,
|
||||
'city' => $this->city,
|
||||
'state' => $this->state,
|
||||
'postal_code' => $this->postal_code,
|
||||
'website' => $this->website,
|
||||
'private_notes' => $this->private_notes,
|
||||
'public_notes' => $this->public_notes,
|
||||
'shipping_address1' => $this->shipping_address1,
|
||||
'shipping_address2' => $this->shipping_address2,
|
||||
'shipping_city' => $this->shipping_city,
|
||||
'shipping_state' => $this->shipping_state,
|
||||
'shipping_postal_code' => $this->shipping_postal_code,
|
||||
'custom_value1' => $this->custom_value1,
|
||||
'custom_value2' => $this->custom_value2,
|
||||
'custom_value3' => $this->custom_value3,
|
||||
'custom_value4' => $this->custom_value4,
|
||||
'company_key' => $this->company->company_key,
|
||||
];
|
||||
}
|
||||
|
||||
public function getEntityType()
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ namespace App\Models;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Scout\Searchable;
|
||||
use App\Jobs\Mail\NinjaMailer;
|
||||
use App\Utils\Traits\AppSetup;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
@ -100,6 +101,8 @@ class ClientContact extends Authenticatable implements HasLocalePreference
|
||||
use HasFactory;
|
||||
use AppSetup;
|
||||
|
||||
use Searchable;
|
||||
|
||||
/* Used to authenticate a contact */
|
||||
protected $guard = 'contact';
|
||||
|
||||
@ -165,6 +168,23 @@ class ClientContact extends Authenticatable implements HasLocalePreference
|
||||
'email',
|
||||
];
|
||||
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'name' => $this->present()->search_display(),
|
||||
'hashed_id' => $this->client->hashed_id,
|
||||
'email' => $this->email,
|
||||
'first_name' => $this->first_name,
|
||||
'last_name' => $this->last_name,
|
||||
'phone' => $this->phone,
|
||||
'custom_value1' => $this->custom_value1,
|
||||
'custom_value2' => $this->custom_value2,
|
||||
'custom_value3' => $this->custom_value3,
|
||||
'custom_value4' => $this->custom_value4,
|
||||
'company_key' => $this->company->company_key,
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
V2 type of scope
|
||||
*/
|
||||
|
@ -489,7 +489,6 @@ class CompanyGateway extends BaseModel
|
||||
|
||||
public function getSettings()
|
||||
{
|
||||
// return $this->settings;
|
||||
return $this->settings ?? new \stdClass;
|
||||
}
|
||||
|
||||
|
@ -11,22 +11,24 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Events\Invoice\InvoiceReminderWasEmailed;
|
||||
use App\Events\Invoice\InvoiceWasEmailed;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Helpers\Invoice\InvoiceSumInclusive;
|
||||
use App\Models\Presenters\EntityPresenter;
|
||||
use App\Services\Invoice\InvoiceService;
|
||||
use App\Services\Ledger\LedgerService;
|
||||
use App\DataMapper\InvoiceSync;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\Invoice\ActionsInvoice;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use App\Utils\Traits\MakesInvoiceValues;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Utils\Traits\MakesReminders;
|
||||
use App\Utils\Traits\NumberFormatter;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Services\Ledger\LedgerService;
|
||||
use App\Services\Invoice\InvoiceService;
|
||||
use App\Utils\Traits\MakesInvoiceValues;
|
||||
use App\Events\Invoice\InvoiceWasEmailed;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
use App\Models\Presenters\EntityPresenter;
|
||||
use App\Helpers\Invoice\InvoiceSumInclusive;
|
||||
use App\Utils\Traits\Invoice\ActionsInvoice;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Events\Invoice\InvoiceReminderWasEmailed;
|
||||
|
||||
/**
|
||||
* App\Models\Invoice
|
||||
@ -52,6 +54,7 @@ use Laracasts\Presenter\PresentableTrait;
|
||||
* @property bool $is_deleted
|
||||
* @property object|array|string $line_items
|
||||
* @property object|null $backup
|
||||
* @property object|null $sync
|
||||
* @property string|null $footer
|
||||
* @property string|null $public_notes
|
||||
* @property string|null $private_notes
|
||||
@ -144,6 +147,8 @@ class Invoice extends BaseModel
|
||||
use MakesReminders;
|
||||
use ActionsInvoice;
|
||||
|
||||
use Searchable;
|
||||
|
||||
protected $presenter = EntityPresenter::class;
|
||||
|
||||
protected $touches = [];
|
||||
@ -210,6 +215,8 @@ class Invoice extends BaseModel
|
||||
'custom_surcharge_tax3' => 'bool',
|
||||
'custom_surcharge_tax4' => 'bool',
|
||||
'e_invoice' => 'object',
|
||||
'sync' => InvoiceSync::class,
|
||||
|
||||
];
|
||||
|
||||
protected $with = [];
|
||||
@ -235,6 +242,25 @@ class Invoice extends BaseModel
|
||||
|
||||
public const STATUS_UNPAID = -2; //status < 4 || < 3 && !is_deleted && !trashed()
|
||||
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'name' => $this->client->present()->name() . ' - ' . $this->number,
|
||||
'hashed_id' => $this->hashed_id,
|
||||
'number' => $this->number,
|
||||
'is_deleted' => $this->is_deleted,
|
||||
'amount' => (float) $this->amount,
|
||||
'balance' => (float) $this->balance,
|
||||
'due_date' => $this->due_date,
|
||||
'date' => $this->date,
|
||||
'custom_value1' => $this->custom_value1,
|
||||
'custom_value2' => $this->custom_value2,
|
||||
'custom_value3' => $this->custom_value3,
|
||||
'custom_value4' => $this->custom_value4,
|
||||
'company_key' => $this->company->company_key,
|
||||
];
|
||||
}
|
||||
|
||||
public function getEntityType()
|
||||
{
|
||||
return self::class;
|
||||
@ -559,7 +585,7 @@ class Invoice extends BaseModel
|
||||
* Filtering logic to determine
|
||||
* whether an invoice is locked
|
||||
* based on the current status of the invoice.
|
||||
* @return bool [description]
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocked(): bool
|
||||
{
|
||||
@ -569,7 +595,7 @@ class Invoice extends BaseModel
|
||||
case 'off':
|
||||
return false;
|
||||
case 'when_sent':
|
||||
return $this->status_id == self::STATUS_SENT;
|
||||
return $this->status_id >= self::STATUS_SENT;
|
||||
case 'when_paid':
|
||||
return $this->status_id == self::STATUS_PAID || $this->status_id == self::STATUS_PARTIAL;
|
||||
case 'end_of_month':
|
||||
@ -739,7 +765,7 @@ class Invoice extends BaseModel
|
||||
$send_email_enabled = ctrans('texts.send_email') . " " .ctrans('texts.enabled');
|
||||
$send_email_disabled = ctrans('texts.send_email') . " " .ctrans('texts.disabled');
|
||||
|
||||
$sends_email_1 = $settings->enable_reminder2 ? $send_email_enabled : $send_email_disabled;
|
||||
$sends_email_1 = $settings->enable_reminder1 ? $send_email_enabled : $send_email_disabled;
|
||||
$days_1 = $settings->num_days_reminder1 . " " . ctrans('texts.days');
|
||||
$schedule_1 = ctrans("texts.{$settings->schedule_reminder1}"); //after due date etc or disabled
|
||||
$label_1 = ctrans('texts.reminder1');
|
||||
@ -749,7 +775,7 @@ class Invoice extends BaseModel
|
||||
$schedule_2 = ctrans("texts.{$settings->schedule_reminder2}"); //after due date etc or disabled
|
||||
$label_2 = ctrans('texts.reminder2');
|
||||
|
||||
$sends_email_3 = $settings->enable_reminder2 ? $send_email_enabled : $send_email_disabled;
|
||||
$sends_email_3 = $settings->enable_reminder3 ? $send_email_enabled : $send_email_disabled;
|
||||
$days_3 = $settings->num_days_reminder3 . " " . ctrans('texts.days');
|
||||
$schedule_3 = ctrans("texts.{$settings->schedule_reminder3}"); //after due date etc or disabled
|
||||
$label_3 = ctrans('texts.reminder3');
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\DataMapper\ProductSync;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
@ -43,6 +44,7 @@ use League\CommonMark\CommonMarkConverter;
|
||||
* @property int|null $deleted_at
|
||||
* @property int|null $created_at
|
||||
* @property int|null $updated_at
|
||||
* @property object|null $sync
|
||||
* @property bool $is_deleted
|
||||
* @property float $in_stock_quantity
|
||||
* @property bool $stock_notification
|
||||
@ -100,6 +102,13 @@ class Product extends BaseModel
|
||||
'tax_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'updated_at' => 'timestamp',
|
||||
'created_at' => 'timestamp',
|
||||
'deleted_at' => 'timestamp',
|
||||
'sync' => ProductSync::class,
|
||||
];
|
||||
|
||||
public array $ubl_tax_map = [
|
||||
self::PRODUCT_TYPE_REVERSE_TAX => 'AE', // VAT_REVERSE_CHARGE =
|
||||
self::PRODUCT_TYPE_EXEMPT => 'E', // EXEMPT_FROM_TAX =
|
||||
|
@ -203,7 +203,6 @@ class VendorContact extends Authenticatable implements HasLocalePreference
|
||||
{
|
||||
return $this
|
||||
->withTrashed()
|
||||
// ->company()
|
||||
->where('id', $this->decodePrimaryKey($value))
|
||||
->firstOrFail();
|
||||
}
|
||||
@ -219,4 +218,15 @@ class VendorContact extends Authenticatable implements HasLocalePreference
|
||||
|
||||
return $domain.'/vendor/key_login/'.$this->contact_key;
|
||||
}
|
||||
|
||||
public function getAdminLink($use_react_link = false): string
|
||||
{
|
||||
return $use_react_link ? $this->getReactLink() : config('ninja.app_url');
|
||||
}
|
||||
|
||||
private function getReactLink(): string
|
||||
{
|
||||
return config('ninja.react_url')."/#/vendors/{$this->vendor->hashed_id}";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
@ -29,7 +29,7 @@ use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
|
||||
|
||||
class CreditCard implements LivewireMethodInterface
|
||||
{
|
||||
private Gateway $cba_gateway;
|
||||
private ?Gateway $cba_gateway;
|
||||
|
||||
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
|
||||
{
|
||||
@ -39,7 +39,6 @@ class CreditCard implements LivewireMethodInterface
|
||||
public function authorizeView(array $data)
|
||||
{
|
||||
$data['payment_method_id'] = GatewayType::CREDIT_CARD;
|
||||
$data['threeds'] = $this->powerboard->company_gateway->getConfigField('threeds');
|
||||
|
||||
return render('gateways.powerboard.credit_card.authorize', $this->paymentData($data));
|
||||
}
|
||||
@ -51,8 +50,6 @@ class CreditCard implements LivewireMethodInterface
|
||||
{
|
||||
$payment_source = $this->storePaymentSource($request);
|
||||
|
||||
nlog($payment_source);
|
||||
|
||||
$browser_details = json_decode($request->browser_details, true);
|
||||
|
||||
$payload = [
|
||||
@ -221,15 +218,15 @@ class CreditCard implements LivewireMethodInterface
|
||||
{
|
||||
$this->powerboard->init();
|
||||
|
||||
if($this->cba_gateway->verification_status != "completed")
|
||||
throw new PaymentFailed("This payment method is not configured as yet. Reference Powerboard portal for further information", 400);
|
||||
// if(!isset($this->cba_gateway->verification_status) || $this->cba_gateway->verification_status != "completed")
|
||||
// throw new PaymentFailed("This payment method is not configured as yet. Reference Powerboard portal for further information", 400);
|
||||
|
||||
$merge = [
|
||||
'public_key' => $this->powerboard->company_gateway->getConfigField('publicKey'),
|
||||
'widget_endpoint' => $this->powerboard->widget_endpoint,
|
||||
'gateway' => $this->powerboard,
|
||||
'environment' => $this->powerboard->environment,
|
||||
'gateway_id' => $this->cba_gateway->_id,
|
||||
'gateway_id' => $this->cba_gateway->_id ?? false,
|
||||
];
|
||||
|
||||
return array_merge($data, $merge);
|
||||
@ -451,7 +448,6 @@ class CreditCard implements LivewireMethodInterface
|
||||
match($error_object->error->code) {
|
||||
"UnfulfilledCondition" => $error_message = $error_object->error->details->messages[0] ?? $error_object->error->message ?? "Unknown error",
|
||||
"GatewayError" => $error_message = $error_object->error->message,
|
||||
"UnfulfilledCondition" => $error_message = $error_object->error->message,
|
||||
"transaction_declined" => $error_message = $error_object->error->details[0]->status_code_description,
|
||||
default => $error_message = $error_object->error->message ?? "Unknown error",
|
||||
};
|
||||
|
@ -13,35 +13,17 @@ namespace App\PaymentDrivers\CBAPowerBoard\Models;
|
||||
|
||||
class Gateway
|
||||
{
|
||||
/** @var string */
|
||||
public string $_id;
|
||||
/** @var string */
|
||||
public string $name;
|
||||
/** @var string */
|
||||
public string $type;
|
||||
/** @var string */
|
||||
public string $mode;
|
||||
/** @var string */
|
||||
public string $created_at;
|
||||
/** @var string */
|
||||
public string $updated_at;
|
||||
/** @var bool */
|
||||
public bool $archived;
|
||||
/** @var bool */
|
||||
public bool $default;
|
||||
/** @var string */
|
||||
public string $verification_status;
|
||||
|
||||
public function __construct(
|
||||
string $_id,
|
||||
string $name,
|
||||
string $type,
|
||||
string $mode,
|
||||
string $created_at,
|
||||
string $updated_at,
|
||||
bool $archived,
|
||||
bool $default,
|
||||
string $verification_status
|
||||
public string $_id,
|
||||
public string $name,
|
||||
public string $type,
|
||||
public string $mode,
|
||||
public string $created_at,
|
||||
public string $updated_at,
|
||||
public bool $archived,
|
||||
public bool $default,
|
||||
public string $verification_status = ''
|
||||
) {
|
||||
$this->_id = $_id;
|
||||
$this->name = $name;
|
||||
|
@ -13,10 +13,11 @@
|
||||
namespace App\PaymentDrivers\CBAPowerBoard;
|
||||
|
||||
|
||||
use App\PaymentDrivers\CBAPowerBoard\Models\Gateways;
|
||||
use App\Models\GatewayType;
|
||||
|
||||
use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
|
||||
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
|
||||
use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
|
||||
use App\PaymentDrivers\CBAPowerBoard\Models\Gateways;
|
||||
|
||||
class Settings
|
||||
{
|
||||
@ -29,20 +30,28 @@ class Settings
|
||||
{
|
||||
}
|
||||
|
||||
public function getGateways()
|
||||
/**
|
||||
* Returns the API response for the gateways
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGateways(): mixed
|
||||
{
|
||||
$r = $this->powerboard->gatewayRequest('/v1/gateways', (\App\Enum\HttpVerb::GET)->value, [], []);
|
||||
|
||||
if($r->failed())
|
||||
$r->throw();
|
||||
|
||||
nlog($r->object());
|
||||
|
||||
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $r->object()->resource->data);
|
||||
|
||||
}
|
||||
|
||||
/** We will need to have a process that updates this at intervals */
|
||||
/**
|
||||
* updateSettings from the API
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function updateSettings():self
|
||||
{
|
||||
$gateways = $this->getGateways();
|
||||
@ -54,11 +63,22 @@ class Settings
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* getSettings
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSettings(): mixed
|
||||
{
|
||||
return $this->powerboard->company_gateway->getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for getting the payment gateway configuration
|
||||
*
|
||||
* @param int $gateway_type_id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPaymentGatewayConfiguration(int $gateway_type_id): mixed
|
||||
{
|
||||
$type = self::GATEWAY_CBA;
|
||||
@ -71,24 +91,45 @@ class Settings
|
||||
return $this->getGatewayByType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CBA gateway object for a given gateway type
|
||||
*
|
||||
* @param string $gateway_type_const
|
||||
* @return mixed
|
||||
*/
|
||||
private function getGatewayByType(string $gateway_type_const): mixed
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if(!property_exists($settings,'gateways')){
|
||||
if(!property_exists($settings, 'gateways')){
|
||||
$this->updateSettings();
|
||||
$settings = $this->getSettings();
|
||||
}
|
||||
|
||||
$gateways = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $settings->gateways);
|
||||
|
||||
if ($gateway_type_const == self::GATEWAY_CBA && strlen($this->powerboard->company_gateway->getConfigField('gatewayId') ?? '') > 1) {
|
||||
|
||||
return collect($gateways)->first(function (Gateway $gateway) {
|
||||
return $gateway->_id == $this->powerboard->company_gateway->getConfigField('gatewayId');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return collect($gateways)->first(function (Gateway $gateway) use ($gateway_type_const){
|
||||
return $gateway->type == $gateway_type_const;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CBA gateway ID for a given gateway type
|
||||
*
|
||||
* @param int $gateway_type_id
|
||||
* @return string
|
||||
*/
|
||||
public function getGatewayId(int $gateway_type_id): string
|
||||
{
|
||||
|
||||
$gateway = $this->getPaymentGatewayConfiguration($gateway_type_id);
|
||||
|
||||
return $gateway->_id;
|
||||
|
@ -185,16 +185,10 @@ class CBAPowerBoardPaymentDriver extends BaseDriver
|
||||
{
|
||||
$this->init();
|
||||
|
||||
$this->settings()->updateSettings();
|
||||
|
||||
return true;
|
||||
// try {
|
||||
// $this->verifyConnect();
|
||||
// return true;
|
||||
// } catch(\Exception $e) {
|
||||
|
||||
// }
|
||||
|
||||
// return false;
|
||||
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
@ -4,7 +4,7 @@
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://opensource.org/licenses/AAL
|
||||
*/
|
||||
|
@ -111,7 +111,7 @@ class PaymentMethod implements MethodInterface, LivewireMethodInterface
|
||||
|
||||
$message = json_decode($e->getMessage(), true);
|
||||
|
||||
return redirect()->route('client.payment_methods.index')->withErrors(array_values($message['errors']));
|
||||
return redirect()->route('client.payment_methods.index')->withErrors(array_values($message['errors'] ?? [$e->getMessage()]));
|
||||
|
||||
}
|
||||
|
||||
|
@ -141,8 +141,8 @@ trait ChartCalculations
|
||||
}
|
||||
|
||||
match ($data['calculation']) {
|
||||
'sum' => $result = $q->sum('refunded'),
|
||||
'avg' => $result = $q->avg('refunded'),
|
||||
'sum' => $result = $q->sum('amount'),
|
||||
'avg' => $result = $q->avg('amount'),
|
||||
'count' => $result = $q->count(),
|
||||
default => $result = 0,
|
||||
};
|
||||
@ -287,14 +287,14 @@ trait ChartCalculations
|
||||
|
||||
return $query->get()
|
||||
->when($data['currency_id'] == '999', function ($collection) {
|
||||
$collection->map(function ($e) {
|
||||
return $collection->map(function ($e) {
|
||||
/** @var \App\Models\Expense $e */
|
||||
return $e->amount * $e->exchange_rate;
|
||||
});
|
||||
})
|
||||
->when($data['currency_id'] != '999', function ($collection) {
|
||||
|
||||
$collection->map(function ($e) {
|
||||
return $collection->map(function ($e) {
|
||||
|
||||
/** @var \App\Models\Expense $e */
|
||||
return $e->amount;
|
||||
|
@ -107,7 +107,7 @@ class Partner
|
||||
{
|
||||
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}";
|
||||
|
||||
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value)->object();
|
||||
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +138,7 @@ class Partner
|
||||
{
|
||||
$uri = "/partner/{$this->partner_number}/account/{$accountRegNo}/peppol/{$peppolId}";
|
||||
|
||||
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value)->object();
|
||||
return $this->qvalia->httpClient($uri, (\App\Enum\HttpVerb::DELETE)->value, [])->object();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -131,6 +131,7 @@ class ZugferdEDocument extends AbstractService
|
||||
$vendor->postal_code = $postcode;
|
||||
|
||||
$country = app('countries')->first(function ($c) use ($country) {
|
||||
/** @var \App\Models\Country $c */
|
||||
return $c->iso_3166_2 == $country || $c->iso_3166_3 == $country;
|
||||
});
|
||||
if ($country)
|
||||
|
@ -291,6 +291,20 @@ class Email implements ShouldQueue
|
||||
LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject))
|
||||
->send();
|
||||
|
||||
} catch(\Symfony\Component\Mailer\Exception\TransportException $e){
|
||||
nlog("Mailer failed with a Transport Exception {$e->getMessage()}");
|
||||
|
||||
if(Ninja::isHosted() && $this->mailer == 'smtp'){
|
||||
$settings = $this->email_object->settings;
|
||||
$settings->email_sending_method = 'default';
|
||||
$this->company->settings = $settings;
|
||||
$this->company->save();
|
||||
}
|
||||
|
||||
$this->fail();
|
||||
$this->cleanUpMailers();
|
||||
$this->logMailError($e->getMessage(), $this->company->clients()->first());
|
||||
|
||||
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
|
||||
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
|
||||
$this->fail();
|
||||
|
@ -47,7 +47,7 @@ class UpdateReminder extends AbstractService
|
||||
|
||||
if (is_null($this->invoice->reminder1_sent) &&
|
||||
$this->settings->schedule_reminder1 == 'after_invoice_date') {
|
||||
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1);
|
||||
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1)->addSeconds($offset);
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
$date_collection->push($reminder_date);
|
||||
@ -58,7 +58,7 @@ class UpdateReminder extends AbstractService
|
||||
($this->invoice->partial_due_date || $this->invoice->due_date) &&
|
||||
$this->settings->schedule_reminder1 == 'before_due_date') {
|
||||
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder1);
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder1)->addSeconds($offset);
|
||||
// nlog("1. {$reminder_date->format('Y-m-d')}");
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
@ -71,7 +71,7 @@ class UpdateReminder extends AbstractService
|
||||
$this->settings->schedule_reminder1 == 'after_due_date') {
|
||||
|
||||
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1);
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder1)->addSeconds($offset);
|
||||
// nlog("2. {$reminder_date->format('Y-m-d')}");
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
@ -81,7 +81,7 @@ class UpdateReminder extends AbstractService
|
||||
|
||||
if (is_null($this->invoice->reminder2_sent) &&
|
||||
$this->settings->schedule_reminder2 == 'after_invoice_date') {
|
||||
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2);
|
||||
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2)->addSeconds($offset);
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
$date_collection->push($reminder_date);
|
||||
@ -93,7 +93,7 @@ class UpdateReminder extends AbstractService
|
||||
$this->settings->schedule_reminder2 == 'before_due_date') {
|
||||
|
||||
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder2);
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder2)->addSeconds($offset);
|
||||
// nlog("3. {$reminder_date->format('Y-m-d')}");
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
@ -106,7 +106,7 @@ class UpdateReminder extends AbstractService
|
||||
$this->settings->schedule_reminder2 == 'after_due_date') {
|
||||
|
||||
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2);
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder2)->addSeconds($offset);
|
||||
// nlog("4. {$reminder_date->format('Y-m-d')}");
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
@ -116,7 +116,7 @@ class UpdateReminder extends AbstractService
|
||||
|
||||
if (is_null($this->invoice->reminder3_sent) &&
|
||||
$this->settings->schedule_reminder3 == 'after_invoice_date') {
|
||||
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3);
|
||||
$reminder_date = Carbon::parse($this->invoice->date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3)->addSeconds($offset);
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
$date_collection->push($reminder_date);
|
||||
@ -128,7 +128,7 @@ class UpdateReminder extends AbstractService
|
||||
$this->settings->schedule_reminder3 == 'before_due_date') {
|
||||
|
||||
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder3);
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->subDays((int)$this->settings->num_days_reminder3)->addSeconds($offset);
|
||||
// nlog("5. {$reminder_date->format('Y-m-d')}");
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
@ -141,7 +141,7 @@ class UpdateReminder extends AbstractService
|
||||
$this->settings->schedule_reminder3 == 'after_due_date') {
|
||||
|
||||
$partial_or_due_date = ($this->invoice->partial > 0 && isset($this->invoice->partial_due_date)) ? $this->invoice->partial_due_date : $this->invoice->due_date;
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3);
|
||||
$reminder_date = Carbon::parse($partial_or_due_date)->startOfDay()->addDays((int)$this->settings->num_days_reminder3)->addSeconds($offset);
|
||||
// nlog("6. {$reminder_date->format('Y-m-d')}");
|
||||
|
||||
if ($reminder_date->gt(now())) {
|
||||
@ -154,17 +154,15 @@ class UpdateReminder extends AbstractService
|
||||
($this->invoice->reminder1_sent || $this->settings->schedule_reminder1 == "" || !$this->settings->enable_reminder1) &&
|
||||
($this->invoice->reminder2_sent || $this->settings->schedule_reminder2 == "" || !$this->settings->enable_reminder2) &&
|
||||
($this->invoice->reminder3_sent || $this->settings->schedule_reminder3 == "" || !$this->settings->enable_reminder3)) {
|
||||
$reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int) $this->settings->endless_reminder_frequency_id);
|
||||
$reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int) $this->settings->endless_reminder_frequency_id)->addSeconds($offset);
|
||||
|
||||
if ($reminder_date) {
|
||||
if ($reminder_date->gt(now())) {
|
||||
$date_collection->push($reminder_date);
|
||||
}
|
||||
if ($reminder_date && $reminder_date->gt(now())) {
|
||||
$date_collection->push($reminder_date);
|
||||
}
|
||||
}
|
||||
|
||||
if ($date_collection->count() >= 1 && $date_collection->sort()->first()->gte(now())) {
|
||||
$this->invoice->next_send_date = $date_collection->sort()->first()->addSeconds($offset);
|
||||
$this->invoice->next_send_date = $date_collection->sort()->first();
|
||||
} else {
|
||||
$this->invoice->next_send_date = null;
|
||||
}
|
||||
|
245
app/Services/Quickbooks/Jobs/QuickbooksImport.php
Normal file
245
app/Services/Quickbooks/Jobs/QuickbooksImport.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?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\Services\Quickbooks\Jobs;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\Company;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\VendorFactory;
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\DataMapper\QuickbooksSync;
|
||||
use App\Factory\ClientContactFactory;
|
||||
use App\Factory\VendorContactFactory;
|
||||
use App\DataMapper\QuickbooksSettings;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
use QuickBooksOnline\API\Data\IPPSalesReceipt;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use App\Services\Quickbooks\Transformers\ClientTransformer;
|
||||
use App\Services\Quickbooks\Transformers\VendorTransformer;
|
||||
use App\Services\Quickbooks\Transformers\ExpenseTransformer;
|
||||
use App\Services\Quickbooks\Transformers\InvoiceTransformer;
|
||||
use App\Services\Quickbooks\Transformers\PaymentTransformer;
|
||||
use App\Services\Quickbooks\Transformers\ProductTransformer;
|
||||
|
||||
class QuickbooksImport implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
private array $entities = [
|
||||
'product' => 'Item',
|
||||
'client' => 'Customer',
|
||||
'invoice' => 'Invoice',
|
||||
'sales' => 'SalesReceipt',
|
||||
// 'quote' => 'Estimate',
|
||||
// 'purchase_order' => 'PurchaseOrder',
|
||||
// 'payment' => 'Payment',
|
||||
// 'vendor' => 'Vendor',
|
||||
// 'expense' => 'Purchase',
|
||||
];
|
||||
|
||||
private QuickbooksService $qbs;
|
||||
|
||||
private QuickbooksSync $settings;
|
||||
|
||||
private Company $company;
|
||||
|
||||
public function __construct(public int $company_id, public string $db)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
MultiDB::setDb($this->db);
|
||||
|
||||
$this->company = Company::query()->find($this->company_id);
|
||||
$this->qbs = new QuickbooksService($this->company);
|
||||
$this->settings = $this->company->quickbooks->settings;
|
||||
|
||||
foreach($this->entities as $key => $entity) {
|
||||
|
||||
if(!$this->qbs->syncable($key, \App\Enum\SyncDirection::PULL)) {
|
||||
nlog('skipping ' . $key);
|
||||
continue;
|
||||
}
|
||||
|
||||
$records = $this->qbs->sdk()->fetchRecords($entity);
|
||||
|
||||
$this->processEntitySync($key, $records);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the sync for a given entity
|
||||
*
|
||||
* @param string $entity
|
||||
* @param mixed $records
|
||||
* @return void
|
||||
*/
|
||||
private function processEntitySync(string $entity, $records): void
|
||||
{
|
||||
match($entity){
|
||||
'client' => $this->qbs->client->syncToNinja($records),
|
||||
'product' => $this->qbs->product->syncToNinja($records),
|
||||
'invoice' => $this->qbs->invoice->syncToNinja($records),
|
||||
'sales' => $this->qbs->invoice->syncToNinja($records),
|
||||
// 'vendor' => $this->syncQbToNinjaVendors($records),
|
||||
// 'quote' => $this->syncInvoices($records),
|
||||
// 'expense' => $this->syncQbToNinjaExpenses($records),
|
||||
// 'purchase_order' => $this->syncInvoices($records),
|
||||
// 'payment' => $this->syncPayment($records),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function syncQbToNinjaInvoices($records): void
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function syncQbToNinjaVendors(array $records): void
|
||||
{
|
||||
|
||||
$transformer = new VendorTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
$ninja_data = $transformer->qbToNinja($record);
|
||||
|
||||
if($vendor = $this->findVendor($ninja_data))
|
||||
{
|
||||
$vendor->fill($ninja_data[0]);
|
||||
$vendor->saveQuietly();
|
||||
|
||||
$contact = $vendor->contacts()->where('email', $ninja_data[1]['email'])->first();
|
||||
|
||||
if(!$contact)
|
||||
{
|
||||
$contact = VendorContactFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$contact->vendor_id = $vendor->id;
|
||||
$contact->send_email = true;
|
||||
$contact->is_primary = true;
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
elseif($this->qbs->syncable('vendor', \App\Enum\SyncDirection::PULL)){
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function syncQbToNinjaExpenses(array $records): void
|
||||
{
|
||||
|
||||
$transformer = new ExpenseTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
$ninja_data = $transformer->qbToNinja($record);
|
||||
|
||||
if($expense = $this->findExpense($ninja_data))
|
||||
{
|
||||
$expense->fill($ninja_data);
|
||||
$expense->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function findExpense(array $qb_data): ?Expense
|
||||
{
|
||||
$expense = $qb_data;
|
||||
|
||||
$search = Expense::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('number', $expense['number']);
|
||||
|
||||
if($search->count() == 0) {
|
||||
return ExpenseFactory::create($this->company->id, $this->company->owner()->id);
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->qbs->syncable('expense', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findVendor(array $qb_data) :?Vendor
|
||||
{
|
||||
$vendor = $qb_data[0];
|
||||
$contact = $qb_data[1];
|
||||
$vendor_meta = $qb_data[2];
|
||||
|
||||
$search = Vendor::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where(function ($q) use ($vendor, $vendor_meta, $contact){
|
||||
|
||||
$q->where('vendor_hash', $vendor_meta['vendor_hash'])
|
||||
->orWhere('number', $vendor['number'])
|
||||
->orWhereHas('contacts', function ($q) use ($contact){
|
||||
$q->where('email', $contact['email']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if($search->count() == 0) {
|
||||
//new client
|
||||
return VendorFactory::create($this->company->id, $this->company->owner()->id);
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
|
||||
return $this->qbs->syncable('vendor', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function middleware()
|
||||
{
|
||||
return [new WithoutOverlapping("qbs-{$this->company_id}-{$this->db}")];
|
||||
}
|
||||
|
||||
public function failed($exception)
|
||||
{
|
||||
nlog("QuickbooksSync failed => ".$exception->getMessage());
|
||||
config(['queue.failed.driver' => null]);
|
||||
|
||||
}
|
||||
}
|
@ -1,436 +0,0 @@
|
||||
<?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\Services\Quickbooks\Jobs;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Vendor;
|
||||
use App\Models\Company;
|
||||
use App\Models\Expense;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\VendorFactory;
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\Factory\ClientContactFactory;
|
||||
use App\Factory\VendorContactFactory;
|
||||
use App\DataMapper\QuickbooksSettings;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
use QuickBooksOnline\API\Data\IPPSalesReceipt;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use App\Services\Quickbooks\Transformers\ClientTransformer;
|
||||
use App\Services\Quickbooks\Transformers\VendorTransformer;
|
||||
use App\Services\Quickbooks\Transformers\ExpenseTransformer;
|
||||
use App\Services\Quickbooks\Transformers\InvoiceTransformer;
|
||||
use App\Services\Quickbooks\Transformers\PaymentTransformer;
|
||||
use App\Services\Quickbooks\Transformers\ProductTransformer;
|
||||
|
||||
class QuickbooksSync implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
private array $entities = [
|
||||
'product' => 'Item',
|
||||
'client' => 'Customer',
|
||||
'invoice' => 'Invoice',
|
||||
'quote' => 'Estimate',
|
||||
'purchase_order' => 'PurchaseOrder',
|
||||
'payment' => 'Payment',
|
||||
'sales' => 'SalesReceipt',
|
||||
'vendor' => 'Vendor',
|
||||
'expense' => 'Purchase',
|
||||
];
|
||||
|
||||
private QuickbooksService $qbs;
|
||||
|
||||
private ?array $settings;
|
||||
|
||||
private Company $company;
|
||||
|
||||
public function __construct(public int $company_id, public string $db)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
MultiDB::setDb($this->db);
|
||||
|
||||
$this->company = Company::query()->find($this->company_id);
|
||||
$this->qbs = new QuickbooksService($this->company);
|
||||
$this->settings = $this->company->quickbooks->settings;
|
||||
|
||||
nlog("here we go!");
|
||||
foreach($this->entities as $key => $entity) {
|
||||
if(!$this->syncGate($key, 'pull')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$records = $this->qbs->sdk()->fetchRecords($entity);
|
||||
|
||||
$this->processEntitySync($key, $records);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function syncGate(string $entity, string $direction): bool
|
||||
{
|
||||
return (bool) $this->settings[$entity]['sync'] && in_array($this->settings[$entity]['direction'], [$direction,'bidirectional']);
|
||||
}
|
||||
|
||||
private function updateGate(string $entity): bool
|
||||
{
|
||||
return (bool) $this->settings[$entity]['sync'] && $this->settings[$entity]['update_record'];
|
||||
}
|
||||
|
||||
// private function harvestQbEntityName(string $entity): string
|
||||
// {
|
||||
// return $this->entities[$entity];
|
||||
// }
|
||||
|
||||
private function processEntitySync(string $entity, $records)
|
||||
{
|
||||
match($entity){
|
||||
'client' => $this->syncQbToNinjaClients($records),
|
||||
'product' => $this->syncQbToNinjaProducts($records),
|
||||
'invoice' => $this->syncQbToNinjaInvoices($records),
|
||||
'sales' => $this->syncQbToNinjaInvoices($records),
|
||||
'vendor' => $this->syncQbToNinjaVendors($records),
|
||||
// 'quote' => $this->syncInvoices($records),
|
||||
'expense' => $this->syncQbToNinjaExpenses($records),
|
||||
// 'purchase_order' => $this->syncInvoices($records),
|
||||
// 'payment' => $this->syncPayment($records),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function syncQbToNinjaInvoices($records): void
|
||||
{
|
||||
nlog("invoice sync ". count($records));
|
||||
$invoice_transformer = new InvoiceTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
nlog($record);
|
||||
|
||||
$ninja_invoice_data = $invoice_transformer->qbToNinja($record);
|
||||
|
||||
nlog($ninja_invoice_data);
|
||||
|
||||
$payment_ids = $ninja_invoice_data['payment_ids'] ?? [];
|
||||
$client_id = $ninja_invoice_data['client_id'] ?? null;
|
||||
|
||||
if(is_null($client_id))
|
||||
continue;
|
||||
|
||||
unset($ninja_invoice_data['payment_ids']);
|
||||
|
||||
if($invoice = $this->findInvoice($ninja_invoice_data))
|
||||
{
|
||||
$invoice->fill($ninja_invoice_data);
|
||||
$invoice->saveQuietly();
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
foreach($payment_ids as $payment_id)
|
||||
{
|
||||
|
||||
$payment = $this->qbs->sdk->FindById('Payment', $payment_id);
|
||||
|
||||
$payment_transformer = new PaymentTransformer($this->company);
|
||||
|
||||
$transformed = $payment_transformer->qbToNinja($payment);
|
||||
|
||||
$ninja_payment = $payment_transformer->buildPayment($payment);
|
||||
$ninja_payment->service()->applyNumber()->save();
|
||||
|
||||
$paymentable = new \App\Models\Paymentable();
|
||||
$paymentable->payment_id = $ninja_payment->id;
|
||||
$paymentable->paymentable_id = $invoice->id;
|
||||
$paymentable->paymentable_type = 'invoices';
|
||||
$paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount');
|
||||
$paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line
|
||||
$paymentable->save();
|
||||
|
||||
$invoice->service()->applyPayment($ninja_payment, $paymentable->amount);
|
||||
|
||||
}
|
||||
|
||||
if($record instanceof IPPSalesReceipt)
|
||||
{
|
||||
$invoice->service()->markPaid()->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$ninja_invoice_data = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function findInvoice(array $ninja_invoice_data): ?Invoice
|
||||
{
|
||||
$search = Invoice::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('number', $ninja_invoice_data['number']);
|
||||
|
||||
if($search->count() == 0) {
|
||||
//new invoice
|
||||
$invoice = InvoiceFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$invoice->client_id = $ninja_invoice_data['client_id'];
|
||||
|
||||
return $invoice;
|
||||
} elseif($search->count() == 1) {
|
||||
return $this->settings['invoice']['update_record'] ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private function syncQbToNinjaClients(array $records): void
|
||||
{
|
||||
|
||||
$client_transformer = new ClientTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
$ninja_client_data = $client_transformer->qbToNinja($record);
|
||||
|
||||
if($client = $this->findClient($ninja_client_data))
|
||||
{
|
||||
$client->fill($ninja_client_data[0]);
|
||||
$client->saveQuietly();
|
||||
|
||||
$contact = $client->contacts()->where('email', $ninja_client_data[1]['email'])->first();
|
||||
|
||||
if(!$contact)
|
||||
{
|
||||
$contact = ClientContactFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$contact->client_id = $client->id;
|
||||
$contact->send_email = true;
|
||||
$contact->is_primary = true;
|
||||
$contact->fill($ninja_client_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
elseif($this->updateGate('client')){
|
||||
$contact->fill($ninja_client_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function syncQbToNinjaVendors(array $records): void
|
||||
{
|
||||
|
||||
$transformer = new VendorTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
$ninja_data = $transformer->qbToNinja($record);
|
||||
|
||||
if($vendor = $this->findVendor($ninja_data))
|
||||
{
|
||||
$vendor->fill($ninja_data[0]);
|
||||
$vendor->saveQuietly();
|
||||
|
||||
$contact = $vendor->contacts()->where('email', $ninja_data[1]['email'])->first();
|
||||
|
||||
if(!$contact)
|
||||
{
|
||||
$contact = VendorContactFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$contact->vendor_id = $vendor->id;
|
||||
$contact->send_email = true;
|
||||
$contact->is_primary = true;
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
elseif($this->updateGate('vendor')){
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private function syncQbToNinjaExpenses(array $records): void
|
||||
{
|
||||
|
||||
$transformer = new ExpenseTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
$ninja_data = $transformer->qbToNinja($record);
|
||||
|
||||
if($expense = $this->findExpense($ninja_data))
|
||||
{
|
||||
$expense->fill($ninja_data);
|
||||
$expense->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function syncQbToNinjaProducts($records): void
|
||||
{
|
||||
$product_transformer = new ProductTransformer($this->company);
|
||||
|
||||
foreach($records as $record)
|
||||
{
|
||||
$ninja_data = $product_transformer->qbToNinja($record);
|
||||
|
||||
if($product = $this->findProduct($ninja_data['hash']))
|
||||
{
|
||||
$product->fill($ninja_data);
|
||||
$product->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function findExpense(array $qb_data): ?Expense
|
||||
{
|
||||
$expense = $qb_data;
|
||||
|
||||
$search = Expense::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('number', $expense['number']);
|
||||
|
||||
if($search->count() == 0) {
|
||||
return ExpenseFactory::create($this->company->id, $this->company->owner()->id);
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->settings['expense']['update_record'] ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findVendor(array $qb_data) :?Vendor
|
||||
{
|
||||
$vendor = $qb_data[0];
|
||||
$contact = $qb_data[1];
|
||||
$vendor_meta = $qb_data[2];
|
||||
|
||||
$search = Vendor::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where(function ($q) use ($vendor, $vendor_meta, $contact){
|
||||
|
||||
$q->where('vendor_hash', $vendor_meta['vendor_hash'])
|
||||
->orWhere('number', $vendor['number'])
|
||||
->orWhereHas('contacts', function ($q) use ($contact){
|
||||
$q->where('email', $contact['email']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if($search->count() == 0) {
|
||||
//new client
|
||||
return VendorFactory::create($this->company->id, $this->company->owner()->id);
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->settings['vendor']['update_record'] ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findClient(array $qb_data) :?Client
|
||||
{
|
||||
$client = $qb_data[0];
|
||||
$contact = $qb_data[1];
|
||||
$client_meta = $qb_data[2];
|
||||
|
||||
$search = Client::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where(function ($q) use ($client, $client_meta, $contact){
|
||||
|
||||
$q->where('client_hash', $client_meta['client_hash'])
|
||||
->orWhere('number', $client['number'])
|
||||
->orWhereHas('contacts', function ($q) use ($contact){
|
||||
$q->where('email', $contact['email']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if($search->count() == 0) {
|
||||
//new client
|
||||
$client = ClientFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$client->client_hash = $client_meta['client_hash'];
|
||||
$client->settings = $client_meta['settings'];
|
||||
|
||||
return $client;
|
||||
}
|
||||
elseif($search->count() == 1) {
|
||||
return $this->settings['client']['update_record'] ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function findProduct(string $key): ?Product
|
||||
{
|
||||
$search = Product::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('hash', $key);
|
||||
|
||||
if($search->count() == 0) {
|
||||
//new product
|
||||
$product = ProductFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$product->hash = $key;
|
||||
|
||||
return $product;
|
||||
} elseif($search->count() == 1) {
|
||||
return $this->settings['product']['update_record'] ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function middleware()
|
||||
{
|
||||
return [new WithoutOverlapping("qbs-{$this->company_id}-{$this->db}")];
|
||||
}
|
||||
|
||||
public function failed($exception)
|
||||
{
|
||||
nlog("QuickbooksSync failed => ".$exception->getMessage());
|
||||
config(['queue.failed.driver' => null]);
|
||||
|
||||
}
|
||||
}
|
116
app/Services/Quickbooks/Models/QbClient.php
Normal file
116
app/Services/Quickbooks/Models/QbClient.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?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\Services\Quickbooks\Models;
|
||||
|
||||
use App\Models\Client;
|
||||
use App\DataMapper\ClientSync;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Interfaces\SyncInterface;
|
||||
use App\Factory\ClientContactFactory;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
use App\Services\Quickbooks\Transformers\ClientTransformer;
|
||||
|
||||
class QbClient implements SyncInterface
|
||||
{
|
||||
public function __construct(public QuickbooksService $service)
|
||||
{
|
||||
}
|
||||
|
||||
public function find(string $id): mixed
|
||||
{
|
||||
return $this->service->sdk->FindById('Customer', $id);
|
||||
}
|
||||
|
||||
public function syncToNinja(array $records): void
|
||||
{
|
||||
|
||||
$transformer = new ClientTransformer($this->service->company);
|
||||
|
||||
foreach ($records as $record) {
|
||||
|
||||
$ninja_data = $transformer->qbToNinja($record);
|
||||
|
||||
if($ninja_data[0]['terms']){
|
||||
|
||||
$days = $this->service->findEntityById('Term', $ninja_data[0]['terms']);
|
||||
|
||||
nlog($days);
|
||||
|
||||
if($days){
|
||||
$ninja_data[0]['settings']->payment_terms = (string)$days->DueDays;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($client = $this->findClient($ninja_data[0]['id'])) {
|
||||
|
||||
$qbc = $this->find($ninja_data[0]['id']);
|
||||
|
||||
$client->fill($ninja_data[0]);
|
||||
$client->service()->applyNumber()->save();
|
||||
|
||||
$contact = $client->contacts()->where('email', $ninja_data[1]['email'])->first();
|
||||
|
||||
if(!$contact)
|
||||
{
|
||||
$contact = ClientContactFactory::create($this->service->company->id, $this->service->company->owner()->id);
|
||||
$contact->client_id = $client->id;
|
||||
$contact->send_email = true;
|
||||
$contact->is_primary = true;
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
elseif($this->service->syncable('client', \App\Enum\SyncDirection::PULL)){
|
||||
$contact->fill($ninja_data[1]);
|
||||
$contact->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function syncToForeign(array $records): void
|
||||
{
|
||||
}
|
||||
|
||||
public function sync(string $id, string $last_updated): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private function findClient(string $key): ?Client
|
||||
{
|
||||
$search = Client::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->service->company->id)
|
||||
->where('sync->qb_id', $key);
|
||||
|
||||
if ($search->count() == 0) {
|
||||
|
||||
$client = ClientFactory::create($this->service->company->id, $this->service->company->owner()->id);
|
||||
|
||||
$sync = new ClientSync();
|
||||
$sync->qb_id = $key;
|
||||
$client->sync = $sync;
|
||||
|
||||
return $client;
|
||||
|
||||
} elseif ($search->count() == 1) {
|
||||
return $this->service->syncable('client', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
|
||||
}
|
||||
}
|
221
app/Services/Quickbooks/Models/QbInvoice.php
Normal file
221
app/Services/Quickbooks/Models/QbInvoice.php
Normal file
@ -0,0 +1,221 @@
|
||||
<?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\Services\Quickbooks\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Invoice;
|
||||
use App\DataMapper\InvoiceSync;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Interfaces\SyncInterface;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
use App\Services\Quickbooks\Transformers\InvoiceTransformer;
|
||||
use App\Services\Quickbooks\Transformers\PaymentTransformer;
|
||||
|
||||
class QbInvoice implements SyncInterface
|
||||
{
|
||||
protected InvoiceTransformer $invoice_transformer;
|
||||
|
||||
protected InvoiceRepository $invoice_repository;
|
||||
|
||||
public function __construct(public QuickbooksService $service)
|
||||
{
|
||||
$this->invoice_transformer = new InvoiceTransformer($this->service->company);
|
||||
$this->invoice_repository = new InvoiceRepository();
|
||||
}
|
||||
|
||||
public function find(string $id): mixed
|
||||
{
|
||||
return $this->service->sdk->FindById('Invoice', $id);
|
||||
}
|
||||
|
||||
public function syncToNinja(array $records): void
|
||||
{
|
||||
|
||||
foreach ($records as $record) {
|
||||
|
||||
$this->syncNinjaInvoice($record);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function syncToForeign(array $records): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private function qbInvoiceUpdate(array $ninja_invoice_data, Invoice $invoice): void
|
||||
{
|
||||
$current_ninja_invoice_balance = $invoice->balance;
|
||||
$qb_invoice_balance = $ninja_invoice_data['balance'];
|
||||
|
||||
if(floatval($current_ninja_invoice_balance) == floatval($qb_invoice_balance))
|
||||
{
|
||||
nlog('Invoice balance is the same, skipping update of line items');
|
||||
unset($ninja_invoice_data['line_items']);
|
||||
$invoice->fill($ninja_invoice_data);
|
||||
$invoice->saveQuietly();
|
||||
}
|
||||
else{
|
||||
nlog('Invoice balance is different, updating line items');
|
||||
$this->invoice_repository->save($ninja_invoice_data, $invoice);
|
||||
}
|
||||
}
|
||||
|
||||
private function findInvoice(string $id, ?string $client_id = null): ?Invoice
|
||||
{
|
||||
$search = Invoice::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->service->company->id)
|
||||
->where('sync->qb_id', $id);
|
||||
|
||||
if($search->count() == 0) {
|
||||
$invoice = InvoiceFactory::create($this->service->company->id, $this->service->company->owner()->id);
|
||||
$invoice->client_id = (int)$client_id;
|
||||
|
||||
$sync = new InvoiceSync();
|
||||
$sync->qb_id = $id;
|
||||
$invoice->sync = $sync;
|
||||
|
||||
return $invoice;
|
||||
} elseif($search->count() == 1) {
|
||||
return $this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public function sync($id, string $last_updated): void
|
||||
{
|
||||
|
||||
$qb_record = $this->find($id);
|
||||
|
||||
nlog($qb_record);
|
||||
|
||||
if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL))
|
||||
{
|
||||
|
||||
$invoice = $this->findInvoice($id);
|
||||
|
||||
nlog("Comparing QB last updated: " . $last_updated);
|
||||
nlog("Comparing Ninja last updated: " . $invoice->updated_at);
|
||||
|
||||
if(data_get($qb_record, 'TxnStatus') === 'Voided')
|
||||
{
|
||||
$this->delete($id);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$invoice->id){
|
||||
$this->syncNinjaInvoice($qb_record);
|
||||
}
|
||||
elseif(Carbon::parse($last_updated)->gt(Carbon::parse($invoice->updated_at)) || $qb_record->SyncToken == '0')
|
||||
{
|
||||
$ninja_invoice_data = $this->invoice_transformer->qbToNinja($qb_record);
|
||||
nlog($ninja_invoice_data);
|
||||
|
||||
$this->invoice_repository->save($ninja_invoice_data, $invoice);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* syncNinjaInvoice
|
||||
*
|
||||
* @param $record
|
||||
* @return void
|
||||
*/
|
||||
public function syncNinjaInvoice($record): void
|
||||
{
|
||||
|
||||
$ninja_invoice_data = $this->invoice_transformer->qbToNinja($record);
|
||||
|
||||
nlog($ninja_invoice_data);
|
||||
|
||||
$payment_ids = $ninja_invoice_data['payment_ids'] ?? [];
|
||||
|
||||
$client_id = $ninja_invoice_data['client_id'] ?? null;
|
||||
|
||||
if (is_null($client_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($ninja_invoice_data['payment_ids']);
|
||||
|
||||
if ($invoice = $this->findInvoice($ninja_invoice_data['id'], $ninja_invoice_data['client_id'])) {
|
||||
|
||||
if ($invoice->id) {
|
||||
$this->qbInvoiceUpdate($ninja_invoice_data, $invoice);
|
||||
}
|
||||
|
||||
//new invoice scaffold
|
||||
$invoice->fill($ninja_invoice_data);
|
||||
$invoice->saveQuietly();
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->applyNumber()->createInvitations()->save();
|
||||
|
||||
foreach ($payment_ids as $payment_id) {
|
||||
|
||||
$payment = $this->service->sdk->FindById('Payment', $payment_id);
|
||||
|
||||
$payment_transformer = new PaymentTransformer($this->service->company);
|
||||
|
||||
$transformed = $payment_transformer->qbToNinja($payment);
|
||||
|
||||
$ninja_payment = $payment_transformer->buildPayment($payment);
|
||||
$ninja_payment->service()->applyNumber()->save();
|
||||
|
||||
$paymentable = new \App\Models\Paymentable();
|
||||
$paymentable->payment_id = $ninja_payment->id;
|
||||
$paymentable->paymentable_id = $invoice->id;
|
||||
$paymentable->paymentable_type = 'invoices';
|
||||
$paymentable->amount = $transformed['applied'] + $ninja_payment->credits->sum('amount');
|
||||
$paymentable->created_at = $ninja_payment->date; //@phpstan-ignore-line
|
||||
$paymentable->save();
|
||||
|
||||
$invoice->service()->applyPayment($ninja_payment, $paymentable->amount);
|
||||
|
||||
}
|
||||
|
||||
if ($record instanceof \QuickBooksOnline\API\Data\IPPSalesReceipt) {
|
||||
$invoice->service()->markPaid()->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$ninja_invoice_data = false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the invoice from Ninja and sets the sync to null
|
||||
*
|
||||
* @param string $id
|
||||
* @return void
|
||||
*/
|
||||
public function delete($id): void
|
||||
{
|
||||
$qb_record = $this->find($id);
|
||||
|
||||
if($this->service->syncable('invoice', \App\Enum\SyncDirection::PULL) && $invoice = $this->findInvoice($id))
|
||||
{
|
||||
$invoice->sync = null;
|
||||
$invoice->saveQuietly();
|
||||
$this->invoice_repository->delete($invoice);
|
||||
}
|
||||
}
|
||||
}
|
102
app/Services/Quickbooks/Models/QbProduct.php
Normal file
102
app/Services/Quickbooks/Models/QbProduct.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?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\Services\Quickbooks\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\Product;
|
||||
use App\DataMapper\ProductSync;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\Interfaces\SyncInterface;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
use App\Services\Quickbooks\Transformers\ProductTransformer;
|
||||
|
||||
|
||||
class QbProduct implements SyncInterface
|
||||
{
|
||||
protected ProductTransformer $product_transformer;
|
||||
|
||||
public function __construct(public QuickbooksService $service)
|
||||
{
|
||||
|
||||
$this->product_transformer = new ProductTransformer($service->company);
|
||||
|
||||
}
|
||||
|
||||
public function find(string $id): mixed
|
||||
{
|
||||
return $this->service->sdk->FindById('Item', $id);
|
||||
}
|
||||
|
||||
public function syncToNinja(array $records): void
|
||||
{
|
||||
|
||||
foreach ($records as $record) {
|
||||
|
||||
$ninja_data = $this->product_transformer->qbToNinja($record);
|
||||
|
||||
if ($product = $this->findProduct($ninja_data['id'])) {
|
||||
$product->fill($ninja_data);
|
||||
$product->save();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function syncToForeign(array $records): void
|
||||
{
|
||||
}
|
||||
|
||||
private function findProduct(string $key): ?Product
|
||||
{
|
||||
$search = Product::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->service->company->id)
|
||||
->where('sync->qb_id', $key);
|
||||
|
||||
if($search->count() == 0) {
|
||||
|
||||
$product = ProductFactory::create($this->service->company->id, $this->service->company->owner()->id);
|
||||
|
||||
$sync = new ProductSync();
|
||||
$sync->qb_id = $key;
|
||||
$product->sync = $sync;
|
||||
|
||||
return $product;
|
||||
|
||||
} elseif($search->count() == 1) {
|
||||
return $this->service->syncable('product', \App\Enum\SyncDirection::PULL) ? $search->first() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public function sync(string $id, string $last_updated): void
|
||||
{
|
||||
$qb_record = $this->find($id);
|
||||
|
||||
if($this->service->syncable('product', \App\Enum\SyncDirection::PULL) && $ninja_record = $this->findProduct($id))
|
||||
{
|
||||
|
||||
if(Carbon::parse($last_updated) > Carbon::parse($ninja_record->updated_at))
|
||||
{
|
||||
$ninja_data = $this->product_transformer->qbToNinja($qb_record);
|
||||
|
||||
$ninja_record->fill($ninja_data);
|
||||
$ninja_record->save();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -11,17 +11,21 @@
|
||||
|
||||
namespace App\Services\Quickbooks;
|
||||
|
||||
use App\Factory\ClientContactFactory;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\Services\Quickbooks\Jobs\QuickbooksSync;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\DataMapper\QuickbooksSync;
|
||||
use App\Factory\ClientContactFactory;
|
||||
use QuickBooksOnline\API\Core\CoreConstants;
|
||||
use App\Services\Quickbooks\Models\QbInvoice;
|
||||
use App\Services\Quickbooks\Models\QbProduct;
|
||||
use QuickBooksOnline\API\DataService\DataService;
|
||||
use App\Services\Quickbooks\Jobs\QuickbooksImport;
|
||||
use App\Services\Quickbooks\Models\QbClient;
|
||||
use App\Services\Quickbooks\Transformers\ClientTransformer;
|
||||
use App\Services\Quickbooks\Transformers\InvoiceTransformer;
|
||||
use App\Services\Quickbooks\Transformers\PaymentTransformer;
|
||||
@ -31,9 +35,19 @@ class QuickbooksService
|
||||
{
|
||||
public DataService $sdk;
|
||||
|
||||
public QbInvoice $invoice;
|
||||
|
||||
public QbProduct $product;
|
||||
|
||||
public QbClient $client;
|
||||
|
||||
public QuickbooksSync $settings;
|
||||
|
||||
private bool $testMode = true;
|
||||
|
||||
public function __construct(private Company $company)
|
||||
private bool $try_refresh = true;
|
||||
|
||||
public function __construct(public Company $company)
|
||||
{
|
||||
$this->init();
|
||||
}
|
||||
@ -46,7 +60,6 @@ class QuickbooksService
|
||||
'ClientSecret' => config('services.quickbooks.client_secret'),
|
||||
'auth_mode' => 'oauth2',
|
||||
'scope' => "com.intuit.quickbooks.accounting",
|
||||
// 'RedirectURI' => 'https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl',
|
||||
'RedirectURI' => $this->testMode ? 'https://grok.romulus.com.au/quickbooks/authorized' : 'https://invoicing.co/quickbooks/authorized',
|
||||
'baseUrl' => $this->testMode ? CoreConstants::SANDBOX_DEVELOPMENT : CoreConstants::QBO_BASEURL,
|
||||
];
|
||||
@ -55,18 +68,85 @@ class QuickbooksService
|
||||
|
||||
$this->sdk = DataService::Configure($merged);
|
||||
|
||||
$this->sdk->setLogLocation(storage_path("logs/quickbooks.log"));
|
||||
// $this->sdk->setLogLocation(storage_path("logs/quickbooks.log"));
|
||||
$this->sdk->enableLog();
|
||||
|
||||
$this->sdk->setMinorVersion("73");
|
||||
$this->sdk->throwExceptionOnError(true);
|
||||
|
||||
$this->checkToken();
|
||||
|
||||
$this->invoice = new QbInvoice($this);
|
||||
|
||||
$this->product = new QbProduct($this);
|
||||
|
||||
$this->client = new QbClient($this);
|
||||
|
||||
$this->settings = $this->company->quickbooks->settings;
|
||||
|
||||
$this->checkDefaultAccounts();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function checkDefaultAccounts(): self
|
||||
{
|
||||
|
||||
$accountQuery = "SELECT * FROM Account WHERE AccountType IN ('Income', 'Cost of Goods Sold')";
|
||||
|
||||
if(strlen($this->settings->default_income_account) == 0 || strlen($this->settings->default_expense_account) == 0){
|
||||
|
||||
nlog("Checking default accounts for company {$this->company->company_key}");
|
||||
$accounts = $this->sdk->Query($accountQuery);
|
||||
|
||||
nlog($accounts);
|
||||
|
||||
$find_income_account = true;
|
||||
$find_expense_account = true;
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if ($account->AccountType->value == 'Income' && $find_income_account) {
|
||||
$this->settings->default_income_account = $account->Id->value;
|
||||
$find_income_account = false;
|
||||
} elseif ($account->AccountType->value == 'Cost of Goods Sold' && $find_expense_account) {
|
||||
$this->settings->default_expense_account = $account->Id->value;
|
||||
$find_expense_account = false;
|
||||
}
|
||||
}
|
||||
|
||||
nlog($this->settings);
|
||||
|
||||
$this->company->quickbooks->settings = $this->settings;
|
||||
$this->company->save();
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function checkToken(): self
|
||||
{
|
||||
|
||||
if($this->company->quickbooks->accessTokenExpiresAt == 0 || $this->company->quickbooks->accessTokenExpiresAt > time())
|
||||
return $this;
|
||||
|
||||
if($this->company->quickbooks->accessTokenExpiresAt && $this->company->quickbooks->accessTokenExpiresAt < time() && $this->try_refresh){
|
||||
$this->sdk()->refreshToken($this->company->quickbooks->refresh_token);
|
||||
$this->company = $this->company->fresh();
|
||||
$this->try_refresh = false;
|
||||
$this->init();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
nlog('Quickbooks token expired and could not be refreshed => ' .$this->company->company_key);
|
||||
throw new \Exception('Quickbooks token expired and could not be refreshed');
|
||||
|
||||
}
|
||||
|
||||
private function ninjaAccessToken(): array
|
||||
{
|
||||
return isset($this->company->quickbooks->accessTokenKey) ? [
|
||||
return $this->company->quickbooks->accessTokenExpiresAt > 0 ? [
|
||||
'accessTokenKey' => $this->company->quickbooks->accessTokenKey,
|
||||
'refreshTokenKey' => $this->company->quickbooks->refresh_token,
|
||||
'QBORealmID' => $this->company->quickbooks->realmID,
|
||||
@ -85,7 +165,24 @@ class QuickbooksService
|
||||
*/
|
||||
public function syncFromQb(): void
|
||||
{
|
||||
QuickbooksSync::dispatch($this->company->id, $this->company->db);
|
||||
QuickbooksImport::dispatch($this->company->id, $this->company->db);
|
||||
}
|
||||
|
||||
public function findEntityById(string $entity, string $id): mixed
|
||||
{
|
||||
return $this->sdk->FindById($entity, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to determine if a sync is allowed in either direction
|
||||
*
|
||||
* @param string $entity
|
||||
* @param \App\Enum\SyncDirection $direction
|
||||
* @return bool
|
||||
*/
|
||||
public function syncable(string $entity, \App\Enum\SyncDirection $direction): bool
|
||||
{
|
||||
return $this->settings->{$entity}->direction === $direction || $this->settings->{$entity}->direction === \App\Enum\SyncDirection::BIDIRECTIONAL;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class SdkWrapper
|
||||
private function init(): self
|
||||
{
|
||||
|
||||
isset($this->company->quickbooks->accessTokenKey) ? $this->setNinjaAccessToken($this->company->quickbooks) : null;
|
||||
$this->setNinjaAccessToken($this->company->quickbooks);
|
||||
|
||||
return $this;
|
||||
|
||||
@ -104,16 +104,24 @@ class SdkWrapper
|
||||
|
||||
$this->setAccessToken($token);
|
||||
|
||||
if($token_object->accessTokenExpiresAt < time()){
|
||||
$new_token = $this->sdk->getOAuth2LoginHelper()->refreshToken();
|
||||
|
||||
$this->setAccessToken($new_token);
|
||||
$this->saveOAuthToken($this->accessToken());
|
||||
if($token_object->accessTokenExpiresAt != 0 && $token_object->accessTokenExpiresAt < time()){
|
||||
$this->refreshToken($token_object->refresh_token);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function refreshToken(string $refresh_token): self
|
||||
{
|
||||
$new_token = $this->sdk->getOAuth2LoginHelper()->refreshAccessTokenWithRefreshToken($refresh_token);
|
||||
|
||||
$this->setAccessToken($new_token);
|
||||
$this->saveOAuthToken($this->accessToken());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetsAccessToken
|
||||
*
|
||||
|
@ -66,7 +66,8 @@ class BaseTransformer
|
||||
$client = Client::query()
|
||||
->withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('number', $customer_reference_id)
|
||||
// ->where('number', $customer_reference_id)
|
||||
->where('sync->qb_id', $customer_reference_id)
|
||||
->first();
|
||||
|
||||
return $client ? $client->id : null;
|
||||
|
@ -31,6 +31,7 @@ class ClientTransformer extends BaseTransformer
|
||||
|
||||
public function transform(mixed $data): array
|
||||
{
|
||||
nlog($data);
|
||||
|
||||
$contact = [
|
||||
'first_name' => data_get($data, 'GivenName'),
|
||||
@ -40,6 +41,7 @@ class ClientTransformer extends BaseTransformer
|
||||
];
|
||||
|
||||
$client = [
|
||||
'id' => data_get($data, 'Id.value', null),
|
||||
'name' => data_get($data,'CompanyName', ''),
|
||||
'address1' => data_get($data, 'BillAddr.Line1', ''),
|
||||
'address2' => data_get($data, 'BillAddr.Line2', ''),
|
||||
@ -53,16 +55,20 @@ class ClientTransformer extends BaseTransformer
|
||||
'shipping_country_id' => $this->resolveCountry(data_get($data, 'ShipAddr.Country', '')),
|
||||
'shipping_state' => data_get($data, 'ShipAddr.CountrySubDivisionCode', ''),
|
||||
'shipping_postal_code' => data_get($data, 'BillAddr.PostalCode', ''),
|
||||
'number' => data_get($data, 'Id.value', ''),
|
||||
'client_hash' => data_get($data, 'V4IDPseudonym', \Illuminate\Support\Str::random(32)),
|
||||
'vat_number' => data_get($data, 'PrimaryTaxIdentifier', ''),
|
||||
'id_number' => data_get($data, 'BusinessNumber', ''),
|
||||
'terms' => data_get($data, 'SalesTermRef.value', false),
|
||||
'is_tax_exempt' => !data_get($data, 'Taxable', false),
|
||||
'private_notes' => data_get($data, 'Notes', ''),
|
||||
];
|
||||
|
||||
$settings = ClientSettings::defaults();
|
||||
$settings->currency_id = (string) $this->resolveCurrency(data_get($data, 'CurrencyRef.value'));
|
||||
|
||||
$new_client_merge = [
|
||||
'client_hash' => data_get($data, 'V4IDPseudonym', \Illuminate\Support\Str::random(32)),
|
||||
'settings' => $settings,
|
||||
];
|
||||
$client['settings'] = $settings;
|
||||
|
||||
$new_client_merge = [];
|
||||
|
||||
return [$client, $contact, $new_client_merge];
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ class InvoiceTransformer extends BaseTransformer
|
||||
$client_id = $this->getClientId(data_get($qb_data, 'CustomerRef.value', null));
|
||||
|
||||
return $client_id ? [
|
||||
'id' => data_get($qb_data, 'Id.value', false),
|
||||
'client_id' => $client_id,
|
||||
'number' => data_get($qb_data, 'DocNumber', false),
|
||||
'date' => data_get($qb_data, 'TxnDate', now()->format('Y-m-d')),
|
||||
@ -45,15 +46,62 @@ class InvoiceTransformer extends BaseTransformer
|
||||
'public_notes' => data_get($qb_data, 'CustomerMemo.value', false),
|
||||
'due_date' => data_get($qb_data, 'DueDate', null),
|
||||
'po_number' => data_get($qb_data, 'PONumber', ""),
|
||||
'partial' => data_get($qb_data, 'Deposit', 0),
|
||||
'line_items' => $this->getLineItems(data_get($qb_data, 'Line', [])),
|
||||
'partial' => (float)data_get($qb_data, 'Deposit', 0),
|
||||
'line_items' => $this->getLineItems(data_get($qb_data, 'Line', []), data_get($qb_data, 'ApplyTaxAfterDiscount', 'true')),
|
||||
'payment_ids' => $this->getPayments($qb_data),
|
||||
'status_id' => Invoice::STATUS_SENT,
|
||||
'tax_rate1' => $rate = data_get($qb_data,'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0),
|
||||
'tax_rate1' => $rate = $this->calculateTotalTax($qb_data),
|
||||
'tax_name1' => $rate > 0 ? "Sales Tax" : "",
|
||||
'custom_surcharge1' => $this->checkIfDiscountAfterTax($qb_data),
|
||||
|
||||
] : false;
|
||||
}
|
||||
|
||||
private function checkIfDiscountAfterTax($qb_data)
|
||||
{
|
||||
|
||||
if($qb_data->ApplyTaxAfterDiscount == 'true'){
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach(data_get($qb_data, 'Line', []) as $line)
|
||||
{
|
||||
|
||||
if(data_get($line, 'DetailType.value') == 'DiscountLineDetail')
|
||||
{
|
||||
|
||||
if(!isset($this->company->custom_fields->surcharge1))
|
||||
{
|
||||
$this->company->custom_fields->surcharge1 = ctrans('texts.discount');
|
||||
$this->company->save();
|
||||
}
|
||||
|
||||
return (float)data_get($line, 'Amount', 0) * -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function calculateTotalTax($qb_data)
|
||||
{
|
||||
$taxLines = data_get($qb_data, 'TxnTaxDetail.TaxLine', []);
|
||||
|
||||
if (!is_array($taxLines)) {
|
||||
$taxLines = [$taxLines];
|
||||
}
|
||||
|
||||
$totalTaxRate = 0;
|
||||
|
||||
foreach ($taxLines as $taxLine) {
|
||||
$taxRate = data_get($taxLine, 'TaxLineDetail.TaxPercent', 0);
|
||||
$totalTaxRate += $taxRate;
|
||||
}
|
||||
|
||||
return (float)$totalTaxRate;
|
||||
}
|
||||
|
||||
|
||||
private function getPayments(mixed $qb_data)
|
||||
{
|
||||
$payments = [];
|
||||
@ -81,27 +129,45 @@ class InvoiceTransformer extends BaseTransformer
|
||||
|
||||
}
|
||||
|
||||
private function getLineItems(mixed $qb_items)
|
||||
private function getLineItems(mixed $qb_items, string $include_discount = 'true')
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach($qb_items as $qb_item)
|
||||
{
|
||||
$item = new InvoiceItem;
|
||||
$item->product_key = data_get($qb_item, 'SalesItemLineDetail.ItemRef.name', '');
|
||||
$item->notes = data_get($qb_item,'Description', '');
|
||||
$item->quantity = data_get($qb_item,'SalesItemLineDetail.Qty', 0);
|
||||
$item->cost = data_get($qb_item, 'SalesItemLineDetail.UnitPrice', 0);
|
||||
$item->discount = data_get($item,'DiscountRate', data_get($qb_item,'DiscountAmount', 0));
|
||||
$item->is_amount_discount = data_get($qb_item,'DiscountAmount', 0) > 0 ? true : false;
|
||||
$item->type_id = stripos(data_get($qb_item, 'ItemAccountRef.name') ?? '', 'Service') !== false ? '2' : '1';
|
||||
$item->tax_id = data_get($qb_item, 'TaxCodeRef.value', '') == 'NON' ? Product::PRODUCT_TYPE_EXEMPT : $item->type_id;
|
||||
$item->tax_rate1 = data_get($qb_item, 'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0);
|
||||
$item->tax_name1 = $item->tax_rate1 > 0 ? "Sales Tax" : "";
|
||||
$items[] = (object)$item;
|
||||
}
|
||||
|
||||
nlog($items);
|
||||
if(data_get($qb_item, 'DetailType.value') == 'SalesItemLineDetail')
|
||||
{
|
||||
$item = new InvoiceItem;
|
||||
$item->product_key = data_get($qb_item, 'SalesItemLineDetail.ItemRef.name', '');
|
||||
$item->notes = data_get($qb_item,'Description', '');
|
||||
$item->quantity = (float)data_get($qb_item,'SalesItemLineDetail.Qty', 0);
|
||||
$item->cost = (float)data_get($qb_item, 'SalesItemLineDetail.UnitPrice', 0);
|
||||
$item->discount = (float)data_get($item,'DiscountRate', data_get($qb_item,'DiscountAmount', 0));
|
||||
$item->is_amount_discount = data_get($qb_item,'DiscountAmount', 0) > 0 ? true : false;
|
||||
$item->type_id = stripos(data_get($qb_item, 'ItemAccountRef.name') ?? '', 'Service') !== false ? '2' : '1';
|
||||
$item->tax_id = data_get($qb_item, 'TaxCodeRef.value', '') == 'NON' ? Product::PRODUCT_TYPE_EXEMPT : $item->type_id;
|
||||
$item->tax_rate1 = (float)data_get($qb_item, 'TxnTaxDetail.TaxLine.TaxLineDetail.TaxPercent', 0);
|
||||
$item->tax_name1 = $item->tax_rate1 > 0 ? "Sales Tax" : "";
|
||||
$items[] = (object)$item;
|
||||
}
|
||||
|
||||
if(data_get($qb_item, 'DetailType.value') == 'DiscountLineDetail' && $include_discount == 'true')
|
||||
{
|
||||
|
||||
$item = new InvoiceItem();
|
||||
$item->product_key = ctrans('texts.discount');
|
||||
$item->notes = ctrans('texts.discount');
|
||||
$item->quantity = 1;
|
||||
$item->cost = (float)data_get($qb_item, 'Amount', 0) * -1;
|
||||
$item->discount = 0;
|
||||
$item->is_amount_discount = true;
|
||||
$item->type_id = '1';
|
||||
$item->tax_id = Product::PRODUCT_TYPE_PHYSICAL;
|
||||
$items[] = (object)$item;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
|
||||
|
@ -91,7 +91,7 @@ class PaymentTransformer extends BaseTransformer
|
||||
if(!$credit_line)
|
||||
return $payment;
|
||||
|
||||
$credit = \App\Factory\CreditFactory::create($this->company->id, $this->company->owner()->id, $payment->client_id);
|
||||
$credit = \App\Factory\CreditFactory::create($this->company->id, $this->company->owner()->id);
|
||||
$credit->client_id = $payment->client_id;
|
||||
|
||||
$line = new \App\DataMapper\InvoiceItem();
|
||||
|
@ -30,10 +30,9 @@ class ProductTransformer extends BaseTransformer
|
||||
|
||||
public function transform(mixed $data): array
|
||||
{
|
||||
nlog(data_get($data, 'Id', null));
|
||||
|
||||
return [
|
||||
'hash' => data_get($data, 'Id.value', null),
|
||||
'id' => data_get($data, 'Id.value', null),
|
||||
'product_key' => data_get($data, 'Name', data_get($data, 'FullyQualifiedName','')),
|
||||
'notes' => data_get($data, 'Description', ''),
|
||||
'cost' => data_get($data, 'PurchaseCost', 0),
|
||||
|
@ -166,6 +166,10 @@ class InvoiceTransformer extends EntityTransformer
|
||||
$data['reminder_schedule'] = (string) $invoice->reminderSchedule();
|
||||
}
|
||||
|
||||
if (request()->has('is_locked') && request()->query('is_locked') == 'true') {
|
||||
$data['is_locked'] = (bool) $invoice->isLocked();
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
}
|
||||
|
@ -41,6 +41,7 @@
|
||||
"authorizenet/authorizenet": "^2.0",
|
||||
"awobaz/compoships": "^2.1",
|
||||
"aws/aws-sdk-php": "^3.319",
|
||||
"babenkoivan/elastic-scout-driver": "^4.0",
|
||||
"bacon/bacon-qr-code": "^2.0",
|
||||
"beganovich/snappdf": "dev-master",
|
||||
"braintree/braintree_php": "^6.0",
|
||||
@ -68,6 +69,7 @@
|
||||
"josemmo/facturae-php": "^1.7",
|
||||
"laracasts/presenter": "^0.2.1",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/scout": "^10.11",
|
||||
"laravel/slack-notification-channel": "^3",
|
||||
"laravel/socialite": "^5",
|
||||
"laravel/tinker": "^2.7",
|
||||
|
1094
composer.lock
generated
1094
composer.lock
generated
File diff suppressed because it is too large
Load Diff
24
config/elastic.client.php
Normal file
24
config/elastic.client.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'default' => env('ELASTIC_CONNECTION', 'default'),
|
||||
'connections' => [
|
||||
'default' => [
|
||||
'hosts' => [
|
||||
env('ELASTIC_HOST'),
|
||||
],
|
||||
// configure basic authentication
|
||||
'basicAuthentication' => [
|
||||
env('ELASTIC_USERNAME'),
|
||||
env('ELASTIC_PASSWORD'),
|
||||
],
|
||||
// configure HTTP client (Guzzle by default)
|
||||
'httpClientOptions' => [
|
||||
'timeout' => 2,
|
||||
'verify_host' => false, // Disable SSL verification
|
||||
'verify_peer' => false,
|
||||
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
5
config/elastic.scout_driver.php
Normal file
5
config/elastic.scout_driver.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'refresh_documents' => env('ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS', false),
|
||||
];
|
202
config/scout.php
Normal file
202
config/scout.php
Normal file
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Search Engine
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default search connection that gets used while
|
||||
| using Laravel Scout. This connection is used when syncing all models
|
||||
| to the search service. You should adjust this based on your needs.
|
||||
|
|
||||
| Supported: "algolia", "meilisearch", "typesense",
|
||||
| "database", "collection", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SCOUT_DRIVER', 'algolia'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify a prefix that will be applied to all search index
|
||||
| names used by Scout. This prefix may be useful if you have multiple
|
||||
| "tenants" or applications sharing the same search infrastructure.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('SCOUT_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Data Syncing
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control if the operations that sync your data
|
||||
| with your search engines are queued. When this is set to "true" then
|
||||
| all automatic data syncing will get queued for better performance.
|
||||
|
|
||||
*/
|
||||
|
||||
'queue' => env('SCOUT_QUEUE', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Transactions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration option determines if your data will only be synced
|
||||
| with your search indexes after every open database transaction has
|
||||
| been committed, thus preventing any discarded data from syncing.
|
||||
|
|
||||
*/
|
||||
|
||||
'after_commit' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Chunk Sizes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options allow you to control the maximum chunk size when you are
|
||||
| mass importing data into the search engine. This allows you to fine
|
||||
| tune each of these chunk sizes based on the power of the servers.
|
||||
|
|
||||
*/
|
||||
|
||||
'chunk' => [
|
||||
'searchable' => 500,
|
||||
'unsearchable' => 500,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows to control whether to keep soft deleted records in
|
||||
| the search indexes. Maintaining soft deleted records can be useful
|
||||
| if your application still needs to search for the records later.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_delete' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Identify User
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to control whether to notify the search engine
|
||||
| of the user performing the search. This is sometimes useful if the
|
||||
| engine supports any analytics based on this application's users.
|
||||
|
|
||||
| Supported engines: "algolia"
|
||||
|
|
||||
*/
|
||||
|
||||
'identify' => env('SCOUT_IDENTIFY', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Algolia Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
||||
| search engine which works great with Scout out of the box. Just plug
|
||||
| in your application ID and admin API key to get started searching.
|
||||
|
|
||||
*/
|
||||
|
||||
'algolia' => [
|
||||
'id' => env('ALGOLIA_APP_ID', ''),
|
||||
'secret' => env('ALGOLIA_SECRET', ''),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Meilisearch Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Meilisearch settings. Meilisearch is an open
|
||||
| source search engine with minimal configuration. Below, you can state
|
||||
| the host and key information for your own Meilisearch installation.
|
||||
|
|
||||
| See: https://www.meilisearch.com/docs/learn/configuration/instance_options#all-instance-options
|
||||
|
|
||||
*/
|
||||
|
||||
'meilisearch' => [
|
||||
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
|
||||
'key' => env('MEILISEARCH_KEY'),
|
||||
'index-settings' => [
|
||||
// 'users' => [
|
||||
// 'filterableAttributes'=> ['id', 'name', 'email'],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Typesense Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your Typesense settings. Typesense is an open
|
||||
| source search engine using minimal configuration. Below, you will
|
||||
| state the host, key, and schema configuration for the instance.
|
||||
|
|
||||
*/
|
||||
|
||||
'typesense' => [
|
||||
'client-settings' => [
|
||||
'api_key' => env('TYPESENSE_API_KEY', 'xyz'),
|
||||
'nodes' => [
|
||||
[
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
],
|
||||
'nearest_node' => [
|
||||
'host' => env('TYPESENSE_HOST', 'localhost'),
|
||||
'port' => env('TYPESENSE_PORT', '8108'),
|
||||
'path' => env('TYPESENSE_PATH', ''),
|
||||
'protocol' => env('TYPESENSE_PROTOCOL', 'http'),
|
||||
],
|
||||
'connection_timeout_seconds' => env('TYPESENSE_CONNECTION_TIMEOUT_SECONDS', 2),
|
||||
'healthcheck_interval_seconds' => env('TYPESENSE_HEALTHCHECK_INTERVAL_SECONDS', 30),
|
||||
'num_retries' => env('TYPESENSE_NUM_RETRIES', 3),
|
||||
'retry_interval_seconds' => env('TYPESENSE_RETRY_INTERVAL_SECONDS', 1),
|
||||
],
|
||||
'model-settings' => [
|
||||
// User::class => [
|
||||
// 'collection-schema' => [
|
||||
// 'fields' => [
|
||||
// [
|
||||
// 'name' => 'id',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'name',
|
||||
// 'type' => 'string',
|
||||
// ],
|
||||
// [
|
||||
// 'name' => 'created_at',
|
||||
// 'type' => 'int64',
|
||||
// ],
|
||||
// ],
|
||||
// 'default_sorting_field' => 'created_at',
|
||||
// ],
|
||||
// 'search-parameters' => [
|
||||
// 'query_by' => 'name'
|
||||
// ],
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
@ -21,7 +21,12 @@ return new class extends Migration
|
||||
$fields->publicKey = '';
|
||||
$fields->secretKey = '';
|
||||
$fields->testMode = false;
|
||||
$fields->threeds = false;
|
||||
$fields->gatewayId = '';
|
||||
|
||||
if($gateway = Gateway::find(64)){
|
||||
$gateway->fields = json_encode($fields);
|
||||
$gateway->save();
|
||||
}else{
|
||||
|
||||
$powerboard = new Gateway();
|
||||
$powerboard->id = 64;
|
||||
@ -34,6 +39,7 @@ return new class extends Migration
|
||||
$powerboard->fields = json_encode($fields);
|
||||
$powerboard->save();
|
||||
|
||||
}
|
||||
|
||||
Schema::table("company_gateways", function (\Illuminate\Database\Schema\Blueprint $table){
|
||||
$table->text('settings')->nullable();
|
||||
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('clients', function (Blueprint $table) {
|
||||
$table->text('sync')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->text('sync')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->text('sync')->nullable();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
@ -89,7 +89,7 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
['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":false}'],
|
||||
['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "Threeds":true}'],
|
||||
['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "gatewayId":""}'],
|
||||
['id' => 65, 'name' => 'Blockonomics', 'is_offsite' => false, 'sort_order' => 27, 'provider' => 'Blockonomics', 'key' => 'wbhf02us6owgo7p4nfjd0ymssdshks4d', 'fields' => '{"apiKey":"", "callbackSecret":""}'],
|
||||
];
|
||||
|
||||
|
@ -5331,6 +5331,7 @@ Développe automatiquement la section des notes dans le tableau de produits pour
|
||||
'country_Melilla' => 'Melilla',
|
||||
'country_Ceuta' => 'Ceuta',
|
||||
'country_Canary Islands' => 'Îles Canaries',
|
||||
'lang_Vietnamese' => 'Vietnamien',
|
||||
'invoice_status_changed' => 'Veuillez noter que l\'état de votre facture a été mis à jour. Nous vous recommandons de rafraîchir la page pour afficher la version la plus récente.',
|
||||
'no_unread_notifications' => 'Vous êtes à jour! Aucune nouvelle notification.',
|
||||
);
|
||||
|
@ -41,7 +41,7 @@ $lang = array(
|
||||
'quantity' => 'Số lượng',
|
||||
'line_total' => 'Tổng',
|
||||
'subtotal' => 'Thành tiền',
|
||||
'net_subtotal' => 'Tịnh',
|
||||
'net_subtotal' => 'Tính',
|
||||
'paid_to_date' => 'Hạn thanh toán',
|
||||
'balance_due' => 'Số tiền thanh toán',
|
||||
'invoice_design_id' => 'Thiết kế',
|
||||
@ -581,8 +581,8 @@ $lang = array(
|
||||
'pro_plan_call_to_action' => 'Nâng cấp ngay!',
|
||||
'pro_plan_feature1' => 'Tạo khách hàng không giới hạn',
|
||||
'pro_plan_feature2' => 'Truy cập vào 10 mẫu thiết kế hóa đơn đẹp',
|
||||
'pro_plan_feature3' => 'URL tùy chỉnh - "YourBrand.InvoiceNinja.com"',
|
||||
'pro_plan_feature4' => 'Xóa "Được tạo bởi Invoice Ninja"',
|
||||
'pro_plan_feature3' => 'URL tùy chỉnh - "YourBrand.InvoiceNinja.com"',
|
||||
'pro_plan_feature4' => 'Xóa "Được tạo bởi Invoice Ninja"',
|
||||
'pro_plan_feature5' => 'Truy cập nhiều người dùng & Theo dõi hoạt động',
|
||||
'pro_plan_feature6' => 'Tạo báo giá và hóa đơn tạm tính',
|
||||
'pro_plan_feature7' => 'Tùy chỉnh Tiêu đề và Số hiệu Trường Hóa đơn',
|
||||
@ -729,7 +729,7 @@ $lang = array(
|
||||
'invoice_counter' => 'Quầy tính tiền',
|
||||
'quote_counter' => 'Bộ đếm Báo giá ',
|
||||
'type' => 'Kiểu',
|
||||
'activity_1' => ':user đã tạo ra máy khách :client',
|
||||
'activity_1' => ':user đã tạo ra khách hàng :client',
|
||||
'activity_2' => ':user khách hàng lưu trữ :client',
|
||||
'activity_3' => ':user đã xóa máy khách :client',
|
||||
'activity_4' => ':user đã tạo hóa đơn :invoice',
|
||||
@ -799,8 +799,8 @@ $lang = array(
|
||||
'archived_token' => 'Đã lưu trữ mã thông báo thành công',
|
||||
'archive_user' => 'Lưu trữ người dùng',
|
||||
'archived_user' => 'Đã lưu trữ người dùng thành công',
|
||||
'archive_account_gateway' => 'Xóa Cổng Thanh Toán',
|
||||
'archived_account_gateway' => 'Cổng lưu trữ thành công',
|
||||
'archive_account_gateway' => 'Xóa cổng thanh toán',
|
||||
'archived_account_gateway' => 'Lưu trữ cổng thành công',
|
||||
'archive_recurring_invoice' => 'Lưu trữ hóa đơn định kỳ',
|
||||
'archived_recurring_invoice' => 'Đã lưu trữ thành công hóa đơn định kỳ',
|
||||
'delete_recurring_invoice' => 'Xóa hóa đơn định kỳ',
|
||||
@ -959,7 +959,7 @@ $lang = array(
|
||||
'quote_message_button' => 'Để xem báo giá cho :amount , hãy nhấp vào nút bên dưới.',
|
||||
'payment_message_button' => 'Cảm ơn bạn đã thanh toán :amount .',
|
||||
'payment_type_direct_debit' => 'Ghi nợ trực tiếp',
|
||||
'bank_accounts' => 'Thẻ tín dụng và ngân hàng',
|
||||
'bank_accounts' => 'Thẻ tín dụng & ngân hàng',
|
||||
'add_bank_account' => 'Thêm tài khoản ngân hàng',
|
||||
'setup_account' => 'Thiết lập tài khoản',
|
||||
'import_expenses' => 'Chi phí nhập khẩu',
|
||||
@ -1244,9 +1244,9 @@ $lang = array(
|
||||
'confirm_remove_payment_method' => 'Bạn có chắc chắn muốn xóa phương thức thanh toán này không?',
|
||||
'remove' => 'Di dời',
|
||||
'payment_method_removed' => 'Đã xóa phương thức thanh toán.',
|
||||
'bank_account_verification_help' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để xuất hiện trên sao kê của bạn. Vui lòng nhập số tiền bên dưới.',
|
||||
'bank_account_verification_next_steps' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để hiển thị trên sao kê của bạn.
|
||||
Khi đã có số tiền, hãy quay lại trang phương thức thanh toán này và nhấp vào "Hoàn tất xác minh" bên cạnh tài khoản.',
|
||||
'bank_account_verification_help' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để xuất hiện trên sao kê của bạn. Vui lòng nhập số tiền bên dưới.',
|
||||
'bank_account_verification_next_steps' => 'Chúng tôi đã thực hiện hai khoản tiền gửi vào tài khoản của bạn với mô tả "XÁC MINH". Các khoản tiền gửi này sẽ mất 1-2 ngày làm việc để hiển thị trên sao kê của bạn.
|
||||
Khi đã có số tiền, hãy quay lại trang phương thức thanh toán này và nhấp vào "Hoàn tất xác minh" bên cạnh tài khoản.',
|
||||
'unknown_bank' => 'Ngân hàng không xác định',
|
||||
'ach_verification_delay_help' => 'Bạn sẽ có thể sử dụng tài khoản sau khi hoàn tất xác minh. Việc xác minh thường mất 1-2 ngày làm việc.',
|
||||
'add_credit_card' => 'Thêm thẻ tín dụng',
|
||||
@ -2096,7 +2096,7 @@ $lang = array(
|
||||
'gateway_fees' => 'Phí cổng vào',
|
||||
'fees_disabled' => 'Phí đã bị vô hiệu hóa',
|
||||
'gateway_fees_help' => 'Tự động thêm phụ phí/chiết khấu khi thanh toán trực tuyến.',
|
||||
'gateway' => 'Cổng vào',
|
||||
'gateway' => 'Gateway',
|
||||
'gateway_fee_change_warning' => 'Nếu có hóa đơn chưa thanh toán kèm phí, bạn cần cập nhật thủ công.',
|
||||
'fees_surcharge_help' => 'Tùy chỉnh phụ phí :link .',
|
||||
'label_and_taxes' => 'nhãn và thuế',
|
||||
@ -2612,7 +2612,7 @@ $lang = array(
|
||||
'signature_on_pdf_help' => 'Hiển thị chữ ký của khách hàng trên hóa đơn/báo giá PDF.',
|
||||
'expired_white_label' => 'The white label license has expired',
|
||||
'return_to_login' => 'Quay lại Đăng nhập',
|
||||
'convert_products_tip' => 'Lưu ý: thêm :link có tên " :name " để xem tỷ giá hối đoái.',
|
||||
'convert_products_tip' => 'Lưu ý: thêm :link có tên " :name " để xem tỷ giá hối đoái.',
|
||||
'amount_greater_than_balance' => 'Số tiền lớn hơn số dư trên hóa đơn, chúng tôi sẽ tạo khoản tín dụng với số tiền còn lại.',
|
||||
'custom_fields_tip' => 'Sử dụng <code>Label|Option1,Option2</code> để hiển thị hộp chọn.',
|
||||
'client_information' => 'Thông tin khách hàng',
|
||||
@ -3062,8 +3062,8 @@ $lang = array(
|
||||
'provider' => 'Nhà cung cấp',
|
||||
'company_gateway' => 'Cổng thanh toán',
|
||||
'company_gateways' => 'Cổng thanh toán',
|
||||
'new_company_gateway' => 'Cổng mới',
|
||||
'edit_company_gateway' => 'Chỉnh sửa Cổng',
|
||||
'new_company_gateway' => 'Gateway mới',
|
||||
'edit_company_gateway' => 'Chỉnh sửa Gateway',
|
||||
'created_company_gateway' => 'Đã tạo cổng thành công',
|
||||
'updated_company_gateway' => 'Đã cập nhật cổng thành công',
|
||||
'archived_company_gateway' => 'Cổng lưu trữ thành công',
|
||||
@ -3097,7 +3097,7 @@ $lang = array(
|
||||
'uploaded_logo' => 'Đã tải logo thành công',
|
||||
'saved_settings' => 'Đã lưu cài đặt thành công',
|
||||
'device_settings' => 'Cài đặt thiết bị',
|
||||
'credit_cards_and_banks' => 'Thẻ tín dụng và ngân hàng',
|
||||
'credit_cards_and_banks' => 'Thẻ tín dụng & ngân hàng',
|
||||
'price' => 'Giá',
|
||||
'email_sign_up' => 'Đăng ký Email',
|
||||
'google_sign_up' => 'Đăng ký Google',
|
||||
@ -3648,7 +3648,7 @@ $lang = array(
|
||||
'view_licenses' => 'Xem Giấy phép',
|
||||
'fullscreen_editor' => 'Biên tập toàn màn hình',
|
||||
'sidebar_editor' => 'Biên tập thanh bên',
|
||||
'please_type_to_confirm' => 'Vui lòng nhập " :value " để xác nhận',
|
||||
'please_type_to_confirm' => 'Vui lòng nhập ":value"để xác nhận',
|
||||
'purge' => 'thanh lọc',
|
||||
'clone_to' => 'Sao chép vào',
|
||||
'clone_to_other' => 'Sao chép sang cái khác',
|
||||
@ -3855,8 +3855,8 @@ $lang = array(
|
||||
'recommended_in_production' => 'Rất khuyến khích trong sản xuất',
|
||||
'enable_only_for_development' => 'Chỉ cho phép phát triển',
|
||||
'test_pdf' => 'Kiểm tra PDF',
|
||||
'checkout_authorize_label' => 'Checkout.com có thể được lưu làm phương thức thanh toán để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Lưu thông tin thẻ tín dụng" trong quá trình thanh toán.',
|
||||
'sofort_authorize_label' => 'Tài khoản ngân hàng (SOFORT) có thể được lưu làm phương thức thanh toán để sử dụng trong tương lai, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Lưu chi tiết thanh toán" trong quá trình thanh toán.',
|
||||
'checkout_authorize_label' => 'Checkout.com có thể được lưu làm phương thức thanh toán để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra" Lưu thông tin thẻ tín dụng" trong quá trình thanh toán.',
|
||||
'sofort_authorize_label' => 'Tài khoản ngân hàng (SOFORT) có thể được lưu làm phương thức thanh toán để sử dụng trong tương lai, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Lưu chi tiết thanh toán" trong quá trình thanh toán.',
|
||||
'node_status' => 'Trạng thái nút',
|
||||
'npm_status' => 'Trạng thái NPM',
|
||||
'node_status_not_found' => 'Tôi không tìm thấy Node ở đâu cả. Nó đã được cài đặt chưa?',
|
||||
@ -3891,7 +3891,7 @@ $lang = array(
|
||||
'payment_method_saving_failed' => 'Không thể lưu phương thức thanh toán để sử dụng sau này.',
|
||||
'pay_with' => 'Thanh toán bằng',
|
||||
'n/a' => 'Không có',
|
||||
'by_clicking_next_you_accept_terms' => 'Bằng cách nhấp vào "Tiếp theo", bạn chấp nhận các điều khoản.',
|
||||
'by_clicking_next_you_accept_terms' => 'Bằng cách nhấp vào "Tiếp theo", bạn chấp nhận các điều khoản.',
|
||||
'not_specified' => 'Không xác định',
|
||||
'before_proceeding_with_payment_warning' => 'Trước khi tiến hành thanh toán, bạn phải điền vào các trường sau',
|
||||
'after_completing_go_back_to_previous_page' => 'Sau khi hoàn tất, hãy quay lại trang trước.',
|
||||
@ -4072,7 +4072,7 @@ $lang = array(
|
||||
'max_companies_desc' => 'Bạn đã đạt đến số lượng công ty tối đa. Xóa các công ty hiện có để di chuyển các công ty mới.',
|
||||
'migration_already_completed' => 'Công ty đã di chuyển',
|
||||
'migration_already_completed_desc' => 'Có vẻ như bạn đã di chuyển <b>:company _name</b> sang phiên bản V5 của Invoice Ninja. Trong trường hợp bạn muốn bắt đầu lại, bạn có thể buộc di chuyển để xóa dữ liệu hiện có.',
|
||||
'payment_method_cannot_be_authorized_first' => 'Phương thức thanh toán này có thể được lưu lại để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Chi tiết cửa hàng" trong quá trình thanh toán.',
|
||||
'payment_method_cannot_be_authorized_first' => 'Phương thức thanh toán này có thể được lưu lại để sử dụng sau này, sau khi bạn hoàn tất giao dịch đầu tiên. Đừng quên kiểm tra "Chi tiết cửa hàng" trong quá trình thanh toán.',
|
||||
'new_account' => 'Tài khoản mới',
|
||||
'activity_100' => ':user đã tạo hóa đơn định kỳ :recurring_invoice',
|
||||
'activity_101' => ':user hóa đơn định kỳ đã cập nhật :recurring_invoice',
|
||||
@ -4110,7 +4110,7 @@ $lang = array(
|
||||
'one_time_purchases' => 'Mua một lần',
|
||||
'recurring_purchases' => 'Mua hàng định kỳ',
|
||||
'you_might_be_interested_in_following' => 'Bạn có thể quan tâm đến những điều sau đây',
|
||||
'quotes_with_status_sent_can_be_approved' => 'Chỉ những báo giá có trạng thái "Đã gửi" mới được chấp thuận. Không thể chấp thuận những báo giá đã hết hạn.',
|
||||
'quotes_with_status_sent_can_be_approved' => 'Chỉ những báo giá có trạng thái "Đã gửi" mới được chấp thuận. Không thể chấp thuận những báo giá đã hết hạn.',
|
||||
'no_quotes_available_for_download' => 'Không có báo giá nào có sẵn để tải xuống.',
|
||||
'copyright' => 'Bản quyền',
|
||||
'user_created_user' => ':user đã tạo :created_user tại :time',
|
||||
@ -4287,7 +4287,7 @@ $lang = array(
|
||||
'migration_not_yet_completed' => 'Việc di chuyển vẫn chưa hoàn tất',
|
||||
'show_task_end_date' => 'Hiển thị ngày kết thúc nhiệm vụ',
|
||||
'show_task_end_date_help' => 'Cho phép chỉ định ngày kết thúc nhiệm vụ',
|
||||
'gateway_setup' => 'Thiết lập cổng',
|
||||
'gateway_setup' => 'Cài đặt Gateway',
|
||||
'preview_sidebar' => 'Xem trước thanh bên',
|
||||
'years_data_shown' => 'Dữ liệu năm được hiển thị',
|
||||
'ended_all_sessions' => 'Đã kết thúc thành công tất cả các phiên',
|
||||
@ -4394,7 +4394,7 @@ $lang = array(
|
||||
'document_download_subject' => 'Tài liệu của bạn đã sẵn sàng để tải xuống',
|
||||
'reminder_message' => 'Nhắc nhở về hóa đơn :number cho :balance',
|
||||
'gmail_credentials_invalid_subject' => 'Gửi bằng GMail thông tin đăng nhập không hợp lệ',
|
||||
'gmail_credentials_invalid_body' => 'Thông tin đăng nhập GMail của bạn không đúng, vui lòng đăng nhập vào cổng thông tin quản trị viên và điều hướng đến Cài đặt > Chi tiết người dùng và ngắt kết nối và kết nối lại tài khoản GMail của bạn. Chúng tôi sẽ gửi cho bạn thông báo này hàng ngày cho đến khi sự cố này được giải quyết',
|
||||
'gmail_credentials_invalid_body' => 'Thông tin đăng nhập GMail của bạn không đúng, vui lòng đăng nhập vào cổng thông tin quản trị viên và điều hướng đến Cài đặt > Chi tiết người dùng và ngắt kết nối và kết nối lại tài khoản GMail của bạn. Chúng tôi sẽ gửi cho bạn thông báo này hàng ngày cho đến khi sự cố này được giải quyết',
|
||||
'total_columns' => 'Tổng số trường',
|
||||
'view_task' => 'Xem Nhiệm vụ',
|
||||
'cancel_invoice' => 'Hủy bỏ',
|
||||
@ -4419,8 +4419,8 @@ $lang = array(
|
||||
'signed_in_as' => 'Đã đăng nhập như',
|
||||
'total_results' => 'Tổng kết quả',
|
||||
'restore_company_gateway' => 'Khôi phục cổng',
|
||||
'archive_company_gateway' => 'Cổng lưu trữ',
|
||||
'delete_company_gateway' => 'Xóa cổng',
|
||||
'archive_company_gateway' => 'Lưu trữ gateway',
|
||||
'delete_company_gateway' => 'Xóa gateway',
|
||||
'exchange_currency' => 'Trao đổi tiền tệ',
|
||||
'tax_amount1' => 'Số tiền thuế 1',
|
||||
'tax_amount2' => 'Số tiền thuế 2',
|
||||
@ -4506,7 +4506,7 @@ $lang = array(
|
||||
'add' => 'Thêm vào',
|
||||
'last_sent_template' => 'Mẫu gửi cuối cùng',
|
||||
'enable_flexible_search' => 'Bật Tìm kiếm linh hoạt',
|
||||
'enable_flexible_search_help' => 'Phù hợp với các ký tự không liền kề, ví dụ: "ct" phù hợp với "cat"',
|
||||
'enable_flexible_search_help' => 'Phù hợp với các ký tự không liền kề, ví dụ: "ct" phù hợp với "cat"',
|
||||
'vendor_details' => 'Chi tiết nhà cung cấp',
|
||||
'purchase_order_details' => 'Chi tiết đơn đặt hàng',
|
||||
'qr_iban' => 'Mã QR IBAN',
|
||||
@ -5333,6 +5333,7 @@ $lang = array(
|
||||
'country_Melilla' => 'Melilla',
|
||||
'country_Ceuta' => 'Ceuta',
|
||||
'country_Canary Islands' => 'Quần đảo Canary',
|
||||
'lang_Vietnamese' => 'Tiếng Việt',
|
||||
'invoice_status_changed' => 'Xin lưu ý rằng trạng thái hóa đơn của bạn đã được cập nhật. Chúng tôi khuyên bạn nên làm mới trang để xem phiên bản mới nhất.',
|
||||
'no_unread_notifications' => 'Bạn đã cập nhật đầy đủ rồi! Không có thông báo mới nào.',
|
||||
);
|
||||
|
109
public/build/assets/app-021b0210.js
vendored
Normal file
109
public/build/assets/app-021b0210.js
vendored
Normal file
File diff suppressed because one or more lines are too long
109
public/build/assets/app-e0713224.js
vendored
109
public/build/assets/app-e0713224.js
vendored
File diff suppressed because one or more lines are too long
13
public/build/assets/powerboard-credit-card-f5f23291.js
vendored
Normal file
13
public/build/assets/powerboard-credit-card-f5f23291.js
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import{i as l,w as m}from"./wait-8f4ae121.js";/**
|
||||
* 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
|
||||
*/let i=!1;function y(){const n=document.querySelector("meta[name=public_key]"),t=document.querySelector("meta[name=gateway_id]"),r=document.querySelector("meta[name=environment]"),e=new cba.HtmlWidget("#widget",n==null?void 0:n.content,t==null?void 0:t.content);e.setEnv(r==null?void 0:r.content),e.useAutoResize(),e.interceptSubmitForm("#stepone"),e.onFinishInsert('#server-response input[name="gateway_response"]',"payment_source"),e.setFormFields(["card_name*"]),e.reload();let o=document.getElementById("pay-now");return o.disabled=!1,o.querySelector("svg").classList.add("hidden"),o.querySelector("span").classList.remove("hidden"),document.querySelector('#server-response input[name="gateway_response"]').value="",e}function g(){var n,t,r;(n=document.querySelector("#widget"))==null||n.replaceChildren(),(t=document.querySelector("#widget"))==null||t.classList.remove("hidden"),(r=document.querySelector("#widget-3dsecure"))==null||r.replaceChildren()}function a(){var o,u;if(g(),!((o=document.querySelector("meta[name=gateway_id]"))==null?void 0:o.content)){let d=document.getElementById("pay-now");d.disabled=!0,d.querySelector("svg").classList.remove("hidden"),d.querySelector("span").classList.add("hidden"),document.getElementById("errors").textContent="Gateway not found or verified",document.getElementById("errors").hidden=!1}const t=y();t.on("finish",()=>{document.getElementById("errors").hidden=!0,p()}),t.on("submit",function(d){document.getElementById("errors").hidden=!0});let r=document.getElementById("pay-now");r.addEventListener("click",()=>{const d=document.getElementById("widget");if(t.getValidationState(),!t.isValidForm()&&d.offsetParent!==null){r.disabled=!1,r.querySelector("svg").classList.add("hidden"),r.querySelector("span").classList.remove("hidden");return}r.disabled=!0,r.querySelector("svg").classList.remove("hidden"),r.querySelector("span").classList.add("hidden");let s=document.querySelector("input[name=token-billing-checkbox]:checked");s&&(document.getElementById("store_card").value=s.value),d.offsetParent!==null?document.getElementById("stepone_submit").click():document.getElementById("server-response").submit()}),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",d=>{var c;document.getElementById("widget").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value="",(c=document.querySelector("#powerboard-payment-container"))==null||c.classList.remove("hidden")}),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(d=>d.addEventListener("click",s=>{var c;document.getElementById("widget").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=s.target.dataset.token,(c=document.querySelector("#powerboard-payment-container"))==null||c.classList.add("hidden")}));const e=document.querySelector('input[name="payment-type"]');e&&e.click(),i&&((u=document.getElementById("toggle-payment-with-credit-card"))==null||u.click())}async function p(){try{const n=await h();if(!n||!n.status||n.status==="not_authenticated"||n==="not_authenticated")throw i=!0,a(),new Error("There was an issue authenticating this payment method.");if(n.status==="authentication_not_supported"){document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(n);let e=document.querySelector("input[name=token-billing-checkbox]:checked");return e&&(document.getElementById("store_card").value=e.value),document.getElementById("server-response").submit()}const t=new cba.Canvas3ds("#widget-3dsecure",n._3ds.token);t.load(),document.getElementById("widget").classList.add("hidden"),t.on("chargeAuthSuccess",function(e){document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(e);let o=document.querySelector("input[name=token-billing-checkbox]:checked");o&&(document.getElementById("store_card").value=o.value),document.getElementById("server-response").submit()}),t.on("chargeAuthReject",function(e){document.getElementById("errors").textContent="Sorry, your transaction could not be processed...",document.getElementById("errors").hidden=!1,i=!0,a()}),t.load()}catch(n){const t=n.message??"Unknown error.";document.getElementById("errors").textContent=`Sorry, your transaction could not be processed...
|
||||
|
||||
${t}`,document.getElementById("errors").hidden=!1,i=!0,a()}}async function h(){const n={name:navigator.userAgent.substring(0,100),java_enabled:navigator.javaEnabled()?"true":"false",language:navigator.language||navigator.userLanguage,screen_height:window.screen.height.toString(),screen_width:window.screen.width.toString(),time_zone:(new Date().getTimezoneOffset()*-1).toString(),color_depth:window.screen.colorDepth.toString()};document.querySelector('input[name="browser_details"]').value=JSON.stringify(n);const t=JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server-response")))),r=document.querySelector("meta[name=payments_route]");try{const e=await fetch(r.content,{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest",Accept:"application/json","X-CSRF-Token":document.querySelector('meta[name="csrf-token"]').content},body:t});return e.ok?await e.json():await e.json().then(o=>{throw new Error(o.message??"Unknown error.")})}catch(e){document.getElementById("errors").textContent=`Sorry, your transaction could not be processed...
|
||||
|
||||
${e.message}`,document.getElementById("errors").hidden=!1,i=!0,a()}}l()?a():m("#powerboard-credit-card-payment").then(()=>a());
|
@ -1,13 +0,0 @@
|
||||
import{i,w as u}from"./wait-8f4ae121.js";/**
|
||||
* 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
|
||||
*/let a=!1;function l(){const t=document.querySelector("meta[name=public_key]"),n=document.querySelector("meta[name=gateway_id]"),o=document.querySelector("meta[name=environment]"),e=new cba.HtmlWidget("#widget",t==null?void 0:t.content,n==null?void 0:n.content);e.setEnv(o==null?void 0:o.content),e.useAutoResize(),e.interceptSubmitForm("#stepone"),e.onFinishInsert('#server-response input[name="gateway_response"]',"payment_source"),e.setFormFields(["card_name*"]),e.reload();let r=document.getElementById("pay-now");return r.disabled=!1,r.querySelector("svg").classList.add("hidden"),r.querySelector("span").classList.remove("hidden"),document.querySelector('#server-response input[name="gateway_response"]').value="",e}function m(){var t,n,o;(t=document.querySelector("#widget"))==null||t.replaceChildren(),(n=document.querySelector("#widget"))==null||n.classList.remove("hidden"),(o=document.querySelector("#widget-3dsecure"))==null||o.replaceChildren()}function d(){var e;m();const t=l();t.on("finish",()=>{document.getElementById("errors").hidden=!0,y()}),t.on("submit",function(r){document.getElementById("errors").hidden=!0});let n=document.getElementById("pay-now");n.addEventListener("click",()=>{const r=document.getElementById("widget");if(t.getValidationState(),!t.isValidForm()&&r.offsetParent!==null){n.disabled=!1,n.querySelector("svg").classList.add("hidden"),n.querySelector("span").classList.remove("hidden");return}n.disabled=!0,n.querySelector("svg").classList.remove("hidden"),n.querySelector("span").classList.add("hidden");let s=document.querySelector("input[name=token-billing-checkbox]:checked");s&&(document.getElementById("store_card").value=s.value),r.offsetParent!==null?document.getElementById("stepone_submit").click():document.getElementById("server-response").submit()}),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",r=>{var c;document.getElementById("widget").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value="",(c=document.querySelector("#powerboard-payment-container"))==null||c.classList.remove("hidden")}),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(r=>r.addEventListener("click",s=>{var c;document.getElementById("widget").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=s.target.dataset.token,(c=document.querySelector("#powerboard-payment-container"))==null||c.classList.add("hidden")}));const o=document.querySelector('input[name="payment-type"]');o&&o.click(),console.log({focusCreditCard:a}),a&&((e=document.getElementById("toggle-payment-with-credit-card"))==null||e.click())}async function y(){try{const t=await g();if(t.status==="not_authenticated"||t==="not_authenticated")throw a=!0,d(),new Error("There was an issue authenticating this payment method.");if(t.status==="authentication_not_supported"){document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(t);let e=document.querySelector("input[name=token-billing-checkbox]:checked");return e&&(document.getElementById("store_card").value=e.value),document.getElementById("server-response").submit()}const n=new cba.Canvas3ds("#widget-3dsecure",t._3ds.token);n.load(),document.getElementById("widget").classList.add("hidden"),n.on("chargeAuthSuccess",function(e){document.querySelector('input[name="browser_details"]').value=null,document.querySelector('input[name="charge"]').value=JSON.stringify(e);let r=document.querySelector("input[name=token-billing-checkbox]:checked");r&&(document.getElementById("store_card").value=r.value),document.getElementById("server-response").submit()}),n.on("chargeAuthReject",function(e){document.getElementById("errors").textContent="Sorry, your transaction could not be processed...",document.getElementById("errors").hidden=!1,a=!0,d()}),n.load()}catch(t){document.getElementById("errors").textContent=`Sorry, your transaction could not be processed...
|
||||
|
||||
${t}`,document.getElementById("errors").hidden=!1,a=!0,d()}}async function g(){const t={name:navigator.userAgent.substring(0,100),java_enabled:navigator.javaEnabled()?"true":"false",language:navigator.language||navigator.userLanguage,screen_height:window.screen.height.toString(),screen_width:window.screen.width.toString(),time_zone:(new Date().getTimezoneOffset()*-1).toString(),color_depth:window.screen.colorDepth.toString()};document.querySelector('input[name="browser_details"]').value=JSON.stringify(t);const n=JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server-response")))),o=document.querySelector("meta[name=payments_route]");try{const e=await fetch(o.content,{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest",Accept:"application/json","X-CSRF-Token":document.querySelector('meta[name="csrf-token"]').content},body:n});return e.ok?await e.json():await e.json().then(r=>{throw new Error(r.message??"Unknown error.")})}catch(e){document.getElementById("errors").textContent=`Sorry, your transaction could not be processed...
|
||||
|
||||
${e.message}`,document.getElementById("errors").hidden=!1,console.error("Fetch error:",e),a=!0,d()}}i()?d():u("#powerboard-credit-card-payment").then(()=>d());
|
@ -12,7 +12,7 @@
|
||||
"file": "assets/wait-8f4ae121.js"
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-e0713224.js",
|
||||
"file": "assets/app-021b0210.js",
|
||||
"imports": [
|
||||
"_index-08e160a7.js",
|
||||
"__commonjsHelpers-725317a4.js"
|
||||
@ -158,7 +158,7 @@
|
||||
"src": "resources/js/clients/payments/paytrace-credit-card.js"
|
||||
},
|
||||
"resources/js/clients/payments/powerboard-credit-card.js": {
|
||||
"file": "assets/powerboard-credit-card-f8810425.js",
|
||||
"file": "assets/powerboard-credit-card-f5f23291.js",
|
||||
"imports": [
|
||||
"_wait-8f4ae121.js"
|
||||
],
|
||||
|
@ -57,6 +57,21 @@ function reload() {
|
||||
function pay() {
|
||||
reload();
|
||||
|
||||
const gatewayId = document.querySelector('meta[name=gateway_id]')?.content;
|
||||
|
||||
if(!gatewayId) {
|
||||
|
||||
let payNow = document.getElementById('pay-now');
|
||||
payNow.disabled = true;
|
||||
payNow.querySelector('svg').classList.remove('hidden');
|
||||
payNow.querySelector('span').classList.add('hidden');
|
||||
document.getElementById(
|
||||
'errors'
|
||||
).textContent = 'Gateway not found or verified';
|
||||
document.getElementById('errors').hidden = false;
|
||||
|
||||
}
|
||||
|
||||
const widget = setup();
|
||||
|
||||
widget.on('finish', () => {
|
||||
@ -139,8 +154,6 @@ function pay() {
|
||||
first.click();
|
||||
}
|
||||
|
||||
console.log({ focusCreditCard })
|
||||
|
||||
if (focusCreditCard) {
|
||||
document.getElementById('toggle-payment-with-credit-card')?.click();
|
||||
}
|
||||
@ -151,6 +164,8 @@ async function process3ds() {
|
||||
const resource = await get3dsToken();
|
||||
|
||||
if (
|
||||
!resource ||
|
||||
!resource.status ||
|
||||
resource.status === 'not_authenticated' ||
|
||||
resource === 'not_authenticated'
|
||||
) {
|
||||
@ -221,9 +236,12 @@ async function process3ds() {
|
||||
|
||||
canvas.load();
|
||||
} catch (error) {
|
||||
|
||||
const msg = error.message ?? 'Unknown error.';
|
||||
|
||||
document.getElementById(
|
||||
'errors'
|
||||
).textContent = `Sorry, your transaction could not be processed...\n\n${error}`;
|
||||
).textContent = `Sorry, your transaction could not be processed...\n\n${msg}`;
|
||||
document.getElementById('errors').hidden = false;
|
||||
|
||||
focusCreditCard = true;
|
||||
@ -274,19 +292,16 @@ async function get3dsToken() {
|
||||
throw new Error(errorData.message ?? 'Unknown error.');
|
||||
});
|
||||
|
||||
// const text = await response.text();
|
||||
// throw new Error(`Network response was not ok: ${response.statusText}. Response text: ${text}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
|
||||
document.getElementById(
|
||||
'errors'
|
||||
).textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
|
||||
document.getElementById('errors').hidden = false;
|
||||
|
||||
console.error('Fetch error:', error); // Log error for debugging
|
||||
|
||||
focusCreditCard = true;
|
||||
|
||||
pay();
|
||||
|
13
resources/views/auth/connect/session_expired.blade.php
Normal file
13
resources/views/auth/connect/session_expired.blade.php
Normal file
@ -0,0 +1,13 @@
|
||||
@extends('layouts.ninja')
|
||||
@section('meta_title', ctrans('texts.error'))
|
||||
|
||||
@section('body')
|
||||
<div class="flex flex-col justify-center items-center mt-10">
|
||||
<div class="mb-4">
|
||||
<svg height="60" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 468 222.5" xml:space="preserve"><style>.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635bff}</style><path class="st0" d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z"/></svg>
|
||||
</div>
|
||||
|
||||
<p>Your session has expired. Connection to Stripe has been cancelled. Please try again.</p>
|
||||
<span>Click <a class="font-semibold hover:underline" href="{{ $url ?? url('/#/settings/company_gateways') }}">here</a> to continue.</span>
|
||||
</div>
|
||||
@endsection
|
@ -2,40 +2,409 @@
|
||||
|
||||
namespace Tests\Feature\Import\Quickbooks;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Import\Providers\Quickbooks;
|
||||
use App\Import\Transformer\BaseTransformer;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Tests\MockAccountData;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Mockery;
|
||||
use App\Models\Client;
|
||||
use App\Models\Product;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
use ReflectionClass;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use Tests\MockAccountData;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\ClientContact;
|
||||
use App\DataMapper\ClientSync;
|
||||
use App\DataMapper\InvoiceItem;
|
||||
use App\DataMapper\InvoiceSync;
|
||||
use App\DataMapper\ProductSync;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Import\Providers\Quickbooks;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use QuickBooksOnline\API\Facades\Item;
|
||||
use App\Import\Transformer\BaseTransformer;
|
||||
use App\Services\Quickbooks\QuickbooksService;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use QuickBooksOnline\API\Facades\Invoice as QbInvoice;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
class QuickbooksTest extends TestCase
|
||||
{
|
||||
use MakesHash;
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected $quickbooks;
|
||||
protected $data;
|
||||
protected QuickbooksService $qb;
|
||||
protected $faker;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->markTestSkipped('no bueno');
|
||||
|
||||
if(config('ninja.is_travis'))
|
||||
{
|
||||
$this->markTestSkipped('No need to run this test on Travis');
|
||||
}
|
||||
elseif(Company::whereNotNull('quickbooks')->count() == 0){
|
||||
$this->markTestSkipped('No need to run this test on Travis');
|
||||
}
|
||||
|
||||
$this->faker = \Faker\Factory::create();
|
||||
}
|
||||
|
||||
public function testCustomerSync()
|
||||
public function createQbProduct()
|
||||
{
|
||||
$data = (json_decode(file_get_contents(base_path('tests/Feature/Import/Quickbooks/customers.json')), false));
|
||||
$service_product = Product::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->company->owner()->id,
|
||||
'notes' => $this->faker->sentence(),
|
||||
'product_key' => $this->faker->word(63),
|
||||
'tax_id' => 2,
|
||||
]);
|
||||
|
||||
|
||||
$non_inventory_product = Product::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->company->owner()->id,
|
||||
'notes' => $this->faker->sentence(),
|
||||
'product_key' => $this->faker->word(63),
|
||||
'tax_id' => 1,
|
||||
]);
|
||||
|
||||
|
||||
$qb_product = Item::create([
|
||||
"Name" => $non_inventory_product->product_key,
|
||||
"Description" => $non_inventory_product->notes,
|
||||
"Active" => true,
|
||||
"FullyQualifiedName" => $non_inventory_product->product_key,
|
||||
"Taxable" => true,
|
||||
"UnitPrice" => $non_inventory_product->price,
|
||||
"Type" => "NonInventory",
|
||||
"IncomeAccountRef" => [
|
||||
"value" => $this->qb->settings->default_income_account, // Replace with your actual income account ID
|
||||
],
|
||||
// "AssetAccountRef" => [
|
||||
// "value" => "81", // Replace with your actual asset account ID
|
||||
// "name" => "Inventory Asset"
|
||||
// ],
|
||||
// "InvStartDate" => date('Y-m-d'),
|
||||
// "QtyOnHand" => 100,
|
||||
"TrackQtyOnHand" => false,
|
||||
// "ExpenseAccountRef" => [
|
||||
// "value" => "80", // Replace with your actual COGS account ID
|
||||
// "name" => "Cost of Goods Sold"
|
||||
// ]
|
||||
]);
|
||||
|
||||
|
||||
$qb_product = $this->qb->sdk->Add($qb_product);
|
||||
|
||||
$sync = new ProductSync();
|
||||
$sync->qb_id = data_get($qb_product, 'Id.value');
|
||||
$non_inventory_product->sync = $sync;
|
||||
$non_inventory_product->save();
|
||||
|
||||
$qb_service = Item::create([
|
||||
"Name" => $service_product->product_key,
|
||||
"Description" => $service_product->notes,
|
||||
"Active" => true,
|
||||
"FullyQualifiedName" => $service_product->product_key,
|
||||
"Taxable" => true,
|
||||
"UnitPrice" => $service_product->price,
|
||||
"Type" => "Service",
|
||||
"IncomeAccountRef" => [
|
||||
"value" => $this->qb->settings->default_income_account, // Replace with your actual income account ID
|
||||
],
|
||||
"TrackQtyOnHand" => false,
|
||||
|
||||
]);
|
||||
|
||||
|
||||
$qb_service = $this->qb->sdk->Add($qb_service);
|
||||
|
||||
$sync = new ProductSync();
|
||||
$sync->qb_id = data_get($qb_service, 'Id.value');
|
||||
$service_product->sync = $sync;
|
||||
$service_product->save();
|
||||
|
||||
return [$non_inventory_product, $service_product];
|
||||
}
|
||||
|
||||
public function createQbCustomer()
|
||||
{
|
||||
$client = Client::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->company->owner()->id,
|
||||
'address1' => $this->faker->address(),
|
||||
'city' => $this->faker->city(),
|
||||
'state' => $this->faker->state(),
|
||||
'postal_code' => $this->faker->postcode(),
|
||||
'country_id' => 840,
|
||||
'shipping_country_id' => 840,
|
||||
]);
|
||||
|
||||
$contact = ClientContact::factory()->create([
|
||||
'user_id' => $this->company->owner()->id,
|
||||
'client_id' => $client->id,
|
||||
'company_id' => $this->company->id,
|
||||
'is_primary' => 1,
|
||||
'send_email' => true,
|
||||
]);
|
||||
|
||||
$customerData = [
|
||||
"DisplayName" => $client->present()->name(), // Required and must be unique
|
||||
"PrimaryEmailAddr" => [
|
||||
"Address" => $client->present()->email(),
|
||||
],
|
||||
"PrimaryPhone" => [
|
||||
"FreeFormNumber" => $client->present()->phone()
|
||||
],
|
||||
"CompanyName" => $client->present()->name(),
|
||||
"BillAddr" => [
|
||||
"Line1" => $client->address1 ?? '',
|
||||
"City" => $client->city ?? '',
|
||||
"CountrySubDivisionCode" => $client->state ?? '',
|
||||
"PostalCode" => $client->postal_code ?? '',
|
||||
"Country" => $client->country->iso_3166_3
|
||||
],
|
||||
"ShipAddr" => [
|
||||
"Line1" => $client->shipping_address1 ?? '',
|
||||
"City" => $client->shipping_city ?? '',
|
||||
"CountrySubDivisionCode" => $client->shipping_state ?? '',
|
||||
"PostalCode" => $client->shipping_postal_code ?? '',
|
||||
"Country" => $client->shipping_country->iso_3166_3
|
||||
],
|
||||
"GivenName" => $client->present()->first_name(),
|
||||
"FamilyName" => $client->present()->last_name(),
|
||||
"PrintOnCheckName" => $client->present()->primary_contact_name(),
|
||||
"Notes" => $client->public_notes,
|
||||
// "TaxIdentifier" => $client->vat_number ?? '', // Federal Employer Identification Number (EIN)
|
||||
"BusinessNumber" => $client->id_number ?? '',
|
||||
"Active" => $client->deleted_at ? false : true,
|
||||
"V4IDPseudonym" => $client->client_hash,
|
||||
"WebAddr" => $client->website ?? '',
|
||||
];
|
||||
|
||||
|
||||
$customer = \QuickBooksOnline\API\Facades\Customer::create($customerData);
|
||||
|
||||
// Send the create request to QuickBooks
|
||||
$resultingCustomerObj = $this->qb->sdk->Add($customer);
|
||||
|
||||
$sync = new ClientSync();
|
||||
$sync->qb_id = data_get($resultingCustomerObj, 'Id.value');
|
||||
|
||||
$client->sync = $sync;
|
||||
$client->save();
|
||||
|
||||
return [$resultingCustomerObj, $client->id];
|
||||
|
||||
}
|
||||
|
||||
public function testCreateInvoiceInQb()
|
||||
{
|
||||
|
||||
$this->company = Company::whereNotNull('quickbooks')->first();
|
||||
|
||||
$this->qb = new QuickbooksService($this->company);
|
||||
|
||||
$customer = $this->createQbCustomer();
|
||||
|
||||
//create ninja invoice
|
||||
$qb_invoice = $this->createQbInvoice($customer);
|
||||
|
||||
$this->assertNotNull($qb_invoice);
|
||||
|
||||
nlog($qb_invoice);
|
||||
}
|
||||
|
||||
public function testCreateCustomerInQb()
|
||||
{
|
||||
$this->company = Company::whereNotNull('quickbooks')->first();
|
||||
$this->qb = new QuickbooksService($this->company);
|
||||
|
||||
$resultingCustomerObj = $this->createQbCustomer();
|
||||
|
||||
// Check for errors
|
||||
$error = $this->qb->sdk->getLastError();
|
||||
if ($error) {
|
||||
$this->fail("The Customer could not be created: " . $error->getResponseBody());
|
||||
}
|
||||
|
||||
$qb_id = data_get($resultingCustomerObj[0], 'Id.value');
|
||||
|
||||
$this->assertNotNull($qb_id);
|
||||
|
||||
$c = Client::find($resultingCustomerObj[1]);
|
||||
|
||||
$this->assertEquals($qb_id, $c->sync->qb_id);
|
||||
|
||||
}
|
||||
|
||||
public function createQbInvoice($customer_payload)
|
||||
{
|
||||
$customer = $customer_payload[0];
|
||||
$client_id = $customer_payload[1];
|
||||
|
||||
$products = $this->createQbProduct();
|
||||
|
||||
$non_inventory_product = $products[0];
|
||||
$service_product = $products[1];
|
||||
|
||||
|
||||
nlog(data_get($customer, 'Id.value'));
|
||||
|
||||
$client = Client::find($client_id);
|
||||
|
||||
$item_product = new InvoiceItem();
|
||||
$item_product->product_key = $non_inventory_product->product_key; // Changed from randomWord() to word()
|
||||
$item_product->notes = $non_inventory_product->notes;
|
||||
$item_product->quantity = 1;
|
||||
$item_product->cost = $non_inventory_product->price;
|
||||
$item_product->line_total = $non_inventory_product->price;
|
||||
$item_product->type_id = '1';
|
||||
|
||||
$item_service = new InvoiceItem();
|
||||
$item_service->product_key = $service_product->product_key; // Changed from randomWord() to word()
|
||||
$item_service->notes = $service_product->notes;
|
||||
$item_service->quantity = 1;
|
||||
$item_service->cost = $service_product->price;
|
||||
$item_service->line_total = $service_product->price;
|
||||
$item_service->type_id = '2';
|
||||
|
||||
$i = Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $client->user_id,
|
||||
'client_id' => $client->id,
|
||||
'date' => '2024-09-01',
|
||||
'due_date' => '2024-09-30',
|
||||
'public_notes' => 'Test invoice',
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'amount' => 30,
|
||||
'paid_to_date' => 0,
|
||||
'is_amount_discount' => 0,
|
||||
'discount' => 0,
|
||||
'line_items' => [$item_product, $item_service],
|
||||
'number' => null,
|
||||
]);
|
||||
|
||||
$i->calc()->getInvoice()->service()->applyNumber()->markSent()->createInvitations()->save();
|
||||
|
||||
$this->assertEquals(2, $i->status_id);
|
||||
// $this->assertEquals(30, $i->amount);
|
||||
$this->assertEquals(0, $i->paid_to_date);
|
||||
$this->assertEquals(0, $i->is_amount_discount);
|
||||
$this->assertEquals(0, $i->discount);
|
||||
// $this->assertEquals(30, $i->balance);
|
||||
|
||||
$line_items = [];
|
||||
$line_num = 1;
|
||||
|
||||
foreach($i->line_items as $line_item)
|
||||
{
|
||||
$product = Product::where('company_id', $this->company->id)
|
||||
->where('product_key', $line_item->product_key)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($product);
|
||||
|
||||
$line_items[] = [
|
||||
'LineNum' => $line_num, // Add the line number
|
||||
'DetailType' => 'SalesItemLineDetail',
|
||||
'SalesItemLineDetail' => [
|
||||
'ItemRef' => [
|
||||
'value' => $product->sync->qb_id,
|
||||
],
|
||||
'Qty' => $line_item->quantity,
|
||||
'UnitPrice' => $line_item->cost,
|
||||
'TaxCodeRef' => [
|
||||
'value' => (in_array($product->tax_id, [5, 8])) ? 'NON' : 'TAX',
|
||||
],
|
||||
],
|
||||
'Description' => $line_item->notes,
|
||||
|
||||
'Amount' => $line_item->line_total,
|
||||
];
|
||||
|
||||
$line_num++;
|
||||
|
||||
}
|
||||
|
||||
$invoiceData = [
|
||||
"Line" => $line_items,
|
||||
"CustomerRef" => [
|
||||
"value" => data_get($customer, 'Id.value')
|
||||
],
|
||||
"BillEmail" => [
|
||||
"Address" => $client->present()->email()
|
||||
],
|
||||
"TxnDate" => $i->date,
|
||||
"DueDate" => $i->due_date,
|
||||
"TotalAmt" => $i->amount,
|
||||
"DocNumber" => $i->number,
|
||||
"ApplyTaxAfterDiscount" => false,
|
||||
"GlobalTaxCalculation" => "TaxExcluded", // This tells QuickBooks to calculate taxes
|
||||
"TxnTaxDetail" => [
|
||||
"UseAutomatedSalesTax" => true,
|
||||
// "TxnTaxCodeRef" => [
|
||||
// "value" => "TAX" // Use the appropriate tax code for your QuickBooks account
|
||||
// "DefaultTaxRateRef" => [
|
||||
// ]
|
||||
]
|
||||
// "Note" => $this->invoice->public_notes,
|
||||
];
|
||||
|
||||
nlog($invoiceData);
|
||||
|
||||
$invoice = \QuickBooksOnline\API\Facades\Invoice::create($invoiceData);
|
||||
|
||||
|
||||
nlog($invoice);
|
||||
|
||||
$qb_invoice = $this->qb->sdk->Add($invoice);
|
||||
|
||||
$sync = new InvoiceSync();
|
||||
$sync->qb_id = data_get($qb_invoice, 'Id.value');
|
||||
$i->sync = $sync;
|
||||
$i->save();
|
||||
|
||||
return $qb_invoice;
|
||||
}
|
||||
|
||||
|
||||
private function getQuickBooksItemType($line_item): string
|
||||
{
|
||||
$typeMap = [
|
||||
'1' => 'NonInventory', // product
|
||||
'2' => 'Service', // service
|
||||
'3' => 'NonInventory', // unpaid gateway fee
|
||||
'4' => 'NonInventory', // paid gateway fee
|
||||
'5' => 'Service', // late fee
|
||||
'6' => 'NonInventory', // expense
|
||||
];
|
||||
|
||||
return $typeMap[$line_item->type_id] ?? 'Service';
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
|
||||
// $this->company->clients()->forceDelete();
|
||||
// $this->company->products()->forceDelete();
|
||||
// $this->company->projects()->forceDelete();
|
||||
// $this->company->tasks()->forceDelete();
|
||||
// $this->company->vendors()->forceDelete();
|
||||
// $this->company->expenses()->forceDelete();
|
||||
// $this->company->purchase_orders()->forceDelete();
|
||||
// $this->company->bank_transaction_rules()->forceDelete();
|
||||
// $this->company->bank_transactions()->forceDelete();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -157,6 +157,80 @@ class ReminderTest extends TestCase
|
||||
'balance' => 10,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
public function testReminderScheduleNy()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->timezone_id = '15';
|
||||
$settings->entity_send_time = 6;
|
||||
$settings->payment_terms = '14';
|
||||
$settings->send_reminders = true;
|
||||
$settings->enable_reminder1 = true;
|
||||
$settings->enable_reminder2 = false;
|
||||
$settings->enable_reminder3 = false;
|
||||
$settings->enable_reminder_endless = true;
|
||||
$settings->schedule_reminder1 = 'after_invoice_date';
|
||||
$settings->schedule_reminder2 = '';
|
||||
$settings->schedule_reminder3 = '';
|
||||
$settings->num_days_reminder1 = 1;
|
||||
$settings->num_days_reminder2 = 0;
|
||||
$settings->num_days_reminder3 = 0;
|
||||
$settings->endless_reminder_frequency_id = '1';
|
||||
|
||||
$this->buildData($settings);
|
||||
|
||||
$this->travelTo(Carbon::parse('2024-09-20')->startOfDay()->addHours(1));
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => 10,
|
||||
'balance' => 10,
|
||||
'date' => '2024-09-19',
|
||||
'number' => 'JJJ1-11-2024',
|
||||
'due_date' => '2024-09-19',
|
||||
'status_id' => 2,
|
||||
'last_sent_date' => '19-09-2024',
|
||||
]);
|
||||
|
||||
$this->assertEquals(1, $invoice->company->settings->num_days_reminder1);
|
||||
|
||||
$invoice->service()->setReminder($settings)->save();
|
||||
|
||||
$this->assertEquals(10, $invoice->balance);
|
||||
$this->assertEquals('2024-09-20', $invoice->next_send_date->format('Y-m-d'));
|
||||
|
||||
|
||||
$x = false;
|
||||
do {
|
||||
|
||||
$this->travelTo(now()->addHour());
|
||||
(new ReminderJob())->handle();
|
||||
$invoice = $invoice->fresh();
|
||||
|
||||
$x = (bool)$invoice->reminder1_sent;
|
||||
} while ($x === false);
|
||||
|
||||
$this->assertNotNull($invoice->reminder_last_sent);
|
||||
$this->assertEquals(now()->addDays(1), $invoice->next_send_date);
|
||||
|
||||
$x = 0;
|
||||
do {
|
||||
|
||||
$this->travelTo(now()->addHour());
|
||||
(new ReminderJob())->handle();
|
||||
$invoice = $invoice->fresh();
|
||||
|
||||
$x++;
|
||||
} while ($x < 24);
|
||||
|
||||
$this->assertEquals(now()->addDays(1), $invoice->next_send_date);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function testDKRemindersNotSending()
|
||||
|
@ -840,6 +840,7 @@ trait MockAccountData
|
||||
|
||||
$item = InvoiceItemFactory::create();
|
||||
$item->quantity = 1;
|
||||
$item->notes = $this->faker->sentence;
|
||||
$item->cost = 10;
|
||||
$item->task_id = $this->encodePrimaryKey($this->task->id);
|
||||
$item->expense_id = $this->encodePrimaryKey($this->expense->id);
|
||||
|
Loading…
x
Reference in New Issue
Block a user