Skip to content

Commit

Permalink
Merge pull request invoiceninja#10113 from beganovich/einvoicing-pepp…
Browse files Browse the repository at this point in the history
…ol-form-api

EInvoicing: PEPPOL setup for hosted
  • Loading branch information
turbo124 authored Oct 21, 2024
2 parents e697790 + 73364b5 commit d67d7f7
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 1 deletion.
76 changes: 76 additions & 0 deletions app/Http/Controllers/EInvoicePeppolController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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\Http\Requests\EInvoice\Peppol\CreateRequest;
use App\Http\Requests\EInvoice\Peppol\DisconnectRequest;
use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Http\Response;

class EInvoicePeppolController extends BaseController
{
public function setup(CreateRequest $request, Storecove $storecove): Response
{
/**
* @var \App\Models\Company
*/
$company = auth()->user()->company();

$data = [
...$request->validated(),
'country' => $request->country()->iso_3166_2,
];

$legal_entity_response = $storecove->createLegalEntity($data, $company);

$add_identifier_response = $storecove->addIdentifier(
legal_entity_id: $legal_entity_response['id'],
identifier: $company->settings->vat_number,
scheme: $request->receiverIdentifier(),
);

if ($add_identifier_response) {
$company->legal_entity_id = $legal_entity_response['id'];
$company->save();

return response()->noContent();
}

// @todo: Improve with proper error.

return response()->noContent(status: 422);
}

public function disconnect(DisconnectRequest $request, Storecove $storecove): Response
{
/**
* @var \App\Models\Company
*/
$company = auth()->user()->company();

$response = $storecove->deleteIdentifier(
legal_entity_id: $company->legal_entity_id,
);

if ($response) {
$company->legal_entity_id = null;
$company->save();

return response()->noContent();

}

// @todo: Improve with proper error.

return response()->noContent(status: 422);
}
}
72 changes: 72 additions & 0 deletions app/Http/Requests/EInvoice/Peppol/CreateRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

namespace App\Http\Requests\EInvoice\Peppol;

use App\Models\Country;
use App\Rules\EInvoice\Peppol\SupportsReceiverIdentifier;
use App\Services\EDocument\Standards\Peppol\ReceiverIdentifier;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest;

class CreateRequest extends FormRequest
{
public function authorize(): bool
{
/**
* @var \App\Models\User
*/
$user = auth()->user();

if (app()->isLocal()) {
return true;
}

return $user->account->isPaid() &&
$user->company()->legal_entity_id === null;
}

/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'party_name' => ['required', 'string'],
'line1' => ['required', 'string'],
'line2' => ['nullable', 'string'],
'city' => ['required', 'string'],
'country' => ['required', 'integer', 'exists:countries,id', new SupportsReceiverIdentifier()],
'zip' => ['required', 'string'],
'county' => ['required', 'string'],
];
}

protected function failedAuthorization(): void
{
throw new AuthorizationException(
message: ctrans('texts.peppol_not_paid_message'),
);
}

public function country(): Country
{
return Country::find($this->country);
}

public function receiverIdentifier(): string
{
$identifier = new ReceiverIdentifier($this->country()->iso_3166_2);

return $identifier->get();
}
}
52 changes: 52 additions & 0 deletions app/Http/Requests/EInvoice/Peppol/DisconnectRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

namespace App\Http\Requests\EInvoice\Peppol;

use App\Models\Country;
use App\Rules\EInvoice\Peppol\SupportsReceiverIdentifier;
use App\Services\EDocument\Standards\Peppol\ReceiverIdentifier;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest;

class DisconnectRequest extends FormRequest
{
public function authorize(): bool
{
/**
* @var \App\Models\User
*/
$user = auth()->user();

if (app()->isLocal()) {
return true;
}

return $user->account->isPaid() &&
$user->company()->legal_entity_id !== null;
}

/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [];
}

protected function failedAuthorization(): void
{
throw new AuthorizationException(
message: ctrans('texts.peppol_not_paid_message'),
);
}
}
31 changes: 31 additions & 0 deletions app/Rules/EInvoice/Peppol/SupportsReceiverIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Rules\EInvoice\Peppol;

use App\Models\Country;
use App\Services\EDocument\Standards\Peppol\ReceiverIdentifier;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class SupportsReceiverIdentifier implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$country = Country::find($value);

if ($country === null) {
$fail(ctrans('texts.peppol_country_not_supported'));
}

$checker = new ReceiverIdentifier($country->iso_3166_2);

