1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 08:21:34 +02:00

Merge branch 'v5-develop' into v5-develop

Signed-off-by: David Bomba <turbo124@gmail.com>
This commit is contained in:
David Bomba 2024-06-01 08:56:06 +10:00 committed by GitHub
commit 5a1f52941d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 3272 additions and 2911 deletions

View File

@ -13,8 +13,8 @@ jobs:
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: ['ubuntu-20.04', 'ubuntu-22.04']
php-versions: ['8.1','8.2']
operating-system: ['ubuntu-22.04','ubuntu-24.04']
php-versions: ['8.2']
phpunit-versions: ['latest']
ci_node_total: [ 8 ]
ci_node_index: [ 0, 1, 2, 3, 4, 5, 6, 7]
@ -95,14 +95,15 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v3
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.php }}-composer-
- name: Install composer dependencies
run: |
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}

View File

@ -1 +1 @@
5.8.57
5.8.57

View File

@ -1,69 +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\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use App\DataProviders\FatturaPADataProvider;
class EDocLint extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:edoclint';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Builds json files component data maps';
private array $classes = [
FatturaPADataProvider::class,
];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
foreach($this->classes as $class)
{
$provider = new $class();
foreach($provider as $key => $value) {
$json = json_encode($provider->{$key}, JSON_PRETTY_PRINT);
Storage::disk('local')->put($key.'.json', $json);
}
}
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataProviders;
/**
* Class FACT1.
*/
class FACT1
{
public function build()
{
$i = new \InvoiceNinja\EInvoice\Models\FACT1\Invoice();
}
}

View File

@ -1,124 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataProviders;
class FatturaPADataProvider
{
public array $regime_fiscale = [
"RF01" => "Regime ordinario",
"RF02" => "Regime dei contribuenti minimi (art. 1,c.96-117, L. 244/2007)",
"RF04" => "Agricoltura e attività connesse e pesca (artt. 34 e 34-bis, D.P.R. 633/1972)",
"RF05" => "Vendita sali e tabacchi (art. 74, c.1, D.P.R. 633/1972)",
"RF06" => "Commercio dei fiammiferi (art. 74, c.1, D.P.R. 633/1972)",
"RF07" => "Editoria (art. 74, c.1, D.P.R. 633/1972)",
"RF08" => "Gestione di servizi di telefonia pubblica (art. 74, c.1, D.P.R. 633/1972)" ,
"RF09" => "Rivendita di documenti di trasporto pubblico e di sosta (art. 74, c.1, D.P.R. 633/1972)" ,
"RF10" => "Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al D.P.R. 640/72 (art. 74, c.6, D.P.R. 633/1972)" ,
"RF11" => "Agenzie di viaggi e turismo (art. 74-ter, D.P.R. 633/1972)" ,
"RF12" => "Agriturismo (art. 5, c.2, L. 413/1991)" ,
"RF13" => "Vendite a domicilio (art. 25-bis, c.6, D.P.R. 600/1973)" ,
"RF14" => "Rivendita di beni usati, di oggetti darte, dantiquariato o da collezione (art. 36, D.L. 41/1995)" ,
"RF15" => "Agenzie di vendite allasta di oggetti darte, antiquariato o da collezione (art. 40-bis, D.L. 41/1995)" ,
"RF16" => "IVA per cassa P.A. (art. 6, c.5, D.P.R. 633/1972)" ,
"RF17" => "IVA per cassa (art. 32-bis, D.L. 83/2012)" ,
"RF19" => "Regime forfettario" ,
"RF18" => "Altro"
];
public array $tipo_documento = [
'TD01' => 'Fattura',
'TD02' => 'Acconto/Anticipo su fattura',
'TD03' => 'Acconto/Anticipo su parcella',
'TD04' => 'Nota di Credito',
'TD05' => 'Nota di Debito',
'TD06' => 'Parcella',
'TD16' => 'Integrazione fattura reverse charge interno',
'TD17' => 'Integrazione/autofattura per acquisto servizi dallestero',
'TD18' => 'Integrazione per acquisto di beni intracomunitari',
'TD19' => 'Integrazione/autofattura per acquisto di beni ex art.17 c.2 DPR 633/72',
'TD20' => 'Autofattura per regolarizzazione e integrazione delle fatture',
'TD21' => 'Autofattura per splafonamento',
'TD22' => 'Estrazione beni da Deposito IVA',
'TD23' => 'Estrazione beni da Deposito IVA con versamento dellIVA',
'TD24' => 'Fattura differita di cui allart.21, comma 4, lett. a)',
'TD25' => 'Fattura differita di cui allart.21, comma 4, terzo periodo lett. b)',
'TD26' => 'Cessione di beni ammortizzabili e per passaggi interni ',
'TD27' => 'Fattura per autoconsumo o per cessioni gratuite senza rivalsa',
];
public array $esigibilita_iva = [
'I' => 'IVA ad esigibilità immediata',
'D' => 'IVA ad esigibilità differita',
'S' => 'Scissione dei pagamenti',
];
public array $modalita_pagamento = [
'MP01' => 'contanti', //cash
'MP02' => 'assegno', //check
'MP03' => 'assegno circolare', //cashier's check
'MP04' => 'contanti presso Tesoreria', //cash at treasury
'MP05' => 'bonifico', //bank transfer
'MP06' => 'vaglia cambiario', //bill of exchange
'MP07' => 'bollettino bancario', //bank bulletin
'MP08' => 'carta di pagamento', //payment card
'MP09' => 'RID', //RID
'MP10' => 'RID utenze', //RID utilities
'MP11' => 'RID veloce', //fast RID
'MP12' => 'Riba', //Riba
'MP13' => 'MAV //MAV',
'MP14' => 'quietanza erario stato', //state treasury receipt
'MP15' => 'giroconto su conti di contabilità speciale', //transfer to special accounting accounts
'MP16' => 'domiciliazione bancaria', //bank domiciliation
'MP17' => 'domiciliazione postale', //postal domiciliation
'MP18' => 'bollettino di c/c postale', //postal giro account
'MP19' => 'SEPA Direct Debit', //SEPA Direct Debit
'MP20' => 'SEPA Direct Debit CORE', //SEPA Direct Debit CORE
'MP21' => 'SEPA Direct Debit B2B', //SEPA Direct Debit B2B
'MP22' => 'Trattenuta su somme già riscosse', //Withholding on sums already collected
'MP23' => 'PagoPA', //PagoPA
];
public array $esigibilita_pagamento = [
'TP01' => 'Pagamento a rate',
'TP02' => 'Pagamento completo',
'TP03' => 'Anticipo',
];
public function __construct()
{
}
public function getRegimeFiscale(): array
{
return $this->regime_fiscale;
}
public function getTipoDocumento(): array
{
return $this->tipo_documento;
}
public function getEsigibilitaIva(): array
{
return $this->esigibilita_iva;
}
public function getModalitaPagamento(): array
{
return $this->modalita_pagamento;
}
public function getEsigibilitaPagamento(): array
{
return $this->esigibilita_pagamento;
}
}

View File

@ -125,8 +125,10 @@ class ClientExport extends BaseExport
$query = Client::query()->with('contacts')
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false)
$query->where('is_deleted', 0);
$query = $this->addDateRange($query);

View File

