Merge pull request #9565 from turbo124/v5-develop

Updates for currencies
This commit is contained in:
David Bomba 2024-05-31 10:33:09 +10:00 committed by GitHub
commit 326f6e1f73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1741 additions and 1923 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers;
use App\Utils\Statics; use App\Utils\Statics;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Invoiceninja\Einvoice\Decoder\Schema;
class StaticController extends BaseController class StaticController extends BaseController
{ {
@ -60,8 +61,8 @@ class StaticController extends BaseController
if(request()->has('einvoice')){ if(request()->has('einvoice')){
// $schema = new Schema(); $schema = new Schema();
// $response_data['einvoice_schema'] = $schema('FACT1'); $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 []; 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(); $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'); $input["redirect"] = isset($context["is_react"]) && $context['is_react'] ? config('ninja.react_url') . "/#/settings/bank_accounts" : config('ninja.app_url');
$this->replace($input); $this->replace($input);

View File

@ -26,7 +26,7 @@ class GenericReportRequest extends Request
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return $this->checkAuthority(); return true;
} }
public function rules() public function rules()
@ -68,27 +68,25 @@ class GenericReportRequest extends Request
$input['user_id'] = auth()->user()->id; $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); $this->replace($input);
} }
private function checkAuthority() private function checkAuthority()
{ {
$this->error_message = ctrans('texts.authorization_failure');
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->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'); 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 private function createPayment($invoices, float $amount): void
{ {
$this->attachable_invoices = [];
$this->available_balance = $amount; $this->available_balance = $amount;
\DB::connection(config('database.default'))->transaction(function () use ($invoices) { \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)) LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject))
->send(); ->send();
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) { }
catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail(); $this->fail();
$this->cleanUpMailers(); $this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first()); $this->logMailError($e->getMessage(), $this->company->clients()->first());
return; return;
} catch (\Symfony\Component\Mime\Exception\LogicException $e) { }
catch (\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}"); nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail(); $this->fail();
$this->cleanUpMailers(); $this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first()); $this->logMailError($e->getMessage(), $this->company->clients()->first());
return; 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()}"); nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage(); $message = $e->getMessage();
@ -221,8 +241,7 @@ class NinjaMailerJob implements ShouldQueue
} }
/* Releasing immediately does not add in the backoff */ /* Releasing immediately does not add in the backoff */
sleep(rand(5, 10)); sleep(rand(2, 3));
$this->release($this->backoff()[$this->attempts() - 1]); $this->release($this->backoff()[$this->attempts() - 1]);
} }
@ -779,15 +798,20 @@ class NinjaMailerJob implements ShouldQueue
return false; return false;
} }
$token = json_decode($guzzle->post($url, [ try {
'form_params' => [ $token = json_decode($guzzle->post($url, [
'client_id' => config('ninja.o365.client_id'), 'form_params' => [
'client_secret' => config('ninja.o365.client_secret'), 'client_id' => config('ninja.o365.client_id'),
'scope' => 'email Mail.Send offline_access profile User.Read openid', 'client_secret' => config('ninja.o365.client_secret'),
'grant_type' => 'refresh_token', 'scope' => 'email Mail.Send offline_access profile User.Read openid',
'refresh_token' => $user->oauth_user_refresh_token 'grant_type' => 'refresh_token',
], 'refresh_token' => $user->oauth_user_refresh_token
])->getBody()->getContents()); ],
])->getBody()->getContents());
}
catch(\Exception $e){
nlog("Problem getting new Microsoft token for User: {$user->email}");
}
if ($token) { if ($token) {
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_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\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Str;
class CreateUser class CreateUser
{ {
@ -62,6 +63,7 @@ class CreateUser
$user->fill($this->request); $user->fill($this->request);
$user->email = $this->request['email']; //todo need to remove this in production $user->email = $this->request['email']; //todo need to remove this in production
$user->last_login = now(); $user->last_login = now();
$user->referral_code = Str::lower(Str::random(32));
$user->ip = request()->ip(); $user->ip = request()->ip();
if (Ninja::isSelfHost()) { 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.'>' ?? ''; 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') @include('portal.ninja2020.gateways.includes.save_card')
<!-- This include pops up a credit card form --> <!-- 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') @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\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\SystemLog; 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 App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use WePay;
/**
* @deprecated 5.9
*/
class WePayPaymentDriver extends BaseDriver class WePayPaymentDriver extends BaseDriver
{ {
use MakesHash; use MakesHash;
@ -45,27 +44,14 @@ class WePayPaymentDriver extends BaseDriver
/* Maps the Payment Gateway Type - to its implementation */ /* Maps the Payment Gateway Type - to its implementation */
public static $methods = [ public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
GatewayType::BANK_TRANSFER => ACH::class,
]; ];
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_WEPAY; public const SYSTEM_LOG_TYPE = SystemLog::TYPE_WEPAY;
public function init() public function init()
{ {
if (WePay::getEnvironment() == 'none') { throw new \Exception("Gateway no longer supported", 500);
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'));
}
}
if ($this->company_gateway) {
$this->wepay = new WePay($this->company_gateway->getConfigField('accessToken'));
} else {
$this->wepay = new WePay(null);
}
return $this; return $this;
} }
@ -93,7 +79,6 @@ class WePayPaymentDriver extends BaseDriver
*/ */
public function setup(array $data) 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) public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
{ {
$this->init(); $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) public function refund(Payment $payment, $amount, $return_client_response = false)
{ {
$this->init(); $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) 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 public function getClientRequiredFields(): array

View File

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

View File

@ -26,7 +26,6 @@ class BankTransactionRepository extends BaseRepository
$bank_transaction->bank_integration_id = $data['bank_integration_id']; $bank_transaction->bank_integration_id = $data['bank_integration_id'];
} }
$bank_transaction->fill($data); $bank_transaction->fill($data);
$bank_transaction->save(); $bank_transaction->save();
@ -43,7 +42,7 @@ class BankTransactionRepository extends BaseRepository
$data['transactions'] = $bank_transactions->map(function ($bt) { $data['transactions'] = $bank_transactions->map(function ($bt) {
return ['id' => $bt->id, 'invoice_ids' => $bt->invoice_ids, 'ninja_category_id' => $bt->ninja_category_id]; return ['id' => $bt->id, 'invoice_ids' => $bt->invoice_ids, 'ninja_category_id' => $bt->ninja_category_id];
})->toArray(); })->toArray();
$bts = (new MatchBankTransactions($user->company()->id, $user->company()->db, $data))->handle(); $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->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first()); $this->logMailError($e->getMessage(), $this->company->clients()->first());
return; 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()}"); nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage(); $message = $e->getMessage();
@ -916,15 +935,20 @@ class Email implements ShouldQueue
$guzzle = new \GuzzleHttp\Client(); $guzzle = new \GuzzleHttp\Client();
$url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; $url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
$token = json_decode($guzzle->post($url, [ try {
'form_params' => [ $token = json_decode($guzzle->post($url, [
'client_id' => config('ninja.o365.client_id'), 'form_params' => [
'client_secret' => config('ninja.o365.client_secret'), 'client_id' => config('ninja.o365.client_id'),
'scope' => 'email Mail.Send offline_access profile User.Read openid', 'client_secret' => config('ninja.o365.client_secret'),
'grant_type' => 'refresh_token', 'scope' => 'email Mail.Send offline_access profile User.Read openid',
'refresh_token' => $user->oauth_user_refresh_token 'grant_type' => 'refresh_token',
], 'refresh_token' => $user->oauth_user_refresh_token
])->getBody()->getContents()); ],
])->getBody()->getContents());
}
catch(\Exception $e){
nlog("Problem getting new Microsoft token for User: {$user->email}");
}
if ($token) { if ($token) {
$user->oauth_user_refresh_token = property_exists($token, 'refresh_token') ? $token->refresh_token : $user->oauth_user_refresh_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 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. // 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. // 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}; $this->email->email_object->body = $this->email->email_object->settings?->{$this->email->email_object->email_template_body};
} else { } else {

View File

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

View File

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

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

View File

@ -13,6 +13,7 @@ namespace Database\Factories;
use App\DataMapper\ClientSettings; use App\DataMapper\ClientSettings;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Str;
class ClientFactory extends Factory class ClientFactory extends Factory
{ {
@ -49,6 +50,7 @@ class ClientFactory extends Factory
'shipping_country_id' => 4, 'shipping_country_id' => 4,
'settings' => ClientSettings::defaults(), 'settings' => ClientSettings::defaults(),
'client_hash' => \Illuminate\Support\Str::random(40), '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' => 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' => 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' => 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' => 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' => 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' => '.'], ['id' => 92, 'name' => 'Fijian Dollar', 'code' => 'FJD', 'symbol' => 'FJ$', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],

View File

@ -5328,6 +5328,9 @@ $lang = array(
'disconnected' => 'Disconnected', 'disconnected' => 'Disconnected',
'reconnect' => 'Reconnect', 'reconnect' => 'Reconnect',
'e_invoice_settings' => 'E-Invoice Settings', 'e_invoice_settings' => 'E-Invoice Settings',
'currency_mauritanian_ouguiya' => 'Mauritanian Ouguiya',
'currency_bhutan_ngultrum' => 'Bhutan Ngultrum',
); );
return $lang; return $lang;

View File

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

View File

@ -13,7 +13,6 @@ use App\Http\Controllers\Gateways\Mollie3dsController;
use App\Http\Controllers\SetupController; use App\Http\Controllers\SetupController;
use App\Http\Controllers\StripeConnectController; use App\Http\Controllers\StripeConnectController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\WePayController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/', [BaseController::class, 'flutterRoute'])->middleware('guest'); 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::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::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::get('auth/{provider}', [LoginController::class, 'redirectToProvider']);
Route::middleware('url_db')->group(function () { Route::middleware('url_db')->group(function () {

View File

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

View File

@ -11,11 +11,21 @@
namespace Tests\Feature\EInvoice; 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 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 * @test
@ -31,6 +41,9 @@ class FatturaPATest extends TestCase
$this->makeTestData(); $this->makeTestData();
$this->markTestSkipped('prevent running in CI');
$this->withoutMiddleware( $this->withoutMiddleware(
ThrottleRequests::class ThrottleRequests::class
); );
@ -38,9 +51,86 @@ class FatturaPATest extends TestCase
public function testInvoiceBoot() 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;
}
}