mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-09 20:52:56 +01:00
Merge pull request #9960 from turbo124/v5-develop
Add reversal for failed BTC payments
This commit is contained in:
commit
a4a2e237db
83
app/DataMapper/Analytics/LegalEntityCreated.php
Normal file
83
app/DataMapper/Analytics/LegalEntityCreated.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?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\Analytics;
|
||||
|
||||
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
|
||||
|
||||
class LegalEntityCreated extends GenericMixedMetric
|
||||
{
|
||||
/**
|
||||
* The type of Sample.
|
||||
*
|
||||
* Monotonically incrementing counter
|
||||
*
|
||||
* - counter
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type = 'mixed_metric';
|
||||
|
||||
/**
|
||||
* The name of the counter.
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'einvoice.legal_entity.created';
|
||||
|
||||
/**
|
||||
* The datetime of the counter measurement.
|
||||
*
|
||||
* date("Y-m-d H:i:s")
|
||||
*
|
||||
*/
|
||||
public $datetime;
|
||||
|
||||
/**
|
||||
* The Class failure name
|
||||
* set to 0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric5 = 'stub';
|
||||
|
||||
/**
|
||||
* The exception string
|
||||
* set to 0.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric6 = 'stub';
|
||||
|
||||
/**
|
||||
* The counter
|
||||
* set to 1.
|
||||
*
|
||||
*/
|
||||
public $int_metric1 = 1;
|
||||
|
||||
/**
|
||||
* Company Key
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric7 = '';
|
||||
|
||||
/**
|
||||
* Subject
|
||||
* @var string
|
||||
*/
|
||||
public $string_metric8 = '';
|
||||
|
||||
public function __construct($string_metric7 = '', $string_metric8 = '')
|
||||
{
|
||||
$this->string_metric7 = $string_metric7;
|
||||
$this->string_metric8 = $string_metric8;
|
||||
}
|
||||
}
|
@ -518,7 +518,12 @@ class CompanySettings extends BaseSettings
|
||||
|
||||
public string $payment_flow = 'default'; //smooth
|
||||
|
||||
public string $email_subject_payment_failed = '';
|
||||
public string $email_template_payment_failed = '';
|
||||
|
||||
public static $casts = [
|
||||
'email_template_payment_failed' => 'string',
|
||||
'email_subject_payment_failed' => 'string',
|
||||
'payment_flow' => 'string',
|
||||
'enable_quote_reminder1' => 'bool',
|
||||
'quote_num_days_reminder1' => 'int',
|
||||
|
@ -30,6 +30,7 @@ class EmailTemplateDefaults
|
||||
'email_template_custom2',
|
||||
'email_template_custom3',
|
||||
'email_template_purchase_order',
|
||||
'email_template_payment_failed'
|
||||
];
|
||||
|
||||
public static function getDefaultTemplate($template, $locale)
|
||||
@ -39,6 +40,8 @@ class EmailTemplateDefaults
|
||||
switch ($template) {
|
||||
/* Template */
|
||||
|
||||
case 'email_template_payment_failed':
|
||||
return self::emailPaymentFailedTemplate();
|
||||
case 'email_template_invoice':
|
||||
return self::emailInvoiceTemplate();
|
||||
case 'email_template_quote':
|
||||
@ -73,6 +76,9 @@ class EmailTemplateDefaults
|
||||
case 'email_subject_invoice':
|
||||
return self::emailInvoiceSubject();
|
||||
|
||||
case 'email_subject_payment_failed':
|
||||
return self::emailPaymentFailedSubject();
|
||||
|
||||
case 'email_subject_quote':
|
||||
return self::emailQuoteSubject();
|
||||
|
||||
@ -127,6 +133,16 @@ class EmailTemplateDefaults
|
||||
}
|
||||
}
|
||||
|
||||
public static function emailPaymentFailedSubject()
|
||||
{
|
||||
return ctrans('texts.notification_invoice_payment_failed_subject', ['invoice' => '$number']);
|
||||
}
|
||||
|
||||
public static function emailPaymentFailedTemplate()
|
||||
{
|
||||
return '<p>$client<br><br>'.ctrans('texts.client_payment_failure_body', ['invoice' => '$number', 'amount' => '$amount']).'</p><div class="center">$gateway_payment_error</div><br><div class="center">$payment_button</div>';
|
||||
}
|
||||
|
||||
public static function emailQuoteReminder1Subject()
|
||||
{
|
||||
return ctrans('texts.quote_reminder_subject', ['quote' => '$number', 'company' => '$company.name']);
|
||||
@ -135,9 +151,7 @@ class EmailTemplateDefaults
|
||||
public static function emailQuoteReminder1Body()
|
||||
{
|
||||
|
||||
$invoice_message = '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div class="center">$view_button</div>';
|
||||
|
||||
return $invoice_message;
|
||||
return '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div class="center">$view_button</div>';
|
||||
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ class QuickbooksSettings implements Castable
|
||||
|
||||
public int $refreshTokenExpiresAt;
|
||||
|
||||
public string $baseURL;
|
||||
/**
|
||||
* entity client,invoice,quote,purchase_order,vendor,payment
|
||||
* sync true/false
|
||||
|
35
app/Events/Invoice/InvoiceAutoBillFailed.php
Normal file
35
app/Events/Invoice/InvoiceAutoBillFailed.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?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\Events\Invoice;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class InvoiceAutoBillFailed.
|
||||
*/
|
||||
class InvoiceAutoBillFailed
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @param Company $company
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars, public ?string $notes)
|
||||
{
|
||||
}
|
||||
}
|
35
app/Events/Invoice/InvoiceAutoBillSuccess.php
Normal file
35
app/Events/Invoice/InvoiceAutoBillSuccess.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?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\Events\Invoice;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class InvoiceAutoBillSuccess.
|
||||
*/
|
||||
class InvoiceAutoBillSuccess
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param Invoice $invoice
|
||||
* @param Company $company
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(public Invoice $invoice, public Company $company, public array $event_vars)
|
||||
{
|
||||
}
|
||||
}
|
@ -76,6 +76,26 @@ class InvoiceItemFactory
|
||||
$data[] = $item;
|
||||
}
|
||||
|
||||
|
||||
$item = self::create();
|
||||
$item->quantity = $faker->numberBetween(1, 10);
|
||||
$item->cost = $faker->randomFloat(2, 1, 1000);
|
||||
$item->line_total = $item->quantity * $item->cost;
|
||||
$item->is_amount_discount = true;
|
||||
$item->discount = $faker->numberBetween(1, 10);
|
||||
$item->notes = str_replace(['"',"'"], ['',""], $faker->realText(20));
|
||||
$item->product_key = $faker->word();
|
||||
// $item->custom_value1 = $faker->realText(10);
|
||||
// $item->custom_value2 = $faker->realText(10);
|
||||
// $item->custom_value3 = $faker->realText(10);
|
||||
// $item->custom_value4 = $faker->realText(10);
|
||||
$item->tax_name1 = 'GST';
|
||||
$item->tax_rate1 = 10.00;
|
||||
$item->type_id = '2';
|
||||
|
||||
$data[] = $item;
|
||||
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
@ -99,6 +99,12 @@ class ExpenseFilters extends QueryFilters
|
||||
});
|
||||
}
|
||||
|
||||
if (in_array('uninvoiced', $status_parameters)) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->whereNull('invoice_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (in_array('paid', $status_parameters)) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->whereNotNull('payment_date');
|
||||
|
@ -104,6 +104,7 @@ class TransactionTransformer implements BankRevenueInterface
|
||||
}
|
||||
|
||||
$amount = (float) $transaction["transactionAmount"]["amount"];
|
||||
$base_type = $amount < 0 ? 'DEBIT' : 'CREDIT';
|
||||
|
||||
// description could be in varios places
|
||||
$description = '';
|
||||
@ -140,7 +141,7 @@ class TransactionTransformer implements BankRevenueInterface
|
||||
return [
|
||||
'transaction_id' => 0,
|
||||
'nordigen_transaction_id' => $transactionId,
|
||||
'amount' => $amount,
|
||||
'amount' => abs($amount),
|
||||
'currency_id' => $this->convertCurrency($transaction["transactionAmount"]["currency"]),
|
||||
'category_id' => null,
|
||||
'category_type' => array_key_exists('additionalInformation', $transaction) ? $transaction["additionalInformation"] : '',
|
||||
@ -148,7 +149,7 @@ class TransactionTransformer implements BankRevenueInterface
|
||||
'description' => $description,
|
||||
'participant' => $participant,
|
||||
'participant_name' => $participant_name,
|
||||
'base_type' => $amount < 0 ? 'DEBIT' : 'CREDIT',
|
||||
'base_type' => $base_type,
|
||||
];
|
||||
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ class InvitationController extends Controller
|
||||
|
||||
if(!auth()->guard('contact')->check()) {
|
||||
$this->middleware('auth:contact');
|
||||
/** @var \App\Models\InvoiceInvitation | \App\Models\QuoteInvitation | \App\Models\CreditInvitation | \App\Models\RecurringInvoiceInvitation $invitation */
|
||||
return redirect()->route('client.login', ['intended' => route('client.'.$entity.'.show', [$entity => $this->encodePrimaryKey($invitation->{$key}), 'silent' => $is_silent])]);
|
||||
}
|
||||
|
||||
|
@ -35,11 +35,16 @@ class PrePaymentController extends Controller
|
||||
/**
|
||||
* Show the list of payments.
|
||||
*
|
||||
* @return Factory|View
|
||||
* @return Factory|View|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
|
||||
$client = auth()->guard('contact')->user()->client;
|
||||
|
||||
if(!$client->getSetting('client_initiated_payments'))
|
||||
return redirect()->route('client.dashboard');
|
||||
|
||||
$minimum = $client->getSetting('client_initiated_payments_minimum');
|
||||
$minimum_amount = $minimum == 0 ? "" : Number::formatMoney($minimum, $client);
|
||||
|
||||
|
26
app/Http/Controllers/EInvoice/SelfhostController.php
Normal file
26
app/Http/Controllers/EInvoice/SelfhostController.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?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\Http\Controllers\EInvoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\EInvoice\SignupRequest;
|
||||
|
||||
class SelfhostController extends Controller
|
||||
{
|
||||
|
||||
public function index(SignupRequest $request)
|
||||
{
|
||||
return view('einvoice.index');
|
||||
}
|
||||
|
||||
}
|
@ -118,9 +118,36 @@ class ImportController extends Controller
|
||||
|
||||
})->toArray();
|
||||
|
||||
|
||||
//Exact string match
|
||||
foreach($headers as $key => $value) {
|
||||
|
||||
foreach($translated_keys as $tkey => $tvalue) {
|
||||
|
||||
$concat_needle = str_ireplace(" ", "", $tvalue['index'].$tvalue['label']);
|
||||
$concat_value = str_ireplace(" ", "", $value);
|
||||
|
||||
if($this->testMatch($concat_value, $concat_needle)) {
|
||||
|
||||
$hit = $tvalue['key'];
|
||||
$hints[$key] = $hit;
|
||||
unset($translated_keys[$tkey]);
|
||||
break;
|
||||
|
||||
} else {
|
||||
$hints[$key] = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Label Match
|
||||
foreach($headers as $key => $value) {
|
||||
|
||||
if(isset($hints[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach($translated_keys as $tkey => $tvalue) {
|
||||
|
||||
if($this->testMatch($value, $tvalue['label'])) {
|
||||
@ -134,10 +161,9 @@ class ImportController extends Controller
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//second pass using the index of the translation here
|
||||
//Index matching pass using the index of the translation here
|
||||
foreach($headers as $key => $value) {
|
||||
if(isset($hints[$key])) {
|
||||
continue;
|
||||
|
@ -60,75 +60,4 @@ class ImportQuickbooksController extends BaseController
|
||||
return redirect()->to($authorizationUrl);
|
||||
}
|
||||
|
||||
public function preimport(string $type, string $hash)
|
||||
{
|
||||
// // Check for authorization otherwise
|
||||
// // Create a reference
|
||||
// $data = [
|
||||
// 'hash' => $hash,
|
||||
// 'type' => $type
|
||||
// ];
|
||||
// $this->getData($data);
|
||||
}
|
||||
|
||||
protected function getData($data)
|
||||
{
|
||||
|
||||
// $entity = $this->import_entities[$data['type']];
|
||||
// $cache_name = "{$data['hash']}-{$data['type']}";
|
||||
// // TODO: Get or put cache or DB?
|
||||
// if(! Cache::has($cache_name)) {
|
||||
// $contents = call_user_func([$this->service, "fetch{$entity}s"]);
|
||||
// if($contents->isEmpty()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Cache::put($cache_name, base64_encode($contents->toJson()), 600);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/import_json",
|
||||
* operationId="getImportJson",
|
||||
* tags={"import"},
|
||||
* summary="Import data from the system",
|
||||
* description="Import data from the system",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="success",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
// $hash = Str::random(32);
|
||||
// foreach($request->input('import_types') as $type) {
|
||||
// $this->preimport($type, $hash);
|
||||
// }
|
||||
// /** @var \App\Models\User $user */
|
||||
// // $user = auth()->user() ?? Auth::loginUsingId(60);
|
||||
// $data = ['import_types' => $request->input('import_types') ] + compact('hash');
|
||||
// if (Ninja::isHosted()) {
|
||||
// QuickbooksIngest::dispatch($data, $user->company());
|
||||
// } else {
|
||||
// QuickbooksIngest::dispatch($data, $user->company());
|
||||
// }
|
||||
|
||||
// return response()->json(['message' => 'Processing'], 200);
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class UpdateCompanyRequest extends Request
|
||||
$rules['smtp_local_domain'] = 'sometimes|string|nullable';
|
||||
// $rules['smtp_verify_peer'] = 'sometimes|string';
|
||||
|
||||
$rules['e_invoice'] = ['sometimes','nullable', new ValidCompanyScheme()];
|
||||
$rules['e_invoice'] = ['sometimes', 'nullable', new ValidCompanyScheme()];
|
||||
|
||||
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
|
||||
$rules['portal_domain'] = 'bail|nullable|sometimes|url';
|
||||
|
28
app/Http/Requests/EInvoice/SignupRequest.php
Normal file
28
app/Http/Requests/EInvoice/SignupRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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\Http\Requests\EInvoice;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
class SignupRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Ninja::isSelfHost();
|
||||
}
|
||||
}
|
@ -1,253 +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\Import\Providers;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Factory\ClientFactory;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Factory\ProductFactory;
|
||||
use App\Import\ImportException;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Repositories\ClientRepository;
|
||||
use App\Repositories\InvoiceRepository;
|
||||
use App\Repositories\PaymentRepository;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Http\Requests\Client\StoreClientRequest;
|
||||
use App\Http\Requests\Invoice\StoreInvoiceRequest;
|
||||
use App\Http\Requests\Payment\StorePaymentRequest;
|
||||
use App\Http\Requests\Product\StoreProductRequest;
|
||||
use App\Import\Transformer\Quickbooks\ClientTransformer;
|
||||
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
|
||||
use App\Import\Transformer\Quickbooks\PaymentTransformer;
|
||||
use App\Import\Transformer\Quickbooks\ProductTransformer;
|
||||
|
||||
class Quickbooks extends BaseImport
|
||||
{
|
||||
public array $entity_count = [];
|
||||
|
||||
public function import(string $entity)
|
||||
{
|
||||
if (
|
||||
in_array($entity, [
|
||||
'client',
|
||||
'invoice',
|
||||
'product',
|
||||
'payment',
|
||||
// 'vendor',
|
||||
// 'expense',
|
||||
])
|
||||
) {
|
||||
$this->{$entity}();
|
||||
}
|
||||
|
||||
//collate any errors
|
||||
|
||||
// $this->finalizeImport();
|
||||
}
|
||||
|
||||
public function client()
|
||||
{
|
||||
$entity_type = 'client';
|
||||
$data = $this->getData($entity_type);
|
||||
if (empty($data)) {
|
||||
$this->entity_count['clients'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StoreClientRequest::class;
|
||||
$this->repository_name = ClientRepository::class;
|
||||
$this->factory_name = ClientFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new ClientTransformer($this->company);
|
||||
$client_count = $this->ingest($data, $entity_type);
|
||||
$this->entity_count['clients'] = $client_count;
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
$entity_type = 'product';
|
||||
$data = $this->getData($entity_type);
|
||||
if (empty($data)) {
|
||||
$this->entity_count['products'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StoreProductRequest::class;
|
||||
$this->repository_name = ProductRepository::class;
|
||||
$this->factory_name = ProductFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new ProductTransformer($this->company);
|
||||
$count = $this->ingest($data, $entity_type);
|
||||
$this->entity_count['products'] = $count;
|
||||
}
|
||||
|
||||
public function getData($type)
|
||||
{
|
||||
|
||||
// get the data from cache? file? or api ?
|
||||
return json_decode(base64_decode(Cache::get("{$this->hash}-{$type}")), true);
|
||||
}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
$entity_type = 'payment';
|
||||
$data = $this->getData($entity_type);
|
||||
if (empty($data)) {
|
||||
$this->entity_count['payments'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StorePaymentRequest::class;
|
||||
$this->repository_name = PaymentRepository::class;
|
||||
$this->factory_name = PaymentFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new PaymentTransformer($this->company);
|
||||
$count = $this->ingest($data, $entity_type);
|
||||
$this->entity_count['payments'] = $count;
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
//make sure we update and create products
|
||||
$initial_update_products_value = $this->company->update_products;
|
||||
$this->company->update_products = true;
|
||||
|
||||
$this->company->save();
|
||||
|
||||
$entity_type = 'invoice';
|
||||
$data = $this->getData($entity_type);
|
||||
|
||||
if (empty($data)) {
|
||||
$this->entity_count['invoices'] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->request_name = StoreInvoiceRequest::class;
|
||||
$this->repository_name = InvoiceRepository::class;
|
||||
$this->factory_name = InvoiceFactory::class;
|
||||
$this->repository = app()->make($this->repository_name);
|
||||
$this->repository->import_mode = true;
|
||||
$this->transformer = new InvoiceTransformer($this->company);
|
||||
$invoice_count = $this->ingestInvoices($data, '');
|
||||
$this->entity_count['invoices'] = $invoice_count;
|
||||
$this->company->update_products = $initial_update_products_value;
|
||||
$this->company->save();
|
||||
}
|
||||
|
||||
public function ingestInvoices($invoices, $invoice_number_key)
|
||||
{
|
||||
$count = 0;
|
||||
$invoice_transformer = $this->transformer;
|
||||
/** @var ClientRepository $client_repository */
|
||||
$client_repository = app()->make(ClientRepository::class);
|
||||
$client_repository->import_mode = true;
|
||||
$invoice_repository = new InvoiceRepository();
|
||||
$invoice_repository->import_mode = true;
|
||||
|
||||
foreach ($invoices as $raw_invoice) {
|
||||
if(!is_array($raw_invoice)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$invoice_data = $invoice_transformer->transform($raw_invoice);
|
||||
$invoice_data['user_id'] = $this->company->owner()->id;
|
||||
$invoice_data['line_items'] = (array) $invoice_data['line_items'];
|
||||
$invoice_data['line_items'] = $this->cleanItems(
|
||||
$invoice_data['line_items'] ?? []
|
||||
);
|
||||
|
||||
if (
|
||||
empty($invoice_data['client_id']) &&
|
||||
! empty($invoice_data['client'])
|
||||
) {
|
||||
$client_data = $invoice_data['client'];
|
||||
$client_data['user_id'] = $this->getUserIDForRecord(
|
||||
$invoice_data
|
||||
);
|
||||
$client_repository->save(
|
||||
$client_data,
|
||||
$client = ClientFactory::create(
|
||||
$this->company->id,
|
||||
$client_data['user_id']
|
||||
)
|
||||
);
|
||||
$invoice_data['client_id'] = $client->id;
|
||||
unset($invoice_data['client']);
|
||||
}
|
||||
|
||||
$validator = $this->request_name::runFormRequest($invoice_data);
|
||||
if ($validator->fails()) {
|
||||
$this->error_array['invoice'][] = [
|
||||
'invoice' => $invoice_data,
|
||||
'error' => $validator->errors()->all(),
|
||||
];
|
||||
} else {
|
||||
if(!Invoice::where('number', $invoice_data['number'])->first()) {
|
||||
$invoice = InvoiceFactory::create(
|
||||
$this->company->id,
|
||||
$this->company->owner()->id
|
||||
);
|
||||
$invoice->mergeFillable(['partial','amount','balance','line_items']);
|
||||
if (! empty($invoice_data['status_id'])) {
|
||||
$invoice->status_id = $invoice_data['status_id'];
|
||||
}
|
||||
|
||||
$saveable_invoice_data = $invoice_data;
|
||||
if(array_key_exists('payments', $saveable_invoice_data)) {
|
||||
unset($saveable_invoice_data['payments']);
|
||||
}
|
||||
|
||||
$invoice->fill($saveable_invoice_data);
|
||||
$invoice->save();
|
||||
$count++;
|
||||
|
||||
}
|
||||
// $this->actionInvoiceStatus(
|
||||
// $invoice,
|
||||
// $invoice_data,
|
||||
// $invoice_repository
|
||||
// );
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
|
||||
\DB::connection(config('database.default'))->rollBack();
|
||||
}
|
||||
|
||||
if ($ex instanceof ImportException) {
|
||||
$message = $ex->getMessage();
|
||||
} else {
|
||||
report($ex);
|
||||
$message = 'Unknown error ';
|
||||
nlog($ex->getMessage());
|
||||
nlog($raw_invoice);
|
||||
}
|
||||
|
||||
$this->error_array['invoice'][] = [
|
||||
'invoice' => $raw_invoice,
|
||||
'error' => $message,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
}
|
@ -238,6 +238,7 @@ class MatchBankTransactions implements ShouldQueue
|
||||
$amount = $this->bt->amount;
|
||||
|
||||
if ($_invoices->count() > 0 && $this->checkPayable($_invoices)) {
|
||||
|
||||
$this->createPayment($_invoices, $amount);
|
||||
|
||||
$this->bts->push($this->bt->id);
|
||||
@ -293,6 +294,8 @@ class MatchBankTransactions implements ShouldQueue
|
||||
$this->attachable_invoices = [];
|
||||
$this->available_balance = $amount;
|
||||
|
||||
nlog($invoices->count());
|
||||
|
||||
\DB::connection(config('database.default'))->transaction(function () use ($invoices) {
|
||||
$invoices->each(function ($invoice) {
|
||||
$this->invoice = Invoice::withTrashed()->where('id', $invoice->id)->lockForUpdate()->first();
|
||||
@ -326,11 +329,16 @@ class MatchBankTransactions implements ShouldQueue
|
||||
});
|
||||
}, 2);
|
||||
|
||||
nlog("pre");
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
if (!$this->invoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
nlog("post");
|
||||
|
||||
/* Create Payment */
|
||||
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
|
||||
|
||||
@ -395,6 +403,9 @@ class MatchBankTransactions implements ShouldQueue
|
||||
$this->bt->status_id = BankTransaction::STATUS_CONVERTED;
|
||||
$this->bt->payment_id = $payment->id;
|
||||
$this->bt->save();
|
||||
|
||||
nlog($this->bt->toArray());
|
||||
|
||||
}
|
||||
|
||||
private function resolveCategory($input): ?int
|
||||
|
@ -70,7 +70,7 @@ class CreateEDocument implements ShouldQueue
|
||||
if ($this->document instanceof Invoice) {
|
||||
switch ($e_document_type) {
|
||||
case "PEPPOL":
|
||||
return (new Peppol($this->document))->toXml();
|
||||
return (new Peppol($this->document))->run()->toXml();
|
||||
case "FACT1":
|
||||
return (new RoEInvoice($this->document))->generateXml();
|
||||
case "FatturaPA":
|
||||
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Import;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Import\Providers\Quickbooks;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class QuickbooksIngest implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
protected $engine;
|
||||
protected $request;
|
||||
protected $company;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(array $request, $company)
|
||||
{
|
||||
$this->company = $company;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
set_time_limit(0);
|
||||
|
||||
$engine = new Quickbooks(['import_type' => 'client', 'hash' => $this->request['hash'] ], $this->company);
|
||||
foreach ($this->request['import_types'] as $entity) {
|
||||
$engine->import($entity);
|
||||
}
|
||||
|
||||
$engine->finalizeImport();
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ class NinjaMailerJob implements ShouldQueue
|
||||
use MakesHash;
|
||||
|
||||
public $tries = 4; //number of retries
|
||||
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/** @var null|\App\Models\Company $company **/
|
||||
|
@ -84,7 +84,7 @@ class PaymentFailedMailer implements ShouldQueue
|
||||
$invoice = false;
|
||||
|
||||
if ($this->payment_hash) {
|
||||
// $amount = array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total;
|
||||
|
||||
$amount = $this->payment_hash?->amount_with_fee() ?: 0;
|
||||
$invoice = Invoice::query()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
|
||||
}
|
||||
|
59
app/Listeners/Invoice/InvoiceAutoBillFailedActivity.php
Normal file
59
app/Listeners/Invoice/InvoiceAutoBillFailedActivity.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?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\Listeners\Invoice;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Activity;
|
||||
use App\Repositories\ActivityRepository;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use stdClass;
|
||||
|
||||
class InvoiceAutoBillFailedActivity implements ShouldQueue
|
||||
{
|
||||
protected $activity_repo;
|
||||
|
||||
public $delay = 10;
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @param ActivityRepository $activity_repo
|
||||
*/
|
||||
public function __construct(ActivityRepository $activity_repo)
|
||||
{
|
||||
$this->activity_repo = $activity_repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
MultiDB::setDB($event->company->db);
|
||||
|
||||
$fields = new stdClass();
|
||||
|
||||
$user_id = isset($event->event_vars['user_id']) ? $event->event_vars['user_id'] : $event->invoice->user_id;
|
||||
|
||||
$fields->user_id = $user_id;
|
||||
$fields->client_id = $event->invoice->client_id;
|
||||
$fields->company_id = $event->invoice->company_id;
|
||||
$fields->activity_type_id = Activity::AUTOBILL_FAILURE;
|
||||
$fields->invoice_id = $event->invoice->id;
|
||||
$fields->notes = $event->notes ?? '';
|
||||
|
||||
$this->activity_repo->save($fields, $event->invoice, $event->event_vars);
|
||||
|
||||
}
|
||||
}
|
58
app/Listeners/Invoice/InvoiceAutoBillSuccessActivity.php
Normal file
58
app/Listeners/Invoice/InvoiceAutoBillSuccessActivity.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?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\Listeners\Invoice;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Activity;
|
||||
use App\Repositories\ActivityRepository;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use stdClass;
|
||||
|
||||
class InvoiceAutoBillSuccessActivity implements ShouldQueue
|
||||
{
|
||||
protected $activity_repo;
|
||||
|
||||
public $delay = 10;
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @param ActivityRepository $activity_repo
|
||||
*/
|
||||
public function __construct(ActivityRepository $activity_repo)
|
||||
{
|
||||
$this->activity_repo = $activity_repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
MultiDB::setDB($event->company->db);
|
||||
|
||||
$fields = new stdClass();
|
||||
|
||||
$user_id = isset($event->event_vars['user_id']) ? $event->event_vars['user_id'] : $event->invoice->user_id;
|
||||
|
||||
$fields->user_id = $user_id;
|
||||
$fields->client_id = $event->invoice->client_id;
|
||||
$fields->company_id = $event->invoice->company_id;
|
||||
$fields->activity_type_id = Activity::AUTOBILL_SUCCESS;
|
||||
$fields->invoice_id = $event->invoice->id;
|
||||
|
||||
$this->activity_repo->save($fields, $event->invoice, $event->event_vars);
|
||||
|
||||
}
|
||||
}
|
142
app/Livewire/EInvoice/Portal.php
Normal file
142
app/Livewire/EInvoice/Portal.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?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\Livewire\EInvoice;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class Portal extends Component
|
||||
{
|
||||
public $email = '';
|
||||
public $password = '';
|
||||
|
||||
public array $companies;
|
||||
|
||||
private string $api_url = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
||||
$this->api_url = config('ninja.hosted_ninja_url');
|
||||
|
||||
$this->getCompanies();
|
||||
|
||||
}
|
||||
|
||||
private function getCompanies(): self
|
||||
{
|
||||
|
||||
$this->companies = auth()->guard('user')->check() ? auth()->guard('user')->user()->account->companies->map(function ($company) {
|
||||
return [
|
||||
'key' => $company->company_key,
|
||||
'city' => $company->settings->city,
|
||||
'country' => $company->country()->iso_3166_2,
|
||||
'county' => $company->settings->state,
|
||||
'line1' => $company->settings->address1,
|
||||
'line2' => $company->settings->address2,
|
||||
'party_name' => $company->settings->name,
|
||||
'vat_number' => $company->settings->vat_number,
|
||||
'zip' => $company->settings->postal_code,
|
||||
'legal_entity_id' => $company->legal_entity_id,
|
||||
'tax_registered' => (bool) strlen($company->settings->vat_number ?? '') > 2,
|
||||
'tenant_id' => $company->company_key,
|
||||
'classification' => strlen($company->settings->classification ?? '') > 2 ? $company->settings->classification : 'business',
|
||||
];
|
||||
})->toArray() : [];
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
$credentials = ['email' => $this->email, 'password' => $this->password];
|
||||
|
||||
if (Auth::attempt($credentials)) {
|
||||
session()->flash('message', 'Logged in successfully.');
|
||||
|
||||
App::setLocale(auth()->guard('user')->user()->account->companies->first()->getLocale());
|
||||
|
||||
$this->getCOmpanies();
|
||||
|
||||
|
||||
} else {
|
||||
session()->flash('error', 'Invalid credentials.');
|
||||
}
|
||||
}
|
||||
|
||||
public function logout()
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
session()->flash('message', 'Logged out!');
|
||||
|
||||
}
|
||||
|
||||
public function register(string $company_key)
|
||||
{
|
||||
|
||||
$register_company = [
|
||||
'acts_as_receiver' => true,
|
||||
'acts_as_sender' => true,
|
||||
'advertisements' => ['invoice']
|
||||
];
|
||||
|
||||
foreach($this->companies as $company)
|
||||
{
|
||||
if($company['key'] == $company_key)
|
||||
$register_company = array_merge($company, $register_company);
|
||||
}
|
||||
|
||||
$r = Http::withHeaders($this->getHeaders())
|
||||
->post("{$this->api_url}/api/einvoice/createLegalEntity", $register_company);
|
||||
|
||||
if($r->successful())
|
||||
{
|
||||
|
||||
nlog($r->body());
|
||||
$response = $r->json();
|
||||
|
||||
$_company = auth()->guard('user')->user()->account->companies()->where('company_key', $company_key)->first();
|
||||
$_company->legal_entity_id = $response['id'];
|
||||
$_company->save();
|
||||
|
||||
$this->getCompanies();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if($r->failed())
|
||||
nlog($r->getBody()->getContents());
|
||||
|
||||
$error = json_decode($r->getBody()->getContents(),true);
|
||||
|
||||
session()->flash('error', $error['message']);
|
||||
|
||||
}
|
||||
|
||||
private function getHeaders()
|
||||
{
|
||||
return [
|
||||
'X-API-SELF-HOST-TOKEN' => config('ninja.license_key'),
|
||||
"X-Requested-With" => "XMLHttpRequest",
|
||||
"Content-Type" => "application/json",
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.e-invoice.portal');
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ class ProcessPayment extends Component
|
||||
}
|
||||
|
||||
$driver = $company_gateway
|
||||
->driver($invitation->contact->client)
|
||||
->driver($invitation->contact->client) // @phpstan-ignore-line
|
||||
->setPaymentMethod($data['payment_method_id'])
|
||||
->setPaymentHash($responder_data['payload']['ph']);
|
||||
|
||||
|
@ -88,7 +88,7 @@ class RequiredFields extends Component
|
||||
$rff = new RFFService(
|
||||
fields: $this->getContext()['fields'],
|
||||
database: $this->getContext()['db'],
|
||||
company_gateway_id: $this->company_gateway->id,
|
||||
company_gateway_id: (string)$this->company_gateway->id,
|
||||
);
|
||||
|
||||
/** @var \App\Models\ClientContact $contact */
|
||||
@ -111,7 +111,7 @@ class RequiredFields extends Component
|
||||
$rff = new RFFService(
|
||||
fields: $this->fields,
|
||||
database: $this->getContext()['db'],
|
||||
company_gateway_id: $this->company_gateway->id,
|
||||
company_gateway_id: (string) $this->company_gateway->id,
|
||||
);
|
||||
|
||||
$contact = auth()->user();
|
||||
|
@ -11,12 +11,14 @@
|
||||
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use stdClass;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Invoice;
|
||||
use App\Utils\HtmlEngine;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use stdClass;
|
||||
use App\DataMapper\EmailTemplateDefaults;
|
||||
use App\Utils\Number;
|
||||
|
||||
class ClientPaymentFailureObject
|
||||
{
|
||||
@ -60,20 +62,20 @@ class ClientPaymentFailureObject
|
||||
}
|
||||
|
||||
App::forgetInstance('translator');
|
||||
/* Init a new copy of the translator*/
|
||||
$t = app('translator');
|
||||
/* Set the locale*/
|
||||
App::setLocale($this->client->locale());
|
||||
/* Set customized translations _NOW_ */
|
||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
$this->invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
|
||||
|
||||
$data = $this->getData();
|
||||
|
||||
$mail_obj = new stdClass();
|
||||
$mail_obj->amount = $this->getAmount();
|
||||
$mail_obj->subject = $this->getSubject();
|
||||
$mail_obj->subject = $data['subject'];
|
||||
$mail_obj->data = $this->getData();
|
||||
$mail_obj->markdown = 'email.client.generic';
|
||||
|
||||
$mail_obj->markdown = 'email.template.client';
|
||||
$mail_obj->tag = $this->company->company_key;
|
||||
$mail_obj->text_view = 'email.template.text';
|
||||
|
||||
@ -82,16 +84,32 @@ class ClientPaymentFailureObject
|
||||
|
||||
private function getAmount()
|
||||
{
|
||||
return array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total;
|
||||
$amount = array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total;
|
||||
|
||||
return Number::formatMoney($amount, $this->client);
|
||||
}
|
||||
|
||||
private function getSubject()
|
||||
{
|
||||
return
|
||||
ctrans(
|
||||
'texts.notification_invoice_payment_failed_subject',
|
||||
['invoice' => implode(',', $this->invoices->pluck('number')->toArray())]
|
||||
);
|
||||
|
||||
if(strlen($this->client->getSetting('email_subject_payment_failed') ?? '') > 2){
|
||||
return $this->client->getSetting('email_subject_payment_failed');
|
||||
}
|
||||
else {
|
||||
return EmailTemplateDefaults::getDefaultTemplate('email_subject_payment_failed', $this->client->locale());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function getBody()
|
||||
{
|
||||
|
||||
if(strlen($this->client->getSetting('email_template_payment_failed') ?? '') > 2) {
|
||||
return $this->client->getSetting('email_template_payment_failed');
|
||||
} else {
|
||||
return EmailTemplateDefaults::getDefaultTemplate('email_template_payment_failed', $this->client->locale());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function getData()
|
||||
@ -104,17 +122,17 @@ class ClientPaymentFailureObject
|
||||
|
||||
$signature = $this->client->getSetting('email_signature');
|
||||
$html_variables = (new HtmlEngine($invitation))->makeValues();
|
||||
|
||||
$html_variables['$gateway_payment_error'] = $this->error ?? '';
|
||||
$html_variables['$total'] = $this->getAmount();
|
||||
|
||||
$signature = str_replace(array_keys($html_variables), array_values($html_variables), $signature);
|
||||
$subject = str_replace(array_keys($html_variables), array_values($html_variables), $this->getSubject());
|
||||
$content = str_replace(array_keys($html_variables), array_values($html_variables), $this->getBody());
|
||||
|
||||
$data = [
|
||||
'title' => ctrans(
|
||||
'texts.notification_invoice_payment_failed_subject',
|
||||
[
|
||||
'invoice' => $this->invoices->first()->number,
|
||||
]
|
||||
),
|
||||
'greeting' => ctrans('texts.email_salutation', ['name' => $this->client->present()->name()]),
|
||||
'content' => ctrans('texts.client_payment_failure_body', ['invoice' => implode(',', $this->invoices->pluck('number')->toArray()), 'amount' => $this->getAmount()]),
|
||||
'subject' => $subject,
|
||||
'body' => $content,
|
||||
'signature' => $signature,
|
||||
'logo' => $this->company->present()->logo(),
|
||||
'settings' => $this->client->getMergedSettings(),
|
||||
|
@ -265,6 +265,12 @@ class Activity extends StaticModel
|
||||
|
||||
public const QUOTE_REMINDER1_SENT = 142;
|
||||
|
||||
public const AUTOBILL_SUCCESS = 143;
|
||||
|
||||
public const AUTOBILL_FAILURE = 144;
|
||||
|
||||
public const EMAIL_EINVOICE_SUCCESS = 145;
|
||||
|
||||
protected $casts = [
|
||||
'is_system' => 'boolean',
|
||||
'updated_at' => 'timestamp',
|
||||
@ -280,13 +286,11 @@ class Activity extends StaticModel
|
||||
'backup',
|
||||
];
|
||||
|
||||
|
||||
public function getHashedIdAttribute(): string
|
||||
{
|
||||
return $this->encodePrimaryKey($this->id);
|
||||
}
|
||||
|
||||
|
||||
public function getEntityType()
|
||||
{
|
||||
return self::class;
|
||||
|
@ -17,6 +17,7 @@ use App\Utils\Traits\MakesHash;
|
||||
use App\Jobs\Entity\CreateRawPdf;
|
||||
use App\Jobs\Util\WebhookHandler;
|
||||
use App\Models\Traits\Excludable;
|
||||
use App\Services\EDocument\Jobes\SendEDocument;
|
||||
use App\Services\PdfMaker\PdfMerge;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Utils\Traits\UserSessionAttributes;
|
||||
@ -31,6 +32,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundExceptio
|
||||
* @package App\Models
|
||||
* @property-read mixed $hashed_id
|
||||
* @property string $number
|
||||
* @property object|array|null $e_invoice
|
||||
* @property int $company_id
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
@ -294,6 +296,12 @@ class BaseModel extends Model
|
||||
if ($subscriptions) {
|
||||
WebhookHandler::dispatch($event_id, $this->withoutRelations(), $this->company, $additional_data);
|
||||
}
|
||||
|
||||
// special catch here for einvoicing eventing
|
||||
if($event_id == Webhook::EVENT_SENT_INVOICE && ($this instanceof Invoice) && is_null($this->backup)){
|
||||
\App\Services\EDocument\Jobs\SendEDocument::dispatch(get_class($this), $this->id, $this->company->db);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,6 +121,7 @@ use Laracasts\Presenter\PresentableTrait;
|
||||
* @property string|null $smtp_local_domain
|
||||
* @property \App\DataMapper\QuickbooksSettings|null $quickbooks
|
||||
* @property boolean $smtp_verify_peer
|
||||
* @property int|null $legal_entity_id
|
||||
* @property-read \App\Models\Account $account
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Activity> $activities
|
||||
* @property-read int|null $activities_count
|
||||
@ -366,7 +367,7 @@ class Company extends BaseModel
|
||||
'smtp_encryption',
|
||||
'smtp_local_domain',
|
||||
'smtp_verify_peer',
|
||||
'e_invoice',
|
||||
// 'e_invoice',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
|
@ -12,18 +12,19 @@
|
||||
|
||||
namespace App\PaymentDrivers;
|
||||
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Models\GatewayType;
|
||||
use App\PaymentDrivers\BTCPay\BTCPay;
|
||||
use App\Models\SystemLog;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Client;
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\Models\PaymentType;
|
||||
use BTCPayServer\Client\Webhook;
|
||||
use App\Http\Requests\Payments\PaymentWebhookRequest;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\SystemLog;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Models\PaymentType;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use BTCPayServer\Client\Webhook;
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\PaymentDrivers\BTCPay\BTCPay;
|
||||
use App\Jobs\Mail\PaymentFailedMailer;
|
||||
use App\Http\Requests\Payments\PaymentWebhookRequest;
|
||||
|
||||
class BTCPayPaymentDriver extends BaseDriver
|
||||
{
|
||||
@ -138,8 +139,6 @@ class BTCPayPaymentDriver extends BaseDriver
|
||||
|
||||
$_invoice = $this->payment_hash->fee_invoice;
|
||||
|
||||
// Invoice::with('client')->withTrashed()->find($this->payment_hash->fee_invoice_id);
|
||||
|
||||
$this->client = $_invoice->client;
|
||||
|
||||
$dataPayment = [
|
||||
@ -158,21 +157,49 @@ class BTCPayPaymentDriver extends BaseDriver
|
||||
}
|
||||
switch ($btcpayRep->type) {
|
||||
case "InvoiceExpired":
|
||||
|
||||
if ($payment->status_id == Payment::STATUS_PENDING) {
|
||||
$payment->service()->deletePayment();
|
||||
$this->failedPaymentNotification($payment);
|
||||
}
|
||||
|
||||
$StatusId = Payment::STATUS_CANCELLED;
|
||||
break;
|
||||
case "InvoiceInvalid":
|
||||
|
||||
if ($payment->status_id == Payment::STATUS_PENDING) {
|
||||
$payment->service()->deletePayment();
|
||||
$this->failedPaymentNotification($payment);
|
||||
}
|
||||
|
||||
$StatusId = Payment::STATUS_FAILED;
|
||||
break;
|
||||
case "InvoiceSettled":
|
||||
$StatusId = Payment::STATUS_COMPLETED;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($payment->status_id != $StatusId) {
|
||||
$payment->status_id = $StatusId;
|
||||
$payment->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function failedPaymentNotification(Payment $payment): void
|
||||
{
|
||||
|
||||
$error = ctrans('texts.client_payment_failure_body', [
|
||||
'invoice' => implode(',', $payment->invoices->pluck('number')->toArray()),
|
||||
'amount' => array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total, ]);
|
||||
|
||||
PaymentFailedMailer::dispatch(
|
||||
$this->payment_hash,
|
||||
$payment->client->company,
|
||||
$payment->client,
|
||||
$error
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public function refund(Payment $payment, $amount, $return_client_response = false)
|
||||
{
|
||||
|
@ -232,7 +232,6 @@ class BaseDriver extends AbstractPaymentDriver
|
||||
*
|
||||
* @param ClientGatewayToken $cgt The client gateway token object
|
||||
* @param PaymentHash $payment_hash The Payment hash containing the payment meta data
|
||||
* @return ?Payment|bool The payment response
|
||||
*/
|
||||
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
|
||||
{
|
||||
|
@ -109,8 +109,8 @@ class CreditCard
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_FORTE,
|
||||
$this->client,
|
||||
$this->client->company,
|
||||
$this->forte->client,
|
||||
$this->forte->client->company,
|
||||
);
|
||||
|
||||
throw new \App\Exceptions\PaymentFailed("Unable to store payment method: {$error->response->response_desc}", 400);
|
||||
|
@ -1,261 +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\PaymentDrivers;
|
||||
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\SystemLog;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Omnipay\Common\Item;
|
||||
use Omnipay\Omnipay;
|
||||
|
||||
class PayPalExpressPaymentDriver extends BaseDriver
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public $token_billing = false;
|
||||
|
||||
public $can_authorise_credit_card = false;
|
||||
|
||||
private $omnipay_gateway;
|
||||
|
||||
private float $fee = 0;
|
||||
|
||||
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL;
|
||||
|
||||
public function gatewayTypes()
|
||||
{
|
||||
return [
|
||||
GatewayType::PAYPAL,
|
||||
];
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Omnipay PayPal_Express gateway.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function initializeOmnipayGateway(): void
|
||||
{
|
||||
$this->omnipay_gateway = Omnipay::create(
|
||||
$this->company_gateway->gateway->provider
|
||||
);
|
||||
|
||||
$this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig());
|
||||
}
|
||||
|
||||
public function setPaymentMethod($payment_method_id)
|
||||
{
|
||||
// PayPal doesn't have multiple ways of paying.
|
||||
// There's just one, off-site redirect.
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function authorizeView($payment_method)
|
||||
{
|
||||
// PayPal doesn't support direct authorization.
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function authorizeResponse($request)
|
||||
{
|
||||
// PayPal doesn't support direct authorization.
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function processPaymentView($data)
|
||||
{
|
||||
$this->initializeOmnipayGateway();
|
||||
|
||||
$this->payment_hash->data = array_merge((array) $this->payment_hash->data, ['amount' => $data['total']['amount_with_fee']]);
|
||||
$this->payment_hash->save();
|
||||
|
||||
$response = $this->omnipay_gateway
|
||||
->purchase($this->generatePaymentDetails($data))
|
||||
->setItems($this->generatePaymentItems($data))
|
||||
->send();
|
||||
|
||||
if ($response->isRedirect()) {
|
||||
return redirect($response->getRedirectUrl());
|
||||
}
|
||||
|
||||
// $this->sendFailureMail($response->getMessage() ?: '');
|
||||
|
||||
$message = [
|
||||
'server_response' => $response->getMessage(),
|
||||
'data' => $this->payment_hash->data,
|
||||
];
|
||||
|
||||
SystemLogger::dispatch(
|
||||
$message,
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_PAYPAL,
|
||||
$this->client,
|
||||
$this->client->company,
|
||||
);
|
||||
|
||||
throw new PaymentFailed($response->getMessage(), $response->getCode());
|
||||
}
|
||||
|
||||
public function processPaymentResponse($request)
|
||||
{
|
||||
$this->initializeOmnipayGateway();
|
||||
|
||||
$response = $this->omnipay_gateway
|
||||
->completePurchase(['amount' => $this->payment_hash->data->amount, 'currency' => $this->client->getCurrencyCode()])
|
||||
->send();
|
||||
|
||||
if ($response->isCancelled() && $this->client->getSetting('enable_client_portal')) {
|
||||
return redirect()->route('client.invoices.index')->with('warning', ctrans('texts.status_cancelled'));
|
||||
} elseif($response->isCancelled() && !$this->client->getSetting('enable_client_portal')) {
|
||||
redirect()->route('client.invoices.show', ['invoice' => $this->payment_hash->fee_invoice])->with('warning', ctrans('texts.status_cancelled'));
|
||||
}
|
||||
|
||||
if ($response->isSuccessful()) {
|
||||
$data = [
|
||||
'payment_method' => $response->getData()['TOKEN'],
|
||||
'payment_type' => PaymentType::PAYPAL,
|
||||
'amount' => $this->payment_hash->data->amount,
|
||||
'transaction_reference' => $response->getTransactionReference(),
|
||||
'gateway_type_id' => GatewayType::PAYPAL,
|
||||
];
|
||||
|
||||
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
|
||||
|
||||
SystemLogger::dispatch(
|
||||
['response' => (array) $response->getData(), 'data' => $data],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||
SystemLog::TYPE_PAYPAL,
|
||||
$this->client,
|
||||
$this->client->company,
|
||||
);
|
||||
|
||||
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
|
||||
}
|
||||
|
||||
if (! $response->isSuccessful()) {
|
||||
$data = $response->getData();
|
||||
|
||||
$this->sendFailureMail($response->getMessage() ?: '');
|
||||
|
||||
$message = [
|
||||
'server_response' => $data['L_LONGMESSAGE0'],
|
||||
'data' => $this->payment_hash->data,
|
||||
];
|
||||
|
||||
SystemLogger::dispatch(
|
||||
$message,
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_PAYPAL,
|
||||
$this->client,
|
||||
$this->client->company,
|
||||
);
|
||||
|
||||
throw new PaymentFailed($response->getMessage(), $response->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
public function generatePaymentDetails(array $data)
|
||||
{
|
||||
$_invoice = collect($this->payment_hash->data->invoices)->first();
|
||||
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
|
||||
|
||||
// $this->fee = $this->feeCalc($invoice, $data['total']['amount_with_fee']);
|
||||
|
||||
return [
|
||||
'currency' => $this->client->getCurrencyCode(),
|
||||
'transactionType' => 'Purchase',
|
||||
'clientIp' => request()->getClientIp(),
|
||||
// 'amount' => round(($data['total']['amount_with_fee'] + $this->fee),2),
|
||||
'amount' => round($data['total']['amount_with_fee'], 2),
|
||||
'returnUrl' => route('client.payments.response', [
|
||||
'company_gateway_id' => $this->company_gateway->id,
|
||||
'payment_hash' => $this->payment_hash->hash,
|
||||
'payment_method_id' => GatewayType::PAYPAL,
|
||||
]),
|
||||
'cancelUrl' => $this->client->company->domain()."/client/invoices/{$invoice->hashed_id}",
|
||||
'description' => implode(',', collect($this->payment_hash->data->invoices)
|
||||
->map(function ($invoice) {
|
||||
return sprintf('%s: %s', ctrans('texts.invoice_number'), $invoice->invoice_number);
|
||||
})->toArray()),
|
||||
'transactionId' => $this->payment_hash->hash.'-'.time(),
|
||||
'ButtonSource' => 'InvoiceNinja_SP',
|
||||
'solutionType' => 'Sole',
|
||||
'no_shipping' => $this->company_gateway->require_shipping_address ? 0 : 1,
|
||||
];
|
||||
}
|
||||
|
||||
public function generatePaymentItems(array $data)
|
||||
{
|
||||
$_invoice = collect($this->payment_hash->data->invoices)->first();
|
||||
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
|
||||
|
||||
$items = [];
|
||||
|
||||
$items[] = new Item([
|
||||
'name' => ' ',
|
||||
'description' => ctrans('texts.invoice_number').'# '.$invoice->number,
|
||||
'price' => $data['total']['amount_with_fee'],
|
||||
'quantity' => 1,
|
||||
]);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function feeCalc($invoice, $invoice_total)
|
||||
{
|
||||
$invoice->service()->removeUnpaidGatewayFees();
|
||||
$invoice = $invoice->fresh();
|
||||
|
||||
$balance = floatval($invoice->balance);
|
||||
|
||||
$_updated_invoice = $invoice->service()->addGatewayFee($this->company_gateway, GatewayType::PAYPAL, $invoice_total)->save();
|
||||
|
||||
if (floatval($_updated_invoice->balance) > $balance) {
|
||||
$fee = floatval($_updated_invoice->balance) - $balance;
|
||||
|
||||
$this->payment_hash->fee_total = $fee;
|
||||
$this->payment_hash->save();
|
||||
|
||||
return $fee;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function livewirePaymentView(array $data): string
|
||||
{
|
||||
$this->processPaymentView($data);
|
||||
|
||||
return ''; // Gateway is offsite.
|
||||
}
|
||||
|
||||
public function processPaymentViewData(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -49,6 +49,18 @@ class UpdatePaymentMethods
|
||||
$this->addOrUpdateCard($method, $customer->id, $client, GatewayType::CREDIT_CARD);
|
||||
}
|
||||
|
||||
$link_methods = PaymentMethod::all(
|
||||
[
|
||||
'customer' => $customer->id,
|
||||
'type' => 'link',
|
||||
],
|
||||
$this->stripe->stripe_connect_auth
|
||||
);
|
||||
|
||||
foreach ($link_methods as $method) {
|
||||
$this->addOrUpdateCard($method, $customer->id, $client, GatewayType::CREDIT_CARD);
|
||||
}
|
||||
|
||||
$alipay_methods = PaymentMethod::all(
|
||||
[
|
||||
'customer' => $customer->id,
|
||||
@ -217,9 +229,14 @@ class UpdatePaymentMethods
|
||||
|
||||
private function buildPaymentMethodMeta(PaymentMethod $method, $type_id)
|
||||
{
|
||||
nlog($method->type);
|
||||
|
||||
switch ($type_id) {
|
||||
case GatewayType::CREDIT_CARD:
|
||||
|
||||
if($method->type == 'link')
|
||||
return new \stdClass();
|
||||
|
||||
/**
|
||||
* @class \Stripe\PaymentMethod $method
|
||||
* @property \Stripe\StripeObject $card
|
||||
@ -240,7 +257,7 @@ class UpdatePaymentMethods
|
||||
return $payment_meta;
|
||||
case GatewayType::ALIPAY:
|
||||
case GatewayType::SOFORT:
|
||||
|
||||
|
||||
return new \stdClass();
|
||||
|
||||
case GatewayType::SEPA:
|
||||
|
@ -155,6 +155,8 @@ use App\Listeners\Activity\TaskUpdatedActivity;
|
||||
use App\Listeners\Invoice\InvoiceEmailActivity;
|
||||
use App\Listeners\SendVerificationNotification;
|
||||
use App\Events\Credit\CreditWasEmailedAndFailed;
|
||||
use App\Events\Invoice\InvoiceAutoBillFailed;
|
||||
use App\Events\Invoice\InvoiceAutoBillSuccess;
|
||||
use App\Listeners\Activity\CreatedQuoteActivity;
|
||||
use App\Listeners\Activity\DeleteClientActivity;
|
||||
use App\Listeners\Activity\DeleteCreditActivity;
|
||||
@ -250,6 +252,8 @@ use App\Events\RecurringExpense\RecurringExpenseWasArchived;
|
||||
use App\Events\RecurringExpense\RecurringExpenseWasRestored;
|
||||
use App\Events\RecurringInvoice\RecurringInvoiceWasArchived;
|
||||
use App\Events\RecurringInvoice\RecurringInvoiceWasRestored;
|
||||
use App\Listeners\Invoice\InvoiceAutoBillFailedActivity;
|
||||
use App\Listeners\Invoice\InvoiceAutoBillSuccessActivity;
|
||||
use App\Listeners\PurchaseOrder\CreatePurchaseOrderActivity;
|
||||
use App\Listeners\PurchaseOrder\PurchaseOrderViewedActivity;
|
||||
use App\Listeners\PurchaseOrder\UpdatePurchaseOrderActivity;
|
||||
@ -426,6 +430,12 @@ class EventServiceProvider extends ServiceProvider
|
||||
ExpenseRestoredActivity::class,
|
||||
],
|
||||
//Invoices
|
||||
InvoiceAutoBillSuccess::class => [
|
||||
InvoiceAutoBillSuccessActivity::class,
|
||||
],
|
||||
InvoiceAutoBillFailed::class => [
|
||||
InvoiceAutoBillFailedActivity::class,
|
||||
],
|
||||
InvoiceWasMarkedSent::class => [
|
||||
],
|
||||
InvoiceWasUpdated::class => [
|
||||
|
@ -222,6 +222,10 @@ class StaticServiceProvider extends ServiceProvider
|
||||
'subject' => EmailTemplateDefaults::emailPaymentSubject(),
|
||||
'body' => EmailTemplateDefaults::emailPaymentTemplate(),
|
||||
],
|
||||
'payment_failed' => [
|
||||
'subject' => EmailTemplateDefaults::emailPaymentFailedSubject(),
|
||||
'body' => EmailTemplateDefaults::emailPaymentFailedTemplate(),
|
||||
],
|
||||
'quote_reminder1' => [
|
||||
'subject' => EmailTemplateDefaults::emailQuoteReminder1Subject(),
|
||||
'body' => EmailTemplateDefaults::emailQuoteReminder1Body(),
|
||||
|
@ -11,8 +11,9 @@
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Company;
|
||||
use App\Repositories\BaseRepository;
|
||||
|
||||
/**
|
||||
* CompanyRepository.
|
||||
@ -57,11 +58,36 @@ class CompanyRepository extends BaseRepository
|
||||
$company->smtp_password = $data['smtp_password'];
|
||||
}
|
||||
|
||||
if(isset($data['e_invoice']) && is_array($data['e_invoice'])){
|
||||
//ensure it is normalized first!
|
||||
$data['e_invoice'] = $this->arrayFilterRecursive($data['e_invoice']);
|
||||
|
||||
$company->e_invoice = $data['e_invoice'];
|
||||
}
|
||||
|
||||
$company->save();
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
||||
|
||||
private function arrayFilterRecursive(array $array): array
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
// Recursively filter the nested array
|
||||
$array[$key] = $this->arrayFilterRecursive($value);
|
||||
}
|
||||
// Remove null values
|
||||
if (is_null($array[$key])) {
|
||||
unset($array[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* parseCustomFields
|
||||
*
|
||||
|
@ -13,6 +13,7 @@ namespace App\Services\Bank;
|
||||
|
||||
use App\Factory\ExpenseCategoryFactory;
|
||||
use App\Factory\ExpenseFactory;
|
||||
use App\Jobs\Bank\MatchBankTransactions;
|
||||
use App\Models\BankTransaction;
|
||||
use App\Models\Client;
|
||||
use App\Models\ExpenseCategory;
|
||||
@ -20,12 +21,14 @@ use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Services\AbstractService;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ProcessBankRules extends AbstractService
|
||||
{
|
||||
use GeneratesCounter;
|
||||
use MakesHash;
|
||||
|
||||
protected $credit_rules;
|
||||
|
||||
@ -87,6 +90,8 @@ class ProcessBankRules extends AbstractService
|
||||
|
||||
foreach ($bank_transaction_rule['rules'] as $rule) {
|
||||
|
||||
$results = [];
|
||||
|
||||
$payments = Payment::query()
|
||||
->withTrashed()
|
||||
->whereIn('status_id', [1,4])
|
||||
@ -101,28 +106,26 @@ class ProcessBankRules extends AbstractService
|
||||
->where('is_deleted', 0)
|
||||
->get();
|
||||
|
||||
$results = [];
|
||||
|
||||
match($rule['search_key']) {
|
||||
'$payment.amount' => $results = [Payment::class, $this->searchPaymentResource('amount', $rule, $payments)],
|
||||
'$payment.transaction_reference' => $results = [Payment::class, $this->searchPaymentResource('transaction_reference', $rule, $payments)],
|
||||
'$payment.custom1' => $results = [Payment::class, $this->searchPaymentResource('custom1', $rule, $payments)],
|
||||
'$payment.custom2' => $results = [Payment::class, $this->searchPaymentResource('custom2', $rule, $payments)],
|
||||
'$payment.custom3' => $results = [Payment::class, $this->searchPaymentResource('custom3', $rule, $payments)],
|
||||
'$payment.custom4' => $results = [Payment::class, $this->searchPaymentResource('custom4', $rule, $payments)],
|
||||
'$payment.custom1' => $results = [Payment::class, $this->searchPaymentResource('custom_value1', $rule, $payments)],
|
||||
'$payment.custom2' => $results = [Payment::class, $this->searchPaymentResource('custom_value2', $rule, $payments)],
|
||||
'$payment.custom3' => $results = [Payment::class, $this->searchPaymentResource('custom_value3', $rule, $payments)],
|
||||
'$payment.custom4' => $results = [Payment::class, $this->searchPaymentResource('custom_value4', $rule, $payments)],
|
||||
'$invoice.amount' => $results = [Invoice::class, $this->searchInvoiceResource('amount', $rule, $invoices)],
|
||||
'$invoice.number' => $results = [Invoice::class, $this->searchInvoiceResource('number', $rule, $invoices)],
|
||||
'$invoice.po_number' => $results = [Invoice::class, $this->searchInvoiceResource('po_number', $rule, $invoices)],
|
||||
'$invoice.custom1' => $results = [Invoice::class, $this->searchInvoiceResource('custom1', $rule, $invoices)],
|
||||
'$invoice.custom2' => $results = [Invoice::class, $this->searchInvoiceResource('custom2', $rule, $invoices)],
|
||||
'$invoice.custom3' => $results = [Invoice::class, $this->searchInvoiceResource('custom3', $rule, $invoices)],
|
||||
'$invoice.custom4' => $results = [Invoice::class, $this->searchInvoiceResource('custom4', $rule, $invoices)],
|
||||
'$invoice.custom1' => $results = [Invoice::class, $this->searchInvoiceResource('custom_value1', $rule, $invoices)],
|
||||
'$invoice.custom2' => $results = [Invoice::class, $this->searchInvoiceResource('custom_value2', $rule, $invoices)],
|
||||
'$invoice.custom3' => $results = [Invoice::class, $this->searchInvoiceResource('custom_value3', $rule, $invoices)],
|
||||
'$invoice.custom4' => $results = [Invoice::class, $this->searchInvoiceResource('custom_value4', $rule, $invoices)],
|
||||
'$client.id_number' => $results = [Client::class, $this->searchClientResource('id_number', $rule, $invoices, $payments)],
|
||||
'$client.email' => $results = [Client::class, $this->searchClientResource('email', $rule, $invoices, $payments)],
|
||||
'$client.custom1' => $results = [Client::class, $this->searchClientResource('custom1', $rule, $invoices, $payments)],
|
||||
'$client.custom2' => $results = [Client::class, $this->searchClientResource('custom2', $rule, $invoices, $payments)],
|
||||
'$client.custom3' => $results = [Client::class, $this->searchClientResource('custom3', $rule, $invoices, $payments)],
|
||||
'$client.custom4' => $results = [Client::class, $this->searchClientResource('custom4', $rule, $invoices, $payments)],
|
||||
'$client.custom1' => $results = [Client::class, $this->searchClientResource('custom_value1', $rule, $invoices, $payments)],
|
||||
'$client.custom2' => $results = [Client::class, $this->searchClientResource('custom_value2', $rule, $invoices, $payments)],
|
||||
'$client.custom3' => $results = [Client::class, $this->searchClientResource('custom_value3', $rule, $invoices, $payments)],
|
||||
'$client.custom4' => $results = [Client::class, $this->searchClientResource('custom_value4', $rule, $invoices, $payments)],
|
||||
default => $results = [Client::class, [collect([]), Invoice::class]],
|
||||
};
|
||||
|
||||
@ -139,74 +142,50 @@ class ProcessBankRules extends AbstractService
|
||||
$match_set[] = $results;
|
||||
}
|
||||
}
|
||||
|
||||
if (($bank_transaction_rule['matches_on_all'] && $this->checkMatchSetForKey($match_set, $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && count($match_set) > 0))
|
||||
{
|
||||
|
||||
if (($bank_transaction_rule['matches_on_all'] && $this->checkMatchSetForKey($match_set, $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && count($match_set) > 0)) {
|
||||
|
||||
$this->bank_transaction->vendor_id = $bank_transaction_rule->vendor_id;
|
||||
$this->bank_transaction->ninja_category_id = $bank_transaction_rule->category_id;
|
||||
$this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
|
||||
$this->bank_transaction->bank_transaction_rule_id = $bank_transaction_rule->id;
|
||||
$this->bank_transaction->save();
|
||||
|
||||
$first_result = reset($match_set);
|
||||
|
||||
//auto-convert
|
||||
$invoice_id = false;
|
||||
$payment_id = false;
|
||||
|
||||
if($first_result[0] == Payment::class) {
|
||||
$payment_id = $first_result[1][0];
|
||||
}
|
||||
elseif($first_result[0] == Invoice::class) {
|
||||
$invoice_id = $first_result[1][0];
|
||||
}
|
||||
|
||||
if ($bank_transaction_rule['auto_convert']) {
|
||||
(new MatchBankTransactions($this->bank_transaction->company->id, $this->bank_transaction->company->db, [
|
||||
'transactions' => [
|
||||
[
|
||||
'id' => $this->bank_transaction->id,
|
||||
'invoice_ids' => $invoice_id ?? '',
|
||||
'payment_id' => $payment_id ?? '',
|
||||
],
|
||||
],
|
||||
]))->handle();
|
||||
}
|
||||
else {
|
||||
|
||||
//all types must match.
|
||||
$entity = $match_set[0][0];
|
||||
|
||||
foreach($match_set as $set)
|
||||
{
|
||||
if($set[0] != $entity)
|
||||
return false;
|
||||
if($invoice_id){
|
||||
$this->bank_transaction->invoice_ids = $this->encodePrimaryKey($invoice_id);
|
||||
}
|
||||
elseif($payment_id){
|
||||
$this->bank_transaction->payment_id = $payment_id;
|
||||
}
|
||||
|
||||
|
||||
// $result_set = [];
|
||||
|
||||
// foreach($match_set as $key => $set) {
|
||||
|
||||
// $parseable_set = $match_set;
|
||||
// unset($parseable_set[$key]);
|
||||
|
||||
// $entity_ids = $set[1];
|
||||
|
||||
// foreach($parseable_set as $kkey => $vvalue) {
|
||||
|
||||
// $i = array_intersect($vvalue[1], $entity_ids);
|
||||
|
||||
// if(count($i) == 0) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
|
||||
// $result_set[] = $i;
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// $commonValues = $result_set[0]; // Start with the first sub-array
|
||||
|
||||
// foreach ($result_set as $subArray) {
|
||||
// $commonValues = array_intersect($commonValues, $subArray);
|
||||
// }
|
||||
|
||||
// echo print_r($commonValues, true);
|
||||
|
||||
//just need to ensure the result count = rule count
|
||||
// }
|
||||
|
||||
|
||||
|
||||
//there must be a key in each set
|
||||
|
||||
//no misses allowed
|
||||
|
||||
$this->bank_transaction->status_id = BankTransaction::STATUS_CONVERTED;
|
||||
$this->bank_transaction->save();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -221,13 +200,13 @@ class ProcessBankRules extends AbstractService
|
||||
private function searchInvoiceResource(string $column, array $rule, $invoices)
|
||||
{
|
||||
|
||||
return $invoices->when($rule['search_key'] == 'description', function ($q) use ($rule, $column) {
|
||||
return $q->cursor()->filter(function ($record) use ($rule, $column) {
|
||||
return $invoices->when($column != 'amount', function ($q) use ($rule, $column) {
|
||||
return $q->filter(function ($record) use ($rule, $column) {
|
||||
return $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
|
||||
});
|
||||
})
|
||||
->when($rule['search_key'] == 'amount', function ($q) use ($rule, $column) {
|
||||
return $q->cursor()->filter(function ($record) use ($rule, $column) {
|
||||
->when($column == 'amount', function ($q) use ($rule, $column) {
|
||||
return $q->filter(function ($record) use ($rule, $column) {
|
||||
return $this->matchNumberOperator($this->bank_transaction->amount, $record->{$column}, $rule['operator']);
|
||||
});
|
||||
})->pluck("id");
|
||||
@ -236,17 +215,18 @@ class ProcessBankRules extends AbstractService
|
||||
|
||||
private function searchPaymentResource(string $column, array $rule, $payments)
|
||||
{
|
||||
|
||||
return $payments->when($rule['search_key'] == 'description', function ($q) use ($rule, $column) {
|
||||
return $q->cursor()->filter(function ($record) use ($rule, $column) {
|
||||
return $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
|
||||
return $payments->when($column != 'amount', function ($q) use ($rule, $column) {
|
||||
return $q->filter(function ($record) use ($rule, $column) {
|
||||
|
||||
$bool = $this->matchStringOperator($this->bank_transaction->description, $record->{$column}, $rule['operator']);
|
||||
return $bool;
|
||||
});
|
||||
})
|
||||
->when($rule['search_key'] == 'amount', function ($q) use ($rule, $column) {
|
||||
return $q->cursor()->filter(function ($record) use ($rule, $column) {
|
||||
return $this->matchNumberOperator($this->bank_transaction->amount, $record->{$column}, $rule['operator']);
|
||||
});
|
||||
})->pluck("id");
|
||||
->when($column == 'amount', function ($q) use ($rule, $column) {
|
||||
return $q->filter(function ($record) use ($rule, $column) {
|
||||
return $this->matchNumberOperator($this->bank_transaction->amount, $record->{$column}, $rule['operator']);
|
||||
});
|
||||
})->pluck("id");
|
||||
|
||||
}
|
||||
|
||||
@ -297,325 +277,7 @@ class ProcessBankRules extends AbstractService
|
||||
return [Client::class, collect([])];
|
||||
|
||||
}
|
||||
// $payment.amount => "Payment Amount", float
|
||||
// $payment.transaction_reference => "Payment Transaction Reference", string
|
||||
// $invoice.amount => "Invoice Amount", float
|
||||
// $invoice.number => "Invoice Number", string
|
||||
// $client.id_number => "Client ID Number", string
|
||||
// $client.email => "Client Email", string
|
||||
// $invoice.po_number => "Invoice Purchase Order Number", string
|
||||
|
||||
|
||||
// private function matchCredit()
|
||||
// {
|
||||
// $this->invoices = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->get();
|
||||
|
||||
// $invoice = $this->invoices->first(function ($value, $key) {
|
||||
// return str_contains($this->bank_transaction->description, $value->number) || str_contains(str_replace("\n", "", $this->bank_transaction->description), $value->number);
|
||||
// });
|
||||
|
||||
// if ($invoice) {
|
||||
// $this->bank_transaction->invoice_ids = $invoice->hashed_id;
|
||||
// $this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
|
||||
// $this->bank_transaction->save();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// $this->credit_rules = $this->bank_transaction->company->credit_rules();
|
||||
|
||||
// //stub for credit rules
|
||||
// foreach ($this->credit_rules as $bank_transaction_rule) {
|
||||
// $matches = 0;
|
||||
|
||||
// if (!is_array($bank_transaction_rule['rules'])) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// foreach ($bank_transaction_rule['rules'] as $rule) {
|
||||
// $rule_count = count($bank_transaction_rule['rules']);
|
||||
|
||||
// $invoiceNumbers = false;
|
||||
// $invoiceNumber = false;
|
||||
// $invoiceAmounts = false;
|
||||
// $paymentAmounts = false;
|
||||
// $paymentReferences = false;
|
||||
// $clientIdNumbers = false;
|
||||
// $clientEmails = false;
|
||||
// $invoicePONumbers = false;
|
||||
|
||||
// if ($rule['search_key'] == '$invoice.number') {
|
||||
|
||||
// $invoiceNumbers = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->get();
|
||||
|
||||
// $invoiceNumber = $invoiceNumbers->first(function ($value, $key) {
|
||||
// return str_contains($this->bank_transaction->description, $value->number) || str_contains(str_replace("\n", "", $this->bank_transaction->description), $value->number);
|
||||
// });
|
||||
|
||||
// if($invoiceNumber)
|
||||
// $matches++;
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$invoice.po_number') {
|
||||
|
||||
// $invoicePONumbers = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->where('po_number', $this->bank_transaction->description)
|
||||
// ->get();
|
||||
|
||||
// if($invoicePONumbers->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$invoice.amount') {
|
||||
|
||||
// $$invoiceAmounts = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->where('amount', $rule['operator'], $this->bank_transaction->amount)
|
||||
// ->get();
|
||||
|
||||
// $invoiceAmounts = $this->invoices;
|
||||
|
||||
// if($invoiceAmounts->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$payment.amount') {
|
||||
|
||||
|
||||
// $paymentAmounts = Payment::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,4])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->whereNull('transaction_id')
|
||||
// ->where('amount', $rule['operator'], $this->bank_transaction->amount)
|
||||
// ->get();
|
||||
|
||||
|
||||
|
||||
// if($paymentAmounts->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if ($rule['search_key'] == '$payment.transaction_reference') {
|
||||
|
||||
// $ref_search = $this->bank_transaction->description;
|
||||
|
||||
// switch ($rule['operator']) {
|
||||
// case 'is':
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// case 'contains':
|
||||
// $ref_search = "%".$ref_search."%";
|
||||
// $operator = 'LIKE';
|
||||
// break;
|
||||
|
||||
// default:
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// }
|
||||
|
||||
// $paymentReferences = Payment::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,4])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->whereNull('transaction_id')
|
||||
// ->where('transaction_reference', $operator, $ref_search)
|
||||
// ->get();
|
||||
|
||||
|
||||
|
||||
// if($paymentReferences->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$client.id_number') {
|
||||
|
||||
// $ref_search = $this->bank_transaction->description;
|
||||
|
||||
// switch ($rule['operator']) {
|
||||
// case 'is':
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// case 'contains':
|
||||
// $ref_search = "%".$ref_search."%";
|
||||
// $operator = 'LIKE';
|
||||
// break;
|
||||
|
||||
// default:
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// }
|
||||
|
||||
// $clientIdNumbers = Client::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->where('id_number', $operator, $ref_search)
|
||||
// ->get();
|
||||
|
||||
// if($clientIdNumbers->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if ($rule['search_key'] == '$client.email') {
|
||||
|
||||
// $clientEmails = Client::query()
|
||||
// ->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereHas('contacts', function ($q){
|
||||
// $q->where('email', $this->bank_transaction->description);
|
||||
// })
|
||||
// ->get();
|
||||
|
||||
|
||||
// if($clientEmails->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// if (($bank_transaction_rule['matches_on_all'] && ($matches == $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && $matches > 0)) {
|
||||
|
||||
// //determine which combination has succeeded, ie link a payment / or / invoice
|
||||
// $invoice_ids = null;
|
||||
// $payment_id = null;
|
||||
|
||||
// if($invoiceNumber){
|
||||
// $invoice_ids = $invoiceNumber->hashed_id;
|
||||
// }
|
||||
|
||||
// if($invoicePONumbers && strlen($invoice_ids ?? '') == 0){
|
||||
|
||||
// if($clientEmails){ // @phpstan-ignore-line
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoicePONumbers, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if($clientIdNumbers && strlen($invoice_ids ?? '') == 0)
|
||||
// {
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoicePONumbers, $clientIdNumbers);
|
||||
|
||||
// }
|
||||
|
||||
// if(strlen($invoice_ids ?? '') == 0)
|
||||
// {
|
||||
// $invoice_ids = $invoicePONumbers->first()->hashed_id;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if($invoiceAmounts && strlen($invoice_ids ?? '') == 0) {
|
||||
|
||||
// if($clientEmails) {// @phpstan-ignore-line
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoiceAmounts, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if($clientIdNumbers && strlen($invoice_ids ?? '') == 0) {
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoiceAmounts, $clientIdNumbers);
|
||||
|
||||
// }
|
||||
|
||||
// if(strlen($invoice_ids ?? '') == 0) {
|
||||
// $invoice_ids = $invoiceAmounts->first()->hashed_id;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if($paymentAmounts && strlen($invoice_ids ?? '') == 0 && is_null($payment_id)) {
|
||||
|
||||
// if($clientEmails) {// @phpstan-ignore-line
|
||||
|
||||
// $payment_id = $this->matchPaymentAndClient($paymentAmounts, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if($clientIdNumbers && is_null($payment_id)) {
|
||||
|
||||
|
||||
// $payment_id = $this->matchPaymentAndClient($paymentAmounts, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if(is_null($payment_id)) {
|
||||
// $payment_id = $paymentAmounts->first()->id;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if(strlen($invoice_ids ?? '') > 1 || is_int($payment_id))
|
||||
// {
|
||||
|
||||
// $this->bank_transaction->payment_id = $payment_id;
|
||||
// $this->bank_transaction->invoice_ids = $invoice_ids;
|
||||
// $this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
|
||||
// $this->bank_transaction->bank_transaction_rule_id = $bank_transaction_rule->id;
|
||||
// $this->bank_transaction->save();
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// private function matchPaymentAndClient($payments, $clients): ?int
|
||||
// {
|
||||
// /** @var \Illuminate\Support\Collection<Payment> $payments */
|
||||
// foreach($payments as $payment) {
|
||||
// foreach($clients as $client) {
|
||||
|
||||
// if($payment->client_id == $client->id) {
|
||||
// return $payment->id;
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// private function matchInvoiceAndClient($invoices, $clients): ?Invoice
|
||||
// {
|
||||
// /** @var \Illuminate\Support\Collection<Invoice> $invoices */
|
||||
// foreach($invoices as $invoice) {
|
||||
// foreach($clients as $client) {
|
||||
|
||||
// if($invoice->client_id == $client->id) {
|
||||
// return $invoice->hashed_id;
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
||||
|
||||
private function matchDebit()
|
||||
{
|
||||
$this->debit_rules = $this->bank_transaction->company->debit_rules();
|
||||
@ -726,13 +388,336 @@ class ProcessBankRules extends AbstractService
|
||||
$bt_value = strtolower(str_replace(" ", "", $bt_value));
|
||||
$rule_value = strtolower(str_replace(" ", "", $rule_value));
|
||||
$rule_length = iconv_strlen($rule_value);
|
||||
|
||||
// nlog($bt_value);
|
||||
// nlog($rule_value);
|
||||
// nlog($rule_length);
|
||||
return match ($operator) {
|
||||
'is' => $bt_value == $rule_value,
|
||||
'contains' => stripos($bt_value, $rule_value) !== false,
|
||||
'starts_with' => substr($bt_value, 0, $rule_length) == $rule_value,
|
||||
'contains' => stripos($bt_value, $rule_value) !== false && strlen($rule_value) > 1,
|
||||
'starts_with' => substr($bt_value, 0, $rule_length) == $rule_value && strlen($rule_value) > 1,
|
||||
'is_empty' => empty($bt_value),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// $payment.amount => "Payment Amount", float
|
||||
// $payment.transaction_reference => "Payment Transaction Reference", string
|
||||
// $invoice.amount => "Invoice Amount", float
|
||||
// $invoice.number => "Invoice Number", string
|
||||
// $client.id_number => "Client ID Number", string
|
||||
// $client.email => "Client Email", string
|
||||
// $invoice.po_number => "Invoice Purchase Order Number", string
|
||||
|
||||
|
||||
// private function matchCredit()
|
||||
// {
|
||||
// $this->invoices = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->get();
|
||||
|
||||
// $invoice = $this->invoices->first(function ($value, $key) {
|
||||
// return str_contains($this->bank_transaction->description, $value->number) || str_contains(str_replace("\n", "", $this->bank_transaction->description), $value->number);
|
||||
// });
|
||||
|
||||
// if ($invoice) {
|
||||
// $this->bank_transaction->invoice_ids = $invoice->hashed_id;
|
||||
// $this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
|
||||
// $this->bank_transaction->save();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// $this->credit_rules = $this->bank_transaction->company->credit_rules();
|
||||
|
||||
// //stub for credit rules
|
||||
// foreach ($this->credit_rules as $bank_transaction_rule) {
|
||||
// $matches = 0;
|
||||
|
||||
// if (!is_array($bank_transaction_rule['rules'])) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// foreach ($bank_transaction_rule['rules'] as $rule) {
|
||||
// $rule_count = count($bank_transaction_rule['rules']);
|
||||
|
||||
// $invoiceNumbers = false;
|
||||
// $invoiceNumber = false;
|
||||
// $invoiceAmounts = false;
|
||||
// $paymentAmounts = false;
|
||||
// $paymentReferences = false;
|
||||
// $clientIdNumbers = false;
|
||||
// $clientEmails = false;
|
||||
// $invoicePONumbers = false;
|
||||
|
||||
// if ($rule['search_key'] == '$invoice.number') {
|
||||
|
||||
// $invoiceNumbers = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->get();
|
||||
|
||||
// $invoiceNumber = $invoiceNumbers->first(function ($value, $key) {
|
||||
// return str_contains($this->bank_transaction->description, $value->number) || str_contains(str_replace("\n", "", $this->bank_transaction->description), $value->number);
|
||||
// });
|
||||
|
||||
// if($invoiceNumber)
|
||||
// $matches++;
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$invoice.po_number') {
|
||||
|
||||
// $invoicePONumbers = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->where('po_number', $this->bank_transaction->description)
|
||||
// ->get();
|
||||
|
||||
// if($invoicePONumbers->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$invoice.amount') {
|
||||
|
||||
// $$invoiceAmounts = Invoice::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,2,3])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->where('amount', $rule['operator'], $this->bank_transaction->amount)
|
||||
// ->get();
|
||||
|
||||
// $invoiceAmounts = $this->invoices;
|
||||
|
||||
// if($invoiceAmounts->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$payment.amount') {
|
||||
|
||||
|
||||
// $paymentAmounts = Payment::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,4])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->whereNull('transaction_id')
|
||||
// ->where('amount', $rule['operator'], $this->bank_transaction->amount)
|
||||
// ->get();
|
||||
|
||||
|
||||
|
||||
// if($paymentAmounts->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if ($rule['search_key'] == '$payment.transaction_reference') {
|
||||
|
||||
// $ref_search = $this->bank_transaction->description;
|
||||
|
||||
// switch ($rule['operator']) {
|
||||
// case 'is':
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// case 'contains':
|
||||
// $ref_search = "%".$ref_search."%";
|
||||
// $operator = 'LIKE';
|
||||
// break;
|
||||
|
||||
// default:
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// }
|
||||
|
||||
// $paymentReferences = Payment::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereIn('status_id', [1,4])
|
||||
// ->where('is_deleted', 0)
|
||||
// ->whereNull('transaction_id')
|
||||
// ->where('transaction_reference', $operator, $ref_search)
|
||||
// ->get();
|
||||
|
||||
|
||||
|
||||
// if($paymentReferences->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if ($rule['search_key'] == '$client.id_number') {
|
||||
|
||||
// $ref_search = $this->bank_transaction->description;
|
||||
|
||||
// switch ($rule['operator']) {
|
||||
// case 'is':
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// case 'contains':
|
||||
// $ref_search = "%".$ref_search."%";
|
||||
// $operator = 'LIKE';
|
||||
// break;
|
||||
|
||||
// default:
|
||||
// $operator = '=';
|
||||
// break;
|
||||
// }
|
||||
|
||||
// $clientIdNumbers = Client::query()->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->where('id_number', $operator, $ref_search)
|
||||
// ->get();
|
||||
|
||||
// if($clientIdNumbers->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if ($rule['search_key'] == '$client.email') {
|
||||
|
||||
// $clientEmails = Client::query()
|
||||
// ->where('company_id', $this->bank_transaction->company_id)
|
||||
// ->whereHas('contacts', function ($q){
|
||||
// $q->where('email', $this->bank_transaction->description);
|
||||
// })
|
||||
// ->get();
|
||||
|
||||
|
||||
// if($clientEmails->count() > 0) {
|
||||
// $matches++;
|
||||
// }
|
||||
|
||||
// if (($bank_transaction_rule['matches_on_all'] && ($matches == $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && $matches > 0)) {
|
||||
|
||||
// //determine which combination has succeeded, ie link a payment / or / invoice
|
||||
// $invoice_ids = null;
|
||||
// $payment_id = null;
|
||||
|
||||
// if($invoiceNumber){
|
||||
// $invoice_ids = $invoiceNumber->hashed_id;
|
||||
// }
|
||||
|
||||
// if($invoicePONumbers && strlen($invoice_ids ?? '') == 0){
|
||||
|
||||
// if($clientEmails){ // @phpstan-ignore-line
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoicePONumbers, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if($clientIdNumbers && strlen($invoice_ids ?? '') == 0)
|
||||
// {
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoicePONumbers, $clientIdNumbers);
|
||||
|
||||
// }
|
||||
|
||||
// if(strlen($invoice_ids ?? '') == 0)
|
||||
// {
|
||||
// $invoice_ids = $invoicePONumbers->first()->hashed_id;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if($invoiceAmounts && strlen($invoice_ids ?? '') == 0) {
|
||||
|
||||
// if($clientEmails) {// @phpstan-ignore-line
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoiceAmounts, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if($clientIdNumbers && strlen($invoice_ids ?? '') == 0) {
|
||||
|
||||
// $invoice_ids = $this->matchInvoiceAndClient($invoiceAmounts, $clientIdNumbers);
|
||||
|
||||
// }
|
||||
|
||||
// if(strlen($invoice_ids ?? '') == 0) {
|
||||
// $invoice_ids = $invoiceAmounts->first()->hashed_id;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// if($paymentAmounts && strlen($invoice_ids ?? '') == 0 && is_null($payment_id)) {
|
||||
|
||||
// if($clientEmails) {// @phpstan-ignore-line
|
||||
|
||||
// $payment_id = $this->matchPaymentAndClient($paymentAmounts, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if($clientIdNumbers && is_null($payment_id)) {
|
||||
|
||||
|
||||
// $payment_id = $this->matchPaymentAndClient($paymentAmounts, $clientEmails);
|
||||
|
||||
// }
|
||||
|
||||
// if(is_null($payment_id)) {
|
||||
// $payment_id = $paymentAmounts->first()->id;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// if(strlen($invoice_ids ?? '') > 1 || is_int($payment_id))
|
||||
// {
|
||||
|
||||
// $this->bank_transaction->payment_id = $payment_id;
|
||||
// $this->bank_transaction->invoice_ids = $invoice_ids;
|
||||
// $this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED;
|
||||
// $this->bank_transaction->bank_transaction_rule_id = $bank_transaction_rule->id;
|
||||
// $this->bank_transaction->save();
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
|
||||
// private function matchPaymentAndClient($payments, $clients): ?int
|
||||
// {
|
||||
// /** @var \Illuminate\Support\Collection<Payment> $payments */
|
||||
// foreach($payments as $payment) {
|
||||
// foreach($clients as $client) {
|
||||
|
||||
// if($payment->client_id == $client->id) {
|
||||
// return $payment->id;
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// private function matchInvoiceAndClient($invoices, $clients): ?Invoice
|
||||
// {
|
||||
// /** @var \Illuminate\Support\Collection<Invoice> $invoices */
|
||||
// foreach($invoices as $invoice) {
|
||||
// foreach($clients as $client) {
|
||||
|
||||
// if($invoice->client_id == $client->id) {
|
||||
// return $invoice->hashed_id;
|
||||
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return null;
|
||||
// }
|
||||
|
@ -109,11 +109,10 @@ class LivewireInstantPayment
|
||||
$client = $invoices->first()->client;
|
||||
|
||||
/* pop non payable invoice from the $payable_invoices array */
|
||||
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) {
|
||||
$payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices) { // @phpstan-ignore-line
|
||||
return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first();
|
||||
});
|
||||
|
||||
//$payable_invoices = $payable_invoices->map(function ($payable_invoice) use ($invoices, $settings) {
|
||||
$payable_invoice_collection = collect();
|
||||
|
||||
foreach ($payable_invoices as $payable_invoice) {
|
||||
|
@ -11,8 +11,13 @@
|
||||
|
||||
namespace App\Services\EDocument\Gateway\Storecove;
|
||||
|
||||
use App\DataMapper\Analytics\LegalEntityCreated;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Turbo124\Beacon\Facades\LightLogs;
|
||||
|
||||
enum HttpVerb: string
|
||||
{
|
||||
@ -24,9 +29,11 @@ enum HttpVerb: string
|
||||
}
|
||||
|
||||
class Storecove
|
||||
{
|
||||
{
|
||||
/** @var string $base_url */
|
||||
private string $base_url = 'https://api.storecove.com/api/v2/';
|
||||
|
||||
|
||||
/** @var array $peppol_discovery */
|
||||
private array $peppol_discovery = [
|
||||
"documentTypes" => ["invoice"],
|
||||
"network" => "peppol",
|
||||
@ -34,7 +41,8 @@ class Storecove
|
||||
"scheme" => "de:lwid",
|
||||
"identifier" => "DE:VAT"
|
||||
];
|
||||
|
||||
|
||||
/** @var array $dbn_discovery */
|
||||
private array $dbn_discovery = [
|
||||
"documentTypes" => ["invoice"],
|
||||
"network" => "dbnalliance",
|
||||
@ -43,18 +51,22 @@ class Storecove
|
||||
"identifier" => "1200109963131"
|
||||
];
|
||||
|
||||
public StorecoveRouter $router;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->router = new StorecoveRouter();
|
||||
}
|
||||
|
||||
//config('ninja.storecove_api_key');
|
||||
|
||||
//https://app.storecove.com/en/docs#_test_identifiers
|
||||
//check if identifier is able to send on the network.
|
||||
|
||||
//response = { "code": "OK", "email": false}
|
||||
public function discovery($identifier, $scheme, $network = 'peppol')
|
||||
|
||||
/**
|
||||
* Discovery
|
||||
*
|
||||
* @param string $identifier
|
||||
* @param string $scheme
|
||||
* @param string $network
|
||||
* @return bool
|
||||
*/
|
||||
public function discovery(string $identifier, string $scheme, string $network = 'peppol'): bool
|
||||
{
|
||||
$network_data = [];
|
||||
|
||||
@ -71,50 +83,23 @@ class Storecove
|
||||
return ($r->successful() && $r->json()['code'] == 'OK') ? true : false;
|
||||
|
||||
}
|
||||
|
||||
//response = "guid" : "xx",
|
||||
|
||||
|
||||
/**
|
||||
* If the receiver cannot be found, then an
|
||||
* email is sent to that user if a appropriate
|
||||
* email is included in the document payload
|
||||
* Unused as yet
|
||||
*
|
||||
* {
|
||||
"routing": {
|
||||
"emails": [
|
||||
"test@example.com"
|
||||
],
|
||||
"eIdentifiers": []
|
||||
}
|
||||
}
|
||||
*
|
||||
*
|
||||
*
|
||||
// documentType : invoice/invoice_response/order
|
||||
// rawDocumentData : {
|
||||
// document: base64_encode($ubl)
|
||||
// parse: true
|
||||
// parseStrategy: ubl
|
||||
// }
|
||||
* @param mixed $document
|
||||
* @return string|bool
|
||||
*/
|
||||
public function sendJsonDocument($document)
|
||||
{
|
||||
|
||||
$payload = [
|
||||
"legalEntityId" => 290868,
|
||||
// "legalEntityId" => 290868,
|
||||
"idempotencyGuid" => \Illuminate\Support\Str::uuid(),
|
||||
"routing" => [
|
||||
"eIdentifiers" => [],
|
||||
"emails" => ["david@invoiceninja.com"]
|
||||
],
|
||||
// "document" => [
|
||||
// 'documentType' => 'invoice',
|
||||
// "rawDocumentData" => [
|
||||
// "document" => base64_encode($document),
|
||||
// "parse" => true,
|
||||
// "parseStrategy" => "ubl",
|
||||
// ],
|
||||
// ],
|
||||
"document" => [
|
||||
"documentType" => "invoice",
|
||||
"invoice" => $document,
|
||||
@ -123,13 +108,8 @@ class Storecove
|
||||
|
||||
$uri = "document_submissions";
|
||||
|
||||
nlog($payload);
|
||||
|
||||
$r = $this->httpClient($uri, (HttpVerb::POST)->value, $payload, $this->getHeaders());
|
||||
|
||||
nlog($r->body());
|
||||
nlog($r->json());
|
||||
|
||||
if($r->successful()) {
|
||||
return $r->json()['guid'];
|
||||
}
|
||||
@ -137,7 +117,16 @@ class Storecove
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send Document via StoreCove
|
||||
*
|
||||
* @param string $document
|
||||
* @param int $routing_id
|
||||
* @param array $override_payload
|
||||
*
|
||||
* @return string|\Illuminate\Http\Client\Response
|
||||
*/
|
||||
public function sendDocument(string $document, int $routing_id, array $override_payload = [])
|
||||
{
|
||||
|
||||
@ -155,7 +144,6 @@ class Storecove
|
||||
|
||||
$payload = array_merge($payload, $override_payload);
|
||||
|
||||
|
||||
$payload['document']['documentType'] = 'invoice';
|
||||
$payload['document']["rawDocumentData"] = [
|
||||
"document" => base64_encode($document),
|
||||
@ -165,103 +153,72 @@ class Storecove
|
||||
|
||||
$uri = "document_submissions";
|
||||
|
||||
nlog($payload);
|
||||
|
||||
$r = $this->httpClient($uri, (HttpVerb::POST)->value, $payload, $this->getHeaders());
|
||||
|
||||
nlog($r->body());
|
||||
nlog($r->json());
|
||||
// nlog($r->json());
|
||||
|
||||
if($r->successful()) {
|
||||
return $r->json()['guid'];
|
||||
}
|
||||
|
||||
return false;
|
||||
return $r;
|
||||
|
||||
}
|
||||
|
||||
//document submission sending evidence
|
||||
/**
|
||||
* Get Sending Evidence
|
||||
*
|
||||
* @param string $guid
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSendingEvidence(string $guid)
|
||||
{
|
||||
$uri = "document_submissions/{$guid}";
|
||||
|
||||
$r = $this->httpClient($uri, (HttpVerb::GET)->value, [], $this->getHeaders());
|
||||
|
||||
if($r->successful())
|
||||
return $r->json();
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
// {
|
||||
// "party_name": "<string>",
|
||||
// "line1": "<string>",
|
||||
// "city": "<string>",
|
||||
// "zip": "<string>",
|
||||
// "country": "EH",
|
||||
// "line2": "<string>",
|
||||
// "county": "<string>",
|
||||
// "tenant_id": "<string>",
|
||||
// "public": true,
|
||||
// "advertisements": [
|
||||
// "invoice"
|
||||
// ],
|
||||
// "third_party_username": "<string>",
|
||||
// "third_party_password": "<string>",
|
||||
// "rea": {
|
||||
// "province": "AR",
|
||||
// "identifier": "<string>",
|
||||
// "capital": "<number>",
|
||||
// "partners": "SM",
|
||||
// "liquidation_status": "LN"
|
||||
// },
|
||||
// "acts_as_sender": true,
|
||||
// "acts_as_receiver": true,
|
||||
// "tax_registered": true
|
||||
// }
|
||||
|
||||
// acts_as_receiver - optional - Default : true
|
||||
// acts_as_sender - optional - Default : true
|
||||
// advertisements - optional < enum (invoice, invoice_response, order, ordering, order_response, selfbilling) > array
|
||||
// city - required - Length : 2 - 64
|
||||
// country - required - ISO 3166-1 alpha-2
|
||||
// county - optional - Maximal length : 64
|
||||
// line1 - required - The first address line - Length : 2 - 192
|
||||
// line2 - optional - The second address line, if applicable Maximal length : 192
|
||||
// party_name - required - The name of the company. Length : 2 - 64
|
||||
// public - optional - Whether or not this LegalEntity is public. Public means it will be entered into the PEPPOL directory at https://directory.peppol.eu/ Default : true
|
||||
// rea - optional - The REA details for the LegalEntity. Only applies to IT (Italian) LegalEntities. - https://www.storecove.com/docs/#_openapi_rea (schema)
|
||||
|
||||
// capital - optional - The captial for the company. - number
|
||||
// identifier - optional - The identifier. Length : 2 - 20
|
||||
// liquidation_status - optional - The liquidation status of the company. enum (LN, LS)
|
||||
// partners - optional - The number of partners. enum (SU, SM)
|
||||
// province - optional - The provincia of the ufficio that issued the identifier.enum (AG, AL, AN, AO, AQ, AR, AP, AT, AV, BA, BT, BL, BN, BG, BI, BO, BZ, BS, BR, CA, CL, CB, CI, CE, CT, CZ, CH, CO, CS, CR, KR, CN, EN, FM, FE, FI, FG, FC, FR, GE, GO, GR, IM, IS, SP, LT, LE, LC, LI, LO, LU, MC, MN, MS, MT, VS, ME, MI, MO, MB, NA, NO, NU, OG, OT, OR, PD, PA, PR, PV, PG, PU, PE, PC, PI, PT, PN, PZ, PO, RG, RA, RC, RE, RI, RN, RO, SA, SS, SV, SI, SR, SO, TA, TE, TR, TO, TP, TN, TV, TS, UD, VA, VE, VB, VC, VR, VV, VI, VT)
|
||||
|
||||
// tax_registered - optional - Whether or not this LegalEntity is tax registered. This influences the validation of the data presented when sending documents. Default : true
|
||||
// tenant_id - optional - The id of the tenant, to be used in case of single-tenant solutions that share webhook URLs. This property will included in webhook events. Maximal length : 64
|
||||
// third_party_password - optional - The password to use to authenticate to a system through which to send the document, or to obtain tax authority approval to send it. This field is currently relevant only for India and mandatory when creating an IN LegalEntity. Length : 2 - 64
|
||||
// third_party_username - optional - The username to use to authenticate to a system through which to send the document, or to obtain tax authority approval to send it. This field is currently relevant only for India and mandatory when creating an IN LegalEntity. Length : 2 - 64
|
||||
// zip - required - The zipcode. Length : 2 - 32
|
||||
|
||||
/**
|
||||
* CreateLegalEntity
|
||||
*
|
||||
* Creates a base entity.
|
||||
*
|
||||
* Following creation, you will also need to create a Peppol Identifier
|
||||
*
|
||||
* @url https://www.storecove.com/docs/#_openapi_legalentitycreate
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function createLegalEntity(array $data, Company $company)
|
||||
public function createLegalEntity(array $data, ?Company $company = null)
|
||||
{
|
||||
$uri = 'legal_entities';
|
||||
|
||||
if($company){
|
||||
|
||||
$data = array_merge([
|
||||
'city' => $company->settings->city,
|
||||
'country' => $company->country()->iso_3166_2,
|
||||
'county' => $company->settings->state,
|
||||
'line1' => $company->settings->address1,
|
||||
'line2' => $company->settings->address2,
|
||||
'party_name' => $company->settings->name,
|
||||
'tax_registered' => (bool)strlen($company->settings->vat_number ?? '') > 2,
|
||||
'tenant_id' => $company->company_key,
|
||||
'zip' => $company->settings->postal_code,
|
||||
], $data);
|
||||
|
||||
}
|
||||
|
||||
$company_defaults = [
|
||||
'acts_as_receiver' => true,
|
||||
'acts_as_sender' => true,
|
||||
'advertisements' => ['invoice'],
|
||||
'city' => $company->settings->city,
|
||||
'country' => $company->country()->iso_3166_2,
|
||||
'county' => $company->settings->state,
|
||||
'line1' => $company->settings->address1,
|
||||
'line2' => $company->settings->address2,
|
||||
'party_name' => $company->settings->name,
|
||||
'tax_registered' => true,
|
||||
'tenant_id' => $company->company_key,
|
||||
'zip' => $company->settings->postal_code,
|
||||
];
|
||||
|
||||
$payload = array_merge($company_defaults, $data);
|
||||
@ -275,10 +232,18 @@ class Storecove
|
||||
return $r;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GetLegalEntity
|
||||
*
|
||||
* @param int $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLegalEntity($id)
|
||||
{
|
||||
|
||||
// $uri = "legal_entities";
|
||||
|
||||
$uri = "legal_entities/{$id}";
|
||||
|
||||
$r = $this->httpClient($uri, (HttpVerb::GET)->value, []);
|
||||
@ -290,8 +255,15 @@ class Storecove
|
||||
return $r;
|
||||
|
||||
}
|
||||
|
||||
public function updateLegalEntity($id, array $data)
|
||||
|
||||
/**
|
||||
* UpdateLegalEntity
|
||||
*
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function updateLegalEntity(int $id, array $data)
|
||||
{
|
||||
|
||||
$uri = "legal_entities/{$id}";
|
||||
@ -305,7 +277,17 @@ class Storecove
|
||||
return $r;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AddIdentifier
|
||||
*
|
||||
* Add a Peppol identifier to the legal entity
|
||||
*
|
||||
* @param int $legal_entity_id
|
||||
* @param string $identifier
|
||||
* @param string $scheme
|
||||
* @return mixed
|
||||
*/
|
||||
public function addIdentifier(int $legal_entity_id, string $identifier, string $scheme)
|
||||
{
|
||||
$uri = "legal_entities/{$legal_entity_id}/peppol_identifiers";
|
||||
@ -319,12 +301,29 @@ class Storecove
|
||||
$r = $this->httpClient($uri, (HttpVerb::POST)->value, $data);
|
||||
|
||||
if($r->successful()) {
|
||||
return $r->json();
|
||||
$data = $r->json();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $r;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* deleteIdentifier
|
||||
*
|
||||
* @param int $legal_entity_id
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteIdentifier(int $legal_entity_id): bool
|
||||
{
|
||||
$uri = "/legal_entities/{$legal_entity_id}";
|
||||
|
||||
$r = $this->httpClient($uri, (HttpVerb::DELETE)->value, []);
|
||||
|
||||
return $r->successful();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private function getHeaders(array $headers = [])
|
||||
@ -336,15 +335,37 @@ class Storecove
|
||||
], $headers);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* httpClient
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string $verb
|
||||
* @param array $data
|
||||
* @param array $headers
|
||||
* @return \Illuminate\Http\Client\Response
|
||||
*/
|
||||
private function httpClient(string $uri, string $verb, array $data, ?array $headers = [])
|
||||
{
|
||||
|
||||
$r = Http::withToken(config('ninja.storecove_api_key'))
|
||||
->withHeaders($this->getHeaders($headers))
|
||||
->{$verb}("{$this->base_url}{$uri}", $data);
|
||||
try {
|
||||
$r = Http::withToken(config('ninja.storecove_api_key'))
|
||||
->withHeaders($this->getHeaders($headers))
|
||||
->{$verb}("{$this->base_url}{$uri}", $data)->throw();
|
||||
}
|
||||
catch (ClientException $e) {
|
||||
// 4xx errors
|
||||
nlog("Client error: " . $e->getMessage());
|
||||
nlog("Response body: " . $e->getResponse()->getBody()->getContents());
|
||||
} catch (ServerException $e) {
|
||||
// 5xx errors
|
||||
nlog("Server error: " . $e->getMessage());
|
||||
nlog("Response body: " . $e->getResponse()->getBody()->getContents());
|
||||
} catch (RequestException $e) {
|
||||
nlog("Request error: {$e->getCode()}: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $r;
|
||||
return $r; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
}
|
||||
|
165
app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php
Normal file
165
app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?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\EDocument\Gateway\Storecove;
|
||||
|
||||
class StorecoveRouter
|
||||
{
|
||||
private array $routing_rules = [
|
||||
"US" => [
|
||||
["B","DUNS, GLN, LEI","US:EIN","DUNS, GLN, LEI"],
|
||||
// ["B","DUNS, GLN, LEI","US:SSN","DUNS, GLN, LEI"],
|
||||
],
|
||||
"CA" => ["B","CA:CBN",false,"CA:CBN"],
|
||||
"MX" => ["B","MX:RFC",false,"MX:RFC"],
|
||||
"AU" => ["B+G","AU:ABN",false,"AU:ABN"],
|
||||
"NZ" => ["B+G","GLN","NZ:GST","GLN"],
|
||||
"CH" => ["B+G","CH:UIDB","CH:VAT","CH:UIDB"],
|
||||
"IS" => ["B+G","IS:KTNR","IS:VAT","IS:KTNR"],
|
||||
"LI" => ["B+G","","LI:VAT","LI:VAT"],
|
||||
"NO" => ["B+G","NO:ORG","NO:VAT","NO:ORG"],
|
||||
"AD" => ["B+G","","AD:VAT","AD:VAT"],
|
||||
"AL" => ["B+G","","AL:VAT","AL:VAT"],
|
||||
"AT" => [
|
||||
["G","AT:GOV",false,"9915:b"],
|
||||
["B","","AT:VAT","AT:VAT"],
|
||||
],
|
||||
"BA" => ["B+G","","BA:VAT","BA:VAT"],
|
||||
"BE" => ["B+G","BE:EN","BE:VAT","BE:EN"],
|
||||
"BG" => ["B+G","","BG:VAT","BG:VAT"],
|
||||
"CY" => ["B+G","","CY:VAT","CY:VAT"],
|
||||
"CZ" => ["B+G","","CZ:VAT","CZ:VAT"],
|
||||
"DE" => [
|
||||
["G","DE:LWID",false,"DE:LWID"],
|
||||
["B","","DE:VAT","DE:VAT"],
|
||||
],
|
||||
"DK" => ["B+G","DK:DIGST","DK:ERST","DK:DIGST"],
|
||||
"EE" => ["B+G","EE:CC","EE:VAT","EE:CC"],
|
||||
"ES" => ["B","","ES:VAT","ES:VAT"],
|
||||
"FI" => ["B+G","FI:OVT","FI:VAT","FI:OVT"],
|
||||
"FR" => [
|
||||
["G","FR:SIRET + customerAssignedAccountIdValue",false,"0009:11000201100044"],
|
||||
["B","FR:SIRENE or FR:SIRET","FR:VAT","FR:SIRENE or FR:SIRET"],
|
||||
],
|
||||
"GR" => ["B+G","","GR:VAT","GR:VAT"],
|
||||
"HR" => ["B+G","","HR:VAT","HR:VAT"],
|
||||
"HU" => ["B+G","","HU:VAT","HU:VAT"],
|
||||
"IE" => ["B+G","","IE:VAT","IE:VAT"],
|
||||
"IT" => [
|
||||
["G","","IT:IVA","IT:CUUO"], // (Peppol)
|
||||
["B","","IT:IVA","IT:CUUO"], // (SDI)
|
||||
// ["B","","IT:CF","IT:CUUO"], // (SDI)
|
||||
["C","","IT:CF","Email"],// (SDI)
|
||||
["G","","IT:IVA","IT:CUUO"],// (SDI)
|
||||
],
|
||||
"LT" => ["B+G","LT:LEC","LT:VAT","LT:LEC"],
|
||||
"LU" => ["B+G","LU:MAT","LU:VAT","LU:VAT"],
|
||||
"LV" => ["B+G","","LV:VAT","LV:VAT"],
|
||||
"MC" => ["B+G","","MC:VAT","MC:VAT"],
|
||||
"ME" => ["B+G","","ME:VAT","ME:VAT"],
|
||||
"MK" => ["B+G","","MK:VAT","MK:VAT"],
|
||||
"MT" => ["B+G","","MT:VAT","MT:VAT"],
|
||||
"NL" => ["G","NL:OINO",false,"NL:OINO"],
|
||||
"NL" => ["B","NL:KVK","NL:VAT","NL:KVK or NL:VAT"],
|
||||
"PL" => ["G+B","","PL:VAT","PL:VAT"],
|
||||
"PT" => ["G+B","","PT:VAT","PT:VAT"],
|
||||
"RO" => ["G+B","","RO:VAT","RO:VAT"],
|
||||
"RS" => ["G+B","","RS:VAT","RS:VAT"],
|
||||
"SE" => ["G+B","SE:ORGNR","SE:VAT","SE:ORGNR"],
|
||||
"SI" => ["G+B","","SI:VAT","SI:VAT"],
|
||||
"SK" => ["G+B","","SK:VAT","SK:VAT"],
|
||||
"SM" => ["G+B","","SM:VAT","SM:VAT"],
|
||||
"TR" => ["G+B","","TR:VAT","TR:VAT"],
|
||||
"VA" => ["G+B","","VA:VAT","VA:VAT"],
|
||||
"IN" => ["B","","IN:GSTIN","Email"],
|
||||
"JP" => ["B","JP:SST","JP:IIN","JP:SST"],
|
||||
"MY" => ["B","MY:EIF","MY:TIN","MY:EIF"],
|
||||
"SG" => [
|
||||
["G","SG:UEN",false,"0195:SGUENT08GA0028A"],
|
||||
["B","SG:UEN","SG:GST","SG:UEN"],
|
||||
],
|
||||
"GB" => ["B","","GB:VAT","GB:VAT"],
|
||||
"SA" => ["B","","SA:TIN","Email"],
|
||||
"Other" => ["B","DUNS, GLN, LEI",false,"DUNS, GLN, LEI"],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the routing code based on country and entity classification
|
||||
*
|
||||
* @param string $country
|
||||
* @param ?string $classification
|
||||
* @return string
|
||||
*/
|
||||
public function resolveRouting(string $country, ?string $classification = 'business'): string
|
||||
{
|
||||
$rules = $this->routing_rules[$country];
|
||||
|
||||
if(is_array($rules) && !is_array($rules[0])) {
|
||||
return $rules[3];
|
||||
}
|
||||
|
||||
$code = 'B';
|
||||
|
||||
match($classification) {
|
||||
"business" => $code = "B",
|
||||
"government" => $code = "G",
|
||||
"individual" => $code = "C",
|
||||
default => $code = "B",
|
||||
};
|
||||
|
||||
foreach($rules as $rule) {
|
||||
if(stripos($rule[0], $code) !== false) {
|
||||
return $rule[3];
|
||||
}
|
||||
}
|
||||
|
||||
return $rules[0][3];
|
||||
}
|
||||
|
||||
/**
|
||||
* resolveTaxScheme
|
||||
*
|
||||
* @param string $country
|
||||
* @param ?string $classification
|
||||
* @return string
|
||||
*/
|
||||
public function resolveTaxScheme(string $country, ?string $classification = "business"): string
|
||||
{
|
||||
|
||||
$rules = isset($this->routing_rules[$country]) ? $this->routing_rules[$country] : [false, false, false, false];
|
||||
|
||||
$code = "B";
|
||||
|
||||
match($classification) {
|
||||
"business" => $code = "B",
|
||||
"government" => $code = "G",
|
||||
"individual" => $code = "C",
|
||||
default => $code = "B",
|
||||
};
|
||||
|
||||
//single array
|
||||
if(is_array($rules) && !is_array($rules[0])) {
|
||||
return $rules[2];
|
||||
}
|
||||
|
||||
foreach($rules as $rule) {
|
||||
if(stripos($rule[0], $code) !== false) {
|
||||
return $rule[2];
|
||||
}
|
||||
}
|
||||
|
||||
return $rules[0][2];
|
||||
}
|
||||
}
|
161
app/Services/EDocument/Jobs/SendEDocument.php
Normal file
161
app/Services/EDocument/Jobs/SendEDocument.php
Normal file
@ -0,0 +1,161 @@
|
||||
<?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\EDocument\Jobs;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Invoice;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Activity;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Services\EDocument\Standards\Peppol;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use App\Services\EDocument\Gateway\Storecove\Storecove;
|
||||
|
||||
class SendEDocument implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public $tries = 2;
|
||||
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
public function __construct(private string $entity, private int $id, private string $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function backoff()
|
||||
{
|
||||
return [rand(5, 29), rand(30, 59)];
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
MultiDB::setDB($this->db);
|
||||
|
||||
$model = $this->entity::find($this->id);
|
||||
$e_invoice_standard = $model->client ? $model->client->getSetting('e_invoice_type') : $model->company->getSetting('e_invoice_type');
|
||||
|
||||
if($e_invoice_standard != 'PEPPOL')
|
||||
return;
|
||||
|
||||
if(Ninja::isSelfHost() && ($model instanceof Invoice) && $model->company->legal_entity_id)
|
||||
{
|
||||
|
||||
$p = new Peppol($model);
|
||||
|
||||
$p->run();
|
||||
$xml = $p->toXml();
|
||||
$identifiers = $p->getStorecoveMeta();
|
||||
|
||||
$payload = [
|
||||
'legal_entity_id' => $model->company->legal_entity_id,
|
||||
'document' => base64_encode($xml),
|
||||
'tenant_id' => $model->company->company_key,
|
||||
'identifiers' => $identifiers,
|
||||
];
|
||||
|
||||
$r = Http::withHeaders($this->getHeaders())
|
||||
->post(config('ninja.hosted_ninja_url')."/api/einvoice/submission", $payload);
|
||||
|
||||
if($r->successful()) {
|
||||
nlog("Model {$model->number} was successfully sent for third party processing via hosted Invoice Ninja");
|
||||
|
||||
$data = $r->json();
|
||||
return $this->writeActivity($model, $data['guid']);
|
||||
|
||||
}
|
||||
|
||||
if($r->failed()) {
|
||||
nlog("Model {$model->number} failed to be accepted by invoice ninja, error follows:");
|
||||
nlog($r->getBody()->getContents());
|
||||
}
|
||||
|
||||
//self hosted sender
|
||||
}
|
||||
|
||||
if(Ninja::isHosted() && ($model instanceof Invoice) && $model->company->legal_entity_id)
|
||||
{
|
||||
//hosted sender
|
||||
$p = new Peppol($model);
|
||||
|
||||
$p->run();
|
||||
$xml = $p->toXml();
|
||||
$identifiers = $p->getStorecoveMeta();
|
||||
|
||||
$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove();
|
||||
$r = $sc->sendDocument($xml, $model->company->legal_entity_id, $identifiers);
|
||||
|
||||
if(is_string($r))
|
||||
return $this->writeActivity($model, $r);
|
||||
|
||||
if($r->failed()) {
|
||||
nlog("Model {$model->number} failed to be accepted by invoice ninja, error follows:");
|
||||
nlog($r->getBody()->getContents());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function writeActivity($model, string $guid)
|
||||
{
|
||||
$activity = new Activity();
|
||||
$activity->user_id = $model->user_id;
|
||||
$activity->client_id = $model->client_id ?? $model->vendor_id;
|
||||
$activity->company_id = $model->company_id;
|
||||
$activity->activity_type_id = Activity::EMAIL_EINVOICE_SUCCESS;
|
||||
$activity->invoice_id = $model->id;
|
||||
$activity->notes = $guid;
|
||||
$activity->save();
|
||||
|
||||
$model->backup = $guid;
|
||||
$model->saveQuietly();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Self hosted request headers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getHeaders(): array
|
||||
{
|
||||
return [
|
||||
'X-API-SELF-HOST-TOKEN' => config('ninja.license_key'),
|
||||
"X-Requested-With" => "XMLHttpRequest",
|
||||
"Content-Type" => "application/json",
|
||||
];
|
||||
}
|
||||
|
||||
public function failed($exception = null)
|
||||
{
|
||||
if ($exception) {
|
||||
nlog("EXCEPTION:: SENDEDOCUMENT::");
|
||||
nlog($exception->getMessage());
|
||||
}
|
||||
|
||||
config(['queue.failed.driver' => null]);
|
||||
}
|
||||
|
||||
public function middleware()
|
||||
{
|
||||
return [new WithoutOverlapping($this->entity.$this->id.$this->db)];
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification;
|
||||
use App\Services\EDocument\Gateway\Storecove\StorecoveRouter;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal;
|
||||
@ -42,18 +43,17 @@ use InvoiceNinja\EInvoice\Models\Peppol\TaxTotal as PeppolTaxTotal;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\InvoiceLineType\InvoiceLine;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\LocationType\PhysicalLocation;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\OrderReferenceType\OrderReference;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID;
|
||||
use InvoiceNinja\EInvoice\Models\Peppol\LocationType\PhysicalLocation;
|
||||
|
||||
class Peppol extends AbstractService
|
||||
{
|
||||
@ -261,6 +261,38 @@ class Peppol extends AbstractService
|
||||
$this->e = new EInvoice();
|
||||
$this->setSettings()->setInvoice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for building document
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function run(): self
|
||||
{
|
||||
$this->p_invoice->ID = $this->invoice->number;
|
||||
$this->p_invoice->IssueDate = new \DateTime($this->invoice->date);
|
||||
|
||||
if($this->invoice->due_date) {
|
||||
$this->p_invoice->DueDate = new \DateTime($this->invoice->due_date);
|
||||
}
|
||||
|
||||
$this->p_invoice->InvoiceTypeCode = 380; //
|
||||
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
|
||||
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
|
||||
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
|
||||
|
||||
// $this->p_invoice->TaxTotal = $this->getTotalTaxes(); it only wants the aggregate here!!
|
||||
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
|
||||
|
||||
$this->senderSpecificLevelMutators()
|
||||
->receiverSpecificLevelMutators();
|
||||
|
||||
$this->invoice->e_invoice = $this->toObject();
|
||||
$this->invoice->save();
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehydrates an existing e invoice - or - scaffolds a new one
|
||||
@ -272,7 +304,7 @@ class Peppol extends AbstractService
|
||||
|
||||
if($this->invoice->e_invoice) {
|
||||
|
||||
$this->p_invoice = $this->e->decode('Peppol', json_encode($this->invoice->e_invoice->Invoice), 'json');
|
||||
$this->p_invoice = $this->e->decode('Peppol', json_encode($this->invoice->e_invoice), 'json');
|
||||
|
||||
return $this;
|
||||
|
||||
@ -299,14 +331,24 @@ class Peppol extends AbstractService
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getInvoice
|
||||
*
|
||||
* @return InvoiceNinja\EInvoice\Models\Peppol\Invoice
|
||||
*/
|
||||
public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice
|
||||
{
|
||||
//@todo - need to process this and remove null values
|
||||
return $this->p_invoice;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* toXml
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toXml(): string
|
||||
{
|
||||
$e = new EInvoice();
|
||||
@ -321,7 +363,12 @@ class Peppol extends AbstractService
|
||||
return str_ireplace(['\n','<?xml version="1.0"?>'], ['', $prefix], $xml);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* toJson
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
$e = new EInvoice();
|
||||
@ -330,36 +377,32 @@ class Peppol extends AbstractService
|
||||
return $json;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* toObject
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function toObject(): mixed
|
||||
{
|
||||
return json_decode($this->toJson());
|
||||
}
|
||||
|
||||
/**
|
||||
* toArray
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return json_decode($this->toJson(), true);
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->p_invoice->ID = $this->invoice->number;
|
||||
$this->p_invoice->IssueDate = new \DateTime($this->invoice->date);
|
||||
|
||||
if($this->invoice->due_date) {
|
||||
$this->p_invoice->DueDate = new \DateTime($this->invoice->due_date);
|
||||
}
|
||||
|
||||
$this->p_invoice->InvoiceTypeCode = 380; //
|
||||
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
|
||||
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
|
||||
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
|
||||
|
||||
// $this->p_invoice->TaxTotal = $this->getTotalTaxes(); it only wants the aggregate here!!
|
||||
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
|
||||
|
||||
$this->senderSpecificLevelMutators()
|
||||
->receiverSpecificLevelMutators();
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getLegalMonetaryTotal
|
||||
*
|
||||
* @return LegalMonetaryTotal
|
||||
*/
|
||||
private function getLegalMonetaryTotal(): LegalMonetaryTotal
|
||||
{
|
||||
$taxable = $this->getTaxable();
|
||||
@ -388,7 +431,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $lmt;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getTotalTaxAmount
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function getTotalTaxAmount(): float
|
||||
{
|
||||
if(!$this->invoice->total_taxes) {
|
||||
@ -399,7 +447,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this->calcAmountLineTax($this->invoice->tax_rate1, $this->invoice->amount) ?? 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getTotalTaxes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getTotalTaxes(): array
|
||||
{
|
||||
$taxes = [];
|
||||
@ -421,10 +474,14 @@ class Peppol extends AbstractService
|
||||
$tax_subtotal->TaxableAmount = $taxable_amount;
|
||||
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
|
||||
$id = new ID();
|
||||
$id->value = $type_id == '2' ? 'HUR' : 'C62';
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = $this->invoice->tax_rate1;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = strlen($this->invoice->tax_name1 ?? '') > 1 ? $this->invoice->tax_name1 : '0';
|
||||
$ts = new TaxScheme();
|
||||
$id = new ID();
|
||||
$id->value = strlen($this->invoice->tax_name1 ?? '') > 1 ? $this->invoice->tax_name1 : '0';
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
@ -453,10 +510,14 @@ class Peppol extends AbstractService
|
||||
|
||||
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
|
||||
$id = new ID();
|
||||
$id->value = $type_id == '2' ? 'HUR' : 'C62';
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = $this->invoice->tax_rate2;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = $this->invoice->tax_name2;
|
||||
$ts = new TaxScheme();
|
||||
$id = new ID();
|
||||
$id->value = $this->invoice->tax_name2;
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
@ -483,16 +544,21 @@ class Peppol extends AbstractService
|
||||
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount - $this->invoice->total_taxes : $this->invoice->amount;
|
||||
$tax_subtotal->TaxableAmount = $taxable_amount;
|
||||
|
||||
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $type_id == '2' ? 'HUR' : 'C62';
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = $this->invoice->tax_rate3;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = $this->invoice->tax_name3;
|
||||
$ts = new TaxScheme();
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $this->invoice->tax_name3;
|
||||
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
|
||||
$tax_total = new TaxTotal();
|
||||
$tax_total->TaxAmount = $tax_amount;
|
||||
$tax_total->TaxSubtotal[] = $tax_subtotal;
|
||||
@ -501,7 +567,6 @@ class Peppol extends AbstractService
|
||||
|
||||
}
|
||||
|
||||
|
||||
return $taxes;
|
||||
}
|
||||
|
||||
@ -516,7 +581,10 @@ class Peppol extends AbstractService
|
||||
$_item->Description = $item->notes;
|
||||
|
||||
$line = new InvoiceLine();
|
||||
$line->ID = $key + 1;
|
||||
|
||||
$id = new ID();
|
||||
$id->value = (string) ($key+1);
|
||||
$line->ID = $id;
|
||||
$line->InvoicedQuantity = $item->quantity;
|
||||
|
||||
$lea = new LineExtensionAmount();
|
||||
@ -538,7 +606,7 @@ class Peppol extends AbstractService
|
||||
$price = new Price();
|
||||
$pa = new PriceAmount();
|
||||
$pa->currencyID = $this->invoice->client->currency()->code;
|
||||
$pa->amount = $this->costWithDiscount($item) - ($this->invoice->uses_inclusive_taxes ? ($this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) / $item->quantity) : 0);
|
||||
$pa->amount = (string) ($this->costWithDiscount($item) - ($this->invoice->uses_inclusive_taxes ? ($this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) / $item->quantity) : 0));
|
||||
$price->PriceAmount = $pa;
|
||||
|
||||
$line->Price = $price;
|
||||
@ -548,8 +616,14 @@ class Peppol extends AbstractService
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
private function costWithDiscount($item)
|
||||
|
||||
/**
|
||||
* costWithDiscount
|
||||
*
|
||||
* @param mixed $item
|
||||
* @return float
|
||||
*/
|
||||
private function costWithDiscount($item): float
|
||||
{
|
||||
$cost = $item->cost;
|
||||
|
||||
@ -563,7 +637,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $cost;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* zeroTaxAmount
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function zeroTaxAmount(): array
|
||||
{
|
||||
$blank_tax = [];
|
||||
@ -579,10 +658,15 @@ class Peppol extends AbstractService
|
||||
$taxable_amount->amount = '0';
|
||||
$tax_subtotal->TaxableAmount = $taxable_amount;
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = 'Z';
|
||||
$tc->Percent = 0;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = '0';
|
||||
$id = new ID();
|
||||
$id->value = 'Z';
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = '0';
|
||||
$ts = new TaxScheme();
|
||||
|
||||
$id = new ID();
|
||||
$id->value = '0';
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
@ -594,7 +678,13 @@ class Peppol extends AbstractService
|
||||
|
||||
return $blank_tax;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getItemTaxes
|
||||
*
|
||||
* @param object $item
|
||||
* @return array
|
||||
*/
|
||||
private function getItemTaxes(object $item): array
|
||||
{
|
||||
$item_taxes = [];
|
||||
@ -612,10 +702,18 @@ class Peppol extends AbstractService
|
||||
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $item->line_total - $tax_amount->amount : $item->line_total;
|
||||
$tax_subtotal->TaxableAmount = $taxable_amount;
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = $item->type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $item->type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = $item->tax_rate1;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = $item->tax_name1;
|
||||
$ts = new TaxScheme();
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $item->tax_name1;
|
||||
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
@ -645,10 +743,18 @@ class Peppol extends AbstractService
|
||||
|
||||
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = $item->type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $item->type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = $item->tax_rate2;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = $item->tax_name2;
|
||||
$ts = new TaxScheme();
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $item->tax_name2;
|
||||
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
@ -658,7 +764,6 @@ class Peppol extends AbstractService
|
||||
$tax_total->TaxSubtotal[] = $tax_subtotal;
|
||||
$item_taxes[] = $tax_total;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -679,10 +784,18 @@ class Peppol extends AbstractService
|
||||
|
||||
|
||||
$tc = new TaxCategory();
|
||||
$tc->ID = $item->type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $item->type_id == '2' ? 'HUR' : 'C62';
|
||||
|
||||
$tc->ID = $id;
|
||||
$tc->Percent = $item->tax_rate3;
|
||||
$ts = new PeppolTaxScheme();
|
||||
$ts->ID = $item->tax_name3;
|
||||
$ts = new TaxScheme();
|
||||
|
||||
$id = new ID();
|
||||
$id->value = $item->tax_name3;
|
||||
|
||||
$ts->ID = $id;
|
||||
$tc->TaxScheme = $ts;
|
||||
$tax_subtotal->TaxCategory = $tc;
|
||||
|
||||
@ -696,7 +809,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $item_taxes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getAccountingSupplierParty
|
||||
*
|
||||
* @return AccountingSupplierParty
|
||||
*/
|
||||
private function getAccountingSupplierParty(): AccountingSupplierParty
|
||||
{
|
||||
|
||||
@ -733,34 +851,46 @@ class Peppol extends AbstractService
|
||||
|
||||
return $asp;
|
||||
}
|
||||
|
||||
private function resolveTaxScheme(): mixed
|
||||
|
||||
/**
|
||||
* resolveTaxScheme
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function resolveTaxScheme(): string
|
||||
{
|
||||
$rules = isset($this->routing_rules[$this->invoice->client->country->iso_3166_2]) ? $this->routing_rules[$this->invoice->client->country->iso_3166_2] : [false, false, false, false,];
|
||||
return (new StorecoveRouter())->resolveTaxScheme($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification);
|
||||
|
||||
$code = false;
|
||||
// $rules = isset($this->routing_rules[$this->invoice->client->country->iso_3166_2]) ? $this->routing_rules[$this->invoice->client->country->iso_3166_2] : [false, false, false, false,];
|
||||
|
||||
match($this->invoice->client->classification) {
|
||||
"business" => $code = "B",
|
||||
"government" => $code = "G",
|
||||
"individual" => $code = "C",
|
||||
default => $code = false,
|
||||
};
|
||||
// $code = false;
|
||||
|
||||
//single array
|
||||
if(is_array($rules) && !is_array($rules[0])) {
|
||||
return $rules[2];
|
||||
}
|
||||
// match($this->invoice->client->classification) {
|
||||
// "business" => $code = "B",
|
||||
// "government" => $code = "G",
|
||||
// "individual" => $code = "C",
|
||||
// default => $code = false,
|
||||
// };
|
||||
|
||||
foreach($rules as $rule) {
|
||||
if(stripos($rule[0], $code) !== false) {
|
||||
return $rule[2];
|
||||
}
|
||||
}
|
||||
// //single array
|
||||
// if(is_array($rules) && !is_array($rules[0])) {
|
||||
// return $rules[2];
|
||||
// }
|
||||
|
||||
return false;
|
||||
// foreach($rules as $rule) {
|
||||
// if(stripos($rule[0], $code) !== false) {
|
||||
// return $rule[2];
|
||||
// }
|
||||
// }
|
||||
|
||||
// return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getAccountingCustomerParty
|
||||
*
|
||||
* @return AccountingCustomerParty
|
||||
*/
|
||||
private function getAccountingCustomerParty(): AccountingCustomerParty
|
||||
{
|
||||
|
||||
@ -819,7 +949,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $acp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getTaxable
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function getTaxable(): float
|
||||
{
|
||||
$total = 0;
|
||||
@ -867,25 +1002,32 @@ class Peppol extends AbstractService
|
||||
}
|
||||
|
||||
///////////////// Helper Methods /////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* getClientRoutingCode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getClientRoutingCode(): string
|
||||
{
|
||||
$receiver_identifiers = $this->routing_rules[$this->invoice->client->country->iso_3166_2];
|
||||
$client_classification = $this->invoice->client->classification == 'government' ? 'G' : 'B';
|
||||
// $receiver_identifiers = $this->routing_rules[$this->invoice->client->country->iso_3166_2];
|
||||
// $client_classification = $this->invoice->client->classification == 'government' ? 'G' : 'B';
|
||||
|
||||
if(count($receiver_identifiers) > 1) {
|
||||
// if(count($receiver_identifiers) > 1) {
|
||||
|
||||
foreach($receiver_identifiers as $ident) {
|
||||
if(str_contains($ident[0], $client_classification)) {
|
||||
return $ident[3];
|
||||
}
|
||||
}
|
||||
// foreach($receiver_identifiers as $ident) {
|
||||
// if(str_contains($ident[0], $client_classification)) {
|
||||
// return $ident[3];
|
||||
// }
|
||||
// }
|
||||
|
||||
} elseif(count($receiver_identifiers) == 1) {
|
||||
return $receiver_identifiers[3];
|
||||
}
|
||||
// } elseif(count($receiver_identifiers) == 1) {
|
||||
// return $receiver_identifiers[3];
|
||||
// }
|
||||
|
||||
return (new StorecoveRouter())->resolveRouting($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification);
|
||||
|
||||
throw new \Exception("e-invoice generation halted:: Could not resolve the Tax Code for this client? {$this->invoice->client->hashed_id}");
|
||||
// throw new \Exception("e-invoice generation halted:: Could not resolve the Tax Code for this client? {$this->invoice->client->hashed_id}");
|
||||
|
||||
}
|
||||
|
||||
@ -946,16 +1088,29 @@ class Peppol extends AbstractService
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getClientSetting
|
||||
*
|
||||
* @param string $property_path
|
||||
* @return mixed
|
||||
*/
|
||||
private function getClientSetting(string $property_path): mixed
|
||||
{
|
||||
return PropertyResolver::resolve($this->_client_settings, $property_path);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getCompanySetting
|
||||
*
|
||||
* @param string $property_path
|
||||
* @return mixed
|
||||
*/
|
||||
private function getCompanySetting(string $property_path): mixed
|
||||
{
|
||||
return PropertyResolver::resolve($this->_company_settings, $property_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* senderSpecificLevelMutators
|
||||
*
|
||||
@ -1111,7 +1266,6 @@ class Peppol extends AbstractService
|
||||
*/
|
||||
private function buildRouting(array $identifiers): array
|
||||
{
|
||||
|
||||
return
|
||||
[
|
||||
"routing" => [
|
||||
@ -1121,7 +1275,13 @@ class Peppol extends AbstractService
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* setEmailRouting
|
||||
*
|
||||
* @param string $email
|
||||
* @return self
|
||||
*/
|
||||
private function setEmailRouting(string $email): self
|
||||
{
|
||||
nlog($email);
|
||||
@ -1156,7 +1316,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* getStorecoveMeta
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getStorecoveMeta(): array
|
||||
{
|
||||
return $this->storecove_meta;
|
||||
@ -1165,9 +1330,6 @@ class Peppol extends AbstractService
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////// Country level mutators /////////////////////////////////////
|
||||
|
||||
/**
|
||||
@ -1281,7 +1443,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* FI
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function FI(): self
|
||||
{
|
||||
|
||||
@ -1344,7 +1511,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* IT
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function IT(): self
|
||||
{
|
||||
|
||||
@ -1390,7 +1562,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* client_IT
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function client_IT(): self
|
||||
{
|
||||
|
||||
@ -1407,13 +1584,23 @@ class Peppol extends AbstractService
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* MY
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function MY(): self
|
||||
{
|
||||
//way too much to digest here, delayed.
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* NL
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function NL(): self
|
||||
{
|
||||
|
||||
@ -1423,13 +1610,23 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* NZ
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function NZ(): self
|
||||
{
|
||||
// New Zealand uses a GLN to identify businesses. In addition, when sending invoices to a New Zealand customer, make sure you include the pseudo identifier NZ:GST as their tax identifier.
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* PL
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function PL(): self
|
||||
{
|
||||
|
||||
@ -1455,7 +1652,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RO
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function RO(): self
|
||||
{
|
||||
// Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows:
|
||||
@ -1489,7 +1691,12 @@ class Peppol extends AbstractService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* SG
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function SG(): self
|
||||
{
|
||||
//delayed - stage 2
|
||||
|
@ -646,7 +646,7 @@ class Email implements ShouldQueue
|
||||
|
||||
$user = $this->resolveSendingUser();
|
||||
|
||||
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
|
||||
$sending_email = (isset($this->email_object->settings->custom_sending_email) && (stripos($this->email_object->settings->custom_sending_email, "@")) !== false) ? $this->email_object->settings->custom_sending_email : $user->email;
|
||||
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
|
||||
|
||||
$this->mailable
|
||||
|
@ -23,6 +23,8 @@ use App\Models\PaymentHash;
|
||||
use App\Models\PaymentType;
|
||||
use Illuminate\Support\Str;
|
||||
use App\DataMapper\InvoiceItem;
|
||||
use App\Events\Invoice\InvoiceAutoBillFailed;
|
||||
use App\Events\Invoice\InvoiceAutoBillSuccess;
|
||||
use App\Factory\PaymentFactory;
|
||||
use App\Services\AbstractService;
|
||||
use App\Models\ClientGatewayToken;
|
||||
@ -157,6 +159,8 @@ class AutoBillInvoice extends AbstractService
|
||||
} catch (\Exception $e) {
|
||||
|
||||
nlog('payment NOT captured for '.$this->invoice->number.' with error '.$e->getMessage());
|
||||
event(new InvoiceAutoBillFailed($this->invoice, $this->invoice->company, Ninja::eventVars(), $e->getMessage()));
|
||||
|
||||
}
|
||||
|
||||
$this->invoice->auto_bill_tries += 1;
|
||||
@ -170,6 +174,7 @@ class AutoBillInvoice extends AbstractService
|
||||
|
||||
if ($payment) {
|
||||
info('Auto Bill payment captured for '.$this->invoice->number);
|
||||
event(new InvoiceAutoBillSuccess($this->invoice, $this->invoice->company, Ninja::eventVars()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ class QuickbooksSync implements ShouldQueue
|
||||
{
|
||||
MultiDB::setDb($this->db);
|
||||
|
||||
$this->company = Company::find($this->company_id);
|
||||
$this->company = Company::query()->find($this->company_id);
|
||||
$this->qbs = new QuickbooksService($this->company);
|
||||
$this->settings = $this->company->quickbooks->settings;
|
||||
|
||||
|
@ -88,7 +88,7 @@ class QuickbooksService
|
||||
*/
|
||||
public function syncFromQb()
|
||||
{
|
||||
QuickbooksSync::dispatch($this->company);
|
||||
QuickbooksSync::dispatch($this->company->id, $this->company->db);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -97,8 +97,8 @@ class SdkWrapper
|
||||
8726400
|
||||
);
|
||||
|
||||
$token->setAccessTokenExpiresAt($token_object->accessTokenExpiresAt);
|
||||
$token->setRefreshTokenExpiresAt($token_object->refreshTokenExpiresAt);
|
||||
$token->setAccessTokenExpiresAt($token_object->accessTokenExpiresAt); //@phpstan-ignore-line
|
||||
$token->setRefreshTokenExpiresAt($token_object->refreshTokenExpiresAt); //@phpstan-ignore-line
|
||||
$token->setAccessTokenValidationPeriodInSeconds(3600);
|
||||
$token->setRefreshTokenValidationPeriodInSeconds(8726400);
|
||||
|
||||
|
@ -39,6 +39,7 @@ class ClientBalanceReport extends BaseExport
|
||||
'invoices',
|
||||
'invoice_balance',
|
||||
'credit_balance',
|
||||
'payment_balance',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -119,6 +120,7 @@ class ClientBalanceReport extends BaseExport
|
||||
$query->count(),
|
||||
$query->sum('balance'),
|
||||
$client->credit_balance,
|
||||
$client->payment_balance,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -221,6 +221,7 @@ class TemplateService
|
||||
$this->entity = $this->company->invoices()->first() ?? $this->company->quotes()->first();
|
||||
|
||||
$this->data = $tm->engines;
|
||||
|
||||
$this->variables = $tm->variables[0];
|
||||
$this->twig->addGlobal('currency_code', $this->company->currency()->code);
|
||||
$this->twig->addGlobal('show_credits', true);
|
||||
@ -979,6 +980,7 @@ class TemplateService
|
||||
return [
|
||||
'name' => $user->present()->name(),
|
||||
'email' => $user->email,
|
||||
'signature' => $user->signature ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -399,7 +399,9 @@ class HtmlEngine
|
||||
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')];
|
||||
$data['$invoice.taxes'] = &$data['$taxes'];
|
||||
|
||||
|
||||
$data['$user.name'] = ['value' => $this->entity->user->present()->name(), 'label' => ctrans('texts.name')];
|
||||
$data['$user.signature'] = ['value' => $this->entity->user->signature ?? '', 'label' => ctrans('texts.signature')];
|
||||
$data['$user.first_name'] = ['value' => $this->entity->user->first_name, 'label' => ctrans('texts.first_name')];
|
||||
$data['$user.last_name'] = ['value' => $this->entity->user->last_name, 'label' => ctrans('texts.last_name')];
|
||||
$data['$created_by_user'] = &$data['$user.name'];
|
||||
@ -731,6 +733,7 @@ class HtmlEngine
|
||||
$data['$payment.number'] = ['value' => '', 'label' => ctrans('texts.payment_number')];
|
||||
$data['$payment.transaction_reference'] = ['value' => '', 'label' => ctrans('texts.transaction_reference')];
|
||||
$data['$payment.refunded'] = ['value' => '', 'label' => ctrans('texts.refund')];
|
||||
$data['$gateway_payment_error'] = ['value' => '', 'label' => ctrans('texts.error')];
|
||||
|
||||
if ($this->entity_string == 'invoice' && $this->entity->net_payments()->exists()) {
|
||||
$payment_list = '<br><br>';
|
||||
|
@ -81,7 +81,7 @@
|
||||
"nelexa/zip": "^4.0",
|
||||
"nordigen/nordigen-php": "^1.1",
|
||||
"nwidart/laravel-modules": "^11.0",
|
||||
"phpoffice/phpspreadsheet": "^1.29",
|
||||
"phpoffice/phpspreadsheet": "^2.2",
|
||||
"pragmarx/google2fa": "^8.0",
|
||||
"predis/predis": "^2",
|
||||
"psr/http-message": "^1.0",
|
||||
|
255
composer.lock
generated
255
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9e7ea46cfef2848f4eac13cc9c0c679a",
|
||||
"content-hash": "ffb9ecf55b32b2e829fdfd750cf2416b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adrienrn/php-mimetyper",
|
||||
@ -535,16 +535,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.320.5",
|
||||
"version": "3.321.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "afda5aefd59da90208d2f59427ce81e91535b1f2"
|
||||
"reference": "f4ad64dffc2665dde6275e6dcc3f653f15c6e57f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/afda5aefd59da90208d2f59427ce81e91535b1f2",
|
||||
"reference": "afda5aefd59da90208d2f59427ce81e91535b1f2",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f4ad64dffc2665dde6275e6dcc3f653f15c6e57f",
|
||||
"reference": "f4ad64dffc2665dde6275e6dcc3f653f15c6e57f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -627,9 +627,9 @@
|
||||
"support": {
|
||||
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.320.5"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.321.1"
|
||||
},
|
||||
"time": "2024-08-21T18:14:31+00:00"
|
||||
"time": "2024-08-29T19:10:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@ -975,16 +975,16 @@
|
||||
},
|
||||
{
|
||||
"name": "checkout/checkout-sdk-php",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/checkout/checkout-sdk-php.git",
|
||||
"reference": "ac757648271894e3c30b7bc58ff08ba1b5b84de8"
|
||||
"reference": "cec8d6a3e0959d89f739041ea3ea605d86af634e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/ac757648271894e3c30b7bc58ff08ba1b5b84de8",
|
||||
"reference": "ac757648271894e3c30b7bc58ff08ba1b5b84de8",
|
||||
"url": "https://api.github.com/repos/checkout/checkout-sdk-php/zipball/cec8d6a3e0959d89f739041ea3ea605d86af634e",
|
||||
"reference": "cec8d6a3e0959d89f739041ea3ea605d86af634e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1037,9 +1037,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/checkout/checkout-sdk-php/issues",
|
||||
"source": "https://github.com/checkout/checkout-sdk-php/tree/3.2.2"
|
||||
"source": "https://github.com/checkout/checkout-sdk-php/tree/3.2.4"
|
||||
},
|
||||
"time": "2024-08-02T08:07:53+00:00"
|
||||
"time": "2024-08-29T07:34:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
@ -2015,67 +2015,6 @@
|
||||
},
|
||||
"time": "2022-08-04T05:24:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ezyang/htmlpurifier",
|
||||
"version": "v4.17.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ezyang/htmlpurifier.git",
|
||||
"reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c",
|
||||
"reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"cerdic/css-tidy": "^1.7 || ^2.0",
|
||||
"simpletest/simpletest": "dev-master"
|
||||
},
|
||||
"suggest": {
|
||||
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
|
||||
"ext-bcmath": "Used for unit conversion and imagecrash protection",
|
||||
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
|
||||
"ext-tidy": "Used for pretty-printing HTML"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"library/HTMLPurifier.composer.php"
|
||||
],
|
||||
"psr-0": {
|
||||
"HTMLPurifier": "library/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/library/HTMLPurifier/Language/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Edward Z. Yang",
|
||||
"email": "admin@htmlpurifier.org",
|
||||
"homepage": "http://ezyang.com"
|
||||
}
|
||||
],
|
||||
"description": "Standards compliant HTML filter written in PHP",
|
||||
"homepage": "http://htmlpurifier.org/",
|
||||
"keywords": [
|
||||
"html"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/ezyang/htmlpurifier/issues",
|
||||
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0"
|
||||
},
|
||||
"time": "2023-11-17T15:01:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fakerphp/faker",
|
||||
"version": "v1.23.1",
|
||||
@ -2522,16 +2461,16 @@
|
||||
},
|
||||
{
|
||||
"name": "google/apiclient-services",
|
||||
"version": "v0.369.0",
|
||||
"version": "v0.370.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-api-php-client-services.git",
|
||||
"reference": "002f610e4c3acf0636b4fb1f46314a2097e1c8b4"
|
||||
"reference": "25ad8515701dd832313d0f5f0a828670d60e541a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/002f610e4c3acf0636b4fb1f46314a2097e1c8b4",
|
||||
"reference": "002f610e4c3acf0636b4fb1f46314a2097e1c8b4",
|
||||
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/25ad8515701dd832313d0f5f0a828670d60e541a",
|
||||
"reference": "25ad8515701dd832313d0f5f0a828670d60e541a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2560,22 +2499,22 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
|
||||
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.369.0"
|
||||
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.370.0"
|
||||
},
|
||||
"time": "2024-08-14T20:31:16+00:00"
|
||||
"time": "2024-08-26T01:04:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/auth",
|
||||
"version": "v1.41.0",
|
||||
"version": "v1.42.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-auth-library-php.git",
|
||||
"reference": "1043ea18fe7f5dfbf5b208ce3ee6d6b6ab8cb038"
|
||||
"reference": "0c25599a91530b5847f129b271c536f75a7563f5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/1043ea18fe7f5dfbf5b208ce3ee6d6b6ab8cb038",
|
||||
"reference": "1043ea18fe7f5dfbf5b208ce3ee6d6b6ab8cb038",
|
||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/0c25599a91530b5847f129b271c536f75a7563f5",
|
||||
"reference": "0c25599a91530b5847f129b271c536f75a7563f5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2620,9 +2559,9 @@
|
||||
"support": {
|
||||
"docs": "https://googleapis.github.io/google-auth-library-php/main/",
|
||||
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
|
||||
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.41.0"
|
||||
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.42.0"
|
||||
},
|
||||
"time": "2024-07-10T15:21:07+00:00"
|
||||
"time": "2024-08-26T18:33:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
@ -3999,12 +3938,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/invoiceninja/einvoice.git",
|
||||
"reference": "d4f80316744bbd31245900ec9799a6f66a663ed6"
|
||||
"reference": "1ec178ec134981629932aae12677e947ee3df091"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/d4f80316744bbd31245900ec9799a6f66a663ed6",
|
||||
"reference": "d4f80316744bbd31245900ec9799a6f66a663ed6",
|
||||
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/1ec178ec134981629932aae12677e947ee3df091",
|
||||
"reference": "1ec178ec134981629932aae12677e947ee3df091",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -4046,7 +3985,7 @@
|
||||
"source": "https://github.com/invoiceninja/einvoice/tree/main",
|
||||
"issues": "https://github.com/invoiceninja/einvoice/issues"
|
||||
},
|
||||
"time": "2024-07-22T02:40:27+00:00"
|
||||
"time": "2024-08-28T07:20:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "invoiceninja/inspector",
|
||||
@ -8102,16 +8041,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.29.0",
|
||||
"version": "2.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
|
||||
"reference": "ffbcee68069b073bff07a71eb321dcd9f2763513"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
|
||||
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/ffbcee68069b073bff07a71eb321dcd9f2763513",
|
||||
"reference": "ffbcee68069b073bff07a71eb321dcd9f2763513",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -8128,25 +8067,24 @@
|
||||
"ext-xmlwriter": "*",
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"ezyang/htmlpurifier": "^4.15",
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"php": "^8.1",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
"dompdf/dompdf": "^1.0 || ^2.0",
|
||||
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"mitoteam/jpgraph": "^10.3",
|
||||
"mpdf/mpdf": "^8.1.1",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.1",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
|
||||
"phpunit/phpunit": "^9.6 || ^10.5",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
},
|
||||
@ -8201,9 +8139,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
|
||||
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.2.2"
|
||||
},
|
||||
"time": "2023-06-14T22:48:31+00:00"
|
||||
"time": "2024-08-08T02:31:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
@ -8392,16 +8330,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "1.29.1",
|
||||
"version": "1.30.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4"
|
||||
"reference": "5ceb0e384997db59f38774bf79c2a6134252c08f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4",
|
||||
"reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f",
|
||||
"reference": "5ceb0e384997db59f38774bf79c2a6134252c08f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -8433,9 +8371,9 @@
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1"
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0"
|
||||
},
|
||||
"time": "2024-05-31T08:52:43+00:00"
|
||||
"time": "2024-08-29T09:54:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pragmarx/google2fa",
|
||||
@ -14284,20 +14222,20 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/intl-extra",
|
||||
"version": "v3.11.0",
|
||||
"version": "v3.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/intl-extra.git",
|
||||
"reference": "e9cadd61342e71e45b2f4f0558122433fd7e4566"
|
||||
"reference": "61e1189333120a475d2b67b93664b8002668fc27"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/e9cadd61342e71e45b2f4f0558122433fd7e4566",
|
||||
"reference": "e9cadd61342e71e45b2f4f0558122433fd7e4566",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/61e1189333120a475d2b67b93664b8002668fc27",
|
||||
"reference": "61e1189333120a475d2b67b93664b8002668fc27",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"php": ">=8.0.2",
|
||||
"symfony/intl": "^5.4|^6.4|^7.0",
|
||||
"twig/twig": "^3.10"
|
||||
},
|
||||
@ -14332,7 +14270,7 @@
|
||||
"twig"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/intl-extra/tree/v3.11.0"
|
||||
"source": "https://github.com/twigphp/intl-extra/tree/v3.12.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -14344,28 +14282,27 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-21T06:25:01+00:00"
|
||||
"time": "2024-08-10T10:32:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.11.0",
|
||||
"version": "v3.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "e80fb8ebba85c7341a97a9ebf825d7fd4b77708d"
|
||||
"reference": "4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/e80fb8ebba85c7341a97a9ebf825d7fd4b77708d",
|
||||
"reference": "e80fb8ebba85c7341a97a9ebf825d7fd4b77708d",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea",
|
||||
"reference": "4d19472d4ac1838e0b1f0e029ce1fa4040eb34ea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"php": ">=8.0.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/polyfill-ctype": "^1.8",
|
||||
"symfony/polyfill-mbstring": "^1.3",
|
||||
"symfony/polyfill-php80": "^1.22",
|
||||
"symfony/polyfill-php81": "^1.29"
|
||||
},
|
||||
"require-dev": {
|
||||
@ -14412,7 +14349,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.11.0"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.12.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -14424,7 +14361,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-08T16:15:16+00:00"
|
||||
"time": "2024-08-29T09:51:12+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twilio/sdk",
|
||||
@ -15254,16 +15191,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/pcre.git",
|
||||
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81"
|
||||
"reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/1637e067347a0c40bbb1e3cd786b20dcab556a81",
|
||||
"reference": "1637e067347a0c40bbb1e3cd786b20dcab556a81",
|
||||
"url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4",
|
||||
"reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -15313,7 +15250,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/pcre/issues",
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.0"
|
||||
"source": "https://github.com/composer/pcre/tree/3.3.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -15329,7 +15266,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-19T19:43:53+00:00"
|
||||
"time": "2024-08-27T18:44:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
@ -15659,16 +15596,16 @@
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
"version": "v3.62.0",
|
||||
"version": "v3.63.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||
"reference": "627692f794d35c43483f34b01d94740df2a73507"
|
||||
"reference": "9d427f3f14984403a6ae9fc726b61765ca0c005e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/627692f794d35c43483f34b01d94740df2a73507",
|
||||
"reference": "627692f794d35c43483f34b01d94740df2a73507",
|
||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9d427f3f14984403a6ae9fc726b61765ca0c005e",
|
||||
"reference": "9d427f3f14984403a6ae9fc726b61765ca0c005e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -15750,7 +15687,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.62.0"
|
||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.63.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -15758,7 +15695,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-07T17:03:09+00:00"
|
||||
"time": "2024-08-28T10:47:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "hamcrest/hamcrest-php",
|
||||
@ -16340,16 +16277,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpmyadmin/sql-parser",
|
||||
"version": "5.9.1",
|
||||
"version": "5.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpmyadmin/sql-parser.git",
|
||||
"reference": "169a9f11f1957ea36607c9b29eac1b48679f1ecc"
|
||||
"reference": "91d980ab76c3f152481e367f62b921adc38af451"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/169a9f11f1957ea36607c9b29eac1b48679f1ecc",
|
||||
"reference": "169a9f11f1957ea36607c9b29eac1b48679f1ecc",
|
||||
"url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/91d980ab76c3f152481e367f62b921adc38af451",
|
||||
"reference": "91d980ab76c3f152481e367f62b921adc38af451",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -16423,20 +16360,20 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-13T19:01:01+00:00"
|
||||
"time": "2024-08-29T20:56:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.11.11",
|
||||
"version": "1.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3"
|
||||
"reference": "384af967d35b2162f69526c7276acadce534d0e1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/707c2aed5d8d0075666e673a5e71440c1d01a5a3",
|
||||
"reference": "707c2aed5d8d0075666e673a5e71440c1d01a5a3",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/384af967d35b2162f69526c7276acadce534d0e1",
|
||||
"reference": "384af967d35b2162f69526c7276acadce534d0e1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -16481,36 +16418,36 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-08-19T14:37:29+00:00"
|
||||
"time": "2024-08-27T09:18:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "10.1.15",
|
||||
"version": "10.1.16",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae"
|
||||
"reference": "7e308268858ed6baedc8704a304727d20bc07c77"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae",
|
||||
"reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
|
||||
"reference": "7e308268858ed6baedc8704a304727d20bc07c77",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"nikic/php-parser": "^4.18 || ^5.0",
|
||||
"nikic/php-parser": "^4.19.1 || ^5.1.0",
|
||||
"php": ">=8.1",
|
||||
"phpunit/php-file-iterator": "^4.0",
|
||||
"phpunit/php-text-template": "^3.0",
|
||||
"sebastian/code-unit-reverse-lookup": "^3.0",
|
||||
"sebastian/complexity": "^3.0",
|
||||
"sebastian/environment": "^6.0",
|
||||
"sebastian/lines-of-code": "^2.0",
|
||||
"sebastian/version": "^4.0",
|
||||
"theseer/tokenizer": "^1.2.0"
|
||||
"phpunit/php-file-iterator": "^4.1.0",
|
||||
"phpunit/php-text-template": "^3.0.1",
|
||||
"sebastian/code-unit-reverse-lookup": "^3.0.0",
|
||||
"sebastian/complexity": "^3.2.0",
|
||||
"sebastian/environment": "^6.1.0",
|
||||
"sebastian/lines-of-code": "^2.0.2",
|
||||
"sebastian/version": "^4.0.1",
|
||||
"theseer/tokenizer": "^1.2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.1"
|
||||
@ -16522,7 +16459,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "10.1-dev"
|
||||
"dev-main": "10.1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@ -16551,7 +16488,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -16559,7 +16496,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-29T08:25:15+00:00"
|
||||
"time": "2024-08-22T04:31:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
|
@ -38,7 +38,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'layout' => 'layouts.app',
|
||||
'layout' => 'components.layouts.app',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
@ -74,6 +74,7 @@ return [
|
||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||
],
|
||||
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
||||
],
|
||||
|
||||
/*
|
||||
@ -100,7 +101,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'legacy_model_binding' => true,
|
||||
'legacy_model_binding' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
|
@ -46,6 +46,7 @@ return [
|
||||
'preconfigured_install' => env('PRECONFIGURED_INSTALL', false),
|
||||
'update_secret' => env('UPDATE_SECRET', ''),
|
||||
'license_key' => env('LICENSE_KEY', false),
|
||||
'hosted_ninja_url' => env('HOSTED_NINJA_URL', 'https://invoicing.co'),
|
||||
// Settings used by invoiceninja.com
|
||||
'disks' => [
|
||||
'backup' => env('BACKUP_DISK', 's3'),
|
||||
|
@ -14,6 +14,17 @@ return new class extends Migration
|
||||
Schema::table('products', function (Blueprint $table){
|
||||
$table->string('hash')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('companies', function (Blueprint $table){
|
||||
$table->bigInteger('legal_entity_id')->nullable();
|
||||
});
|
||||
|
||||
|
||||
if($currency = \App\Models\Currency::find(39))
|
||||
{
|
||||
$currency->symbol = 'лв';
|
||||
$currency->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,7 +61,7 @@ class CurrenciesSeeder extends Seeder
|
||||
['id' => 36, 'name' => 'Trinidad and Tobago Dollar', 'code' => 'TTD', 'symbol' => 'TT$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||
['id' => 37, 'name' => 'East Caribbean Dollar', 'code' => 'XCD', 'symbol' => 'EC$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||
['id' => 38, 'name' => 'Ghanaian Cedi', 'code' => 'GHS', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||
['id' => 39, 'name' => 'Bulgarian Lev', 'code' => 'BGN', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ' ', 'decimal_separator' => '.'],
|
||||
['id' => 39, 'name' => 'Bulgarian Lev', 'code' => 'BGN', 'symbol' => 'лв', 'precision' => '2', 'thousand_separator' => ' ', 'decimal_separator' => '.'],
|
||||
['id' => 40, 'name' => 'Aruban Florin', 'code' => 'AWG', 'symbol' => 'Afl. ', 'precision' => '2', 'thousand_separator' => ' ', 'decimal_separator' => '.'],
|
||||
['id' => 41, 'name' => 'Turkish Lira', 'code' => 'TRY', 'symbol' => 'TL ', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
|
||||
['id' => 42, 'name' => 'Romanian New Leu', 'code' => 'RON', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||
|
@ -5321,6 +5321,11 @@ $lang = array(
|
||||
'applies_to' => 'Applies To',
|
||||
'accept_purchase_order' => 'Accept Purchase Order',
|
||||
'round_to_seconds' => 'Round To Seconds',
|
||||
'activity_142' => 'Quote :number reminder 1 sent',
|
||||
'activity_143' => 'Auto Bill succeeded for invoice :invoice',
|
||||
'activity_144' => 'Auto Bill failed for invoice :invoice. :notes',
|
||||
'activity_145' => 'EInvoice :invoice for :client was e-delivered. :notes',
|
||||
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -4,7 +4,6 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "invoiceninja",
|
||||
"dependencies": {
|
||||
"@invoiceninja/simple-card": "^0.0.2",
|
||||
"axios": "^0.25",
|
||||
|
@ -37,4 +37,6 @@ parameters:
|
||||
- '#Expression on left side of ?? is not nullable.#'
|
||||
- '#Left side of && is always true.#'
|
||||
- '#Right side of && is always true.#'
|
||||
- '#is never read, only written.#'
|
||||
- '#is never written#'
|
||||
|
109
public/build/assets/app-234e3402.js
vendored
109
public/build/assets/app-234e3402.js
vendored
File diff suppressed because one or more lines are too long
109
public/build/assets/app-e0713224.js
vendored
Normal file
109
public/build/assets/app-e0713224.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-234e3402.js",
|
||||
"file": "assets/app-e0713224.js",
|
||||
"imports": [
|
||||
"_index-08e160a7.js",
|
||||
"__commonjsHelpers-725317a4.js"
|
||||
|
12
public/js/manifest.json
Normal file
12
public/js/manifest.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"Resources/assets/css/app.css": {
|
||||
"file": "assets/app-2d547327.css",
|
||||
"isEntry": true,
|
||||
"src": "Resources/assets/css/app.css"
|
||||
},
|
||||
"Resources/assets/js/admin.js": {
|
||||
"file": "assets/admin-4ed993c7.js",
|
||||
"isEntry": true,
|
||||
"src": "Resources/assets/js/admin.js"
|
||||
}
|
||||
}
|
10870
public/vendor/livewire/livewire.esm.js
vendored
Normal file
10870
public/vendor/livewire/livewire.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1450
public/vendor/livewire/livewire.js
vendored
1450
public/vendor/livewire/livewire.js
vendored
File diff suppressed because it is too large
Load Diff
12
public/vendor/livewire/livewire.min.js
vendored
12
public/vendor/livewire/livewire.min.js
vendored
File diff suppressed because one or more lines are too long
7
public/vendor/livewire/livewire.min.js.map
vendored
Normal file
7
public/vendor/livewire/livewire.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/vendor/livewire/manifest.json
vendored
3
public/vendor/livewire/manifest.json
vendored
@ -1 +1,2 @@
|
||||
{"/livewire.js":"/livewire.js?id=90730a3b0e7144480175"}
|
||||
|
||||
{"/livewire.js":"cc800bf4"}
|
||||
|
5
resources/views/einvoice/index.blade.php
Normal file
5
resources/views/einvoice/index.blade.php
Normal file
@ -0,0 +1,5 @@
|
||||
@extends('layouts.ninja')
|
||||
|
||||
@section('body')
|
||||
@livewire('e-invoice.portal')
|
||||
@stop
|
139
resources/views/livewire/e-invoice/portal.blade.php
Normal file
139
resources/views/livewire/e-invoice/portal.blade.php
Normal file
@ -0,0 +1,139 @@
|
||||
<div class="flex flex-col p-10">
|
||||
|
||||
@if (Auth::guard('user')->check())
|
||||
<div class="flex mx-auto gap-4">
|
||||
|
||||
<div class="w-7/8 mr-auto">
|
||||
<h2 class="text-2xl font-semibold text-gray-800">E-Invoice Beta Phase</h2>
|
||||
<p class="py-2">Hey there!</p>
|
||||
<p class="py-2">Thanks for joining us on our pilot program for e-invoicing for self hosted users. Our aim is to allow you to send your einvoices through the PEPPOL network via Invoice Ninja.</p>
|
||||
<p class="py-2">Our hosted servers will proxy your einvoices into the PEPPOL network for you, and also route einvoices back to you via Webhooks.</p>
|
||||
<h3 class="text-2xl font-semibold text-gray-800 py-4">Configuration:</h3>
|
||||
<p class="py-2">To start sending einvoices via the PEPPOL network, you are required to create a Legal Entity ID, this will be your network address in the PEPPOL network. The tabled data below is what will be used to register your legal entity, please confirm the details are correct prior to registering.</p>
|
||||
<p class="py-2">If you are in a region which requires routing directly to the government, such as Spain, Italy or Romania, you are required to have already registered with your government for the sending of einvoices.</p>
|
||||
<p class="py-2">In your .env file, add the variable LICENSE_KEY= with your self hosted white label license key - this is used for authentication with our servers, and to register the sending entity. You will also want to contact us to ensure we have configured your license for this beta test!
|
||||
<p class="py-2">For discussion, help and troubleshooting, please use the slack channel #einvoicing.</p>
|
||||
</div>
|
||||
|
||||
<div class="w-1/8 ml-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-800">Welcome, {{ Auth::guard('user')->user()->first_name }}!</h1>
|
||||
<div class="flex justify-between">
|
||||
<button wire:click="logout" class="w-full flex bg-blue-500 justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex-grow py-10 items-center justify-between">
|
||||
|
||||
@if (session()->has('error'))
|
||||
<div class="mt-4 text-red-600 text-sm font-semibold">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid lg:grid-cols-3 mx-6 md:mx-0 md:my-2 border border-gray-300 rounded-lg shadow-md bg-gray-100">
|
||||
|
||||
<div class="font-semibold p-2 bg-gray-200 border-b border-gray-300">Name</div>
|
||||
<div class="font-semibold p-2 bg-gray-200 border-b border-gray-300">Legal Entity Id</div>
|
||||
<div class="font-semibold p-2 bg-gray-200 border-b border-gray-300">Register</div>
|
||||
|
||||
@foreach($companies as $company)
|
||||
|
||||
<div class="w-full mx-6 md:mx-0 border-b border-gray-300">
|
||||
<dl class="grid grid-cols-2 gap-4 mb-4">
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.name') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['party_name'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.address1') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['line1'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.address2') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['line2'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.city') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['city'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.state') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['county'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.postal_code') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['zip'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.country') }}:</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['country'] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center p-1">
|
||||
<span class="font-semibold text-gray-700">{{ ctrans('texts.vat_number') }}</span>
|
||||
<span class="ml-2 text-gray-600">{{ $company['vat_number'] }}</span>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="p-2 border-b border-gray-300">
|
||||
{{ $company['legal_entity_id'] }}
|
||||
</div>
|
||||
|
||||
<div class="p-2 border-b border-gray-300">
|
||||
@if($company['legal_entity_id'])
|
||||
<p>Registered</p>
|
||||
@else
|
||||
<button class="bg-blue-500 justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" wire:click="register('{{ $company['key'] }}')" wire:loading.attr="disabled">Register</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endforeach
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="w-full flex items-center justify-center min-h-screen bg-gray-100">
|
||||
<div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-md sm:max-w-sm md:max-w-xs lg:max-w-md xl:max-w-lg">
|
||||
<h2 class="text-2xl font-bold text-center text-gray-800 mb-6">Login to Your Account</h2>
|
||||
|
||||
<form wire:submit.prevent="login" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input type="email" id="email" wire:model="email" class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input type="password" id="password" wire:model="password" class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="submit" class="w-full flex bg-blue-500 justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@if (session()->has('error'))
|
||||
<div class="mt-4 text-red-600 text-sm font-semibold">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
118
resources/views/vendor/livewire/bootstrap.blade.php
vendored
118
resources/views/vendor/livewire/bootstrap.blade.php
vendored
@ -12,49 +12,91 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
|
||||
<span class="page-link" aria-hidden="true">‹</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="prev" aria-label="@lang('pagination.previous')">‹</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Pagination Elements --}}
|
||||
@foreach ($elements as $element)
|
||||
{{-- "Three Dots" Separator --}}
|
||||
@if (is_string($element))
|
||||
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
|
||||
<nav class="d-flex justify-items-center justify-content-between">
|
||||
<div class="d-flex justify-content-between flex-fill d-sm-none">
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.previous')</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Array Of Links --}}
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<li class="page-item active" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}" aria-current="page"><span class="page-link">{{ $page }}</span></li>
|
||||
@else
|
||||
<li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}">{{ $page }}</button></li>
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link" aria-hidden="true">@lang('pagination.next')</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-none flex-sm-fill d-sm-flex align-items-sm-center justify-content-sm-between">
|
||||
<div>
|
||||
<p class="small text-muted">
|
||||
{!! __('Showing') !!}
|
||||
<span class="fw-semibold">{{ $paginator->firstItem() }}</span>
|
||||
{!! __('to') !!}
|
||||
<span class="fw-semibold">{{ $paginator->lastItem() }}</span>
|
||||
{!! __('of') !!}
|
||||
<span class="fw-semibold">{{ $paginator->total() }}</span>
|
||||
{!! __('results') !!}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
|
||||
<span class="page-link" aria-hidden="true">‹</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.previous')">‹</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Pagination Elements --}}
|
||||
@foreach ($elements as $element)
|
||||
{{-- "Three Dots" Separator --}}
|
||||
@if (is_string($element))
|
||||
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
|
||||
@endif
|
||||
|
||||
{{-- Array Of Links --}}
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<li class="page-item active" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}" aria-current="page"><span class="page-link">{{ $page }}</span></li>
|
||||
@else
|
||||
<li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}">{{ $page }}</button></li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="next" aria-label="@lang('pagination.next')">›</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
|
||||
<span class="page-link" aria-hidden="true">›</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.next')">›</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
|
||||
<span class="page-link" aria-hidden="true">›</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
|
@ -22,11 +22,11 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
@else
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<li class="page-item">
|
||||
<button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
|
||||
<button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="prev">@lang('pagination.previous')</button>
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
@ -35,11 +35,11 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
@if ($paginator->hasMorePages())
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<li class="page-item">
|
||||
<button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
|
||||
<button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" rel="next">@lang('pagination.next')</button>
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@endif
|
||||
@else
|
||||
|
@ -16,17 +16,17 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
<span>
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md select-none">
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.previous') !!}
|
||||
</span>
|
||||
@else
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@endif
|
||||
@ -37,16 +37,16 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@else
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md select-none">
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md">
|
||||
{!! __('pagination.next') !!}
|
||||
</span>
|
||||
@endif
|
||||
|
@ -16,11 +16,11 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
<div class="flex justify-between flex-1 sm:hidden">
|
||||
<span>
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md select-none">
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@endif
|
||||
@ -28,11 +28,11 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
|
||||
<span>
|
||||
@if ($paginator->hasMorePages())
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@else
|
||||
<span class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md select-none">
|
||||
<span class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.next') !!}
|
||||
</span>
|
||||
@endif
|
||||
@ -41,7 +41,7 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 leading-5">
|
||||
<p class="text-sm text-gray-700 leading-5 dark:text-gray-400">
|
||||
<span>{!! __('Showing') !!}</span>
|
||||
<span class="font-medium">{{ $paginator->firstItem() }}</span>
|
||||
<span>{!! __('to') !!}</span>
|
||||
@ -53,19 +53,19 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="relative z-0 inline-flex rounded-md shadow-sm">
|
||||
<span class="relative z-0 inline-flex rtl:flex-row-reverse rounded-md shadow-sm">
|
||||
<span>
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span aria-disabled="true" aria-label="{{ __('pagination.previous') }}">
|
||||
<span class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-l-md leading-5" aria-hidden="true">
|
||||
<span class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-l-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" rel="prev" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="{{ __('pagination.previous') }}">
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring ring-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('pagination.previous') }}">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
@ -78,7 +78,7 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
{{-- "Three Dots" Separator --}}
|
||||
@if (is_string($element))
|
||||
<span aria-disabled="true">
|
||||
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 select-none">{{ $element }}</span>
|
||||
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600 dark:bg-gray-800 dark:border-gray-600">{{ $element }}</span>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@ -88,10 +88,10 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
<span wire:key="paginator-{{ $paginator->getPageName() }}-page{{ $page }}">
|
||||
@if ($page == $paginator->currentPage())
|
||||
<span aria-current="page">
|
||||
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 select-none">{{ $page }}</span>
|
||||
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600">{{ $page }}</span>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
|
||||
<button type="button" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring ring-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-300 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
|
||||
{{ $page }}
|
||||
</button>
|
||||
@endif
|
||||
@ -103,14 +103,14 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
<span>
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" rel="next" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="{{ __('pagination.next') }}">
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring ring-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('pagination.next') }}">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<span aria-disabled="true" aria-label="{{ __('pagination.next') }}">
|
||||
<span class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-r-md leading-5" aria-hidden="true">
|
||||
<span class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-r-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
@ -216,7 +216,6 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
|
||||
Route::get('credit/{invitation_key}/download', [CreditController::class, 'downloadPdf'])->name('credits.downloadPdf');
|
||||
Route::get('credit/{invitation_key}/download_e_credit', [CreditController::class, 'downloadECredit'])->name('credits.downloadECredit');
|
||||
|
||||
|
||||
Route::resource('designs', DesignController::class); // name = (payments. index / create / show / update / destroy / edit
|
||||
Route::post('designs/bulk', [DesignController::class, 'bulk'])->name('designs.bulk');
|
||||
Route::post('designs/set/default', [DesignController::class, 'default'])->name('designs.default');
|
||||
@ -428,8 +427,6 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
|
||||
|
||||
Route::get('nordigen/institutions', [NordigenController::class, 'institutions'])->name('nordigen.institutions');
|
||||
|
||||
Route::post('import/quickbooks', [ImportQuickbooksController::class, 'import'])->name('import.quickbooks');
|
||||
|
||||
});
|
||||
|
||||
Route::post('api/v1/sms_reset', [TwilioController::class, 'generate2faResetCode'])->name('sms_reset.generate')->middleware('throttle:3,1');
|
||||
|
@ -7,6 +7,7 @@ use App\Http\Controllers\Bank\NordigenController;
|
||||
use App\Http\Controllers\Bank\YodleeController;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use App\Http\Controllers\ClientPortal\ApplePayDomainController;
|
||||
use App\Http\Controllers\EInvoice\SelfhostController;
|
||||
use App\Http\Controllers\Gateways\Checkout3dsController;
|
||||
use App\Http\Controllers\Gateways\GoCardlessController;
|
||||
use App\Http\Controllers\Gateways\Mollie3dsController;
|
||||
@ -50,5 +51,6 @@ Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Mol
|
||||
Route::get('gocardless/ibp_redirect/{company_key}/{company_gateway_id}/{hash}', [GoCardlessController::class, 'ibpRedirect'])->middleware('domain_db')->name('gocardless.ibp_redirect');
|
||||
Route::get('.well-known/apple-developer-merchantid-domain-association', [ApplePayDomainController::class, 'showAppleMerchantId']);
|
||||
|
||||
Route::get('einvoice/beta', [SelfhostController::class, 'index'])->name('einvoice.beta');
|
||||
|
||||
\Illuminate\Support\Facades\Broadcast::routes(['middleware' => ['token_auth']]);
|
||||
|
@ -13,13 +13,17 @@
|
||||
namespace Tests\Feature\Bank;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\Payment;
|
||||
use Tests\MockAccountData;
|
||||
use App\Models\BankIntegration;
|
||||
use App\Models\BankTransaction;
|
||||
use App\Models\BankTransactionRule;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\Bank\ProcessBankRules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Str;
|
||||
|
||||
class BankTransactionRuleTest extends TestCase
|
||||
{
|
||||
@ -39,6 +43,537 @@ class BankTransactionRuleTest extends TestCase
|
||||
$this->withoutExceptionHandling();
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesInvoiceStartsWith()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$invoice.number',
|
||||
'operator' => 'starts_with',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$i = Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'balance' => $rand_amount,
|
||||
'number' => $hash,
|
||||
'status_id' => 2,
|
||||
'custom_value1' => substr($hash, 0, 8)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($i->id);
|
||||
$this->assertNotNull($bt->invoice_ids);
|
||||
$this->assertEquals($i->hashed_id, $bt->invoice_ids);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesInvoiceContains()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$invoice.number',
|
||||
'operator' => 'contains',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$i = Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'balance' => $rand_amount,
|
||||
'number' => $hash,
|
||||
'status_id' => 2,
|
||||
'custom_value1' => substr($hash, 0, 8)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
$bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($i->id);
|
||||
$this->assertNotNull($bt->invoice_ids);
|
||||
$this->assertEquals($i->hashed_id, $bt->invoice_ids);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesInvoiceNumber()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$invoice.number',
|
||||
'operator' => 'is',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$i = Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'balance' => $rand_amount,
|
||||
'number' => $hash,
|
||||
'status_id' => 2,
|
||||
'custom_value1' => substr($hash, 0, 8)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($i->id);
|
||||
$this->assertNotNull($bt->invoice_ids);
|
||||
$this->assertEquals($i->hashed_id, $bt->invoice_ids);
|
||||
}
|
||||
|
||||
|
||||
public function testNewCreditMatchingRulesInvoiceAmount()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$invoice.amount',
|
||||
'operator' => '=',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$i = Invoice::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'balance' => $rand_amount,
|
||||
'status_id' => 2,
|
||||
'custom_value1' => substr($hash, 0, 8)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
$bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($i->id);
|
||||
$this->assertNotNull($bt->invoice_ids);
|
||||
$this->assertEquals($i->hashed_id, $bt->invoice_ids);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesPaymentCustomValue()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$payment.custom1',
|
||||
'operator' => 'starts_with',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$p = Payment::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'custom_value1' => substr($hash, 0, 8)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($p->id);
|
||||
$this->assertNotNull($bt->payment_id);
|
||||
$this->assertEquals($p->id, $bt->payment_id);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesPaymentStartsWith()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$payment.transaction_reference',
|
||||
'operator' => 'starts_with',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$p = Payment::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'transaction_reference' => substr($hash, 0, 8)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($p->id);
|
||||
$this->assertNotNull($bt->payment_id);
|
||||
$this->assertEquals($p->id, $bt->payment_id);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesPaymentAmount()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = md5(time());
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$payment.amount',
|
||||
'operator' => '=',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$p = Payment::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'transaction_reference' => 'nein'
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($p->id);
|
||||
$this->assertNotNull($bt->payment_id);
|
||||
$this->assertEquals($p->id, $bt->payment_id);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesPaymentTransactionReferenceExactMatch()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = md5(time());
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$payment.transaction_reference',
|
||||
'operator' => 'is',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$p = Payment::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'transaction_reference' => $hash
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($p->id);
|
||||
$this->assertNotNull($bt->payment_id);
|
||||
$this->assertEquals($p->id, $bt->payment_id);
|
||||
}
|
||||
|
||||
public function testNewCreditMatchingRulesPaymentTransactionReferenceContains()
|
||||
{
|
||||
|
||||
$bi = BankIntegration::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'account_id' => $this->account->id,
|
||||
]);
|
||||
|
||||
$hash = Str::random(32);
|
||||
$rand_amount = rand(1000,10000000);
|
||||
|
||||
$bt = BankTransaction::factory()->create([
|
||||
'bank_integration_id' => $bi->id,
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'description' => $hash,
|
||||
'base_type' => 'CREDIT',
|
||||
'amount' => $rand_amount
|
||||
]);
|
||||
|
||||
$this->assertNull($bt->payment_id);
|
||||
|
||||
$br = BankTransactionRule::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'matches_on_all' => false,
|
||||
'auto_convert' => false,
|
||||
'applies_to' => 'CREDIT',
|
||||
'rules' => [
|
||||
[
|
||||
'search_key' => '$payment.transaction_reference',
|
||||
'operator' => 'contains',
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$p = Payment::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $this->client->id,
|
||||
'amount' => $rand_amount,
|
||||
'transaction_reference' => substr($hash, 3, 13)
|
||||
]);
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_UNMATCHED, $bt->status_id);
|
||||
|
||||
(new ProcessBankRules($bt))->run();
|
||||
|
||||
|
||||
$bt = $bt->fresh();
|
||||
|
||||
$this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
|
||||
$this->assertNotNull($p->id);
|
||||
$this->assertNotNull($bt->payment_id);
|
||||
$this->assertEquals($p->id, $bt->payment_id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testMatchCreditOnInvoiceNumber()
|
||||
{
|
||||
|
||||
|
@ -129,6 +129,8 @@ class FatturaPATest extends TestCase
|
||||
$e = new EInvoice();
|
||||
$errors = $e->validate($fe);
|
||||
|
||||
|
||||
|
||||
if(count($errors) > 0) {
|
||||
nlog($errors);
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class StorecoveTest extends TestCase
|
||||
// ];
|
||||
|
||||
// $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove();
|
||||
// $r = $sc->createLegalEntity($data, $this->company);
|
||||
// $r = $sc->createLegalEntity($data);
|
||||
|
||||
// $this->assertIsArray($r);
|
||||
|
||||
@ -358,7 +358,19 @@ class StorecoveTest extends TestCase
|
||||
|
||||
}
|
||||
*/
|
||||
public function XXestCreateCHClient()
|
||||
public function testCreateTestData()
|
||||
{
|
||||
$this->createESData();
|
||||
$this->createATData();
|
||||
$this->createDEData();
|
||||
$this->createFRData();
|
||||
$this->createITData();
|
||||
$this->createROData();
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testCreateCHClient()
|
||||
{
|
||||
|
||||
Client::unguard();
|
||||
@ -564,6 +576,115 @@ class StorecoveTest extends TestCase
|
||||
|
||||
}
|
||||
|
||||
private function createDEData()
|
||||
{
|
||||
// $this->routing_id = 293098;
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
|
||||
$settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png';
|
||||
$settings->website = 'www.invoiceninja.de';
|
||||
$settings->address1 = 'Musterstraße 12';
|
||||
$settings->address2 = 'Gebäude B';
|
||||
$settings->city = 'Berlin';
|
||||
$settings->state = 'Berlin';
|
||||
$settings->postal_code = '10115';
|
||||
$settings->phone = '030 1234567';
|
||||
$settings->email = $this->faker->unique()->safeEmail();
|
||||
$settings->country_id = '276'; // Germany's ISO country code
|
||||
$settings->vat_number = 'DE123456789';
|
||||
$settings->id_number = 'HRB 98765';
|
||||
$settings->use_credits_payment = 'always';
|
||||
$settings->timezone_id = '1'; // CET (Central European Time)
|
||||
$settings->entity_send_time = 0;
|
||||
$settings->e_invoice_type = 'PEPPOL';
|
||||
$settings->currency_id = '3'; // Euro
|
||||
$settings->classification = 'business';
|
||||
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
$this->user->companies()->attach($company->id, [
|
||||
'account_id' => $this->account->id,
|
||||
'is_owner' => true,
|
||||
'is_admin' => 1,
|
||||
'is_locked' => 0,
|
||||
'permissions' => '',
|
||||
'notifications' => CompanySettings::notificationAdminDefaults(),
|
||||
'settings' => null,
|
||||
]);
|
||||
|
||||
Client::unguard();
|
||||
|
||||
$c =
|
||||
Client::create([
|
||||
'company_id' => $company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'name' => 'Beispiel GmbH',
|
||||
'website' => 'https://www.beispiel.de',
|
||||
'private_notes' => 'Dies sind private Notizen für den Testkunden.',
|
||||
'balance' => 0,
|
||||
'paid_to_date' => 0,
|
||||
'vat_number' => 'DE123456789', // German VAT number with DE prefix
|
||||
'id_number' => 'HRB 12345', // Typical format for German company registration numbers
|
||||
'custom_value1' => '2024-07-22 10:00:00',
|
||||
'custom_value2' => 'blau', // German for blue
|
||||
'custom_value3' => 'beispielwort', // German for sample word
|
||||
'custom_value4' => 'test@beispiel.de',
|
||||
'address1' => 'Beispielstraße 123',
|
||||
'address2' => '2. Stock, Büro 45',
|
||||
'city' => 'Berlin',
|
||||
'state' => 'Berlin',
|
||||
'postal_code' => '10115',
|
||||
'country_id' => '276', // Germany
|
||||
'shipping_address1' => 'Beispielstraße 123',
|
||||
'shipping_address2' => '2. Stock, Büro 45',
|
||||
'shipping_city' => 'Berlin',
|
||||
'shipping_state' => 'Berlin',
|
||||
'shipping_postal_code' => '10115',
|
||||
'shipping_country_id' => '276', // Germany
|
||||
'settings' => ClientSettings::Defaults(),
|
||||
'client_hash' => \Illuminate\Support\Str::random(32),
|
||||
'routing_id' => 'DEDEDE',
|
||||
]);
|
||||
|
||||
$item = new InvoiceItem();
|
||||
$item->product_key = "Product Key";
|
||||
$item->notes = "Product Description";
|
||||
$item->cost = 10;
|
||||
$item->quantity = 10;
|
||||
$item->tax_rate1 = 19;
|
||||
$item->tax_name1 = 'mwst';
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'client_id' => $c->id,
|
||||
'discount' => 0,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'status_id' => 1,
|
||||
'tax_rate1' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'line_items' => [$item],
|
||||
'number' => 'DE-'.rand(1000, 100000),
|
||||
'date' => now()->format('Y-m-d'),
|
||||
'due_date' => now()->addDays(14)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice();
|
||||
$invoice->service()->markSent()->save();
|
||||
|
||||
return $invoice;
|
||||
|
||||
}
|
||||
|
||||
private function createESData()
|
||||
{
|
||||
$this->routing_id = 293098;
|
||||
|
Loading…
Reference in New Issue
Block a user