@ -82,8 +82,12 @@ class ExpenseExport extends BaseExport
$query = Expense::query()
->with('client')
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -60,8 +60,12 @@ class InvoiceExport extends BaseExport
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -73,8 +73,11 @@ class InvoiceItemExport extends BaseExport
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -73,8 +73,12 @@ class ProductExport extends BaseExport
$query = Product::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -61,8 +61,11 @@ class PurchaseOrderExport extends BaseExport
->whereHas('vendor', function ($q){
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -65,8 +65,11 @@ class PurchaseOrderItemExport extends BaseExport
->whereHas('vendor', function ($q){
$q->where('is_deleted', false);
})
->with('vendor')->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->with('vendor')->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -67,8 +67,11 @@ class QuoteExport extends BaseExport
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -68,8 +68,11 @@ class QuoteItemExport extends BaseExport
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->with('client')->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->with('client')->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -59,8 +59,11 @@ class RecurringInvoiceExport extends BaseExport
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -68,8 +68,11 @@ class TaskExport extends BaseExport
$query = Task::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -62,8 +62,11 @@ class VendorExport extends BaseExport
$query = Vendor::query()->with('contacts')
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', $this->input['include_deleted'] ?? false);
->where('company_id', $this->company->id);
if(!$this->input['include_deleted'] ?? false){
$query->where('is_deleted', 0);
}
$query = $this->addDateRange($query);

View File

@ -12,6 +12,7 @@
namespace App\Factory;
use App\Models\User;
use Illuminate\Support\Str;
class UserFactory
{
@ -29,6 +30,7 @@ class UserFactory
$user->signature = '';
$user->theme_id = 0;
$user->user_logged_in_notification = true;
$user->referral_code = Str::lower(Str::random(32));
return $user;
}

View File

@ -64,23 +64,23 @@ class GmailTransport extends AbstractTransport
$body->setRaw($this->base64_encode($bcc_list.$message->toString()));
try {
// try {
$service->users_messages->send('me', $body, []);
} catch(\Google\Service\Exception $e) {
/* Need to slow down */
if ($e->getCode() == '429') {
nlog("429 google - retrying ");
// } catch(\Google\Service\Exception $e) {
// /* Need to slow down */
// if ($e->getCode() == '429') {
// nlog("429 google - retrying ");
sleep(rand(3,8));
// sleep(rand(3,8));
try {
$service->users_messages->send('me', $body, []);
} catch(\Google\Service\Exception $e) {
// try {
// $service->users_messages->send('me', $body, []);
// } catch(\Google\Service\Exception $e) {
}
// }
}
}
// }
// }
}
private function base64_encode($data)

View File

@ -35,7 +35,7 @@ class NordigenController extends BaseController
/** @var array $context */
$context = $request->getTokenContent();
$company = $request->getCompany();
$lang = $company->locale();
$lang = substr($company->locale(), 0, 2);
$context["lang"] = $lang;
if (!$context) {
@ -143,7 +143,7 @@ class NordigenController extends BaseController
$data = $request->all();
$company = $request->getCompany();
$account = $company->account;
$lang = $company->locale();
$lang = substr($company->locale(), 0, 2);
/** @var array $context */
$context = $request->getTokenContent();

View File

@ -38,6 +38,7 @@ use App\Transformers\ArraySerializer;
use App\Transformers\EntityTransformer;
use League\Fractal\Resource\Collection;
use Illuminate\Database\Eloquent\Builder;
use Invoiceninja\Einvoice\Decoder\Schema;
use League\Fractal\Serializer\JsonApiSerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Illuminate\Contracts\Container\BindingResolutionException;
@ -996,8 +997,8 @@ class BaseController extends Controller
if(request()->has('einvoice')){
// $ro = new Schema();
// $response_data['einvoice_schema'] = $ro('FACT1');
$ro = new Schema();
$response_data['einvoice_schema'] = $ro('FACT1');
}

View File

@ -29,6 +29,7 @@ use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceHtml;
use App\Utils\Traits\Pdf\PageNumbering;
@ -40,6 +41,7 @@ use Twig\Error\SyntaxError;
class PreviewController extends BaseController
{
use GeneratesCounter;
use MakesHash;
use MakesInvoiceHtml;
use PageNumbering;
@ -404,23 +406,24 @@ class PreviewController extends BaseController
/** @var \App\Models\Client $client */
$client = Client::factory()->create([
'user_id' => auth()->user()->id,
'user_id' => $user->id,
'company_id' => $company->id,
]);
/** @var \App\Models\ClientContact $contact */
$contact = ClientContact::factory()->create([
'user_id' => auth()->user()->id,
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'is_primary' => 1,
'send_email' => true,
]);
/** @var \App\Models\Invoice $invoice */
$settings = $company->settings;
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::factory()->create([
'user_id' => auth()->user()->id,
'user_id' => $user->id,
'company_id' => $company->id,
'client_id' => $client->id,
'terms' => $company->settings->invoice_terms,
@ -428,8 +431,18 @@ class PreviewController extends BaseController
'public_notes' => 'Sample Public Notes',
]);
if ($settings->invoice_number_pattern) {
$invoice->number = $this->getFormattedEntityNumber(
$invoice,
rand(1, 9999),
$settings->counter_padding ?: 4,
$settings->invoice_number_pattern,
);
$invoice->save();
}
$invitation = InvoiceInvitation::factory()->create([
'user_id' => auth()->user()->id,
'user_id' => $user->id,
'company_id' => $company->id,
'invoice_id' => $invoice->id,
'client_contact_id' => $contact->id,
@ -454,7 +467,7 @@ class PreviewController extends BaseController
'template' => $design->elements([
'client' => $invoice->client,
'entity' => $invoice,
'pdf_variables' => (array) $invoice->company->settings->pdf_variables,
'pdf_variables' => (array) $settings->pdf_variables,
'products' => request()->design['design']['product'],
]),
'variables' => $html->generateLabelsAndValues(),

View File

@ -26,8 +26,6 @@ class SelfUpdateController extends BaseController
use ClientGroupSettingsSaver;
use AppSetup;
// private bool $use_zip = false;
private string $filename = 'invoiceninja.tar';
private array $purge_file_list = [

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers;
use App\Utils\Statics;
use Illuminate\Http\Response;
use Invoiceninja\Einvoice\Decoder\Schema;
class StaticController extends BaseController
{
@ -60,8 +61,8 @@ class StaticController extends BaseController
if(request()->has('einvoice')){
// $schema = new Schema();
// $response_data['einvoice_schema'] = $schema('FACT1');
$schema = new Schema();
$response_data['einvoice_schema'] = $schema('FACT1');
}

View File

@ -1,56 +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\Http\Controllers;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\User;
use App\PaymentDrivers\WePayPaymentDriver;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Cache;
class WePayController extends BaseController
{
use MakesHash;
/**
* Initialize WePay Signup.
*/
public function signup(string $token)
{
// return render('gateways.wepay.signup.finished');
$hash = Cache::get($token);
MultiDB::findAndSetDbByCompanyKey($hash['company_key']);
$user = User::findOrFail($hash['user_id']);
$company = Company::where('company_key', $hash['company_key'])->firstOrFail();
$data['user_id'] = $user->id;
$data['user_company'] = $company;
// $data['company_key'] = $company->company_key;
// $data['db'] = $company->db;
$wepay_driver = new WePayPaymentDriver(new CompanyGateway(), null, null);
return $wepay_driver->setup($data);
}
public function finished()
{
return render('gateways.wepay.signup.finished');
}
}

View File

@ -35,4 +35,14 @@ class UpdateCompanyUserRequest extends Request
{
return [];
}
public function prepareForValidation()
{
$input = $this->all();
if(isset($input['company_user']['user']))
unset($input['company_user']['user']);
$this->replace($input);
}
}

View File

@ -45,6 +45,9 @@ class ConnectNordigenBankIntegrationRequest extends Request
$context = $this->getTokenContent();
if(isset($context['institution_id']))
$input['institution_id'] = $context['institution_id'];
$input["redirect"] = isset($context["is_react"]) && $context['is_react'] ? config('ninja.react_url') . "/#/settings/bank_accounts" : config('ninja.app_url');
$this->replace($input);

View File

@ -26,7 +26,7 @@ class GenericReportRequest extends Request
*/
public function authorize(): bool
{
return $this->checkAuthority();
return true;
}
public function rules()
@ -68,27 +68,25 @@ class GenericReportRequest extends Request
$input['user_id'] = auth()->user()->id;
if(!$this->checkAuthority()){
$input['date_range'] = '';
$input['start_date'] = '';
$input['end_date'] = '';
$input['send_email'] = true;
$input['report_keys'] = [];
$input['document_email_attachment'] = false;
}
$this->replace($input);
}
private function checkAuthority()
{
$this->error_message = ctrans('texts.authorization_failure');
/** @var \App\Models\User $user */
$user = auth()->user();
if(Ninja::isHosted() && $user->account->isFreeHostedClient()){
$this->error_message = ctrans('texts.upgrade_to_view_reports');
return false;
}
return $user->isAdmin() || $user->hasPermission('view_reports');
}
protected function failedAuthorization()
{
throw new AuthorizationException($this->error_message);
}
}

View File

@ -289,6 +289,8 @@ class MatchBankTransactions implements ShouldQueue
private function createPayment($invoices, float $amount): void
{
$this->attachable_invoices = [];
$this->available_balance = $amount;
\DB::connection(config('database.default'))->transaction(function () use ($invoices) {

View File

@ -152,19 +152,39 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject))
->send();
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
}
catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch (\Symfony\Component\Mime\Exception\LogicException $e) {
}
catch (\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch (\Exception | \Google\Service\Exception $e) {
}
catch(\Symfony\Component\Mailer\Transport\Dsn $e){
nlog("Incorrectly configured mail server - setting to default mail driver.");
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
catch(\Google\Service\Exception $e){
if ($e->getCode() == '429') {
$message = "Google rate limiting triggered, we are queueing based on Gmail requirements.";
$this->logMailError($message, $this->company->clients()->first());
sleep(rand(1, 2));
$this->release(900);
}
}
catch (\Exception $e) {
nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage();
@ -221,8 +241,7 @@ class NinjaMailerJob implements ShouldQueue
}
/* Releasing immediately does not add in the backoff */
sleep(rand(5, 10));
sleep(rand(2, 3));
$this->release($this->backoff()[$this->attempts() - 1]);
}
@ -779,15 +798,20 @@ class NinjaMailerJob implements ShouldQueue
return false;
}
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token
],
])->getBody()->getContents());
try {
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token
],
])->getBody()->getContents());
}
catch(\Exception $e){
nlog("Problem getting new Microsoft token for User: {$user->email}");
}
if ($token) {
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token;

View File

@ -17,6 +17,7 @@ use App\Models\User;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Str;
class CreateUser
{
@ -62,6 +63,7 @@ class CreateUser
$user->fill($this->request);
$user->email = $this->request['email']; //todo need to remove this in production
$user->last_login = now();
$user->referral_code = Str::lower(Str::random(32));
$user->ip = request()->ip();
if (Ninja::isSelfHost()) {

View File

@ -1,215 +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\Livewire;
use App\DataMapper\FeesAndLimits;
use App\Factory\CompanyGatewayFactory;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\User;
use App\PaymentDrivers\WePayPaymentDriver;
use Livewire\Component;
use WePay;
class WepaySignup extends Component
{
public $user;
public $user_id;
public $company_key;
public $first_name;
public $last_name;
public $email;
public $company_name;
public $country;
public $ach;
public $wepay_payment_tos_agree;
public $debit_cards;
public $terms;
public $privacy_policy;
public $saved;
public Company $company;
protected $rules = [
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email'],
'company_name' => ['required'],
'country' => ['required'],
'ach' => ['sometimes'],
'wepay_payment_tos_agree' => ['accepted'],
'debit_cards' => ['sometimes'],
];
public function mount()
{
MultiDB::setDb($this->company->db);
$user = User::find($this->user_id);
$this->company = Company::query()->where('company_key', $this->company->company_key)->first();
$this->fill([
'wepay_payment_tos_agree' => '',
'ach' => '',
'country' => 'US',
'user' => $user,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email,
'company_name' => $this->company->present()->name(),
'saved' => ctrans('texts.confirm'),
'terms' => '<a href="https://go.wepay.com/terms-of-service" target="_blank">'.ctrans('texts.terms_of_service').'</a>',
'privacy_policy' => '<a href="https://go.wepay.com/privacy-policy" target="_blank">'.ctrans('texts.privacy_policy').'</a>',
]);
}
public function render()
{
return render('gateways.wepay.signup.wepay-signup');
}
public function submit()
{
MultiDB::setDb($this->company->db);
$data = $this->validate($this->rules);
//need to create or get a new WePay CompanyGateway
$cg = CompanyGateway::query()->where('gateway_key', '8fdeed552015b3c7b44ed6c8ebd9e992')
->where('company_id', $this->company->id)
->firstOrNew();
if (! $cg->id) {
$fees_and_limits = new \stdClass();
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits();
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits();
$cg = CompanyGatewayFactory::create($this->company->id, $this->user->id);
$cg->gateway_key = '8fdeed552015b3c7b44ed6c8ebd9e992';
$cg->require_cvv = false;
$cg->require_billing_address = false;
$cg->require_shipping_address = false;
$cg->update_details = false;
$cg->config = encrypt(config('ninja.testvars.checkout'));
$cg->fees_and_limits = $fees_and_limits;
$cg->token_billing = 'always';
$cg->save();
}
$this->saved = ctrans('texts.processing');
$wepay_driver = new WePayPaymentDriver($cg, null, null);
$wepay = $wepay_driver->init()->wepay;
$user_details = [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'email' => $data['email'],
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'original_ip' => request()->ip(),
'original_device' => request()->server('HTTP_USER_AGENT'),
'tos_acceptance_time' => time(),
'redirect_uri' => route('wepay.finished'),
'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money',
];
$wepay_user = $wepay->request('user/register/', $user_details);
$access_token = $wepay_user->access_token;
$access_token_expires = $wepay_user->expires_in ? (time() + $wepay_user->expires_in) : null;
$wepay = new WePay($access_token);
$account_details = [
'name' => $data['company_name'],
'description' => ctrans('texts.wepay_account_description'),
'theme_object' => json_decode('{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}'),
'callback_uri' => route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $cg->hashed_id]),
'rbits' => $this->company->rBits(),
'country' => $data['country'],
];
if ($data['country'] == 'CA') {
$account_details['currencies'] = ['CAD'];
$account_details['country_options'] = ['debit_opt_in' => boolval($data['debit_cards'])];
} elseif ($data['country'] == 'GB') {
$account_details['currencies'] = ['GBP'];
}
$wepay_account = $wepay->request('account/create/', $account_details);
$confirmation_required = false;
try {
$wepay->request('user/send_confirmation/', []);
$confirmation_required = true;
} catch (\WePayException $ex) {
if ($ex->getMessage() == 'This access_token is already approved.') {
$confirmation_required = false;
} else {
/** @phpstan-ignore-next-line */
request()->session()->flash('message', $ex->getMessage());
}
nlog('failed in try catch ');
nlog($ex->getMessage());
}
$config = [
'userId' => $wepay_user->user_id,
'accessToken' => $access_token,
'tokenType' => $wepay_user->token_type,
'tokenExpires' => $access_token_expires,
'accountId' => $wepay_account->account_id,
'state' => $wepay_account->state,
'testMode' => config('ninja.wepay.environment') == 'staging',
'country' => $data['country'],
];
$cg->setConfig($config);
$cg->save();
if ($confirmation_required) {
/** @phpstan-ignore-next-line **/
request()->session()->flash('message', trans('texts.created_wepay_confirmation_required'));
} else {
$update_uri = $wepay->request('/account/get_update_uri', [
'account_id' => $wepay_account->account_id,
'redirect_uri' => config('ninja.app_url'),
]);
return redirect($update_uri->uri);
}
return redirect()->to('/wepay/finished');
}
}

View File

@ -44,4 +44,15 @@ class ClientContactPresenter extends EntityPresenter
{
return $this->name().' <'.$this->entity->email.'>' ?? '';
}
public function phone()
{
return strlen($this->phone ?? '') > 1 ? $this->phone : '';
}
public function email()
{
return strlen($this->email ?? '') > 1 ? $this->email : '';
}
}

View File

