Merge pull request #9604 from turbo124/v5-develop

v5.9.3
This commit is contained in:
David Bomba 2024-06-07 20:32:43 +10:00 committed by GitHub
commit dcf474a95c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 11638 additions and 10429 deletions

View File

@ -44,6 +44,7 @@ jobs:
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui
git checkout develop
cp ../vite.config.ts.react ./vite.config.js
npm i
npm run build
cp -r dist/* ../public/
@ -60,7 +61,8 @@ jobs:
sudo rm -rf node_modules
sudo rm -rf .git
sudo rm .env
sudo rm -rf ui
- name: Build project
run: |
shopt -s dotglob

View File

@ -72,7 +72,6 @@ jobs:
- name: Build project
run: |
zip -r /home/runner/work/invoiceninja/invoiceninja.zip .* -x "../*"
shopt -s dotglob
tar --exclude='public/storage' --exclude='./htaccess' --exclude='invoiceninja.zip' -zcvf /home/runner/work/invoiceninja/invoiceninja.tar *
- name: Release
@ -82,5 +81,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
/home/runner/work/invoiceninja/invoiceninja.tar
/home/runner/work/invoiceninja/invoiceninja.zip
/home/runner/work/invoiceninja/invoiceninja.tar

View File

@ -1 +1 @@
5.9.2
5.9.3

View File

@ -66,12 +66,14 @@ class DbQuery extends GenericMixedMetric
public $double_metric2 = 1;
public function __construct($string_metric5, $string_metric6, $int_metric1, $double_metric2, $string_metric7)
public function __construct($string_metric5, $string_metric6, $int_metric1, $double_metric2, $string_metric7, $string_metric8, $string_metric9)
{
$this->int_metric1 = $int_metric1;
$this->string_metric5 = $string_metric5;
$this->string_metric6 = $string_metric6;
$this->double_metric2 = $double_metric2;
$this->string_metric7 = $string_metric7;
$this->string_metric8 = $string_metric8;
$this->string_metric9 = $string_metric9;
}
}

View File

@ -29,7 +29,7 @@ class CompanySettings extends BaseSettings
public $besr_id = ''; //@implemented
public $lock_invoices = 'off'; //off,when_sent,when_paid //@implemented
public $lock_invoices = 'off'; //off,when_sent,when_paid,end_of_month //@implemented
public $enable_client_portal_tasks = false; //@ben to implement

View File

@ -1,24 +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 FACT1.
*/
class FACT1
{
public function build()
{
$i = new \InvoiceNinja\EInvoice\Models\FACT1\Invoice();
}
}

View File

@ -24,9 +24,10 @@ use App\Services\Email\Email;
use App\Models\BankIntegration;
use App\Services\Email\EmailObject;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Mail\Mailables\Address;
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
use App\Helpers\Bank\Nordigen\Transformer\TransactionTransformer;
use Illuminate\Mail\Mailables\Address;
class Nordigen
{
@ -149,6 +150,10 @@ class Nordigen
public function disabledAccountEmail(BankIntegration $bank_integration): void
{
$cache_key = "email_quota:{$bank_integration->company->company_key}:{$bank_integration->id}";
if(Cache::has($cache_key))
return;
App::setLocale($bank_integration->company->getLocale());
@ -163,7 +168,8 @@ class Nordigen
$mo->email_template_subject = 'nordigen_requisition_subject';
Email::dispatch($mo, $bank_integration->company);
Cache::put($cache_key, true, 60 * 60 * 24);
}

View File

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

View File

@ -117,11 +117,21 @@ class CompanyController extends BaseController
/** @var \App\Models\User $user */
$user = auth()->user();
$companies = Company::whereAccountId($user->company()->account->id);
$companies = Company::where('account_id', $user->company()->account->id);
return $this->listResponse($companies);
}
public function current()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$company = Company::find($user->company()->id);
return $this->itemResponse($company);
}
/**
* Show the form for creating a new resource.
*

View File

@ -408,7 +408,7 @@ class InvoiceController extends BaseController
}
if ($invoice->isLocked()) {
return response()->json(['message' => ctrans('texts.locked_invoice')], 422);
return response()->json(['message' => '', 'errors' => ['number' => ctrans('texts.locked_invoice')]], 422);
}
$old_invoice = $invoice->line_items;

View File

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

View File

@ -67,7 +67,7 @@ class UpdateInvoiceRequest extends Request
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])];
$rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
@ -80,7 +80,7 @@ $rules['discount'] = 'sometimes|numeric|max:99999999999999';
$rules['partial'] = 'bail|sometimes|nullable|numeric';
$rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999'];
$rules['date'] = 'bail|sometimes|date:Y-m-d';
$rules['date'] = 'bail|sometimes|date:Y-m-d';
// $rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date'];
// $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];

View File

@ -16,6 +16,7 @@ use Illuminate\Contracts\Validation\Rule;
/**
* Class LockedInvoiceRule.
* @deprecated
*/
class LockedInvoiceRule implements Rule
{
@ -67,6 +68,13 @@ class LockedInvoiceRule implements Rule
}
return true;
//if now is greater than the end of month the invoice was dated - do not modify
case 'end_of_month':
if(\Carbon\Carbon::parse($this->invoice->date)->setTimezone($this->invoice->company->timezone()->name)->endOfMonth()->lte(now()))
return false;
return true;
default:
return true;
}

View File

@ -31,6 +31,7 @@ class ProductMap
12 => 'product.custom_value2',
13 => 'product.custom_value3',
14 => 'product.custom_value4',
15 => 'product.image_url'
];
}
@ -52,6 +53,7 @@ class ProductMap
12 => 'texts.custom_value',
13 => 'texts.custom_value',
14 => 'texts.custom_value',
15 => 'texts.image_url',
];
}
}

View File

@ -42,6 +42,7 @@ class ProductTransformer extends BaseTransformer
'custom_value2' => $this->getString($data, 'product.custom_value2'),
'custom_value3' => $this->getString($data, 'product.custom_value3'),
'custom_value4' => $this->getString($data, 'product.custom_value4'),
'product_image' => $this->getString($data, 'product.image_url'),
];
}
}

View File