if ($checker->get() === null) {
$fail(ctrans('texts.peppol_country_not_supported'));
}
}
}
2 changes: 1 addition & 1 deletion app/Services/EDocument/Gateway/Storecove/Storecove.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class Storecove
"identifier" => "1200109963131"
];

private ?int $legal_entity_id;
private ?int $legal_entity_id = null;

public StorecoveRouter $router;

Expand Down
3 changes: 3 additions & 0 deletions app/Services/EDocument/Jobs/SendEDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public function handle()
'document' => base64_encode($xml),
'tenant_id' => $model->company->company_key,
'identifiers' => $identifiers,
'e_invoicing_token' => $model->company->e_invoicing_token,

// include whitelabel key.
];

$r = Http::withHeaders($this->getHeaders())
Expand Down
34 changes: 34 additions & 0 deletions app/Services/EDocument/Standards/Peppol/ReceiverIdentifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?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\Peppol;

// https://www.storecove.com/docs/#_receiver_identifiers_list

class ReceiverIdentifier
{
public array $mappings = [
'DE' => 'DE:VAT',

// @todo: Check with Dave what other countries we support.
];

public function __construct(
public string $country,
) {
}

public function get(): ?string
{
return $this->mappings[$this->country] ?? null;
}
}
2 changes: 2 additions & 0 deletions app/Transformers/CompanyTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ public function transform(Company $company)
'e_invoice' => $company->e_invoice ?: new \stdClass(),
'has_quickbooks_token' => $company->quickbooks ? true : false,
'is_quickbooks_token_active' => $company->quickbooks?->accessTokenKey ?? false,
'legal_entity_id' => $company->legal_entity_id ?? null,
'e_invoicing_token' => $company->e_invoicing_token ?? null,
];
}

Expand Down
15 changes: 15 additions & 0 deletions database/migrations/2024_10_11_153311_add_e_invoicing_token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->string('e_invoicing_token')->nullable();
});
}
};
14 changes: 14 additions & 0 deletions lang/en/texts.php
Original file line number Diff line number Diff line change
Expand Up @@ -5359,6 +5359,20 @@
'updated_records' => 'Updated Records',
'vat_not_registered' => 'Seller not VAT registered',
'small_company_info' => 'No disclosure of sales tax in accordance with § 19 UStG',
'peppol_onboarding' => 'Looks like it\'s your first time using PEPPOL.',
'get_started' => 'Get Started',
'configure_peppol' => 'Configure PEPPOL',
'step' => 'Step',
'peppol_whitelabel_warning' => 'In order to use PEPPOL, you need to have white-label license.',
'peppol_plan_warning' => 'In order to use PEPPOL, you need to be on paid plan.',
'peppol_credits_info' => 'Text how they need a credits to operate with PEPPOL.',
'buy_credits' => 'Buy Credits',
'peppol_successfully_configured' => 'PEPPOL successsfully configured.',
'peppol_not_paid_message' => 'PEPPOL is only available to paid plans. Please upgrade your plan to get access to PEPPOL.',
'peppol_country_not_supported' => 'PEPPOL is not available for this country.',
'peppol_disconnect' => 'Disconnect PEPPOL',
'peppol_disconnect_short' => 'Disconnect from PEPPOL.',
'peppol_disconnect_long' => 'PEPPOL will be disconnected from your account and...',
'log_duration_words' => 'Time log duration in words',
'log_duration' => 'Time log duration',
'merged_vendors' => 'Successfully merged vendors',
Expand Down
4 changes: 4 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
use App\Http\Controllers\EInvoicePeppolController;
use App\Http\Controllers\SubscriptionStepsController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BaseController;
Expand Down Expand Up @@ -228,6 +229,9 @@
Route::post('documents/bulk', [DocumentController::class, 'bulk'])->name('documents.bulk');

Route::post('einvoice/validateEntity', [EInvoiceController::class, 'validateEntity'])->name('einvoice.validateEntity');
Route::post('einvoice/peppol/setup', [EInvoicePeppolController::class, 'setup'])->name('einvoice.peppol.setup');
Route::post('einvoice/peppol/disconnect', [EInvoicePeppolController::class, 'disconnect'])->name('einvoice.peppol.disconnect');

Route::post('emails', [EmailController::class, 'send'])->name('email.send')->middleware('user_verified');
Route::post('emails/clientHistory/{client}', [EmailHistoryController::class, 'clientHistory'])->name('email.clientHistory');
Route::post('emails/entityHistory', [EmailHistoryController::class, 'entityHistory'])->name('email.entityHistory');
Expand Down

0 comments on commit d67d7f7

Please sign in to comment.