@ -1,79 +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\Notifications\Ninja;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class WePayFailureNotification extends Notification
{
/**
* Create a new notification instance.
*
* @return void
*/
protected $company_id;
public function __construct($company_id)
{
$this->company_id = $company_id;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
*
*/
public function toMail($notifiable)
{
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$ip = '';
if (request()) {
$ip = request()->getClientIp();
}
return (new SlackMessage())
->success()
->from(ctrans('texts.notification_bot'))
->image('https://app.invoiceninja.com/favicon.png')
->content("New WePay ACH Failure from Company ID: {$this->company_id} IP: {$ip}");
}
}

View File

@ -45,7 +45,7 @@
@include('portal.ninja2020.gateways.includes.save_card')
<!-- This include pops up a credit card form -->
@include('portal.ninja2020.gateways.wepay.includes.credit_card')
@include('portal.ninja2020.gateways.stripe.includes.credit_card')
@include('portal.ninja2020.gateways.includes.pay_now')

View File

@ -1,354 +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\WePay;
use App\Exceptions\PaymentFailed;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Notifications\Ninja\WePayFailureNotification;
use App\PaymentDrivers\WePayPaymentDriver;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class ACH
{
use MakesHash;
use WePayCommon;
public $wepay_payment_driver;
public function __construct(WePayPaymentDriver $wepay_payment_driver)
{
$this->wepay_payment_driver = $wepay_payment_driver;
}
public function authorizeView($data)
{
$data['gateway'] = $this->wepay_payment_driver;
$data['country_code'] = $this->wepay_payment_driver?->client?->country ? $this->wepay_payment_driver->client->country->iso_3166_2 : $this->wepay_payment_driver->company_gateway->company->country()->iso_3166_2;
return render('gateways.wepay.authorize.bank_transfer', $data);
}
public function authorizeResponse($request)
{
//https://developer.wepay.com/api/api-calls/credit_card#authorize
$data = $request->all();
// authorize the credit card
//nlog($data);
/*
'_token' => '1Fk5CRj34up5ntKPvrFyMIAJhDdUNF3boqT3iIN3',
'company_gateway_id' => '39',
'payment_method_id' => '1',
'gateway_response' => NULL,
'is_default' => NULL,
'credit_card_id' => '180642154638',
'q' => '/client/payment_methods',
'method' => '1',
*/
try {
$response = $this->wepay_payment_driver->wepay->request('payment_bank/persist', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'payment_bank_id' => (int) $data['bank_account_id'],
]);
} catch (\Exception $e) {
$this->wepay_payment_driver->sendFailureMail($e->getMessage());
$message = [
'server_response' => $e->getMessage(),
];
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_WEPAY,
$this->wepay_payment_driver->client,
$this->wepay_payment_driver->client->company,
);
if (config('ninja.notification.slack')) {
$this->wepay_payment_driver->company_gateway->company->notification(new WePayFailureNotification($this->wepay_payment_driver->company_gateway->company))->ninja();
}
throw new PaymentFailed($e->getMessage(), 400);
}
// display the response
// nlog($response);
if (in_array($response->state, ['new', 'pending', 'authorized'])) {
$this->storePaymentMethod($response, GatewayType::BANK_TRANSFER);
return redirect()->route('client.payment_methods.index');
}
throw new PaymentFailed('There was a problem adding this payment method.', 400);
/*
{
"payment_bank_id": 12345,
"bank_name": "Wells Fargo",
"account_last_four": "6789",
"state": "authorized"
}
state options: new, pending, authorized, disabled.
*/
}
/* If the bank transfer token is PENDING - we need to verify!! */
//
public function verificationView(ClientGatewayToken $token)
{
$this->wepay_payment_driver->init();
$data = [
'token' => $token,
'gateway' => $this->wepay_payment_driver,
];
return render('gateways.wepay.authorize.verify', $data);
}
/**
{
"client_id": 1234,
"client_secret": "b1fc2f68-4d1f-4a",
"payment_bank_id": 12345,
"type": "microdeposits",
"microdeposits": [
8,
12
]
}
*/
public function processVerification(Request $request, ClientGatewayToken $token)
{
$transactions = $request->input('transactions');
$transformed_transactions = [];
foreach ($transactions as $transaction) {
$transformed_transactions[] = (int) $transaction;
}
try {
$response = $this->wepay_payment_driver->wepay->request('payment_bank/verify', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'payment_bank_id' => $token->token,
'type' => 'microdeposits',
'microdeposits' => $transformed_transactions,
]);
} catch (\Exception $e) {
nlog('we pay exception');
nlog($e->getMessage());
return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER])
->with('error', $e->getMessage());
}
/*
{
"payment_bank_id": 12345,
"bank_name": "Wells Fargo",
"account_last_four": "6789",
"state": "authorized"
}
*/
nlog($response);
//$meta = $token->meta;
if ($response->state == 'authorized') {
$meta = $token->meta;
$meta->state = $response->state;
$token->meta = $meta;
$token->save();
return redirect()->route('client.payment_methods.index');
} else {
return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER])
->with('error', ctrans('texts.verification_failed'));
}
}
///////////////////////////////////////////////////////////////////////////////////////
public function paymentView(array $data)
{
$data['gateway'] = $this->wepay_payment_driver;
$data['currency'] = $this->wepay_payment_driver->client->getCurrencyCode();
$data['payment_method_id'] = GatewayType::BANK_TRANSFER;
$data['amount'] = $data['total']['amount_with_fee'];
return render('gateways.wepay.bank_transfer', $data);
}
public function paymentResponse($request)
{
$token = ClientGatewayToken::query()->find($this->decodePrimaryKey($request->input('source')));
$token_meta = $token->meta;
if (! property_exists($token_meta, 'state') || $token_meta->state != 'authorized') {
$response = $this->wepay_payment_driver->wepay->request('/payment_bank', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'payment_bank_id' => $token->token,
]);
if ($response->state == 'authorized') {
$meta = $token->meta;
$meta->state = $response->state;
$token->meta = $meta;
$token->save();
} else {
return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
}
}
$app_fee = (config('ninja.wepay.fee_ach_multiplier') * $this->wepay_payment_driver->payment_hash->data->amount_with_fee) + config('ninja.wepay.fee_fixed');
try {
$response = $this->wepay_payment_driver->wepay->request('checkout/create', [
// 'callback_uri' => route('payment_webhook', ['company_key' => $this->wepay_payment_driver->company_gateway->company->company_key, 'company_gateway_id' => $this->wepay_payment_driver->company_gateway->hashed_id]),
'unique_id' => Str::random(40),
'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'),
'amount' => $this->wepay_payment_driver->payment_hash->data->amount_with_fee,
'currency' => $this->wepay_payment_driver->client->getCurrencyCode(),
'short_description' => 'Goods and Services',
'type' => 'goods',
'fee' => [
'fee_payer' => config('ninja.wepay.fee_payer'),
'app_fee' => $app_fee,
],
'payment_method' => [
'type' => 'payment_bank',
'payment_bank' => [
'id' => $token->token,
],
],
]);
} catch (\Exception $e) {
throw new PaymentFailed($e->getMessage(), 500);
}
/* Merge all data and store in the payment hash*/
$state = [
'server_response' => $response,
'payment_hash' => $request->payment_hash,
];
$state = array_merge($state, $request->all());
$this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state);
$this->wepay_payment_driver->payment_hash->save();
if (in_array($response->state, ['authorized', 'captured'])) {
//success
nlog('success');
$payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING;
return $this->processSuccessfulPayment($response, $payment_status, GatewayType::BANK_TRANSFER);
}
if (in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])) {
//some type of failure
nlog('failure');
$payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED;
$this->processUnSuccessfulPayment($response, $payment_status);
}
}
private function storePaymentMethod($response, $payment_method_id)
{
$payment_meta = new \stdClass();
$payment_meta->exp_month = (string) '';
$payment_meta->exp_year = (string) '';
$payment_meta->brand = (string) $response->bank_name;
$payment_meta->last4 = (string) $response->account_last_four;
$payment_meta->type = GatewayType::BANK_TRANSFER;
$payment_meta->state = $response->state;
$data = [
'payment_meta' => $payment_meta,
'token' => $response->payment_bank_id,
'payment_method_id' => $payment_method_id,
];
$this->wepay_payment_driver->storeGatewayToken($data);
}
public function tokenBilling($token, $payment_hash)
{
$token_meta = $token->meta;
if (! property_exists($token_meta, 'state') || $token_meta->state != 'authorized') {
return redirect()->route('client.payment_methods.verification', ['payment_method' => $token->hashed_id, 'method' => GatewayType::BANK_TRANSFER]);
}
$amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total;
$app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed');
$response = $this->wepay_payment_driver->wepay->request('checkout/create', [
'unique_id' => Str::random(40),
'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'),
'amount' => $amount,
'currency' => $this->wepay_payment_driver->client->getCurrencyCode(),
'short_description' => 'Goods and Services',
'type' => 'goods',
'fee' => [
'fee_payer' => config('ninja.wepay.fee_payer'),
'app_fee' => $app_fee,
],
'payment_method' => [
'type' => 'payment_bank',
'payment_bank' => [
'id' => $token->token,
],
],
]);
/* Merge all data and store in the payment hash*/
$state = [
'server_response' => $response,
'payment_hash' => $this->wepay_payment_driver->payment_hash,
];
$this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state);
$this->wepay_payment_driver->payment_hash->save();
if (in_array($response->state, ['authorized', 'captured'])) {
//success
nlog('success');
$payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING;
return $this->processSuccessfulPayment($response, $payment_status, GatewayType::BANK_TRANSFER, true);
}
if (in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])) {
//some type of failure
nlog('failure');
$payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED;
$this->processUnSuccessfulPayment($response, $payment_status);
}
}
}

View File