@ -94,7 +94,7 @@ class QuoteTransformer extends BaseTransformer
),
'footer' => $this->getString($quote_data, 'quote.footer'),
'partial' => $this->getFloat($quote_data, 'quote.partial'),
'partial_due_date' => isset($invoice_data['quote.partial_due_date']) ? $this->parseDate($quote_data['quote.partial_due_date']) : null,
'partial_due_date' => isset($quote_data['quote.partial_due_date']) ? $this->parseDate($quote_data['quote.partial_due_date']) : null,
'custom_surcharge1' => $this->getString(
$quote_data,
'quote.custom_surcharge1'

View File

@ -79,6 +79,7 @@ class SubscriptionCron
->cursor()
->each(function ($company_id){
/** @var \App\Models\Company $company */
$company = Company::find($company_id);
$timezone_now = now()->setTimezone($company->timezone()->name ?? 'Pacific/Midway');

View File

@ -168,11 +168,6 @@ class NinjaMailerJob implements ShouldQueue
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
}
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') {
@ -194,7 +189,7 @@ class NinjaMailerJob implements ShouldQueue
* this merges a text string with a json object
* need to harvest the ->Message property using the following
*/
if (stripos($e->getMessage(), 'code 300') || stripos($e->getMessage(), 'code 413')) {
if (stripos($e->getMessage(), 'code 300') !== false || stripos($e->getMessage(), 'code 413') !== false) {
$message = "Either Attachment too large, or recipient has been suppressed.";
$this->fail();
@ -209,7 +204,15 @@ class NinjaMailerJob implements ShouldQueue
return;
}
if (stripos($e->getMessage(), 'code 406')) {
if(stripos($e->getMessage(), 'Dsn') !== false){
nlog("Incorrectly configured mail server - setting to default mail driver.");
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
if (stripos($e->getMessage(), 'code 406') !== false) {
$email = $this->nmo->to_user->email ?? '';
@ -386,17 +389,17 @@ class NinjaMailerJob implements ShouldQueue
$company = $this->company;
$smtp_host = $company->smtp_host;
$smtp_host = $company->smtp_host ?? '';
$smtp_port = $company->smtp_port;
$smtp_username = $company->smtp_username;
$smtp_password = $company->smtp_password;
$smtp_username = $company->smtp_username ?? '';
$smtp_password = $company->smtp_password ?? '';
$smtp_encryption = $company->smtp_encryption ?? 'tls';
$smtp_local_domain = strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null;
$smtp_verify_peer = $company->smtp_verify_peer ?? true;
if(strlen($smtp_host ?? '') <= 1 ||
strlen($smtp_username ?? '') <= 1 ||
strlen($smtp_password ?? '') <= 1
if(strlen($smtp_host) <= 1 ||
strlen($smtp_username) <= 1 ||
strlen($smtp_password) <= 1
)
{
$this->nmo->settings->email_sending_method = 'default';
@ -790,6 +793,7 @@ class NinjaMailerJob implements ShouldQueue
private function refreshOfficeToken(User $user)
{
$expiry = $user->oauth_user_token_expiry ?: now()->subDay();
$token = false;
if ($expiry->lt(now())) {
$guzzle = new \GuzzleHttp\Client();
@ -798,7 +802,7 @@ class NinjaMailerJob implements ShouldQueue
if (!$user->oauth_user_refresh_token || $user->oauth_user_refresh_token == '') {
return false;
}
try {
$token = json_decode($guzzle->post($url, [
'form_params' => [

View File

@ -38,7 +38,7 @@ class NinjaMailerObject
public $template = false;
/* @var bool | App\Models\Invoice | App\Models\Quote | App\Models\Credit | App\Models\RecurringInvoice | App\Models\PurchaseOrder | App\Models\Payment $entity */
/* @var \bool | App\Models\Invoice | App\Models\Quote | App\Models\Credit | App\Models\RecurringInvoice | App\Models\PurchaseOrder | App\Models\Payment $entity */
public $entity = false;
public $reminder_template = '';

View File

@ -168,46 +168,51 @@ class ReminderJob implements ShouldQueue
$amount = $fees[0];
$percent = $fees[1];
$temp_invoice_balance = $over_due_invoice->balance;
$invoice = false;
if ($amount <= 0 && $percent <= 0) {
return;
//2024-06-07 this early return prevented any reminders from sending for users who enabled lock_invoices.
if ($amount > 0 || $percent > 0) {
// return;
$fee = $amount;
if ($over_due_invoice->partial > 0) {
$fee += round($over_due_invoice->partial * $percent / 100, 2);
} else {
$fee += round($over_due_invoice->balance * $percent / 100, 2);
}
/** @var \App\Models\Invoice $invoice */
$invoice = InvoiceFactory::create($over_due_invoice->company_id, $over_due_invoice->user_id);
$invoice->client_id = $over_due_invoice->client_id;
$invoice->date = now()->format('Y-m-d');
$invoice->due_date = now()->format('Y-m-d');
$invoice_item = new InvoiceItem();
$invoice_item->type_id = '5';
$invoice_item->product_key = trans('texts.fee');
$invoice_item->notes = ctrans('texts.late_fee_added_locked_invoice', ['invoice' => $over_due_invoice->number, 'date' => $this->translateDate(now()->startOfDay(), $over_due_invoice->client->date_format(), $over_due_invoice->client->locale())]);
$invoice_item->quantity = 1;
$invoice_item->cost = $fee;
$invoice_items = [];
$invoice_items[] = $invoice_item;
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice();
$invoice->service()
->createInvitations()
->applyNumber()
->markSent()
->save();
}
$fee = $amount;
if ($over_due_invoice->partial > 0) {
$fee += round($over_due_invoice->partial * $percent / 100, 2);
} else {
$fee += round($over_due_invoice->balance * $percent / 100, 2);
if(!$invoice){
$invoice = $over_due_invoice;
}
/** @var \App\Models\Invoice $invoice */
$invoice = InvoiceFactory::create($over_due_invoice->company_id, $over_due_invoice->user_id);
$invoice->client_id = $over_due_invoice->client_id;
$invoice->date = now()->format('Y-m-d');
$invoice->due_date = now()->format('Y-m-d');
$invoice_item = new InvoiceItem();
$invoice_item->type_id = '5';
$invoice_item->product_key = trans('texts.fee');
$invoice_item->notes = ctrans('texts.late_fee_added_locked_invoice', ['invoice' => $over_due_invoice->number, 'date' => $this->translateDate(now()->startOfDay(), $over_due_invoice->client->date_format(), $over_due_invoice->client->locale())]);
$invoice_item->quantity = 1;
$invoice_item->cost = $fee;
$invoice_items = [];
$invoice_items[] = $invoice_item;
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice();
$invoice->service()
->createInvitations()
->applyNumber()
->markSent()
->save();
$enabled_reminder = 'enable_'.$reminder_template;
if ($reminder_template == 'endless_reminder') {
$enabled_reminder = 'enable_reminder_endless';

View File

@ -45,6 +45,7 @@ class RFF extends Component
$contact = auth()->guard('contact');
/** @var \App\Models\ClientContact $contact */
$contact->user()->update([
'first_name' => $data['contact_first_name'],
'last_name' => $data['contact_last_name'],
@ -65,6 +66,7 @@ class RFF extends Component
public function render()
{
/** @var \App\Models\CompanyGateway $gateway */
$gateway = CompanyGateway::find($this->context['form']['company_gateway_id']);
$countries = Cache::get('countries');

View File

@ -402,7 +402,7 @@ class RequiredClientInfo extends Component
if ($this->unfilled_fields === 0 && (!$this->company_gateway->always_show_required_fields || $this->is_subscription)) {
$this->dispatch(
'passed-required-fields-check',
client_postal_code: $this->contact->client->postal_code
client_postal_code: $_contact->client->postal_code
);
}

View File

@ -386,6 +386,7 @@ class Client extends BaseModel implements HasLocalePreference
// }
return $languages->first(function ($item) {
/** @var \stdClass $item */
return $item->id == $this->getSetting('language_id');
});
}
@ -419,6 +420,8 @@ class Client extends BaseModel implements HasLocalePreference
// }
return $date_formats->first(function ($item) {
/** @var \stdClass $item */
return $item->id == $this->getSetting('date_format_id');
})->format;
}
@ -433,6 +436,8 @@ class Client extends BaseModel implements HasLocalePreference
// }
return $currencies->first(function ($item) {
/** @var \stdClass $item */
return $item->id == $this->getSetting('currency_id');
});
}

View File

@ -636,17 +636,21 @@ class Company extends BaseModel
public function country()
{
$companies = Cache::get('countries');
$countries = app('countries');
if (! $companies) {
$this->buildCache(true);
// $countries = Cache::get('countries');
$companies = Cache::get('countries');
}
// if (! $companies) {
// $this->buildCache(true);
return $companies->filter(function ($item) {
// $companies = Cache::get('countries');
// }
return $countries->first(function ($item) {
/** @var \stdClass $item */
return $item->id == $this->getSetting('country_id');
})->first();
});
// return $this->belongsTo(Country::class);
// return Country::find($this->settings->country_id);
@ -659,15 +663,18 @@ class Company extends BaseModel
public function timezone()
{
$timezones = Cache::get('timezones');
// $timezones = Cache::get('timezones');
if (! $timezones) {
$this->buildCache(true);
}
$timezones = app('timezones');
return $timezones->filter(function ($item) {
// if (! $timezones) {
// $this->buildCache(true);
// }
return $timezones->first(function ($item) {
/** @var \stdClass $item */
return $item->id == $this->settings->timezone_id;
})->first();
});
// return Timezone::find($this->settings->timezone_id);
}

View File

@ -563,6 +563,8 @@ class Invoice extends BaseModel
return $this->status_id == self::STATUS_SENT;
case 'when_paid':
return $this->status_id == self::STATUS_PAID || $this->status_id == self::STATUS_PARTIAL;
case 'end_of_month':
return \Carbon\Carbon::parse($this->date)->setTimezone($this->company->timezone()->name)->endOfMonth()->lte(now());
default:
return false;
}

View File

@ -87,7 +87,8 @@ class BTCPayPaymentDriver extends BaseDriver
public function processWebhookRequest()
{
$webhook_payload = file_get_contents('php://input');
$sig = false;
/** @var \stdClass $btcpayRep */
$btcpayRep = json_decode($webhook_payload);
if ($btcpayRep == null) {
throw new PaymentFailed('Empty data');

View File

@ -176,6 +176,27 @@ class PayPalBasePaymentDriver extends BaseDriver
}
public function handleDuplicateInvoiceId(string $orderID)
{
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$new_invoice_number = $invoice->number."_".Str::random(5);
$update_data =
[[
"op" => "replace",
"path" => "/purchase_units/@reference_id=='default'/invoice_id",
"value" => $new_invoice_number,
]];
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $update_data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
return $r;
}
public function getShippingAddress(): ?array
{
return $this->company_gateway->require_shipping_address ?
@ -369,13 +390,30 @@ class PayPalBasePaymentDriver extends BaseDriver
}
public function handleProcessingFailure(array $response)
{
SystemLogger::dispatch(
['response' => $response],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company ?? $this->company_gateway->company,
);
switch ($response['name']) {
case 'NOT_AUTHORIZED':
throw new PaymentFailed("There was a permissions issue processing this payment, please contact the merchant. ", 401);
break;
default:
throw new PaymentFailed("Unknown error occurred processing payment. Please contact merchant.", 500);
break;
}
}
public function handleRetry($response, $request) {
// $response = $r->json();
// nlog($response['details']);
// if(in_array($response['details'][0]['issue'], ['INSTRUMENT_DECLINED', 'PAYER_ACTION_REQUIRED']))
return response()->json($response->json());
}

View File

@ -114,15 +114,19 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
public function processPaymentResponse($request)
{
nlog("response");
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
$response = json_decode($request['gateway_response'], true);
nlog($response);
if($request->has('token') && strlen($request->input('token')) > 2) {
return $this->processTokenPayment($request, $response);
}
//capture
$orderID = $response['orderID'];
$orderID = $response['orderID'] ?? $this->payment_hash->data->orderID;
if($this->company_gateway->require_shipping_address) {
@ -149,36 +153,26 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
if($r->status() == 422) {
//handle conditions where the client may need to try again.
return $this->handleRetry($r, $request);
// return $this->handleRetry($r, $request);
$r = $this->handleDuplicateInvoiceId($orderID);
}
} catch(\Exception $e) {
//Rescue for duplicate invoice_id
if(stripos($e->getMessage(), 'DUPLICATE_INVOICE_ID') !== false) {
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$new_invoice_number = $invoice->number."_".Str::random(5);
$update_data =
[[
"op" => "replace",
"path" => "/purchase_units/@reference_id=='default'/invoice_id",
"value" => $new_invoice_number,
]];
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $update_data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
$r = $this->handleDuplicateInvoiceId($orderID);
}
}
$response = $r;
nlog("Process response =>");
nlog($response->json());
if(isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) {
$data = [
@ -222,7 +216,6 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
return response()->json(['message' => $message], 400);
// throw new PaymentFailed($message, 400);
}
}
@ -303,6 +296,8 @@ class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
$this->payment_hash->withData("orderID", $r->json()['id']);
return $r->json()['id'];
}

View File

@ -60,7 +60,7 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
*/
public function processPaymentResponse($request)
{
nlog("response");
$this->init();
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
@ -72,7 +72,8 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
return $this->processTokenPayment($request, $response);
//capture
$orderID = $response['orderID'];
$orderID = $response['orderID'] ?? $this->payment_hash->data->orderID;
if($this->company_gateway->require_shipping_address) {
@ -100,7 +101,10 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
if($r->status() == 422){
//handle conditions where the client may need to try again.
return $this->handleRetry($r, $request);
$r = $this->handleDuplicateInvoiceId($orderID);
}
}
@ -109,21 +113,8 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
//Rescue for duplicate invoice_id
if(stripos($e->getMessage(), 'DUPLICATE_INVOICE_ID') !== false){
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$new_invoice_number = $invoice->number."_".Str::random(5);
$update_data =
[[
"op" => "replace",
"path" => "/purchase_units/@reference_id=='default'/invoice_id",
"value" => $new_invoice_number,
]];
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $update_data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
$r = $this->handleDuplicateInvoiceId($orderID);
}
@ -157,11 +148,12 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
return response()->json(['message' => $message], 400);
//throw new PaymentFailed($message, 400);
}
}
private function createNinjaPayment($request, $response) {
$data = [
@ -273,10 +265,19 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
nlog($r->json());
return $r->json()['id'];
$response = $r->json();
if(!isset($response['id']))
$this->handleProcessingFailure($response);
$this->payment_hash->withData("orderID", $response['id']);
return $response['id'];
}
/**
* processTokenPayment
*
@ -293,6 +294,7 @@ class PayPalRestPaymentDriver extends PayPalBasePaymentDriver
*/
public function processTokenPayment($request, array $response) {
/** @car \App\Models\ClientGatwayToken $cgt */
$cgt = ClientGatewayToken::where('client_id', $this->client->id)
->where('token', $request['token'])
->firstOrFail();

View File

@ -161,9 +161,9 @@ class WePayPaymentDriver extends BaseDriver
}
public function detach(ClientGatewayToken $token)
public function detach(ClientGatewayToken $token): bool
{
return true;
}
public function getClientRequiredFields(): array

View File

@ -13,24 +13,24 @@ 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;
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
{
@ -178,7 +178,7 @@ class FatturaPANew extends AbstractService
$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..
$this->DatiGeneraliDocumento->Causale[] = substr($this->invoice->public_notes ?? ' ', 0, 200); //unsure..
return $this;
}

View File

@ -26,14 +26,17 @@ class OrderXDocument extends AbstractService
{
public OrderDocumentBuilder $orderxdocument;
public function __construct(public object $document, private readonly bool $returnObject = false, private array $tax_map = [])
/** @var \App\Models\Invoice | \App\Models\Quote | \App\Models\PurchaseOrder | \App\Models\Credit $document */
public function __construct(public mixed $document, private readonly bool $returnObject = false, private array $tax_map = [])
{
}
public function run(): self
{
$company = $this->document->company;
/** @var \App\Models\Client | \App\Models\Vendor $settings_entity */
$settings_entity = ($this->document instanceof PurchaseOrder) ? $this->document->vendor : $this->document->client;
$profile = $settings_entity->getSetting('e_quote_type') ? $settings_entity->getSetting('e_quote_type') : "OrderX_Extended";

View File

@ -0,0 +1,518 @@
<?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 App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item;
use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party;
use InvoiceNinja\EInvoice\Models\Peppol\PriceType\Price;
use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address;
use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact;
use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount;
use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal;
use InvoiceNinja\EInvoice\Models\Peppol\PartyNameType\PartyName;
use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PayableAmount;
use InvoiceNinja\EInvoice\Models\Peppol\InvoiceLineType\InvoiceLine;
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory;
use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal;
use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PriceAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxableAmount;
use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal;
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory;
use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty;
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
use InvoiceNinja\EInvoice\Models\Peppol\TaxTotal as PeppolTaxTotal;
class Peppol extends AbstractService
{
private array $InvoiceTypeCodes = [
"380" => "Commercial invoice",
"381" => "Credit note",
"383" => "Corrected invoice",
"384" => "Prepayment invoice",
"386" => "Proforma invoice",
"875" => "Self-billed invoice",
"976" => "Factored invoice",
"84" => "Invoice for cross border services",
"82" => "Simplified invoice",
"80" => "Debit note",
"875" => "Self-billed credit note",
"896" => "Debit note related to self-billed invoice"
];
private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice;
private InvoiceSum | InvoiceSumInclusive $calc;
/**
* @param Invoice $invoice
*/
public function __construct(public Invoice $invoice)
{
$this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice;
$this->calc = $this->invoice->calc();
}
public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice
{
return $this->p_invoice;
}
public function run()
{
$this->p_invoice->ID = $this->invoice->number;
$this->p_invoice->IssueDate = new \DateTime($this->invoice->date);
$this->p_invoice->InvoiceTypeCode = 380; //
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
$this->p_invoice->TaxTotal = $this->getTotalTaxes();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
// $payeeFinancialAccount = (new PayeeFinancialAccount())
// ->setBankId($company->settings->custom_value1)
// ->setBankName($company->settings->custom_value2);
// $paymentMeans = (new PaymentMeans())
// ->setPaymentMeansCode($invoice->custom_value1)
// ->setPayeeFinancialAccount($payeeFinancialAccount);
// $ubl_invoice->setPaymentMeans($paymentMeans);
}
private function getLegalMonetaryTotal(): LegalMonetaryTotal
{
$taxable = $this->getTaxable();
$lmt = new LegalMonetaryTotal;
$lea = new LineExtensionAmount;
$lea->currencyID = $this->invoice->client->currency()->code;
$lea->amount = $taxable;
$lmt->LineExtensionAmount = $lea;
$tea = new TaxExclusiveAmount;
$tea->currencyID = $this->invoice->client->currency()->code;
$tea->amount = $taxable;
$lmt->TaxExclusiveAmount = $tea;
$tia = new TaxInclusiveAmount;
$tia->currencyID = $this->invoice->client->currency()->code;
$tia->amount = $this->invoice->amount;
$lmt->TaxInclusiveAmount = $tia;
$pa = new PayableAmount;
$pa->currencyID = $this->invoice->client->currency()->code;
$pa->amount = $this->invoice->amount;
$lmt->PayableAmount = $pa;
return $lmt;
}
private function getTotalTaxes(): array
{
$taxes = [];
$type_id = $this->invoice->line_items[0]->type_id;
if( strlen($this->invoice->tax_name1 ?? '') > 1)
{
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = round($this->invoice->amount * (1 / $this->invoice->tax_rate1),2);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->amount;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $this->invoice->tax_rate1;
$ts = new PeppolTaxScheme();
$ts->ID = $this->invoice->tax_name1;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal;
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal = $tax_subtotal;
$taxes[] = $tax_total;
}
if(strlen($this->invoice->tax_name2 ?? '') > 1) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = round($this->invoice->amount * (1 / $this->invoice->tax_rate2), 2);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->amount;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $this->invoice->tax_rate2;
$ts = new PeppolTaxScheme();
$ts->ID = $this->invoice->tax_name2;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal = $tax_subtotal;
$taxes[] = $tax_total;
}
if(strlen($this->invoice->tax_name3 ?? '') > 1) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = round($this->invoice->amount * (1 / $this->invoice->tax_rate1), 2);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->amount;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $this->invoice->tax_rate3;
$ts = new PeppolTaxScheme();
$ts->ID = $this->invoice->tax_name3;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal = $tax_subtotal;
$taxes[] = $tax_total;
}
return $taxes;
}
private function getInvoiceLines(): array
{
$lines = [];
foreach($this->invoice->line_items as $key => $item)
{
$_item = new Item;
$_item->Name = $item->product_key;
$_item->Description = $item->notes;
$line = new InvoiceLine;
$line->ID = $key+1;
$line->InvoicedQuantity = $item->quantity;
$lea = new LineExtensionAmount;
$lea->currencyID = $this->invoice->client->currency()->code;
$lea->amount = $item->line_total;
$line->LineExtensionAmount = $lea;
$line->Item = $_item;
// $ta = new TaxAmount;
// $ta->amount = $this->getItemTaxes($item);
// $ta->currencyID = $this->invoice->client->currency()->Code;
// $tt->TaxAmount = $ta;
$item_taxes = $this->getItemTaxes($item);
if(count($item_taxes) > 0)
$line->TaxTotal = $item_taxes;
$price = new Price;
$pa = new PriceAmount;
$pa->currencyID = $this->invoice->client->currency()->code;
$pa->amount = $this->costWithDiscount($item);
$price->PriceAmount = $pa;
$line->Price = $price;
$lines[] = $line;
}
return $lines;
}
private function costWithDiscount($item)
{
$cost = $item->cost;
if ($item->discount != 0) {
if ($this->invoice->is_amount_discount) {
$cost -= $item->discount / $item->quantity;
} else {
$cost -= $cost * $item->discount / 100;
}
}
return $cost;
}
private function getItemTaxes(object $item): array
{
$item_taxes = [];
if(strlen($item->tax_name1 ?? '') > 1)
{
$tax_amount = new TaxAmount;
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = round(($item->line_total * (1/$item->tax_rate1)),2);
$tax_subtotal = new TaxSubtotal;
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount;
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $item->line_total;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory;
$tc->ID = $item->type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $item->tax_rate1;
$ts = new PeppolTaxScheme;
$ts->ID = $item->tax_name1;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$item_taxes[] = $tax_total;
}
if(strlen($item->tax_name2 ?? '') > 1) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = round(($item->line_total * (1 / $item->tax_rate2)),2);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $item->line_total;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = $item->type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $item->tax_rate2;
$ts = new PeppolTaxScheme();
$ts->ID = $item->tax_name2;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$item_taxes[] = $tax_total;
}
if(strlen($item->tax_name3 ?? '') > 1) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = round(($item->line_total * (1 / $item->tax_rate3)),2);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $item->line_total;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = $item->type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $item->tax_rate3;
$ts = new PeppolTaxScheme();
$ts->ID = $item->tax_name3;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$item_taxes[] = $tax_total;
}
return $item_taxes;
}
private function getAccountingSupplierParty(): AccountingSupplierParty
{
$asp = new AccountingSupplierParty();
$party = new Party();
$party_name = new PartyName;
$party_name->Name = $this->invoice->company->present()->name();
$party->PartyName[] = $party_name;
$address = new Address();
$address->CityName = $this->invoice->company->settings->city;
$address->StreetName = $this->invoice->company->settings->address1;
// $address->BuildingName = $this->invoice->company->settings->address2;
$address->PostalZone = $this->invoice->company->settings->postal_code;
$address->CountrySubentity = $this->invoice->company->settings->state;
// $address->CountrySubentityCode = $this->invoice->company->settings->state;
$country = new Country();
$country->IdentificationCode = $this->invoice->company->country()->iso_3166_2;
$address->Country = $country;
$party->PostalAddress = $address;
$party->PhysicalLocation = $address;
$contact = new Contact();
$contact->ElectronicMail = $this->invoice->company->owner()->email ?? 'owner@gmail.com';
$party->Contact = $contact;
$asp->Party = $party;
return $asp;
}
private function getAccountingCustomerParty(): AccountingCustomerParty
{
$acp = new AccountingCustomerParty();
$party = new Party();
$party_name = new PartyName();
$party_name->Name = $this->invoice->client->present()->name();
$party->PartyName[] = $party_name;
$address = new Address();
$address->CityName = $this->invoice->client->city;
$address->StreetName = $this->invoice->client->address1;
// $address->BuildingName = $this->invoice->client->address2;
$address->PostalZone = $this->invoice->client->postal_code;
$address->CountrySubentity = $this->invoice->client->state;
// $address->CountrySubentityCode = $this->invoice->client->state;
$country = new Country();
$country->IdentificationCode = $this->invoice->client->country->iso_3166_2;
$address->Country = $country;
$party->PostalAddress = $address;
$party->PhysicalLocation = $address;
$contact = new Contact();
$contact->ElectronicMail = $this->invoice->client->present()->email();
$party->Contact = $contact;
$acp->Party = $party;
return $acp;
}
private function getTaxable(): float
{
$total = 0;
foreach ($this->invoice->line_items as $item) {
$line_total = $item->quantity * $item->cost;
if ($item->discount != 0) {
if ($this->invoice->is_amount_discount) {
$line_total -= $item->discount;
} else {
$line_total -= $line_total * $item->discount / 100;
}
}
$total += $line_total;
}
if ($this->invoice->discount > 0) {
if ($this->invoice->is_amount_discount) {
$total -= $this->invoice->discount;
} else {
$total *= (100 - $this->invoice->discount) / 100;
$total = round($total, 2);
}
}
if ($this->invoice->custom_surcharge1 && $this->invoice->custom_surcharge_tax1) {
$total += $this->invoice->custom_surcharge1;
}
if ($this->invoice->custom_surcharge2 && $this->invoice->custom_surcharge_tax2) {
$total += $this->invoice->custom_surcharge2;
}
if ($this->invoice->custom_surcharge3 && $this->invoice->custom_surcharge_tax3) {
$total += $this->invoice->custom_surcharge3;
}
if ($this->invoice->custom_surcharge4 && $this->invoice->custom_surcharge_tax4) {
$total += $this->invoice->custom_surcharge4;
}
return $total;
}
}

View File

@ -32,8 +32,13 @@ class ZugferdEDokument extends AbstractService
public function run(): self
{
/** @var \App\Models\Company $company */
$company = $this->document->company;
/** @var \App\Models\Client $client */
$client = $this->document->client;
$profile = $client->getSetting('e_invoice_type');
$profile = match ($profile) {

View File

@ -303,11 +303,6 @@ class Email implements ShouldQueue
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
}
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') {
@ -326,7 +321,7 @@ class Email implements ShouldQueue
$message = $e->getMessage();
if (stripos($e->getMessage(), 'code 300') || stripos($e->getMessage(), 'code 413')) {
if (stripos($e->getMessage(), 'code 300') !== false || stripos($e->getMessage(), 'code 413') !== false) {
$message = "Either Attachment too large, or recipient has been suppressed.";
$this->fail();
@ -337,8 +332,16 @@ class Email implements ShouldQueue
return;
}
if(stripos($e->getMessage(), 'Dsn') !== false) {
if (stripos($e->getMessage(), 'code 406')) {
nlog("Incorrectly configured mail server - setting to default mail driver.");
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
if (stripos($e->getMessage(), 'code 406') !== false) {
$address_object = reset($this->email_object->to);
@ -613,17 +616,17 @@ class Email implements ShouldQueue
$company = $this->company;
$smtp_host = $company->smtp_host;
$smtp_host = $company->smtp_host ?? '';
$smtp_port = $company->smtp_port;
$smtp_username = $company->smtp_username;
$smtp_password = $company->smtp_password;
$smtp_username = $company->smtp_username ?? '';
$smtp_password = $company->smtp_password ?? '';
$smtp_encryption = $company->smtp_encryption ?? 'tls';
$smtp_local_domain = strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null;
$smtp_verify_peer = $company->smtp_verify_peer ?? true;
if(strlen($smtp_host ?? '') <= 1 ||
strlen($smtp_username ?? '') <= 1 ||
strlen($smtp_password ?? '') <= 1
if(strlen($smtp_host) <= 1 ||
strlen($smtp_username) <= 1 ||
strlen($smtp_password) <= 1
) {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();

View File

@ -11,22 +11,25 @@
namespace App\Services\Invoice;
use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\PaymentFactory;
use App\Libraries\MultiDB;
use Carbon\Carbon;
use App\Utils\Ninja;
use App\Utils\Number;
use App\Models\Client;
use App\Models\ClientGatewayToken;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Payment;
use App\Libraries\MultiDB;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use Illuminate\Support\Str;
use App\DataMapper\InvoiceItem;
use App\Factory\PaymentFactory;
use App\Services\AbstractService;
use App\Models\ClientGatewayToken;
use App\Events\Invoice\InvoiceWasPaid;
use App\Repositories\CreditRepository;
use App\Repositories\PaymentRepository;
use App\Services\AbstractService;
use App\Utils\Ninja;
use Illuminate\Support\Str;
use App\Events\Payment\PaymentWasCreated;
class AutoBillInvoice extends AbstractService
{
@ -205,11 +208,27 @@ class AutoBillInvoice extends AbstractService
info("adjusting credit balance {$current_credit->balance} by this amount ".$credit['amount']);
$item_date = Carbon::parse($payment->date)->format($payment->client->date_format());
$invoice_numbers = $this->invoice->number;
$item = new InvoiceItem();
$item->quantity = 0;
$item->cost = $credit['amount'] * -1;
$item->notes = "{$item_date} - " . ctrans('texts.credit_payment', ['invoice_number' => $invoice_numbers]) . " ". Number::formatMoney($credit['amount'], $payment->client);
$item->type_id = "1";
$line_items = $current_credit->line_items;
$line_items[] = $item;
$current_credit->line_items = $line_items;
$current_credit->service()
->adjustBalance($credit['amount'] * -1)
->updatePaidToDate($credit['amount'])
->setCalculatedStatus()
->save();
}
$payment->ledger()
@ -240,6 +259,7 @@ class AutoBillInvoice extends AbstractService
return $this->invoice
->service()
->setCalculatedStatus()
->workFlow() //07-06-2024 - run the workflow if paid!
->save();
}

183
composer.lock generated
View File

@ -1385,16 +1385,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.309.0",
"version": "3.311.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "9213b2101afa17fe8ea99b8ea2dad85c1b1166a8"
"reference": "90218b9372469babf294f97bdd764c9d47ec8a57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9213b2101afa17fe8ea99b8ea2dad85c1b1166a8",
"reference": "9213b2101afa17fe8ea99b8ea2dad85c1b1166a8",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/90218b9372469babf294f97bdd764c9d47ec8a57",
"reference": "90218b9372469babf294f97bdd764c9d47ec8a57",
"shasum": ""
},
"require": {
@ -1474,9 +1474,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.309.0"
"source": "https://github.com/aws/aws-sdk-php/tree/3.311.1"
},
"time": "2024-06-03T18:10:07+00:00"
"time": "2024-06-06T18:05:50+00:00"
},
{
"name": "bacon/bacon-qr-code",
@ -4630,16 +4630,16 @@
},
{
"name": "horstoeko/zugferd",
"version": "v1.0.51",
"version": "v1.0.53",
"source": {
"type": "git",
"url": "https://github.com/horstoeko/zugferd.git",
"reference": "9e036d4a9660638b4f51d2babb397fcff29ef046"
"reference": "939e93ab2e84ec476735e5957f4db7e7d58880c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/9e036d4a9660638b4f51d2babb397fcff29ef046",
"reference": "9e036d4a9660638b4f51d2babb397fcff29ef046",
"url": "https://api.github.com/repos/horstoeko/zugferd/zipball/939e93ab2e84ec476735e5957f4db7e7d58880c3",
"reference": "939e93ab2e84ec476735e5957f4db7e7d58880c3",
"shasum": ""
},
"require": {
@ -4699,9 +4699,9 @@
],
"support": {
"issues": "https://github.com/horstoeko/zugferd/issues",
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.51"
"source": "https://github.com/horstoeko/zugferd/tree/v1.0.53"
},
"time": "2024-05-31T17:20:07+00:00"
"time": "2024-06-05T16:49:22+00:00"
},
{
"name": "http-interop/http-factory-guzzle",
@ -5076,16 +5076,17 @@
"source": {
"type": "git",
"url": "https://github.com/invoiceninja/einvoice.git",
"reference": "228560c6eea03d9ae10a174a0d9f1456efc5eabd"
"reference": "1b9a488d65715272941f1ae6ddfe2d94ad25c2c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/228560c6eea03d9ae10a174a0d9f1456efc5eabd",
"reference": "228560c6eea03d9ae10a174a0d9f1456efc5eabd",
"url": "https://api.github.com/repos/invoiceninja/einvoice/zipball/1b9a488d65715272941f1ae6ddfe2d94ad25c2c1",
"reference": "1b9a488d65715272941f1ae6ddfe2d94ad25c2c1",
"shasum": ""
},
"require": {
"illuminate/collections": "^10.48",
"illuminate/collections": "^10|^11",
"php": "^8.2",
"phpdocumentor/reflection-docblock": "^5.4",
"sabre/xml": "^4.0",
"symfony/property-access": "^7",
@ -5094,6 +5095,7 @@
"symfony/validator": "^7"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.58",
"milo/schematron": "^1.0",
"nette/php-generator": "^4.1",
"phpstan/phpstan": "^1.11",
@ -5104,7 +5106,7 @@
"type": "library",
"autoload": {
"psr-4": {
"Invoiceninja\\Einvoice\\": "src/"
"InvoiceNinja\\EInvoice\\": "src/"
}
},
"license": [
@ -5121,7 +5123,7 @@
"source": "https://github.com/invoiceninja/einvoice/tree/main",
"issues": "https://github.com/invoiceninja/einvoice/issues"
},
"time": "2024-06-03T07:03:23+00:00"
"time": "2024-06-07T04:32:09+00:00"
},
{
"name": "invoiceninja/inspector",
@ -12517,16 +12519,16 @@
},
{
"name": "symfony/css-selector",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "843f2f7ac5e4c5bf0ec77daef23ca6d4d8922adc"
"reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/843f2f7ac5e4c5bf0ec77daef23ca6d4d8922adc",
"reference": "843f2f7ac5e4c5bf0ec77daef23ca6d4d8922adc",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/1c7cee86c6f812896af54434f8ce29c8d94f9ff4",
"reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4",
"shasum": ""
},
"require": {
@ -12562,7 +12564,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v7.1.0"
"source": "https://github.com/symfony/css-selector/tree/v7.1.1"
},
"funding": [
{
@ -12578,7 +12580,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -12724,16 +12726,16 @@
},
{
"name": "symfony/event-dispatcher",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "522d2772d6c7bab843b0c52466dc7844622bacc2"
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/522d2772d6c7bab843b0c52466dc7844622bacc2",
"reference": "522d2772d6c7bab843b0c52466dc7844622bacc2",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
"reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7",
"shasum": ""
},
"require": {
@ -12784,7 +12786,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.0"
"source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1"
},
"funding": [
{
@ -12800,7 +12802,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@ -13372,16 +13374,16 @@
},
{
"name": "symfony/intl",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
"reference": "1b8974c7ff07c61d4acf0e873e34dfb75190b2bb"
"reference": "66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/1b8974c7ff07c61d4acf0e873e34dfb75190b2bb",
"reference": "1b8974c7ff07c61d4acf0e873e34dfb75190b2bb",
"url": "https://api.github.com/repos/symfony/intl/zipball/66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c",
"reference": "66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c",
"shasum": ""
},
"require": {
@ -13438,7 +13440,7 @@
"localization"
],
"support": {
"source": "https://github.com/symfony/intl/tree/v7.1.0"
"source": "https://github.com/symfony/intl/tree/v7.1.1"
},
"funding": [
{
@ -13454,7 +13456,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/mailer",
@ -13692,16 +13694,16 @@
},
{
"name": "symfony/options-resolver",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "9564f64c16f99e29f252eafc642965e8fcb755ce"
"reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/9564f64c16f99e29f252eafc642965e8fcb755ce",
"reference": "9564f64c16f99e29f252eafc642965e8fcb755ce",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55",
"reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55",
"shasum": ""
},
"require": {
@ -13739,7 +13741,7 @@
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.1.0"
"source": "https://github.com/symfony/options-resolver/tree/v7.1.1"
},
"funding": [
{
@ -13755,7 +13757,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -14685,16 +14687,16 @@
},
{
"name": "symfony/property-access",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
"reference": "1e8c1e6ac1b19cf945d8094a0ee50296872c4cb2"
"reference": "74e39e6a6276b8e384f34c6ddbc10a6c9a60193a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/property-access/zipball/1e8c1e6ac1b19cf945d8094a0ee50296872c4cb2",
"reference": "1e8c1e6ac1b19cf945d8094a0ee50296872c4cb2",
"url": "https://api.github.com/repos/symfony/property-access/zipball/74e39e6a6276b8e384f34c6ddbc10a6c9a60193a",
"reference": "74e39e6a6276b8e384f34c6ddbc10a6c9a60193a",
"shasum": ""
},
"require": {
@ -14741,7 +14743,7 @@
"reflection"
],
"support": {
"source": "https://github.com/symfony/property-access/tree/v7.1.0"
"source": "https://github.com/symfony/property-access/tree/v7.1.1"
},
"funding": [
{
@ -14757,20 +14759,20 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/property-info",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-info.git",
"reference": "b10cb8cf0179aec96769df2affb881ecfc293f79"
"reference": "0f80f818c6728f15de30a4f89866d68e4912ae84"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/property-info/zipball/b10cb8cf0179aec96769df2affb881ecfc293f79",
"reference": "b10cb8cf0179aec96769df2affb881ecfc293f79",
"url": "https://api.github.com/repos/symfony/property-info/zipball/0f80f818c6728f15de30a4f89866d68e4912ae84",
"reference": "0f80f818c6728f15de30a4f89866d68e4912ae84",
"shasum": ""
},
"require": {
@ -14825,7 +14827,7 @@
"validator"
],
"support": {
"source": "https://github.com/symfony/property-info/tree/v7.1.0"
"source": "https://github.com/symfony/property-info/tree/v7.1.1"
},
"funding": [
{
@ -14841,7 +14843,7 @@
"type": "tidelift"
}
],
"time": "2024-05-30T12:09:55+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/psr-http-message-bridge",
@ -15017,16 +15019,16 @@
},
{
"name": "symfony/serializer",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
"reference": "972eb05320d06d07399b71b05e6da9032c865f1d"
"reference": "74817ee48e37cce1a1b33c66ffdb750e7e048c3c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/serializer/zipball/972eb05320d06d07399b71b05e6da9032c865f1d",
"reference": "972eb05320d06d07399b71b05e6da9032c865f1d",
"url": "https://api.github.com/repos/symfony/serializer/zipball/74817ee48e37cce1a1b33c66ffdb750e7e048c3c",
"reference": "74817ee48e37cce1a1b33c66ffdb750e7e048c3c",
"shasum": ""
},
"require": {
@ -15094,7 +15096,7 @@
"description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/serializer/tree/v7.1.0"
"source": "https://github.com/symfony/serializer/tree/v7.1.1"
},
"funding": [
{
@ -15110,7 +15112,7 @@
"type": "tidelift"
}
],
"time": "2024-05-21T15:59:31+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "symfony/service-contracts",
@ -15197,16 +15199,16 @@
},
{
"name": "symfony/string",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "6f41b185e742737917e6f2e3eca37767fba5f17a"
"reference": "60bc311c74e0af215101235aa6f471bcbc032df2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/6f41b185e742737917e6f2e3eca37767fba5f17a",
"reference": "6f41b185e742737917e6f2e3eca37767fba5f17a",
"url": "https://api.github.com/repos/symfony/string/zipball/60bc311c74e0af215101235aa6f471bcbc032df2",
"reference": "60bc311c74e0af215101235aa6f471bcbc032df2",
"shasum": ""
},
"require": {
@ -15264,7 +15266,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v7.1.0"
"source": "https://github.com/symfony/string/tree/v7.1.1"
},
"funding": [
{
@ -15280,7 +15282,7 @@
"type": "tidelift"
}
],
"time": "2024-05-17T10:55:18+00:00"
"time": "2024-06-04T06:40:14+00:00"
},
{
"name": "symfony/translation",
@ -15457,16 +15459,16 @@
},
{
"name": "symfony/type-info",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/type-info.git",
"reference": "b429d0710588fc396ba5def5329cf637d9861f9f"
"reference": "60b28eb733f1453287f1263ed305b96091e0d1dc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/type-info/zipball/b429d0710588fc396ba5def5329cf637d9861f9f",
"reference": "b429d0710588fc396ba5def5329cf637d9861f9f",
"url": "https://api.github.com/repos/symfony/type-info/zipball/60b28eb733f1453287f1263ed305b96091e0d1dc",
"reference": "60b28eb733f1453287f1263ed305b96091e0d1dc",
"shasum": ""
},
"require": {
@ -15519,7 +15521,7 @@
"type"
],
"support": {
"source": "https://github.com/symfony/type-info/tree/v7.1.0"
"source": "https://github.com/symfony/type-info/tree/v7.1.1"
},
"funding": [
{
@ -15535,7 +15537,7 @@
"type": "tidelift"
}
],
"time": "2024-05-02T10:19:13+00:00"
"time": "2024-05-31T14:59:31+00:00"
},
{
"name": "symfony/uid",
@ -15613,16 +15615,16 @@
},
{
"name": "symfony/validator",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/validator.git",
"reference": "ffcc8c56502f6adaeaf6307aef5b98b53a8d0326"
"reference": "fcab7598968b21c361becc930fcae8846638c4c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/validator/zipball/ffcc8c56502f6adaeaf6307aef5b98b53a8d0326",
"reference": "ffcc8c56502f6adaeaf6307aef5b98b53a8d0326",
"url": "https://api.github.com/repos/symfony/validator/zipball/fcab7598968b21c361becc930fcae8846638c4c0",
"reference": "fcab7598968b21c361becc930fcae8846638c4c0",
"shasum": ""
},
"require": {
@ -15669,7 +15671,8 @@
"Symfony\\Component\\Validator\\": ""
},
"exclude-from-classmap": [
"/Tests/"
"/Tests/",
"/Resources/bin/"
]
},
"notification-url": "https://packagist.org/downloads/",
@ -15689,7 +15692,7 @@
"description": "Provides tools to validate values",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/validator/tree/v7.1.0"
"source": "https://github.com/symfony/validator/tree/v7.1.1"
},
"funding": [
{
@ -15705,7 +15708,7 @@
"type": "tidelift"
}
],
"time": "2024-05-21T15:59:31+00:00"
"time": "2024-06-04T05:58:56+00:00"
},
{
"name": "symfony/var-dumper",
@ -18242,16 +18245,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.11.3",
"version": "1.11.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5"
"reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e64220a05c1209fc856d58e789c3b7a32c0bb9a5",
"reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/9100a76ce8015b9aa7125b9171ae3a76887b6c82",
"reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82",
"shasum": ""
},
"require": {
@ -18296,7 +18299,7 @@
"type": "github"
}
],
"time": "2024-05-31T13:53:37+00:00"
"time": "2024-06-06T12:19:22+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -20613,16 +20616,16 @@
},
{
"name": "symfony/stopwatch",
"version": "v7.1.0",
"version": "v7.1.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "13c750a45ac43c45f45d944d22499768aa1b72d8"
"reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/13c750a45ac43c45f45d944d22499768aa1b72d8",
"reference": "13c750a45ac43c45f45d944d22499768aa1b72d8",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
"reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d",
"shasum": ""
},
"require": {
@ -20655,7 +20658,7 @@
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/stopwatch/tree/v7.1.0"
"source": "https://github.com/symfony/stopwatch/tree/v7.1.1"
},
"funding": [
{
@ -20671,7 +20674,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-05-31T14:57:53+00:00"
},
{
"name": "theseer/tokenizer",

View File

@ -201,7 +201,7 @@ return [
App\Providers\MultiDBProvider::class,
App\Providers\ClientPortalServiceProvider::class,
App\Providers\NinjaTranslationServiceProvider::class,
App\Providers\StaticServiceProvider::class,
App\Providers\StaticServiceProvider::class,
],
/*

View File

@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.9.2'),
'app_tag' => env('APP_TAG', '5.9.2'),
'app_version' => env('APP_VERSION', '5.9.3'),
'app_tag' => env('APP_TAG', '5.9.3'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@ -5334,6 +5334,7 @@ $lang = array(
'btcpay_refund_body' => 'A refund intended for you has been issued. To claim it via BTCPay, please click on this link:',
'currency_mauritanian_ouguiya' => 'Mauritanian Ouguiya',
'currency_bhutan_ngultrum' => 'Bhutan Ngultrum',
'end_of_month' => 'End Of Month'
);
return $lang;

View File

@ -1,4 +1,3 @@
{
"Admin": true,
"Ronin": true
"Admin": true
}

File diff suppressed because it is too large Load Diff

View File

@ -123,16 +123,16 @@
examples:
user:
value: user
summary: include=user will include the user object in the response
summary: include=user will include the Cser object in the response
company:
value: company
summary: include=company will include the company object in the response
summary: include=company will include the Company object in the response
token:
value: token
summary: include=token will include the token object in the response
summary: include=token will include the Company Token object in the response
account:
value: account
summary: include=account will include the account object in the response
summary: include=account will include the Account object in the response
per_page_meta:
name: per_page
in: query
@ -160,7 +160,15 @@
include_static:
name: include_static
in: query
description: 'Returns static variables'
description: |
Static variables include:
- Currencies
- Countries
- Languages
- Payment Types
- Email Templatees
- Industries
required: false
schema:
type: string
@ -168,7 +176,11 @@
clear_cache:
name: clear_cache
in: query
description: 'Clears the static cache'
description: |
Clears cache
Sometimes after a system update where the static variables have been updated, it may be necessary to clear the cache so that the static variables repopulate
required: false
schema:
type: string

File diff suppressed because it is too large Load Diff

View File

@ -40,7 +40,53 @@
company:
$ref: '#/components/schemas/Company'
user:
$ref: '#/components/schemas/User'
$ref: '#/components/schemas/UserRef'
token:
$ref: '#/components/schemas/CompanyToken'
type: object
CompanyUserRef:
properties:
permissions:
description: 'The user permissionsfor this company in a comma separated list'
type: string
example: 'create_invoice,create_client,view_client'
settings:
description: 'Settings that are used for the flutter applications to store user preferences / metadata'
type: object
readOnly: true
react_settings:
description: 'Dedicated settings object for the react web application'
type: object
readOnly: true
is_owner:
description: 'Determines whether the user owns this company'
type: boolean
example: true
readOnly: true
is_admin:
description: 'Determines whether the user is the admin of this company'
type: boolean
example: true
readOnly: true
is_locked:
description: 'Determines whether the users access to this company has been locked'
type: boolean
example: true
readOnly: true
updated_at:
description: 'The last time the record was modified, format Unix Timestamp'
type: integer
example: '1231232312321'
deleted_at:
description: 'Timestamp when the user was archived, format Unix Timestamp'
type: integer
example: '12312312321'
account:
$ref: '#/components/schemas/Account'
company:
$ref: '#/components/schemas/Company'
user:
$ref: '#/components/schemas/UserRef'
token:
$ref: '#/components/schemas/CompanyToken'
type: object

View File

@ -100,5 +100,107 @@
example: '123456'
readOnly: true
company_user:
$ref: '#/components/schemas/CompanyUser'
$ref: '#/components/schemas/CompanyUserRef'
type: object
UserRef:
properties:
id:
description: 'The hashed id of the user'
type: string
example: Opnel5aKBz
readOnly: true
first_name:
description: 'The first name of the user'
type: string
example: Brad
last_name:
description: 'The last name of the user'
type: string
example: Pitt
email:
description: 'The users email address'
type: string
example: brad@pitt.com
phone:
description: 'The users phone number'
type: string
example: 555-1233-23232
signature:
description: 'The users sign off signature'
type: string
example: 'Have a nice day!'
avatar:
description: 'The users avatar'
type: string
example: 'https://url.to.your/avatar.png'
accepted_terms_version:
description: 'The version of the invoice ninja terms that has been accepted by the user'
type: string
example: 1.0.1
readOnly: true
oauth_user_id:
description: 'The provider id of the oauth entity'
type: string
example: jkhasdf789as6f675sdf768sdfs
readOnly: true
oauth_provider_id:
description: 'The oauth entity id'
type: string
example: google
readOnly: true
language_id:
description: 'The language id of the user'
type: string
example: 1
verified_phone_number:
description: 'Boolean flag if the user has their phone verified. Required to settings up 2FA'
type: boolean
example: true
readOnly: true
sms_verification_code:
description: 'The sms verification code for the user. Required to settings up 2FA'
type: string
example: '123456'
readOnly: true
oauth_user_token_expiry:
description: 'The expiry date of the oauth token'
type: string
example: '2022-10-10'
readOnly: true
has_password:
description: 'Boolean flag determining if the user has a password'
type: boolean
example: true
readOnly: true
last_confirmed_email_address:
description: 'The last confirmed email address of the user'
type: string
example: 'bob@gmail.com'
readOnly: true
custom_value1:
description: 'A custom value'
type: string
example: 'Custom value 1'
custom_value2:
description: 'A custom value'
type: string
example: '$1000'
custom_value3:
description: 'A custom value'
type: string
example: 'Custom value 3'
custom_value4:
description: 'A custom value'
type: string
example: 'Custom value 4'
is_deleted:
description: 'Boolean flag determining if the user has been deleted'
type: boolean
example: true
readOnly: true
google_2fa_secret:
description: 'The google 2fa secret for the user'
type: string
example: '123456'
readOnly: true
type: object

View File

@ -3,22 +3,88 @@ info:
title: 'Invoice Ninja API Reference.'
description: |
---
<br>
<div style="color: white; background-color:SlateBlue; padding: 12px; border-radius:2px">
The Invoice Ninja API is organized around REST and returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.
</div>
<br>
## Introduction
Welcome to the Invoice Ninja API documentation, your comprehensive guide to integrating Invoice Ninja's powerful features into your applications. Whether you're building a custom client, automating workflows, or integrating with other systems, our API provides the tools you need to streamline your invoicing and billing processes.
## What is Invoice Ninja?
Invoice Ninja is a robust source-available platform designed to simplify invoicing, billing, and payment management for freelancers, small businesses, and enterprises alike. With a user-friendly interface, customizable templates, and a suite of powerful features, Invoice Ninja empowers businesses to create professional invoices, track expenses, manage clients, and get paid faster.
## Why use the Invoice Ninja API?
The Invoice Ninja API allows developers to extend the functionality of Invoice Ninja by programmatically accessing and manipulating data within their Invoice Ninja accounts. With the API, you can automate repetitive tasks, integrate with third-party services, and build custom solutions tailored to your specific business needs.
## Getting Started
To get started with the Invoice Ninja API, you'll need an active Invoice Ninja account (or your own self hosted installation) and API credentials. If you haven't already done so, sign up for an account at Invoice Ninja and generate your API keys from the settings section.
Once you have your API credentials, you can start exploring the API endpoints, authentication methods, request and response formats, and more using the documentation provided here.
## Explore the Documentation
This documentation is organized into sections to help you navigate and understand the various aspects of the Invoice Ninja API:
Authentication: Learn how to authenticate your requests to the API using API tokens.
Endpoints: Explore the available API endpoints for managing invoices, clients, payments, expenses, and more.
Request and Response Formats: Understand the structure of API requests and responses, including parameters, headers, and payloads.
Error Handling: Learn about error codes, status messages, and best practices for handling errors gracefully.
Code Examples: Find code examples and tutorials to help you get started with integrating the Invoice Ninja API into your applications.
## Need Help?
If you have any questions, encounter any issues, or need assistance with using the Invoice Ninja API, don't hesitate to reach out to our support team or join our community forums. We're here to help you succeed with Invoice Ninja and make the most of our API.
Let's start building together!
## Endpoints
<div style="background-color: #2D394E; color: #fff padding: 20px; border-radius: 5px; border: 4px solid #212A3B; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">
<p style="padding:10px; color: #DBDBDB;"">Production: https://invoicing.co</p>
<p style="padding:10px; color: #DBDBDB;">Demo: https://demo.invoiceninja.com</p>
</div>
## Client Libraries
PHP SDK can be found [here](https://github.com/invoiceninja/sdk-php)
## Authentication:
Invoice Ninja uses API tokens to authenticate requests. You can view and manage your API keys in Settings > Account Management > Integrations > API tokens
API requests must be made over HTTPS. Calls made to HTTP will fail.
## Errors:
Invoice Ninja uses standard HTTP response codes to indicate the success or failure of a request. below is a table of standard status codes and responses
| Status Code | Explanation |
|-------------|-----------------------------------------------------------------------------|
| 200 | OK: The request has succeeded. The information returned with the response is dependent on the method used in the request. |
| 301 | Moved Permanently: This and all future requests should be directed to the given URI. |
| 303 | See Other: The response to the request can be found under another URI using the GET method. |
| 400 | Bad Request: The server cannot or will not process the request due to an apparent client error. |
| 401 | Unauthorized: Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. |
| 403 | Forbidden: The request was valid, but the server is refusing action. |
| 404 | Not Found: The requested resource could not be found but may be available in the future. |
| 405 | Method Not Allowed: A request method is not supported for the requested resource. |
| 409 | Conflict: Indicates that the request could not be processed because of conflict in the request. |
| 422 | Unprocessable Entity: The request was well-formed but was unable to be followed due to semantic errors. |
| 429 | Too Many Requests: The user has sent too many requests in a given amount of time ("rate limiting"). |
| 500 | Internal Server Error: A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. |
## Pagination
When using index routes to retrieve lists of data, by default we limit the number of records returned to 20. You can using standard pagination to paginate results, ie:
termsOfService: 'https://invoiceninja.github.io/docs/legal/terms_of_service/#page-content'
contact:
email: contact@invoiceninja.com
name: Invoice Ninja Support
url: 'https://www.invoiceninja.com'
license:
name: 'Elastic License'
url: 'https://www.elastic.co/licensing/elastic-license'
version: 5.8.34
version: 5.9.2
servers:
-
url: 'https://demo.invoiceninja.com'
description: |
## Demo API Server InvoiceNinja.
You can use the demo API key `TOKEN` to test the endpoints from within this API spec
- url: 'https://demo.invoiceninja.com'
description: |
## Demo API endpoint
You can use the demo API key `TOKEN` to test the endpoints from within this API spec
- url: 'https://invoicing.co'
description: |
## Production API endpoint

View File

@ -116,12 +116,10 @@ paths:
description: "The users email address."
type: string
example: "demo@invoiceninja.com"
required: true
password:
description: "The user password. Must meet minimum criteria ~ > 6 characters"
type: string
example: "Password0"
required: true
one_time_password:
description: "The one time password if 2FA is enabled"
type: string

View File

@ -3,6 +3,17 @@
tags:
- clients
summary: 'List clients'
x-code-samples:
- lang: go
label: php
source: |
$ninja = new InvoiceNinja("your_token");
$invoices = $ninja->clients->all();
x-custom-element:
type: markdown
value: |
### Custom Response Description
This is a custom description for the response returned by the `/example` endpoint.
description: |
When retrieving a list of clients you can also chain query parameters in order to filter the dataset that is returned. For example, you can send a request to the following URL to retrieve clients that have a balance greater than 1000:\

2
package-lock.json generated
View File

@ -1,5 +1,5 @@
{
"name": "@invoiceninja/invoiceninja",
"name": "invoiceninja",
"lockfileVersion": 2,
"requires": true,
"packages": {

File diff suppressed because one or more lines are too long

View File

@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
"file": "assets/app-c7c5fad4.css",
"file": "assets/app-f3b33400.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}

View File

@ -1,4 +1,4 @@
<div wire:ignore.self class="@unless($form_only) container mx-auto grid grid-cols-12 @endunless mb-4" data-ref="required-fields-container">
<div wire:ignore.self class="@unless($form_only) container mx-auto grid grid-cols-12 @endunless mb-4 transition ease-out duration-1000 h-500" data-ref="required-fields-container">
<div class="col-span-12 lg:col-span-6 lg:col-start-4 overflow-hidden @unless($form_only) bg-white shadow rounded-lg @endunless">
@unless($form_only)
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">

View File

@ -16,7 +16,12 @@
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div id="paypal-button-container" class="paypal-button-container"></div>
<div id="paypal-button-container" class="paypal-button-container">
</div>
<div id="is_working" class="flex mt-4 place-items-center hidden">
<span class="loader m-auto"></span>
</div>
@endsection
@ -24,7 +29,40 @@
@endsection
@push('footer')
<style type="text/css">
.loader {
width: 48px;
height: 48px;
border-radius: 50%;
position: relative;
animation: rotate 1s linear infinite
}
.loader::before , .loader::after {
content: "";
box-sizing: border-box;
position: absolute;
inset: 0px;
border-radius: 50%;
border: 5px solid #454545;
animation: prixClipFix 2s linear infinite ;
}
.loader::after{
border-color: #FF3D00;
animation: prixClipFix 2s linear infinite , rotate 0.5s linear infinite reverse;
inset: 6px;
}
@keyframes rotate {
0% {transform: rotate(0deg)}
100% {transform: rotate(360deg)}
}
@keyframes prixClipFix {
0% {clip-path:polygon(50% 50%,0 0,0 0,0 0,0 0,0 0)}
25% {clip-path:polygon(50% 50%,0 0,100% 0,100% 0,100% 0,100% 0)}
50% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,100% 100%,100% 100%)}
75% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 100%)}
100% {clip-path:polygon(50% 50%,0 0,100% 0,100% 100%,0 100%,0 0)}
}
</style>
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}&currency={!! $currency !!}&components=buttons,funding-eligibility&intent=capture&enable-funding={!! $funding_source !!}" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
<script>
@ -106,6 +144,8 @@
if(fundingSource != 'card')
document.getElementById('paypal-button-container').hidden = true;
document.getElementById('is_working').classList.remove('hidden');
document.querySelector('div[data-ref="required-fields-container').classList.add('hidden');
},

View File

@ -75,7 +75,6 @@
@endsection
@push('footer')
<link rel="stylesheet" type="text/css" href=https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css />
@if(isset($merchantId))
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}&merchantId={!! $merchantId !!}&components=card-fields" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
@ -87,24 +86,7 @@
const clientId = "{{ $client_id }}";
const orderId = "{!! $order_id !!}";
const cardStyle = {
'input': {
'font-size': '16px',
'font-family': 'courier, monospace',
'font-weight': 'lighter',
'color': '#ccc',
},
'.invalid': {
'color': 'purple',
},
'.expcvv': {
'display': 'grid',
'grid-template-columns': 'auto'
}
};
const cardField = paypal.CardFields({
// style: cardStyle,
client: clientId,
createOrder: function(data, actions) {
return orderId;
@ -125,7 +107,6 @@
}
let storeCard = document.querySelector('input[name=token-billing-checkbox]:checked');
if (storeCard) {
@ -197,7 +178,6 @@
const numberField = cardField.NumberField({
inputEvents: {
onChange: (event)=> {
// console.log("returns a stateObject", event);
}
},
});
@ -207,7 +187,6 @@
const cvvField = cardField.CVVField({
inputEvents: {
onChange: (event)=> {
// console.log("returns a stateObject", event);
}
},
});
@ -216,7 +195,6 @@
const expiryField = cardField.ExpiryField({
inputEvents: {
onChange: (event)=> {
// console.log("returns a stateObject", event);
}
},
});
@ -312,6 +290,18 @@
document.getElementById("token").value = token.value;
}
document.getElementById('errors').textContent = '';
document.getElementById('errors').hidden = true;
document.getElementById('pay-now-token').disabled = true;
document.querySelector('#pay-now-token > svg').classList.remove('hidden');
document.querySelector('#pay-now-token > svg').classList.add('justify-center');
document.querySelector('#pay-now-token > svg').classList.add('mx-auto');
document.querySelector('#pay-now-token > svg').classList.add('item-center');
document.querySelector('#pay-now-token > span').classList.add('hidden');
document.getElementById("gateway_response").value = JSON.stringify( {token: token.value, orderID: "{!! $order_id !!}"} );
document.getElementById("server_response").submit();

View File

@ -50,8 +50,10 @@
document.addEventListener('livewire:init', () => {
Livewire.on('passed-required-fields-check', () => {
document.querySelector('div[data-ref="required-fields-container"]').classList.add('opacity-25');
document.querySelector('div[data-ref="required-fields-container"]').classList.add('pointer-events-none');
document.querySelector('div[data-ref="required-fields-container"]').classList.toggle('h-0');
// document.querySelector('div[data-ref="required-fields-container"]').classList.add('opacity-25');
// document.querySelector('div[data-ref="required-fields-container"]').classList.add('pointer-events-none');
document.querySelector('div[data-ref="gateway-container"]').classList.remove('opacity-25');
document.querySelector('div[data-ref="gateway-container"]').classList.remove('pointer-events-none');

View File

@ -184,7 +184,10 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('client_statement', [ClientStatementController::class, 'statement'])->name('client.statement');
Route::post('companies/purge/{company}', [MigrationController::class, 'purgeCompany'])->middleware('password_protected');
Route::post('companies/purge/{company}', [MigrationController::class, 'purgeCompany'])->middleware('password_protected');
Route::post('companies/current', [CompanyController::class, 'current'])->name('companies.current');
Route::post('companies/purge_save_settings/{company}', [MigrationController::class, 'purgeCompanySaveSettings'])->middleware('password_protected');
Route::resource('companies', CompanyController::class); // name = (companies. index / create / show / update / destroy / edit

View File

@ -15,6 +15,9 @@ module.exports = {
fontFamily: {
sans: ['Open Sans', ...defaultTheme.fontFamily.sans],
},
transitionProperty: {
'height': 'height'
}
},
},
plugins: [

View File

@ -72,6 +72,21 @@ class CompanyTest extends TestCase
$this->assertEquals(1, TaxRate::count());
}
public function testCompanyCurrent()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson("/api/v1/companies/current");
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($arr['data']['id'], $this->company->hashed_id);
}
public function testCompanyLogoInline()
{
$response = $this->withHeaders([

View File

@ -19,13 +19,14 @@ use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\DataMapper\InvoiceItem;
use App\Models\Invoice;
use Invoiceninja\Einvoice\Symfony\Encode;
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;
use InvoiceNinja\EInvoice\EInvoice;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaBodyType\FatturaElettronicaBody;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaHeaderType\FatturaElettronicaHeader;
/**
* @test
@ -125,15 +126,22 @@ class FatturaPATest extends TestCase
$this->assertInstanceOf(FatturaElettronicaBody::class, $fe->FatturaElettronicaBody[0]);
$this->assertInstanceOf(FatturaElettronicaHeader::class, $fe->FatturaElettronicaHeader);
$e = new EInvoice;
$errors = $e->validate($fe);
$encoder = new Encode($fe);
$xml = $encoder->toXml();
if(count($errors) > 0)
nlog($errors);
nlog($xml);
$this->assertCount(0, $errors);
$xml = $e->encode($fe, 'xml');
$this->assertNotNull($xml);
$json = $e->encode($fe, 'json');
$this->assertNotNull($json);
$decode = $e->decode('FatturaPA', $json, 'json');
$this->assertInstanceOf(FatturaElettronica::class, $decode);
}
}

View File

@ -0,0 +1,154 @@
<?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\Feature\EInvoice;
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 App\Services\EDocument\Standards\Peppol;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use InvoiceNinja\EInvoice\EInvoice;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaBodyType\FatturaElettronicaBody;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronicaHeaderType\FatturaElettronicaHeader;
/**
* @test
*/
class PeppolTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
// $this->markTestSkipped('prevent running in CI');
$this->withoutMiddleware(
ThrottleRequests::class
);
}
public function testInvoiceBoot()
{
$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),
'date' => now()->format('Y-m-d')
]);
$invoice->service()->markSent()->save();
$fat = new Peppol($invoice);
$fat->run();
$fe = $fat->getInvoice();
$this->assertNotNull($fe);
$this->assertInstanceOf(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $fe);
$e = new EInvoice();
$xml = $e->encode($fe, 'xml');
$this->assertNotNull($xml);
nlog($xml);
$json = $e->encode($fe, 'json');
$this->assertNotNull($json);
nlog($json);
$decode = $e->decode('Peppol', $json, 'json');
$this->assertInstanceOf(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $decode);
$errors = $e->validate($fe);
if(count($errors) > 0) {
nlog($errors);
}
$this->assertCount(0, $errors);
}
}

View File

@ -184,7 +184,7 @@ class ReminderTest extends TestCase
$settings->schedule_reminder1 = 'after_invoice_date';
$settings->schedule_reminder2 = 'after_invoice_date';
$settings->schedule_reminder3 = 'after_invoice_date';
$settings->lock_invoices = true;
$settings->num_days_reminder1 = 5;
$settings->num_days_reminder2 = 10;
$settings->num_days_reminder3 = 15;
@ -231,7 +231,6 @@ class ReminderTest extends TestCase
}
while($x === false);
$this->assertNotNull($invoice->reminder_last_sent);
//check next send date is on day "10"

View File

@ -23,51 +23,51 @@ 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 InvoiceNinja\EInvoice\Models\Peppol\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 InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party;
use InvoiceNinja\EInvoice\Models\Peppol\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 InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact;
use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount;
use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Invoiceninja\Einvoice\Models\FACT1\AmountType\PriceAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PriceAmount;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Invoiceninja\Einvoice\Models\FACT1\TaxSchemeType\TaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Invoiceninja\Einvoice\Models\FACT1\AddressType\PostalAddress;
use InvoiceNinja\EInvoice\Models\Peppol\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 InvoiceNinja\EInvoice\Models\Peppol\InvoiceLineType\InvoiceLine;
use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal;
use InvoiceNinja\EInvoice\Models\Peppol\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 InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PayableAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxableAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Invoiceninja\Einvoice\Models\FACT1\PartyTaxSchemeType\PartyTaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\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 InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal;
use InvoiceNinja\EInvoice\Models\Peppol\PartyLegalEntityType\PartyLegalEntity;
use InvoiceNinja\EInvoice\Models\Peppol\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;
use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty;
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentificationType\PartyIdentification;
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory;
/**
* @test
*/
class Fact1Test extends TestCase
class FACT1Test extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
@ -140,7 +140,7 @@ class Fact1Test extends TestCase
$_invoice->service()->markSent()->save();
$calc = $_invoice->calc();
$invoice = new \Invoiceninja\Einvoice\Models\FACT1\Invoice();
$invoice = new \InvoiceNinja\EInvoice\Models\Peppol\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;
@ -298,7 +298,7 @@ class Fact1Test extends TestCase
$i->Description = $item->notes;
$i->Name = $item->product_key;
$tax_scheme = new FACT1TaxScheme();
$tax_scheme = new PeppolTaxScheme();
$tax_scheme->ID = $item->tax_name1;
$tax_scheme->Name = $item->tax_rate1;

View File

@ -31,6 +31,29 @@ class DatesTest extends TestCase
// $this->makeTestData();
}
public function testDateNotGreaterThanMonthsEnd()
{
$this->travelTo(now()->createFromDate(2024, 6, 20));
$date = '2024-05-20';
$this->assertTrue(\Carbon\Carbon::parse($date)->endOfMonth()->lte(now()));
$this->travelBack();
}
public function testDatLessThanMonthsEnd()
{
$this->travelTo(now()->createFromDate(2024, 5, 30));
$date = '2024-05-20';
$this->assertFalse(\Carbon\Carbon::parse($date)->endOfMonth()->lte(now()));
$this->travelBack();
}
public function testLastFinancialYear3()
{
$this->travelTo(now()->createFromDate(2020, 6, 30));

21
vite.config.ts.react Normal file
View File

@ -0,0 +1,21 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths(), react()],
server: {
port: 3000,
},
build: {
assetsDir: '',
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
// This will output a single bundle file
entryFileNames: 'bundle.js',
chunkFileNames: 'bundle.js'
}
},
},
});