1
0
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:
David Bomba 2024-08-30 11:12:36 +10:00 committed by GitHub
commit a4a2e237db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 14847 additions and 2270 deletions

View 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;
}
}

View File

@ -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',

View File

@ -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>';
}

View File

@ -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

View 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)
{
}
}

View 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)
{
}
}

View File

@ -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;
}

View File

@ -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');

View File

@ -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,
];
}

View File

@ -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])]);
}

View File

@ -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);

View 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');
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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';

View 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();
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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":

View File

@ -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();
}
}

View File

@ -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 **/

View File

@ -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();
}

View 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);
}
}

View 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);
}
}

View 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');
}
}

View File

@ -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']);

View File

@ -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();

View File

@ -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(),

View File

@ -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;

View File

@ -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);
}
}
/**

View File

@ -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 = [

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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:

View File

@ -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 => [

View File

@ -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(),

View File

@ -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
*

View File

@ -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;
// }

View File

@ -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) {

View File

@ -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
}
}

View 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];
}
}

View 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)];
}
}

View File

@ -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

View File

@ -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

View File

@ -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()));
}
}

View File

@ -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;

View File

@ -88,7 +88,7 @@ class QuickbooksService
*/
public function syncFromQb()
{
QuickbooksSync::dispatch($this->company);
QuickbooksSync::dispatch($this->company->id, $this->company->db);
}
}

View File

@ -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);

View File

@ -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

View File

@ -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 ?? '',
];
}

View File

@ -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>';

View File

@ -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
View File

@ -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",

View File

@ -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,
/*
|---------------------------------------------------------------------------

View File

@ -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'),

View File

@ -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();
}
}
/**

View File

@ -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' => '.'],

View File

@ -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
View File

@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "invoiceninja",
"dependencies": {
"@invoiceninja/simple-card": "^0.0.2",
"axios": "^0.25",

View File

@ -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#'

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1,2 @@
{"/livewire.js":"/livewire.js?id=90730a3b0e7144480175"}
{"/livewire.js":"cc800bf4"}

View File

@ -0,0 +1,5 @@
@extends('layouts.ninja')
@section('body')
@livewire('e-invoice.portal')
@stop

View 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>

View File

@ -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">&lsaquo;</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')">&lsaquo;</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">&lsaquo;</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')">&lsaquo;</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')">&rsaquo;</button>
</li>
@else
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</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')">&rsaquo;</button>
</li>
@else
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</div>
</div>
</nav>
@endif
</div>

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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');

View File

@ -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']]);

View File

@ -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()
{

View File

@ -129,6 +129,8 @@ class FatturaPATest extends TestCase
$e = new EInvoice();
$errors = $e->validate($fe);
if(count($errors) > 0) {
nlog($errors);
}

View File

@ -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;