@ -1,346 +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\WePay;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\SystemLog;
use App\PaymentDrivers\WePayPaymentDriver;
use Illuminate\Support\Str;
class CreditCard
{
use WePayCommon;
public $wepay_payment_driver;
public function __construct(WePayPaymentDriver $wepay_payment_driver)
{
$this->wepay_payment_driver = $wepay_payment_driver;
}
public function authorizeView($data)
{
$data['gateway'] = $this->wepay_payment_driver;
$data['country_code'] = $this->wepay_payment_driver?->client?->country ? $this->wepay_payment_driver->client->country->iso_3166_2 : $this->wepay_payment_driver->company_gateway->company->country()->iso_3166_2;
return render('gateways.wepay.authorize.authorize', $data);
}
public function authorizeResponse($request)
{
//https://developer.wepay.com/api/api-calls/credit_card#authorize
$data = $request->all();
// authorize the credit card
// nlog($data);
/*
'_token' => '1Fk5CRj34up5ntKPvrFyMIAJhDdUNF3boqT3iIN3',
'company_gateway_id' => '39',
'payment_method_id' => '1',
'gateway_response' => NULL,
'is_default' => NULL,
'credit_card_id' => '180642154638',
'q' => '/client/payment_methods',
'method' => '1',
*/
try {
$response = $this->wepay_payment_driver->wepay->request('credit_card/authorize', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'credit_card_id' => (int) $data['credit_card_id'],
]);
} catch (\Exception $e) {
return $this->wepay_payment_driver->processInternallyFailedPayment($this->wepay_payment_driver, $e);
}
// display the response
// nlog($response);
if (in_array($response->state, ['new', 'authorized'])) {
$this->storePaymentMethod($response, GatewayType::CREDIT_CARD);
return redirect()->route('client.payment_methods.index');
}
throw new PaymentFailed('There was a problem adding this payment method.', 400);
/*
[credit_card_id] => 348084962473
[credit_card_name] => Visa xxxxxx4018
[state] => authorized
[user_name] => Joey Diaz
[email] => user@example.com
[create_time] => 1623798172
[expiration_month] => 10
[expiration_year] => 2023
[last_four] => 4018
[input_source] => card_keyed
[virtual_terminal_mode] => none
[card_on_file] =>
[recurring] =>
[cvv_provided] => 1
[auto_update] =>
*/
}
public function paymentView(array $data)
{
$data['gateway'] = $this->wepay_payment_driver;
$data['description'] = ctrans('texts.invoices').': '.collect($data['invoices'])->pluck('invoice_number');
$data['country_code'] = $this->wepay_payment_driver?->client?->country ? $this->wepay_payment_driver->client->country->iso_3166_2 : $this->wepay_payment_driver->company_gateway->company()->iso_3166_2;
return render('gateways.wepay.credit_card.pay', $data);
}
public function paymentResponse(PaymentResponseRequest $request)
{
nlog('payment response');
//it could be an existing token or a new credit_card_id that needs to be converted into a wepay token
if ($request->has('credit_card_id') && $request->input('credit_card_id')) {
nlog('authorize the card first!');
try {
$response = $this->wepay_payment_driver->wepay->request('credit_card/authorize', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'credit_card_id' => (int) $request->input('credit_card_id'),
]);
} catch (\Exception $e) {
return $this->wepay_payment_driver->processInternallyFailedPayment($this->wepay_payment_driver, $e);
}
$credit_card_id = (int) $response->credit_card_id;
if (in_array($response->state, ['new', 'authorized']) && boolval($request->input('store_card'))) {
$this->storePaymentMethod($response, GatewayType::CREDIT_CARD);
}
} else {
$credit_card_id = (int) $request->input('token');
}
// USD, CAD, and GBP.
// nlog($request->all());
$app_fee = (config('ninja.wepay.fee_cc_multiplier') * $this->wepay_payment_driver->payment_hash->data->amount_with_fee) + config('ninja.wepay.fee_fixed');
// charge the credit card
try {
$response = $this->wepay_payment_driver->wepay->request('checkout/create', [
'unique_id' => Str::random(40),
'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'),
'amount' => $this->wepay_payment_driver->payment_hash->data->amount_with_fee,
'currency' => $this->wepay_payment_driver->client->getCurrencyCode(),
'short_description' => 'Goods and services',
'type' => 'goods',
'fee' => [
'fee_payer' => config('ninja.wepay.fee_payer'),
'app_fee' => $app_fee,
],
'payment_method' => [
'type' => 'credit_card',
'credit_card' => [
'id' => $credit_card_id,
],
],
]);
} catch (\Exception $e) {
$this->wepay_payment_driver->sendFailureMail($e->getMessage());
$message = [
'server_response' => $e->getMessage(),
'data' => $this->wepay_payment_driver->payment_hash->data,
];
SystemLogger::dispatch(
$e->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_WEPAY,
$this->wepay_payment_driver->client,
$this->wepay_payment_driver->client->company,
);
throw new PaymentFailed($e->getMessage(), 500);
}
/* Merge all data and store in the payment hash*/
$state = [
'server_response' => $response,
'payment_hash' => $request->payment_hash,
];
$state = array_merge($state, $request->all());
$this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state);
$this->wepay_payment_driver->payment_hash->save();
if (in_array($response->state, ['authorized', 'captured'])) {
//success
nlog('success');
$payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING;
return $this->processSuccessfulPayment($response, $payment_status, GatewayType::CREDIT_CARD);
}
if (in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])) {
//some type of failure
nlog('failure');
$payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED;
$this->processUnSuccessfulPayment($response, $payment_status);
}
}
/*
new The checkout was created by the application. This state typically indicates that checkouts created in WePay's hosted checkout flow are waiting for the payer to submit their information.
authorized The payer entered their payment info and confirmed the payment on WePay. WePay has successfully charged the card.
captured The payment has been reserved from the payer.
released The payment has been credited to the payee account. Note that the released state may be active although there are active partial refunds or partial chargebacks.
cancelled The payment has been cancelled by the payer, payee, or application.
refunded The payment was captured and then refunded by the payer, payee, or application. The payment has been debited from the payee account.
charged back The payment has been charged back by the payer and the payment has been debited from the payee account.
failed The payment has failed.
expired Checkouts expire if they remain in the new state for more than 30 minutes (e.g., they have been abandoned).
*/
/*
https://developer.wepay.com/api/api-calls/checkout
{
"checkout_id": 649945633,
"account_id": 1548718026,
"type": "donation",
"short_description": "test checkout",
"currency": "USD",
"amount": 20,
"state": "authorized",
"soft_descriptor": "WPY*Wolverine",
"auto_release": true,
"create_time": 1463589958,
"gross": 20.88,
"reference_id": null,
"callback_uri": null,
"long_description": null,
"delivery_type": null,
"initiated_by": "merchant",
"in_review": false,
"fee": {
"app_fee": 0,
"processing_fee": 0.88,
"fee_payer": "payer"
},
"chargeback": {
"amount_charged_back": 0,
"dispute_uri": null
},
"refund": {
"amount_refunded": 0,
"refund_reason": null
},
"payment_method": {
"type": "credit_card",
"credit_card": {
"id": 1684847614,
"data": {
"emv_receipt": null,
"signature_url": null
},
"auto_release": false
}
},
"hosted_checkout": null,
"payer": {
"email": "test@example.com",
"name": "Mr Smith",
"home_address": null
},
"npo_information": null,
"payment_error": null
}
*/
private function storePaymentMethod($response, $payment_method_id)
{
nlog('storing card');
$payment_meta = new \stdClass();
$payment_meta->exp_month = (string) $response->expiration_month;
$payment_meta->exp_year = (string) $response->expiration_year;
$payment_meta->brand = (string) $response->credit_card_name;
$payment_meta->last4 = (string) $response->last_four;
$payment_meta->type = GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $response->credit_card_id,
'payment_method_id' => $payment_method_id,
];
$this->wepay_payment_driver->storeGatewayToken($data);
}
public function tokenBilling($cgt, $payment_hash)
{
$amount = array_sum(array_column($this->wepay_payment_driver->payment_hash->invoices(), 'amount')) + $this->wepay_payment_driver->payment_hash->fee_total;
$app_fee = (config('ninja.wepay.fee_cc_multiplier') * $amount) + config('ninja.wepay.fee_fixed');
// charge the credit card
$response = $this->wepay_payment_driver->wepay->request('checkout/create', [
'unique_id' => Str::random(40),
'account_id' => $this->wepay_payment_driver->company_gateway->getConfigField('accountId'),
'amount' => $amount,
'currency' => $this->wepay_payment_driver->client->getCurrencyCode(),
'short_description' => 'Goods and services',
'type' => 'goods',
'fee' => [
'fee_payer' => config('ninja.wepay.fee_payer'),
'app_fee' => $app_fee,
],
'payment_method' => [
'type' => 'credit_card',
'credit_card' => [
'id' => $cgt->token,
],
],
]);
/* Merge all data and store in the payment hash*/
$state = [
'server_response' => $response,
'payment_hash' => $payment_hash,
];
$this->wepay_payment_driver->payment_hash->data = array_merge((array) $this->wepay_payment_driver->payment_hash->data, $state);
$this->wepay_payment_driver->payment_hash->save();
if (in_array($response->state, ['authorized', 'captured'])) {
//success
nlog('success');
$payment_status = $response->state == 'authorized' ? Payment::STATUS_COMPLETED : Payment::STATUS_PENDING;
return $this->processSuccessfulPayment($response, $payment_status, GatewayType::CREDIT_CARD, true);
}
if (in_array($response->state, ['released', 'cancelled', 'failed', 'expired'])) {
//some type of failure
nlog('failure');
$payment_status = $response->state == 'cancelled' ? Payment::STATUS_CANCELLED : Payment::STATUS_FAILED;
$this->processUnSuccessfulPayment($response, $payment_status);
}
}
}

View File

@ -1,34 +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\WePay;
use App\PaymentDrivers\WePayPaymentDriver;
class Setup
{
public $wepay;
public function __construct(WePayPaymentDriver $wepay)
{
$this->wepay = $wepay;
}
public function boot($data)
{
/*
'user_id',
'user_company',
*/
return render('gateways.wepay.signup.index', $data);
}
}

View File

@ -1,75 +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\WePay;
use App\Exceptions\PaymentFailed;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\PaymentType;
use App\Models\SystemLog;
trait WePayCommon
{
private function processSuccessfulPayment($response, $payment_status, $gateway_type, $return_payment = false)
{
if ($gateway_type == GatewayType::BANK_TRANSFER) {
$payment_type = PaymentType::ACH;
} else {
$payment_type = PaymentType::CREDIT_CARD_OTHER;
}
$data = [
'payment_type' => $payment_type,
'amount' => $response->amount,
'transaction_reference' => $response->checkout_id,
'gateway_type_id' => $gateway_type,
];
$payment = $this->wepay_payment_driver->createPayment($data, $payment_status);
SystemLogger::dispatch(
['response' => $this->wepay_payment_driver->payment_hash->data->server_response, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_WEPAY,
$this->wepay_payment_driver->client,
$this->wepay_payment_driver->client->company,
);
if ($return_payment) {
return $payment;
}
return redirect()->route('client.payments.show', ['payment' => $this->wepay_payment_driver->encodePrimaryKey($payment->id)]);
}
private function processUnSuccessfulPayment($response, $payment_status)
{
$this->wepay_payment_driver->sendFailureMail($response->state);
$message = [
'server_response' => $response,
'data' => $this->wepay_payment_driver->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_WEPAY,
$this->wepay_payment_driver->client,
$this->wepay_payment_driver->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
}

View File

@ -17,13 +17,12 @@ use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\PaymentDrivers\WePay\ACH;
use App\PaymentDrivers\WePay\CreditCard;
use App\PaymentDrivers\WePay\Setup;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use WePay;
/**
* @deprecated 5.9
*/
class WePayPaymentDriver extends BaseDriver
{
use MakesHash;
@ -45,27 +44,14 @@ class WePayPaymentDriver extends BaseDriver
/* Maps the Payment Gateway Type - to its implementation */
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
GatewayType::BANK_TRANSFER => ACH::class,
];
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_WEPAY;
public function init()
{
if (WePay::getEnvironment() == 'none') {
if (config('ninja.wepay.environment') == 'staging') {
WePay::useStaging(config('ninja.wepay.client_id'), config('ninja.wepay.client_secret'));
} else {
WePay::useProduction(config('ninja.wepay.client_id'), config('ninja.wepay.client_secret'));
}
}
throw new \Exception("Gateway no longer supported", 500);
if ($this->company_gateway) {
$this->wepay = new WePay($this->company_gateway->getConfigField('accessToken'));
} else {
$this->wepay = new WePay(null);
}
return $this;
}
@ -93,7 +79,6 @@ class WePayPaymentDriver extends BaseDriver
*/
public function setup(array $data)
{
return (new Setup($this))->boot($data);
}
/**
@ -168,140 +153,17 @@ class WePayPaymentDriver extends BaseDriver
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
{
$this->init();
$input = $request->all();
$config = $this->company_gateway->getConfig();
$accountId = $this->company_gateway->getConfigField('accountId');
$objectId = false;
$objectType = '';
foreach (array_keys($input) as $key) {
if ('_id' == substr($key, -3)) {
$objectType = substr($key, 0, -3);
$objectId = $input[$key];
break;
}
}
if (! $objectId) {
throw new \Exception('Could not find object id parameter');
}
if ($objectType == 'credit_card') {
$payment_method = ClientGatewayToken::where('token', $objectId)->first();
if (! $payment_method) {
throw new \Exception('Unknown payment method');
}
$source = $this->wepay->request('credit_card', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'credit_card_id' => (int) $objectId,
]);
if ($source->state == 'deleted') {
$payment_method->delete();
} else {
//$this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save();
}
return 'Processed successfully';
} elseif ($objectType == 'account') {
if ($accountId != $objectId) {
throw new \Exception('Unknown account '.$accountId.' does not equal '.$objectId);
}
$wepayAccount = $this->wepay->request('account', [
'account_id' => (int) $objectId,
]);
if ($wepayAccount->state == 'deleted') {
$this->company_gateway->delete();
} else {
$config->state = $wepayAccount->state;
$this->company_gateway->setConfig($config);
$this->company_gateway->save();
}
return ['message' => 'Processed successfully'];
} elseif ($objectType == 'checkout') {
/** @var \App\Models\Payment $payment */
$payment = Payment::where('company_id', $this->company_gateway->company_id)
->where('transaction_reference', '=', $objectId)
->first();
if (! $payment) {
throw new \Exception('Unknown payment');
}
if ($payment->is_deleted) {
throw new \Exception('Payment is deleted');
}
$checkout = $this->wepay->request('checkout', [
'checkout_id' => intval($objectId),
]);
if ($checkout->state == 'captured') {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
} elseif ($checkout->state == 'cancelled') {
$payment->service()->deletePayment()->save();
} elseif ($checkout->state == 'failed') {
$payment->status_id = Payment::STATUS_FAILED;
$payment->save();
}
return 'Processed successfully';
} else {
return 'Ignoring event';
}
return true;
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
$this->init();
$response = $this->wepay->request('checkout/refund', [
'checkout_id' => $payment->transaction_reference,
'refund_reason' => 'Refund by merchant',
'amount' => $amount,
]);
return [
'transaction_reference' => $response->checkout_id,
'transaction_response' => json_encode($response),
'success' => $response->state == 'refunded' ? true : false,
'description' => 'refund',
'code' => 0,
];
}
public function detach(ClientGatewayToken $token)
{
/*Bank accounts cannot be deleted - only CC*/
if ($token->gateway_type_id == 2) {
return true;
}
$this->init();
$response = $this->wepay->request('/credit_card/delete', [
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'credit_card_id' => intval($token->token),
]);
if ($response->state == 'deleted') {
return true;
} else {
throw new \Exception(trans('texts.failed_remove_payment_method'));
}
}
public function getClientRequiredFields(): array

View File

@ -1,4 +1,13 @@
<?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\Providers;

View File

@ -26,7 +26,6 @@ class BankTransactionRepository extends BaseRepository
$bank_transaction->bank_integration_id = $data['bank_integration_id'];
}
$bank_transaction->fill($data);
$bank_transaction->save();
@ -43,7 +42,7 @@ class BankTransactionRepository extends BaseRepository
$data['transactions'] = $bank_transactions->map(function ($bt) {
return ['id' => $bt->id, 'invoice_ids' => $bt->invoice_ids, 'ninja_category_id' => $bt->ninja_category_id];
})->toArray();
$bts = (new MatchBankTransactions($user->company()->id, $user->company()->db, $data))->handle();
}

View File

@ -0,0 +1,288 @@
<?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\Standards;
use App\Models\Invoice;
use App\Services\AbstractService;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronica;
use Invoiceninja\Einvoice\Models\FatturaPA\IndirizzoType\Sede;
use Invoiceninja\Einvoice\Models\FatturaPA\AnagraficaType\Anagrafica;
use Invoiceninja\Einvoice\Models\FatturaPA\IdFiscaleType\IdFiscaleIVA;
use Invoiceninja\Einvoice\Models\FatturaPA\IdFiscaleType\IdTrasmittente;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiGeneraliType\DatiGenerali;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiPagamentoType\DatiPagamento;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiRiepilogoType\DatiRiepilogo;
use Invoiceninja\Einvoice\Models\FatturaPA\DettaglioLineeType\DettaglioLinee;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiBeniServiziType\DatiBeniServizi;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiTrasmissioneType\DatiTrasmissione;
use Invoiceninja\Einvoice\Models\FatturaPA\CedentePrestatoreType\CedentePrestatore;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiAnagraficiCedenteType\DatiAnagrafici;
use Invoiceninja\Einvoice\Models\FatturaPA\DettaglioPagamentoType\DettaglioPagamento;
use Invoiceninja\Einvoice\Models\FatturaPA\DatiGeneraliDocumentoType\DatiGeneraliDocumento;
use Invoiceninja\Einvoice\Models\FatturaPA\CessionarioCommittenteType\CessionarioCommittente;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronicaBodyType\FatturaElettronicaBody;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronicaHeaderType\FatturaElettronicaHeader;
class FatturaPANew extends AbstractService
{
private FatturaElettronica $FatturaElettronica;
private FatturaElettronicaBody $FatturaElettronicaBody;
private FatturaElettronicaHeader $FatturaElettronicaHeader;
private DatiTrasmissione $DatiTrasmissione;
private IdTrasmittente $IdTrasmittente;
private CedentePrestatore $CedentePrestatore;
private DatiAnagrafici $DatiAnagrafici;
private IdFiscaleIVA $IdFiscaleIVA;
private Anagrafica $Anagrafica;
private DatiGeneraliDocumento $DatiGeneraliDocumento;
private DatiGenerali $DatiGenerali;
private DettaglioPagamento $DettaglioPagamento;
/**
* @param Invoice $invoice
*/
public function __construct(public Invoice $invoice)
{
}
public function run()
{
$this->init()
->setIdTrasmittente() //order of execution matters.
->setDatiTrasmissione()
->setIdFiscaleIVA()
->setAnagrafica()
->setDatiAnagrafici()
->setCedentePrestatore()
->setClientDetails()
->setDatiGeneraliDocumento()
->setDatiGenerali()
->setLineItems()
->setDettaglioPagamento()
->setFatturaElettronica();
}
public function getFatturaElettronica(): FatturaElettronica
{
return $this->FatturaElettronica;
}
private function setDatiTrasmissione(): self
{
$this->DatiTrasmissione->FormatoTrasmissione = "FPR12";
$this->DatiTrasmissione->CodiceDestinatario = $this->invoice->client->routing_id;
$this->DatiTrasmissione->ProgressivoInvio = $this->invoice->number;
$this->DatiTrasmissione->IdTrasmittente = $this->IdTrasmittente;
$this->FatturaElettronicaHeader->DatiTrasmissione = $this->DatiTrasmissione;
return $this;
}
private function setIdTrasmittente():self
{
$this->IdTrasmittente->IdPaese = $this->invoice->company->country()->iso_3166_2;
$this->IdTrasmittente->IdCodice = $this->invoice->company->settings->vat_number;
return $this;
}
private function setCedentePrestatore():self
{
$this->CedentePrestatore->DatiAnagrafici = $this->DatiAnagrafici;
$sede = new Sede;
$sede->Indirizzo = $this->invoice->company->settings->address1;
$sede->CAP = (int)$this->invoice->company->settings->postal_code;
$sede->Comune = $this->invoice->company->settings->city;
$sede->Provincia = $this->invoice->company->settings->state;
$sede->Nazione = $this->invoice->company->country()->iso_3166_2;
$this->CedentePrestatore->Sede = $sede;
$this->FatturaElettronicaHeader->CedentePrestatore = $this->CedentePrestatore;
return $this;
}
private function setDatiAnagrafici():self
{
$this->DatiAnagrafici->RegimeFiscale = "RF01";
$this->DatiAnagrafici->Anagrafica = $this->Anagrafica;
$this->DatiAnagrafici->IdFiscaleIVA = $this->IdFiscaleIVA;
return $this;
}
private function setClientDetails():self
{
$datiAnagrafici = new DatiAnagrafici();
$anagrafica = new Anagrafica();
$anagrafica->Denominazione = $this->invoice->client->present()->name();
$datiAnagrafici->Anagrafica = $anagrafica;
$idFiscale = new IdFiscaleIVA;
$idFiscale->IdCodice= $this->invoice->client->vat_number;
$idFiscale->IdPaese = $this->invoice->client->country->iso_3166_2;
$datiAnagrafici->IdFiscaleIVA = $idFiscale;
$sede = new Sede;
$sede->Indirizzo = $this->invoice->client->address1;
$sede->CAP = (int)$this->invoice->client->postal_code;
$sede->Comune = $this->invoice->client->city;
$sede->Provincia = $this->invoice->client->state;
$sede->Nazione = $this->invoice->client->country->iso_3166_2;
$cessionarioCommittente = new CessionarioCommittente;
$cessionarioCommittente->DatiAnagrafici = $datiAnagrafici;
$cessionarioCommittente->Sede = $sede;
$this->FatturaElettronicaHeader->CessionarioCommittente = $cessionarioCommittente;
return $this;
}
private function setIdFiscaleIVA():self
{
$this->IdFiscaleIVA->IdPaese = $this->invoice->company->country()->iso_3166_2;
$this->IdFiscaleIVA->IdCodice = $this->invoice->company->settings->vat_number;
return $this;
}
//this is a choice, need to switch based on values here.
private function setAnagrafica():self
{
$this->Anagrafica->Denominazione = $this->invoice->company->present()->name();
return $this;
}
private function setDatiGeneraliDocumento():self
{
$this->DatiGeneraliDocumento->TipoDocumento = "TD01";
$this->DatiGeneraliDocumento->Divisa = $this->invoice->client->currency()->code;
$this->DatiGeneraliDocumento->Data = new \DateTime($this->invoice->date);
$this->DatiGeneraliDocumento->Numero = $this->invoice->number;
$this->DatiGeneraliDocumento->Causale[] = substr($this->invoice->public_notes ?? '',0, 200); //unsure..
return $this;
}
private function setDatiGenerali():self
{
$this->DatiGenerali->DatiGeneraliDocumento = $this->DatiGeneraliDocumento;
$this->FatturaElettronicaBody->DatiGenerali = $this->DatiGenerali;
return $this;
}
private function setDettaglioPagamento():self
{
$this->DettaglioPagamento->ModalitaPagamento = "MP01"; //String
$this->DettaglioPagamento->DataScadenzaPagamento = new \DateTime($this->invoice->due_date ?? $this->invoice->date);
$this->DettaglioPagamento->ImportoPagamento = (string) sprintf('%0.2f', $this->invoice->balance);
$DatiPagamento = new DatiPagamento;
$DatiPagamento->CondizioniPagamento = "TP02";
$DatiPagamento->DettaglioPagamento[] = $this->DettaglioPagamento;
$this->FatturaElettronicaBody->DatiPagamento[] = $DatiPagamento;
return $this;
}
private function setLineItems(): self
{
$calc = $this->invoice->calc();
$datiBeniServizi = new DatiBeniServizi();
$tax_rate_level = 0;
//line items
foreach ($this->invoice->line_items as $key => $item) {
$numero = $key + 1;
$dettaglioLinee = new DettaglioLinee;
$dettaglioLinee->NumeroLinea = "{$numero}";
$dettaglioLinee->Descrizione = $item->notes ?? 'Descrizione';
$dettaglioLinee->Quantita = sprintf('%0.2f', $item->quantity);
$dettaglioLinee->PrezzoUnitario = sprintf('%0.2f', $item->cost);
$dettaglioLinee->PrezzoTotale = sprintf('%0.2f', $item->line_total);
$dettaglioLinee->AliquotaIVA = sprintf('%0.2f', $item->tax_rate1);
$datiBeniServizi->DettaglioLinee[] = $dettaglioLinee;
if ($item->tax_rate1 > $tax_rate_level) {
$tax_rate_level = sprintf('%0.2f', $item->tax_rate1);
}
}
//totals
if($this->invoice->tax_rate1 > $tax_rate_level) {
$tax_rate_level = sprintf('%0.2f', $this->invoice->tax_rate1);
}
$subtotal = sprintf('%0.2f', $calc->getSubTotal());
$taxes = sprintf('%0.2f', $calc->getTotalTaxes());
$datiRiepilogo = new DatiRiepilogo;
$datiRiepilogo->AliquotaIVA = "{$tax_rate_level}";
$datiRiepilogo->ImponibileImporto = "{$subtotal}";
$datiRiepilogo->Imposta = "{$taxes}";
$datiRiepilogo->EsigibilitaIVA = "I";
$datiBeniServizi->DatiRiepilogo[] = $datiRiepilogo;
$this->FatturaElettronicaBody->DatiBeniServizi = $datiBeniServizi;
return $this;
}
private function setFatturaElettronica(): self
{
$this->FatturaElettronica->FatturaElettronicaBody[] = $this->FatturaElettronicaBody;
$this->FatturaElettronica->FatturaElettronicaHeader = $this->FatturaElettronicaHeader;
return $this;
}
private function init(): self
{
$this->FatturaElettronica = new FatturaElettronica;
$this->FatturaElettronicaBody = new FatturaElettronicaBody;
$this->FatturaElettronicaHeader = new FatturaElettronicaHeader;
$this->DatiTrasmissione = new DatiTrasmissione;
$this->IdTrasmittente = new IdTrasmittente;
$this->CedentePrestatore = new CedentePrestatore;
$this->DatiAnagrafici = new DatiAnagrafici;
$this->IdFiscaleIVA = new IdFiscaleIVA;
$this->Anagrafica = new Anagrafica;
$this->DatiGeneraliDocumento = new DatiGeneraliDocumento;
$this->DatiGenerali = new DatiGenerali;
$this->DettaglioPagamento = new DettaglioPagamento;
return $this;
}
}

View File

@ -302,7 +302,26 @@ class Email implements ShouldQueue
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch (\Exception | \RuntimeException | \Google\Service\Exception $e) {
}
catch(\Symfony\Component\Mailer\Transport\Dsn $e){
nlog("Incorrectly configured mail server - setting to default mail driver.");
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
catch(\Google\Service\Exception $e){
if ($e->getCode() == '429') {
$message = "Google rate limiting triggered, we are queueing based on Gmail requirements.";
$this->logMailError($message, $this->company->clients()->first());
sleep(rand(1, 2));
$this->release(900);
$message = null;
}
}
catch (\Exception | \RuntimeException $e) {
nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage();
@ -916,15 +935,20 @@ class Email implements ShouldQueue
$guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token
],
])->getBody()->getContents());
try {
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token
],
])->getBody()->getContents());
}
catch(\Exception $e){
nlog("Problem getting new Microsoft token for User: {$user->email}");
}
if ($token) {
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_token;

View File

@ -166,9 +166,9 @@ class EmailDefaults
private function setBody(): self
{
if (strlen($this->email->email_object->body) > 3) {
if (strlen($this->email->email_object->body ?? '') > 3) {
// A Custom Message has been set in the email screen.
} elseif (strlen($this->email->email_object->settings?->{$this->email->email_object->email_template_body}) > 3) {
} elseif (strlen($this->email->email_object->settings?->{$this->email->email_object->email_template_body} ?? '') > 3) {
// A body has been saved in the settings.
$this->email->email_object->body = $this->email->email_object->settings?->{$this->email->email_object->email_template_body};
} else {

View File

@ -59,6 +59,7 @@ class DeletePayment
$this->payment->delete();
BankTransaction::query()->where('payment_id', $this->payment->id)->cursor()->each(function ($bt) {
$bt->invoice_ids = null;
$bt->payment_id = null;
$bt->status_id = 1;
$bt->save();

View File

@ -27,11 +27,13 @@ use App\Models\PurchaseOrderInvitation;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\Vendor;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
class PdfMock
{
use MakesHash;
use GeneratesCounter;
private mixed $mock;
@ -206,6 +208,20 @@ class PdfMock
*/
public function getStubVariables(): array
{
$entity_pattern = $this->entity_string.'_number_pattern';
$entity_number = '0029';
if (!empty($this->settings->{$entity_pattern})) {
// Although $this->mock is the Invoice/etc entity,
// we need the invitation to get company details.
$entity_number = $this->getFormattedEntityNumber(
$this->mock->invitation,
(int) $entity_number,
$this->settings->counter_padding,
$this->settings->{$entity_pattern},
);
}
return ['values' =>
[
'$client.shipping_postal_code' => '46420',
@ -309,10 +325,10 @@ class PdfMock
'$invoice.custom2' => 'custom value',
'$invoice.custom3' => 'custom value',
'$invoice.custom4' => 'custom value',
'$company.custom1' => $this->company->custom_value1,
'$company.custom2' => $this->company->custom_value2,
'$company.custom3' => $this->company->custom_value3,
'$company.custom4' => $this->company->custom_value4,
'$company.custom1' => $this->company->settings->custom_value1,
'$company.custom2' => $this->company->settings->custom_value2,
'$company.custom3' => $this->company->settings->custom_value3,
'$company.custom4' => $this->company->settings->custom_value4,
'$quote.po_number' => 'PO12345',
'$company.website' => $this->settings->website,
'$balance_due_raw' => '0.00',
@ -325,7 +341,7 @@ class PdfMock
'$client.currency' => 'USD',
'$company.country' => $this->company->country()?->name ?? 'USA',
'$company.address' => $this->company->present()->address(),
'$tech_hero_image' => 'http://ninja.test:8000/images/pdf-designs/tech-hero-image.jpg',
'$tech_hero_image' => 'https://invoicing.co/images/pdf-designs/tech-hero-image.jpg',
'$task.tax_name1' => '',
'$task.tax_name2' => '',
'$task.tax_name3' => '',
@ -370,7 +386,7 @@ class PdfMock
'$company.phone' => $this->settings->phone,
'$company.state' => $this->settings->state,
'$credit.number' => '0029',
'$entity_number' => '0029',
'$entity_number' => $entity_number,
'$credit_number' => '0029',
'$global_margin' => '6.35mm',
'$contact.phone' => '681-480-9828',
@ -456,10 +472,10 @@ class PdfMock
'$task.tax' => '',
'$discount' => '$0.00',
'$subtotal' => '$0.00',
'$company1' => $this->company->custom_value1,
'$company2' => $this->company->custom_value2,
'$company3' => $this->company->custom_value3,
'$company4' => $this->company->custom_value4,
'$company1' => $this->company->settings->custom_value1,
'$company2' => $this->company->settings->custom_value2,
'$company3' => $this->company->settings->custom_value3,
'$company4' => $this->company->settings->custom_value4,
'$due_date' => '2022-01-01',
'$poNumber' => 'PO-123456',
'$quote_no' => '0029',
@ -606,10 +622,10 @@ class PdfMock
'$task.description_label' => ctrans('texts.description'),
'$product.discount_label' => ctrans('texts.discount'),
'$product.quantity_label' => ctrans('texts.quantity'),
'$entity_issued_to_label' => ctrans('texts.quote_issued_to'),
'$entity_issued_to_label' => ctrans("texts.{$this->entity_string}_issued_to") ?: ctrans('texts.quote_issued_to'),
'$partial_due_date_label' => ctrans('texts.partial_due_date'),
'$invoice.datetime_label' => ctrans('texts.datetime_format_id'),
'$invoice.due_date_label' => ctrans('texts.due_date'),
'$invoice.due_date_label' => ctrans('texts.invoice_due_date'),
'$company.address1_label' => ctrans('texts.address1'),
'$company.address2_label' => ctrans('texts.address2'),
'$total_tax_labels_label' => ctrans('texts.total_taxes'),
@ -704,16 +720,16 @@ class PdfMock
'$contact.email_label' => ctrans('texts.email'),
'$invoice.taxes_label' => ctrans('texts.taxes'),
'$credit_amount_label' => ctrans('texts.credit_amount'),
'$invoice.total_label' => ctrans('texts.total'),
'$invoice.total_label' => ctrans('texts.invoice_total'),
'$product.date_label' => ctrans('texts.date'),
'$product.item_label' => ctrans('texts.item'),
'$public_notes_label' => ctrans('texts.public_notes'),
'$entity.terms_label' => ctrans('texts.terms'),
'$task.service_label' => ctrans('texts.service'),
'$portalButton_label' => '',
'$payment.date_label' => ctrans('texts.date'),
'$payment.date_label' => ctrans('texts.payment_date'),
'$client.phone_label' => ctrans('texts.phone'),
'$invoice.date_label' => ctrans('texts.date'),
'$invoice.date_label' => ctrans('texts.invoice_date'),
'$client.state_label' => ctrans('texts.state'),
'$number_short_label' => '',
'$quote.number_label' => ctrans('texts.number'),

View File

@ -288,7 +288,6 @@ trait GeneratesCounter
*/
public function getNextProjectNumber(Project $project): string
{
$entity_number = $this->getNextEntityNumber(Project::class, $project->client, false);
return $this->replaceUserVars($project, $entity_number);
@ -412,7 +411,7 @@ trait GeneratesCounter
*
* @param string $pattern
* @param string $prefix
* @return string The padded and prefixed entity number
* @return string The padded, prefixed and unique entity number
*/
private function checkEntityNumber($class, $entity, $counter, $padding, $pattern, $prefix = ''): string
{
@ -420,11 +419,7 @@ trait GeneratesCounter
$check_counter = 1;
do {
$number = $this->padCounter($counter, $padding);
$number = $this->applyNumberPattern($entity, $number, $pattern);
$number = $this->prefixCounter($number, $prefix);
$number = $this->getFormattedEntityNumber($entity, $counter, $padding, $pattern);
$check = $class::where('company_id', $entity->company_id)->where('number', $number)->withTrashed()->exists();
@ -443,6 +438,26 @@ trait GeneratesCounter
return $number;
}
/**
* Formats the entity number according to pattern, prefix and padding.
*
* @param Collection $entity The entity ie App\Models\Client, Invoice, Quote etc
* @param int $counter The counter
* @param int $padding The padding
* @param string $pattern
* @param string $prefix
*
* @return string The padded and prefixed entity number
*/
public function getFormattedEntityNumber($entity, $counter, $padding, $pattern, $prefix = ''): string
{
$number = $this->padCounter($counter, $padding);
$number = $this->applyNumberPattern($entity, $number, $pattern);
return $this->prefixCounter($number, $prefix);
}
/*Check if a number is available for use. */
public function checkNumberAvailable($class, $entity, $number): bool
{

View File

@ -31,7 +31,7 @@
],
"type": "project",
"require": {
"php": "^8.1|^8.2",
"php": "^8.2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
@ -56,7 +56,6 @@
"hashids/hashids": "^4.0",
"hedii/laravel-gelf-logger": "^8",
"horstoeko/zugferd": "^1",
"horstoeko/orderx": "^1",
"imdhemy/laravel-purchases": "^1.7",
"intervention/image": "^2.5",
"invoiceninja/inspector": "^2.0",
@ -87,7 +86,6 @@
"sentry/sentry-laravel": "^3",
"setasign/fpdf": "^1.8",
"setasign/fpdi": "^2.3",
"shopify/shopify-api": "^4.3",
"socialiteproviders/apple": "dev-master",
"socialiteproviders/microsoft": "^4.1",
"spatie/laravel-data": "^3.5",
@ -103,12 +101,13 @@
"twig/twig": "^3",
"twilio/sdk": "^6.40",
"webpatser/laravel-countries": "dev-master#75992ad",
"wepay/php-sdk": "^0.3",
"wildbit/postmark-php": "^4.0",
"hyvor/php-json-exporter": "^0.0.3"
"hyvor/php-json-exporter": "^0.0.3",
"invoiceninja/einvoice": "dev-main",
"horstoeko/orderx": "dev-master"
},
"require-dev": {
"php": "^8.1|^8.2",
"php": "^8.2",
"barryvdh/laravel-debugbar": "^3.6",
"barryvdh/laravel-ide-helper": "^2.13",
"beyondcode/laravel-query-detector": "^1.8",
@ -120,7 +119,7 @@
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^10.0",
"phpunit/phpunit": "^10",
"spatie/laravel-ignition": "^2.0",
"spaze/phpstan-stripe": "^3.0"
},
@ -184,6 +183,10 @@
{
"type":"vcs",
"url": "https://github.com/invoiceninja/einvoice"
},
{
"type": "vcs",
"url": "https://github.com/turbo124/orderx"
}
],
"minimum-stability": "dev",

1037
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -54,17 +54,14 @@ return [
'hosted' => env('TERMS_OF_SERVICE_URL', 'https://www.invoiceninja.com/terms/'),
'selfhost' => env('TERMS_OF_SERVICE_URL', 'https://www.invoiceninja.com/self-hosting-terms-service/'),
],
'privacy_policy_url' => [
'hosted' => env('PRIVACY_POLICY_URL', 'https://www.invoiceninja.com/privacy-policy/'),
'selfhost' => env('PRIVACY_POLICY_URL', 'https://www.invoiceninja.com/self-hosting-privacy-data-control/'),
],
'db' => [
'multi_db_enabled' => env('MULTI_DB_ENABLED', false),
'default' => env('DB_CONNECTION', 'mysql'),
],
'i18n' => [
'timezone_id' => env('DEFAULT_TIMEZONE', 1),
'country_id' => env('DEFAULT_COUNTRY', 840), // United Stated
@ -79,7 +76,6 @@ return [
'first_day_of_week' => env('FIRST_DATE_OF_WEEK', 0),
'first_month_of_year' => env('FIRST_MONTH_OF_YEAR', '2000-01-01'),
],
'testvars' => [
'username' => 'user@example.com',
'clientname' => 'client@example.com',

View File

@ -13,6 +13,7 @@ namespace Database\Factories;
use App\DataMapper\ClientSettings;
use Illuminate\Database\Eloquent\Factories\Factory;
use Str;
class ClientFactory extends Factory
{
@ -49,6 +50,7 @@ class ClientFactory extends Factory
'shipping_country_id' => 4,
'settings' => ClientSettings::defaults(),
'client_hash' => \Illuminate\Support\Str::random(40),
'routing_id' => rand(100000,200000),
];
}
}

View File

@ -0,0 +1,28 @@
<?php
use App\Models\Currency;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if($currency = Currency::find(89)){
$currency->precision = 3;
$currency->save();
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -111,7 +111,7 @@ class CurrenciesSeeder extends Seeder
['id' => 86, 'name' => 'CFP Franc', 'code' => 'XPF', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], // precision should be zero
['id' => 87, 'name' => 'Mauritian Rupee', 'code' => 'MUR', 'symbol' => 'Rs', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 88, 'name' => 'Cape Verdean Escudo', 'code' => 'CVE', 'symbol' => '', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => '$'],
['id' => 89, 'name' => 'Kuwaiti Dinar', 'code' => 'KWD', 'symbol' => 'KD', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 89, 'name' => 'Kuwaiti Dinar', 'code' => 'KWD', 'symbol' => 'KD', 'precision' => '3', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 90, 'name' => 'Algerian Dinar', 'code' => 'DZD', 'symbol' => 'DA', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 91, 'name' => 'Macedonian Denar', 'code' => 'MKD', 'symbol' => 'ден', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
['id' => 92, 'name' => 'Fijian Dollar', 'code' => 'FJD', 'symbol' => 'FJ$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],

View File

@ -199,7 +199,7 @@ $lang = array(
'removed_logo' => 'Successfully removed logo',
'sent_message' => 'Successfully sent message',
'invoice_error' => 'Please make sure to select a client and correct any errors',
'limit_clients' => 'You\'ve hit the :count client limit on Free accounts. Congrats on your success!.',
'limit_clients' => 'You\'ve hit the :count client limit on Free accounts. Congrats on your success!',
'payment_error' => 'There was an error processing your payment. Please try again later.',
'registration_required' => 'Registration Required',
'confirmation_required' => 'Please confirm your email address, :link to resend the confirmation email.',
@ -5147,7 +5147,7 @@ $lang = array(
'payment_refund_receipt' => 'Payment Refund Receipt # :number',
'payment_receipt' => 'Payment Receipt # :number',
'load_template_description' => 'The template will be applied to following:',
'run_template' => 'Run template',
'run_template' => 'Run Template',
'statement_design' => 'Statement Design',
'delivery_note_design' => 'Delivery Note Design',
'payment_receipt_design' => 'Payment Receipt Design',
@ -5306,18 +5306,20 @@ $lang = array(
'activity_140' => 'Statement sent to :client',
'invoice_net_amount' => 'Invoice Net Amount',
'round_to_minutes' => 'Round To Minutes',
'1_second' => '1 Second',
'1_minute' => '1 Minute',
'5_minutes' => '5 Minutes',
'15_minutes' => '15 Minutes',
'30_minutes' => '30 Minutes',
'1_hour' => '1 Hour',
'1_day' => '1 Day',
'round_tasks' => 'Round Tasks',
'round_tasks_help' => 'Round time intervals when saving tasks',
'round_tasks' => 'Task Rounding Direction',
'round_tasks_help' => 'Round task times up or down.',
'direction' => 'Direction',
'round_up' => 'Round Up',
'round_down' => 'Round Down',
'task_round_to_nearest' => 'Round To Nearest',
'task_round_to_nearest_help' => 'The interval to round the task to.',
'bulk_updated' => 'Successfully updated data',
'bulk_update' => 'Bulk Update',
'calculate' => 'Calculate',
@ -5330,6 +5332,8 @@ $lang = array(
'e_invoice_settings' => 'E-Invoice Settings',
'btcpay_refund_subject' => 'Refund of your invoice via BTCPay',
'btcpay_refund_body' => 'A refund intended for you has been issued. To claim it via BTCPay, please click on this link:',
'currency_mauritanian_ouguiya' => 'Mauritanian Ouguiya',
'currency_bhutan_ngultrum' => 'Bhutan Ngultrum',
);
return $lang;

View File

@ -5322,6 +5322,12 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'money' => 'Argent',
'web_app' => 'App web',
'desktop_app' => 'App de bureau',
'disconnected' => 'Déconnecté',
'reconnect' => 'Reconnecté',
'e_invoice_settings' => 'Paramètres E-Facture',
'currency_mauritanian_ouguiya' => 'Ouguiya mauritanien',
'currency_bhutan_ngultrum' => 'Ngultrum Bhoutan',
);
return $lang;

1982
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,18 +9,20 @@
"@babel/compat-data": "7.15.0",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.18",
"cypress": "^12.5.1",
"laravel-mix-purgecss": "^6.0.0",
"laravel-vite-plugin": "^0.8.0",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"vite": "^4.4.9",
"vite-plugin-static-copy": "^0.17.0",
"vue-template-compiler": "^2.6.14"
},
"dependencies": {
"@tailwindcss/forms": "^0.3.4",
"@tailwindcss/line-clamp": "^0.3.1",
"@tailwindcss/typography": "^0.4.1",
"autoprefixer": "^10.3.7",
"axios": "^0.25",
"card-js": "^1.0.13",
"card-validator": "^8.1.1",
@ -31,11 +33,9 @@
"laravel-mix": "^6.0.34",
"linkify-urls": "^4.0.0",
"lodash": "^4.17.21",
"postcss": "^8.3.11",
"resolve-url-loader": "^4.0.0",
"sass": "^1.43.4",
"sass-loader": "^12.3.0",
"tailwindcss": "^2.2.17"
"sass-loader": "^12.3.0"
},
"type": "module"
}

View File

@ -25,7 +25,7 @@
<env name="MAIL_MAILER" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="DB_STRICT" value="FALSE"/>
<env name="DB_STRICT" value="false"/>
</php>
<logging/>
</phpunit>

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/build/assets/app-c7c5fad4.css 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-a52d5f77.js",
"file": "assets/app-8722f22d.js",
"imports": [
"_index-08e160a7.js",
"__commonjsHelpers-725317a4.js"
@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
"file": "assets/app-c6dc74fe.css",
"file": "assets/app-c7c5fad4.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}

View File

@ -3,7 +3,7 @@
}
.alert-success {
@apply border-green-500;
@apply border-emerald-500;
}
.alert-failure {

View File

@ -15,7 +15,7 @@
}
.badge-success {
@apply bg-green-100 text-green-500;
@apply bg-emerald-100 text-emerald-500;
}
.badge-secondary {
@ -23,7 +23,7 @@
}
.badge-warning {
@apply bg-blue-500 text-yellow-600;
@apply bg-blue-500 text-amber-600;
}
.badge-info {

View File

@ -7,5 +7,5 @@
}
.validation-pass {
@apply border-green-500 text-gray-700 text-sm;
@apply border-emerald-500 text-gray-700 text-sm;
}

View File

@ -132,7 +132,7 @@
<div class="flex justify-between items-center mt-8">
<a href="{{route('client.login')}}" class="button button-info bg-green-600 text-white">{{ ctrans('texts.login_label') }}</a>
<a href="{{route('client.login')}}" class="button button-info bg-emerald-600 text-white">{{ ctrans('texts.login_label') }}</a>
<span class="inline-flex items-center" x-data="{ terms_of_service: false, privacy_policy: false }">
@if(!empty($register_company->settings->client_portal_terms) || !empty($register_company->settings->client_portal_privacy_policy))

View File

@ -250,7 +250,7 @@
@if($steps['passwordless_login_sent'])
<span
class="block mt-2 text-sm text-green-600">{!! ctrans('texts.sent') !!}</span>
class="block mt-2 text-sm text-emerald-600">{!! ctrans('texts.sent') !!}</span>
@endif
@endif

View File

@ -5,10 +5,10 @@
<div class="flex flex-col xl:flex-row gap-4">
<div class="w-full rounded-md border border-[#E5E7EB] bg-white p-5 text-sm text-[#6C727F]">
<h3 class="mb-4 text-xl font-semibold text-[#212529]">{{ $contact->first_name }} {{ $contact->last_name }}</h3>
<p class="mb-1.5">{{ $contact->phone }}</p>
<p class="mb-4">{{ $client->address1 }}</p>
<p class="mb-1.5">{{ $client->city }}, {{ $client->state }}</p>
<p class="mb-1.5">{{ $client->postal_code }}</p>
<p>{{ $contact->phone }}</p>
<p>{{ $client->address1 }}</p>
<p>{{ $client->city }}, {{ $client->state }}</p>
<p>{{ $client->postal_code }}</p>
<p>{{ App\Models\Country::find($client->country_id)?->name }}</p>
</div>
@ -81,16 +81,24 @@
</div>
</div>
<div class="text-light-grey-text flex grow basis-full flex-col justify-center pt-5 text-sm md:basis-1/2 md:border-r md:border-[#E5E7EB] md:pt-0 xl:basis-auto xl:px-5">
<p class="mb-2">{{ $client->company->settings->address1 }}</p>
<p class="mb-2">{{ $client->company->settings->address2 }}</p>
<p class="mb-2">{{ $client->company->settings->postal_code }}</p>
<div class="text-light-grey-text flex grow basis-full flex-col justify-center pt-5 text-sm md:basis-1/2 md:border-r md:border-[#E5E7EB] md:pt-0 xl:basis-auto xl:px-5 space-y-2">
<p>{{ $client->company->settings->address1 }}</p>
<p>{{ $client->company->settings->city }} {{ $client->company->settings->state }}</p>
<p>{{ $client->company->settings->postal_code }}</p>
<p>{{ App\Models\Country::find($client->company->settings->country_id)?->name }}</p>
</div>
<div class="text-light-grey-text flex grow basis-full flex-col justify-center text-sm md:basis-1/2 md:pl-4 xl:basis-auto xl:px-5">
<p class="mb-2">{{ $client->company->settings->email }}</p>
<p class="mb-2">{{ $client->company->settings->phone }}</p>
<p>{{ $client->company->settings->website }}</p>
<div class="text-light-grey-text flex grow basis-full flex-col justify-center text-sm md:basis-1/2 md:pl-4 xl:basis-auto xl:px-5 space-y-2 mt-3 xl:mt-0">
<p><span class="font-semibold">{{ ctrans('texts.vat') }}</span>: {{ $client->company->settings->vat_number }}</p>
<p>
<a class="underline" href="mailto:{{ $client->company->settings->email }}" target="_blank">{{ $client->company->settings->email }}</a>
</p>
<p>{{ $client->company->settings->phone }}</p>
<p>
<a class="underline" href="{{ $client->company->settings->website }}" target="_blank">
{{ $client->company->settings->website }}
</a>
</p>
</div>
</div>
@stop

View File

@ -34,7 +34,7 @@
@if(Request::isSecure())
<span class="block mx-4 mb-4 text-xs inline-flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-600"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-emerald-600"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
<span class="ml-1">Secure 256-bit encryption</span>
</span>
@endif

View File

@ -56,7 +56,7 @@
</div>
</div>
</div>
<div class="bg-white shadow rounded-sm mb-4 mt-4 border-l-2 border-green-500" translate>
<div class="bg-white shadow rounded-sm mb-4 mt-4 border-l-2 border-emerald-500" translate>
<div class="px-4 py-5 sm:p-6">
<div class="sm:flex sm:items-start sm:justify-between">
<div>

View File

@ -13,7 +13,6 @@ use App\Http\Controllers\Gateways\Mollie3dsController;
use App\Http\Controllers\SetupController;
use App\Http\Controllers\StripeConnectController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\WePayController;
use Illuminate\Support\Facades\Route;
Route::get('/', [BaseController::class, 'flutterRoute'])->middleware('guest');
@ -31,9 +30,6 @@ Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEm
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->middleware(['domain_db', 'email_db'])->name('password.reset');
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->middleware('email_db')->name('password.update');
Route::get('wepay/signup/{token}', [WePayController::class, 'signup'])->name('wepay.signup');
Route::get('wepay/finished', [WePayController::class, 'finished'])->name('wepay.finished');
Route::get('auth/{provider}', [LoginController::class, 'redirectToProvider']);
Route::middleware('url_db')->group(function () {

View File

@ -1,27 +1,24 @@
const defaultTheme = require("tailwindcss/defaultTheme");
const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = {
purge: [
content: [
'./resources/views/portal/ninja2020/**/*.blade.php',
'./resources/views/email/template/**/*.blade.php',
'./resources/views/email/components/**/*.blade.php',
'./resources/views/themes/ninja2020/**/*.blade.php',
'./resources/views/auth/**/*.blade.php',
'./resources/views/setup/**/*.blade.php',
'./resources/views/billing-portal/**/*.blade.php',
'./resources/views/billing-portal/**/*.blade.php'
],
theme: {
extend: {
fontFamily: {
sans: ["Open Sans", ...defaultTheme.fontFamily.sans]
}
}
sans: ['Open Sans', ...defaultTheme.fontFamily.sans],
},
},
},
variants: {},
plugins: [
require('@tailwindcss/line-clamp'),
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
]
],
};

View File

@ -11,13 +11,14 @@
namespace Tests\Feature;
use App\Models\CompanyGateway;
use Tests\TestCase;
use Tests\MockAccountData;
use App\Models\GatewayType;
use App\Models\CompanyGateway;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
@ -29,6 +30,9 @@ class ClientGatewayTokenApiTest extends TestCase
use DatabaseTransactions;
use MockAccountData;
protected $faker;
protected CompanyGateway $cg;
protected function setUp() :void
{
parent::setUp();

View File

@ -11,11 +11,21 @@
namespace Tests\Feature\EInvoice;
use App\Services\EDocument\Standards\FatturaPA;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Company;
use Tests\MockAccountData;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\DataMapper\InvoiceItem;
use App\Models\Invoice;
use Invoiceninja\Einvoice\Symfony\Encode;
use App\Services\EDocument\Standards\FatturaPANew;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronica;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronicaBodyType\FatturaElettronicaBody;
use Invoiceninja\Einvoice\Models\FatturaPA\FatturaElettronicaHeaderType\FatturaElettronicaHeader;
/**
* @test
@ -31,6 +41,9 @@ class FatturaPATest extends TestCase
$this->makeTestData();
$this->markTestSkipped('prevent running in CI');
$this->withoutMiddleware(
ThrottleRequests::class
);
@ -38,9 +51,86 @@ class FatturaPATest extends TestCase
public function testInvoiceBoot()
{
$fat = new FatturaPA($this->invoice);
$xml = $fat->run();
$this->assertnotNull($xml);
$settings = CompanySettings::defaults();
$settings->address1 = 'Via Silvio Spaventa 108';
$settings->city = 'Calcinelli';
$settings->state = 'PA';
// $settings->state = 'Perugia';
$settings->postal_code = '61030';
$settings->country_id = '380';
$settings->currency_id = '3';
$settings->vat_number = '01234567890';
$settings->id_number = '';
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
]);
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'name' => 'Italian Client Name',
'address1' => 'Via Antonio da Legnago 68',
'city' => 'Monasterace',
'state' => 'CR',
// 'state' => 'Reggio Calabria',
'postal_code' => '89040',
'country_id' => 380,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
]);
$item = new InvoiceItem;
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 10;
$item->quantity = 10;
$item->tax_rate1 = 22;
$item->tax_name1 = 'IVA';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'client_id' => $client->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' => 'ITA-'.rand(1000,100000)
]);
$invoice->service()->markSent()->save();
$fat = new FatturaPANew($invoice);
$fat->run();
$fe = $fat->getFatturaElettronica();
$this->assertNotNull($fe);
$this->assertInstanceOf(FatturaElettronica::class, $fe);
$this->assertInstanceOf(FatturaElettronicaBody::class, $fe->FatturaElettronicaBody[0]);
$this->assertInstanceOf(FatturaElettronicaHeader::class, $fe->FatturaElettronicaHeader);
$encoder = new Encode($fe);
$xml = $encoder->toXml();
$this->assertNotNull($xml);
}
}

View File

@ -0,0 +1,452 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Integration\Einvoice;
use DateTime;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Invoice;
use Tests\MockAccountData;
use App\Models\ClientContact;
use App\DataMapper\InvoiceItem;
use App\DataMapper\ClientSettings;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Invoiceninja\Einvoice\Models\FACT1\ItemType\Item;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Invoiceninja\Einvoice\Models\FACT1\PartyType\Party;
use Invoiceninja\Einvoice\Models\FACT1\PriceType\Price;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Invoiceninja\Einvoice\Models\FACT1\ContactType\Contact;
use Invoiceninja\Einvoice\Models\FACT1\CountryType\Country;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\TaxAmount;
use Invoiceninja\Einvoice\Models\FACT1\TaxTotalType\TaxTotal;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\PriceAmount;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Invoiceninja\Einvoice\Models\FACT1\TaxSchemeType\TaxScheme;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Invoiceninja\Einvoice\Models\FACT1\AddressType\PostalAddress;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Invoiceninja\Einvoice\Models\FACT1\InvoiceLineType\InvoiceLine;
use Invoiceninja\Einvoice\Models\FACT1\TaxScheme as FACT1TaxScheme;
use Invoiceninja\Einvoice\Models\FACT1\TaxSubtotalType\TaxSubtotal;
use Invoiceninja\Einvoice\Models\FACT1\QuantityType\InvoicedQuantity;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\LineExtensionAmount;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\PayableAmount;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\TaxableAmount;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\TaxExclusiveAmount;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\TaxInclusiveAmount;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Invoiceninja\Einvoice\Models\FACT1\PartyTaxSchemeType\PartyTaxScheme;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Invoiceninja\Einvoice\Models\FACT1\MonetaryTotalType\LegalMonetaryTotal;
use Invoiceninja\Einvoice\Models\FACT1\PartyLegalEntityType\PartyLegalEntity;
use Invoiceninja\Einvoice\Models\FACT1\TaxCategoryType\ClassifiedTaxCategory;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
use Invoiceninja\Einvoice\Models\FACT1\CustomerPartyType\AccountingCustomerParty;
use Invoiceninja\Einvoice\Models\FACT1\SupplierPartyType\AccountingSupplierParty;
use Invoiceninja\Einvoice\Models\FACT1\PartyIdentificationType\PartyIdentification;
use Invoiceninja\Einvoice\Models\FACT1\TaxCategoryType\TaxCategory;
/**
* @test
*/
class Fact1Test extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->markTestSkipped('prevent running in CI');
$this->makeTestData();
}
public function testRoBuild()
{
$settings = $this->company->settings;
$settings->currency_id = '42';
$this->company->saveSettings($settings, $this->company);
$this->company->save();
$settings = ClientSettings::defaults();
$settings->currency_id = '42';
//VAT
//19%
$client = Client::factory()
->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'id_number' => '646546549',
'address1' => '40D, Șoseaua București-Ploiești',
'city' => 'SECTOR3',
'state' => 'RO-B',
'country_id' => 642,
'vat_number' => 646546549,
'name' => 'Client Company Name',
'settings' => $settings,
]);
ClientContact::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $client->id,
'first_name' => 'Bob',
'last_name' => 'Jane',
'email' => 'bob@gmail.com',
]);
$items = [];
$item = new InvoiceItem;
$item->cost = 10;
$item->quantity = 10;
$item->tax_name1 = 'VAT';
$item->tax_rate1 = '19';
$item->product_key = "Product Name";
$item->notes = "A great product description";
$_invoice = Invoice::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $client->id,
'number' => 'INV-'.rand(1000,1000000),
'line_items' => [$item],
'due_date' => now()->addDays(20)->format('Y-m-d'),
'status_id' => 1,
'discount' => 0,
]);
$_invoice->service()->markSent()->save();
$calc = $_invoice->calc();
$invoice = new \Invoiceninja\Einvoice\Models\FACT1\Invoice();
$invoice->UBLVersionID = '2.1';
$invoice->CustomizationID = 'urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1';
$invoice->ID = $_invoice->number;
$invoice->InvoiceTypeCode = 380;
$invoice->IssueDate = new DateTime($_invoice->date);
$invoice->DueDate = new DateTime($_invoice->due_date);
$invoice->DocumentCurrencyCode = 'RON';
$invoice->TaxCurrencyCode = 'RON';
$asp = new AccountingSupplierParty();
$party = new Party();
$party_identification = new PartyIdentification();
$party_identification->ID = 'company_id_number';
$party->PartyIdentification[] = $party_identification;
$sp_address = new PostalAddress();
$sp_address->StreetName = $this->company->settings->address1;
$sp_address->CityName = 'SECTOR2';
$sp_address->CountrySubentity = 'RO-B';
$country = new Country();
$country->IdentificationCode='RO';
$sp_address->Country = $country;
$party->PostalAddress = $sp_address;
$pts = new PartyTaxScheme();
$tax_scheme = new TaxScheme();
$tax_scheme->ID = 'VAT';
$pts->CompanyID = 'RO234234234';
$pts->TaxScheme = $tax_scheme;
$party->PartyTaxScheme[] = $pts;
$ple = new PartyLegalEntity();
$ple->RegistrationName = $this->company->settings->name;
$ple->CompanyID = 'J40/2222/2009';
$party->PartyLegalEntity[] = $ple;
$p_contact = new Contact();
$p_contact->Name = $this->company->owner()->present()->name();
$p_contact->Telephone = $this->company->settings->phone;
$p_contact->ElectronicMail = $this->company->owner()->email;
$party->Contact = $p_contact;
$asp->Party = $party;
$invoice->AccountingSupplierParty = $asp;
$acp = new AccountingCustomerParty();
$party = new Party();
$party_identification = new PartyIdentification();
$party_identification->ID = 'client_id_number';
$party->PartyIdentification[] = $party_identification;
$sp_address = new PostalAddress();
$sp_address->StreetName = $client->address1;
$sp_address->CityName = 'SECTOR2';
$sp_address->CountrySubentity = 'RO-B';
$country = new Country();
$country->IdentificationCode = 'RO';
$sp_address->Country = $country;
$party->PostalAddress = $sp_address;
$ple = new PartyLegalEntity();
$ple->RegistrationName = $client->name;
$ple->CompanyID = '646546549';
$party->PartyLegalEntity[] = $ple;
$p_contact = new Contact();
$p_contact->Name = $client->contacts->first()->present()->name();
$p_contact->Telephone = $client->contacts->first()->present()->phone();
$p_contact->ElectronicMail = $client->contacts->first()->present()->email();
$party->Contact = $p_contact;
$acp->Party = $party;
$invoice->AccountingCustomerParty = $acp;
$taxtotal = new TaxTotal();
$tax_amount = new TaxAmount();
$tax_amount->amount = $calc->getItemTotalTaxes();
$tax_amount->currencyID = $_invoice->client->currency()->code;
$tc = new TaxCategory();
$tc->ID = "S";
$taxable = $this->getTaxable($_invoice);
$taxable_amount = new TaxableAmount();
$taxable_amount->amount = $taxable;
$taxable_amount->currencyID = $_invoice->client->currency()->code;
$tax_sub_total = new TaxSubtotal();
$tax_sub_total->TaxAmount = $tax_amount;
$tax_sub_total->TaxCategory = $tc;
$tax_sub_total->TaxableAmount = $taxable_amount;
$taxtotal->TaxSubtotal[] = $tax_sub_total;
$invoice->TaxTotal[] = $taxtotal;
$lmt = new LegalMonetaryTotal();
$lea = new LineExtensionAmount();
$lea->amount = $taxable;
$lea->currencyID = $_invoice->client->currency()->code;
$lmt->LineExtensionAmount = $lea;
$tea = new TaxExclusiveAmount;
$tea->amount = $taxable;
$tea->currencyID = $_invoice->client->currency()->code;
$lmt->TaxExclusiveAmount = $tea;
$tia = new TaxInclusiveAmount;
$tia->amount = $_invoice->amount;
$tia->currencyID = $_invoice->client->currency()->code;
$lmt->TaxInclusiveAmount = $tia;
$pa = new PayableAmount;
$pa->amount = $_invoice->amount;
$pa->currencyID = $_invoice->client->currency()->code;
$lmt->PayableAmount = $pa;
$invoice->LegalMonetaryTotal = $lmt;
foreach($_invoice->line_items as $key => $item)
{
$invoice_line = new InvoiceLine;
$invoice_line->ID = $key++;
$iq = new InvoicedQuantity();
$iq->amount = $item->cost;
$iq->unitCode = 'H87';
$invoice_line->InvoicedQuantity = $iq;
$invoice_line->Note = substr($item->notes, 0, 200);
$ctc = new ClassifiedTaxCategory();
$ctc->ID = 'S';
$i = new Item;
$i->Description = $item->notes;
$i->Name = $item->product_key;
$tax_scheme = new FACT1TaxScheme();
$tax_scheme->ID = $item->tax_name1;
$tax_scheme->Name = $item->tax_rate1;
$ctc = new ClassifiedTaxCategory();
$ctc->TaxScheme = $tax_scheme;
$ctc->ID = 'S';
$i->ClassifiedTaxCategory[] = $ctc;
$invoice_line->Item = $i;
$lea = new LineExtensionAmount;
$lea->amount = $item->line_total;
$lea->currencyID = $_invoice->client->currency()->code;
$invoice_line->LineExtensionAmount = $lea;
$price = new Price();
$pa = new PriceAmount();
$pa->amount = $item->line_total;
$pa->currencyID = $_invoice->client->currency()->code;
$price->PriceAmount = $pa;
$lea = new LineExtensionAmount();
$lea->amount = $item->line_total;
$lea->currencyID = $_invoice->client->currency()->code;
$invoice_line->LineExtensionAmount = $lea;
$invoice->InvoiceLine[] = $invoice_line;
}
$validator = Validation::createValidatorBuilder()
->enableAttributeMapping()
->getValidator();
$errors = $validator->validate($invoice);
foreach($errors as $error) {
// echo $error->getPropertyPath() . ': ' . $error->getMessage() . "\n";
}
$this->assertCount(0, $errors);
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// list of PropertyListExtractorInterface (any iterable)
$listExtractors = [$reflectionExtractor];
// list of PropertyTypeExtractorInterface (any iterable)
$typeExtractors = [$reflectionExtractor,$phpDocExtractor];
// list of PropertyDescriptionExtractorInterface (any iterable)
$descriptionExtractors = [$phpDocExtractor];
// list of PropertyAccessExtractorInterface (any iterable)
$accessExtractors = [$reflectionExtractor];
// list of PropertyInitializableExtractorInterface (any iterable)
$propertyInitializableExtractors = [$reflectionExtractor];
$propertyInfo = new PropertyInfoExtractor(
// $listExtractors,
$propertyInitializableExtractors,
$descriptionExtractors,
$typeExtractors,
// $accessExtractors,
);
$context = [
'xml_format_output' => true,
'remove_empty_tags' => true,
];
$encoder = new XmlEncoder($context);
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory);
$normalizer = new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter, null, $propertyInfo);
$normalizers = [ new DateTimeNormalizer(), $normalizer, new ArrayDenormalizer() , ];
$encoders = [$encoder, new JsonEncoder()];
$serializer = new Serializer($normalizers, $encoders);
$n_context = [
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
// AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
];
// $invoice = $normalizer->normalize($invoice, 'json', $n_context);
// echo print_r($invoice);
// $invoice = $serializer->serialize($invoice, 'xml', $n_context);
$dataxml = $serializer->encode($invoice, 'xml', $context);
// echo $dataxml;
//set default standard props
}
/**
* @return float|int|mixed
*/
private function getTaxable(Invoice $invoice): float
{
$total = 0;
foreach ($invoice->line_items as $item) {
$line_total = $item->quantity * $item->cost;
if ($item->discount != 0) {
if ($invoice->is_amount_discount) {
$line_total -= $item->discount;
} else {
$line_total -= $line_total * $item->discount / 100;
}
}
$total += $line_total;
}
if ($invoice->discount > 0) {
if ($invoice->is_amount_discount) {
$total -= $invoice->discount;
} else {
$total *= (100 - $invoice->discount) / 100;
$total = round($total, 2);
}
}
if ($invoice->custom_surcharge1 && $invoice->custom_surcharge_tax1) {
$total += $invoice->custom_surcharge1;
}
if ($invoice->custom_surcharge2 && $invoice->custom_surcharge_tax2) {
$total += $invoice->custom_surcharge2;
}
if ($invoice->custom_surcharge3 && $invoice->custom_surcharge_tax3) {
$total += $invoice->custom_surcharge3;
}
if ($invoice->custom_surcharge4 && $invoice->custom_surcharge_tax4) {
$total += $invoice->custom_surcharge4;
}
return $total;
}
}