Merge pull request #9464 from turbo124/v5-develop

v5.8.49
This commit is contained in:
David Bomba 2024-04-17 07:36:10 +10:00 committed by GitHub
commit e18665d1e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 751 additions and 167 deletions

View File

@ -1 +1 @@
5.8.48
5.8.49

View File

@ -13,11 +13,25 @@ namespace App\DataMapper\EDoc;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Optional;
use App\DataMapper\EDoc\FatturaPA\DatiContratto;
use App\DataMapper\EDoc\FatturaPA\DatiRicezione;
use App\DataMapper\EDoc\FatturaPA\DatiOrdineAcquisto;
use App\DataMapper\EDoc\FatturaPA\DatiAnagraficiVettore;
class FatturaPA extends Data
{
public string $RegimeFiscale = 'RF01';
public string $TipoDocumento = 'TD01';
public string $ModalitaPagamento = 'MP01';
public string $CondizioniPagamento = 'TP02';
public DatiRicezione|Optional $DatiRicezione;
public DatiContratto|Optional $DatiContratto;
public DatiOrdineAcquisto|Optional $DatiOrdineAcquisto;
public DatiAnagraficiVettore|Optional $DatiAnagraficiVettore;
public function __construct(
public string $RegimeFiscale = 'RF01',
public string $TipoDocumento = 'TD01',
public string $ModalitaPagamento = 'MP01',
public string $CondizioniPagamento = 'TP02',
) {
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\EDoc\FatturaPA;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\WithTransformer;
use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer;
class DatiAnagraficiVettore extends Data
{
public function __construct(
public string $IdFiscaleIVA = '',
public string $CodiceFiscale = '',
public string $Anagrafica = '',
#[WithTransformer(DateTimeInterfaceTransformer::class, format: 'Y-m-d\TH:i:s.uP')]
public \DateTime $DataOraConsegna = new \DateTime(),
){}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\EDoc\FatturaPA;
use Spatie\LaravelData\Data;
class DatiContratto extends Data
{
public function __construct(
public string $RiferimentoNumeroLinea = '',
public string $IdDocumento = '',
public string $Data = '',
public string $NumItem = '',
public string $CodiceCommessaConvenzione = '',
public string $CodiceCUP = '',
public string $CodiceCIG = '',
) {
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\EDoc\FatturaPA;
use Spatie\LaravelData\Data;
class DatiOrdineAcquisto extends Data
{
public function __construct(
public string $RiferimentoNumeroLinea = '',
public string $IdDocumento = '',
public string $Data = '',
public string $NumItem = '',
public string $CodiceCommessaConvenzione = '',
public string $CodiceCUP = '',
public string $CodiceCIG = '',
) {
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\EDoc\FatturaPA;
use Spatie\LaravelData\Data;
class DatiRicezione extends Data
{
public function __construct(
public string $RiferimentoNumeroLinea = '',
public string $IdDocumento = '',
public string $Data = '',
public string $NumItem = '',
public string $CodiceCommessaConvenzione = '',
public string $CodiceCUP = '',
public string $CodiceCIG = '',
) {
}
}

View File

@ -26,48 +26,4 @@ class EDocSettings extends Data
return $this->FatturaPA ??= new FatturaPA();
}
}
class DatiAnagraficiVettore extends Data{
public string $IdFiscaleIVA = '';
public string $CodiceFiscale = '';
public string $Anagrafica = '';
}
class DatiTrasporto extends Data{
public string $DataOraConsegna = ''; //datetime in this format 2017-01-10T16:46:12.000+02:00
//public DatiAnagraficiVettore
}
class DatiOrdineAcquisto extends Data{
public string $RiferimentoNumeroLinea = '';
public string $IdDocumento = '';
public string $Data = '';
public string $NumItem = '';
public string $CodiceCommessaConvenzione = '';
public string $CodiceCUP = '';
public string $CodiceCIG = '';
}
class DatiContratto extends Data{
public string $RiferimentoNumeroLinea = '';
public string $IdDocumento = '';
public string $Data = '';
public string $NumItem = '';
public string $CodiceCommessaConvenzione = '';
public string $CodiceCUP = '';
public string $CodiceCIG = '';
}
class DatiRicezione extends Data{
public string $RiferimentoNumeroLinea = '';
public string $IdDocumento = '';
public string $Data = '';
public string $NumItem = '';
public string $CodiceCommessaConvenzione = '';
public string $CodiceCUP = '';
public string $CodiceCIG = '';
}

View File

@ -235,17 +235,17 @@ class Rule extends BaseRule implements RuleInterface
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->vat_number) { //eu country / no valid vat
if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) {
// nlog("eu zone with sales above threshold");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate;
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate ?? 0;
} else {
// nlog("EU with intra-community supply ie DE to DE");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate;
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
}
} else {
// nlog("default tax");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate;
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
}
return $this;

View File

@ -0,0 +1,103 @@
<?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 EDocRules
{
// [
// "key" => "",
// "label" => "",
// "type" => "dropdown/date/string/text",
// "resource" => "resource.json",
// "required" => true,
// ]
public function rules()
{
return [
'FatturaPA' => $this->FatturaPADefaults(),
];
}
private function FatturaPADefaults()
{
return [
[
"key" => "RegimeFiscale",
"label" => "Regime Fiscale",
"type" => "dropdown",
"resource" => "RegimeFiscale.json",
"required" => true,
],
[
"key" => "TipoDocumento",
"label" => "Tipo Documento",
"type" => "dropdown",
"resource" => "TipoDocumento.json",
"required" => true,
],
[
"key" => "ModalitaPagamento",
"label" => "Modalita Pagamento",
"type" => "dropdown",
"resource" => "ModalitaPagamento.json",
"required" => true,
],
[
"key" => "CondizioniPagamento",
"label" => "Condizioni Pagamento",
"type" => "dropdown",
"resource" => "CondizioniPagamento.json",
"required" => true,
],
[
"key" => "DatiRicezione",
"label" => "Dati Ricezione",
"type" => "dropdown",
"resource" => "CondizioniPagamento",
"required" => false,
"children" => [],
],
[
"key" => "DatiContratto",
"label" => "Dati Contratto",
"type" => "object",
"resource" => "DatiContratto",
"required" => false,
"children" => [
[
"key"=> "RiferimentoNumeroLinea",
"validation" => [
"string","min:1","max:10","required"
],
]
],
],
[
"key" => "DatiOrdineAcquisto",
"label" => "Dati Ordine Acquisto",
"type" => "object",
"resource" => "DatiOrdineAcquisto",
"required" => false,
],
[
"key" => "DatiAnagraficiVettore",
"label" => "Dati Anagrafici Vettore",
"type" => "object",
"resource" => "DatiAnagraficiVettore",
"required" => false,
],
];
}
}

View File

@ -207,7 +207,6 @@ class InvoiceController extends Controller
//format data
$invoices->map(function ($invoice) {
// $invoice->service()->removeUnpaidGatewayFees();
$invoice->balance = $invoice->balance > 0 ? Number::formatValue($invoice->balance, $invoice->client->currency()) : 0;
$invoice->partial = $invoice->partial > 0 ? Number::formatValue($invoice->partial, $invoice->client->currency()) : 0;

View File

@ -144,7 +144,6 @@ class TwilioController extends BaseController
public function generate2faResetCode(Generate2faRequest $request)
{
nlog($request->all());
nlog($request->headers());
$user = User::where('email', $request->email)->first();

View File

@ -27,12 +27,18 @@ class StoreBankTransactionRuleRequest extends Request
*/
public function authorize(): bool
{
return auth()->user()->can('create', BankTransactionRule::class) && auth()->user()->account->hasFeature(Account::FEATURE_API);
/** @var \App\Models\User $user */
$user = auth()->user();
return $user->can('create', BankTransactionRule::class) && $user->account->hasFeature(Account::FEATURE_API);
;
}
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
/* Ensure we have a client name, and that all emails are unique*/
$rules = [
'name' => 'bail|required|string',
@ -45,18 +51,9 @@ class StoreBankTransactionRuleRequest extends Request
'applies_to' => 'bail|sometimes|string',
];
if (isset($this->category_id)) {
$rules['category_id'] = 'bail|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
}
if (isset($this->vendor_id)) {
$rules['vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
}
if (isset($this->client_id)) {
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
}
$rules['category_id'] = 'bail|sometimes|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0';
return $rules;
}

View File

@ -94,6 +94,10 @@ class StoreCreditRequest extends Request
$input['design_id'] = $this->decodePrimaryKey($input['design_id']);
}
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
$input = $this->decodePrimaryKeys($input);
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];

View File

@ -86,6 +86,10 @@ class UpdateCreditRequest extends Request
$input = $this->decodePrimaryKeys($input);
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}

View File

@ -93,8 +93,8 @@ class StoreInvoiceRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}
if(isset($input['partial']) && $input['partial'] == 0 && isset($input['partial_due_date'])) {
$input['partial_due_date'] = '';
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
$input['amount'] = 0;

View File

@ -91,8 +91,8 @@ class UpdateInvoiceRequest extends Request
$input['id'] = $this->invoice->id;
if(isset($input['partial']) && $input['partial'] == 0 && isset($input['partial_due_date'])) {
$input['partial_due_date'] = '';
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items']) && is_array($input['line_items'])) {

View File

@ -45,9 +45,9 @@ class UpdatePaymentRequest extends Request
'client_id' => ['sometimes', 'bail', Rule::in([$this->payment->client_id])],
'number' => ['sometimes', 'bail', Rule::unique('payments')->where('company_id', $user->company()->id)->ignore($this->payment->id)],
'invoices' => ['sometimes', 'bail', 'nullable', 'array', new PaymentAppliedValidAmount($this->all())],
'invoices.*.invoice_id' => ['sometimes','distinct',Rule::exists('invoices','id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'invoices.*.invoice_id' => ['sometimes','distinct',Rule::exists('invoices','id')->where('company_id', $user->company()->id)->where('client_id', $this->payment->client_id)],
'invoices.*.amount' => ['sometimes','numeric','min:0'],
'credits.*.credit_id' => ['sometimes','bail','distinct',Rule::exists('credits','id')->where('company_id', $user->company()->id)->where('client_id', $this->client_id)],
'credits.*.credit_id' => ['sometimes','bail','distinct',Rule::exists('credits','id')->where('company_id', $user->company()->id)->where('client_id', $this->payment->client_id)],
'credits.*.amount' => ['required', 'bail'],
];

View File

@ -79,6 +79,10 @@ class StorePurchaseOrderRequest extends Request
$input = $this->decodePrimaryKeys($input);
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items']) && is_array($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}

View File

@ -83,6 +83,10 @@ class UpdatePurchaseOrderRequest extends Request
$input['id'] = $this->purchase_order->id;
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if (isset($input['line_items']) && is_array($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}

View File

@ -43,7 +43,7 @@ class StoreQuoteRequest extends Request
$rules = [];
$rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id;
$rules['client_id'] = ['required', 'bail', Rule::exists('clients','id')->where('company_id', $user->company()->id)];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
@ -64,12 +64,17 @@ class StoreQuoteRequest extends Request
$rules['is_amount_discount'] = ['boolean'];
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$rules['line_items'] = 'array';
$rules['partial_due_date'] = ['bail', 'sometimes', 'exclude_if:partial,0', Rule::requiredIf(fn () => $this->partial > 0), 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules;
}
public function prepareForValidation()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
@ -82,6 +87,19 @@ class StoreQuoteRequest extends Request
$input['exchange_rate'] = 1;
}
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
if(!isset($input['date']))
$input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
if(isset($input['partial_due_date']) && (!isset($input['due_date']) || strlen($input['due_date']) <=1 )) {
$client = \App\Models\Client::withTrashed()->find($input['client_id']);
$valid_days = ($client && strlen($client->getSetting('valid_until')) >= 1) ? $client->getSetting('valid_until') : 7;
$input['due_date'] = \Carbon\Carbon::parse($input['date'])->addDays($valid_days)->format('Y-m-d');
}
$this->replace($input);
}
}

View File

@ -56,16 +56,16 @@ class UpdateQuoteRequest extends Request
$rules['file'] = $this->fileValidation();
}
$rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)];
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])];
$rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean'];
$rules['exchange_rate'] = 'bail|sometimes|numeric';
$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', 'after_or_equal:date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
return $rules;
}
@ -89,6 +89,9 @@ class UpdateQuoteRequest extends Request
$input['exchange_rate'] = 1;
}
if(isset($input['partial']) && $input['partial'] == 0) {
$input['partial_due_date'] = null;
}
$this->replace($input);
}

View File

@ -31,14 +31,6 @@ class StoreSetupRequest extends Request
/*System*/
'url' => 'required',
/*Mail driver*/
'mail_driver' => 'required',
'encryption' => 'required_unless:mail_driver,log',
'mail_host' => 'required_unless:mail_driver,log',
'mail_username' => 'required_unless:mail_driver,log',
'mail_name' => 'required_unless:mail_driver,log',
'mail_address' => 'required_unless:mail_driver,log',
'mail_password' => 'required_unless:mail_driver,log',
/*user registration*/
'privacy_policy' => 'required',
'terms_of_service' => 'required',
'first_name' => 'required',

View File

@ -137,6 +137,9 @@ class UpdateTaskRequest extends Request
$input['time_log'] = json_encode([]);
}
if(isset($input['user']))
unset($input['user']);
$this->replace($input);
}

View File

@ -255,6 +255,9 @@ class BaseImport
unset($record['']);
if(!is_array($record))
continue;
try {
$entity = $this->transformer->transform($record);
@ -310,6 +313,11 @@ class BaseImport
$count = 0;
foreach ($data as $key => $record) {
if(!is_array($record)) {
continue;
}
try {
$entity = $this->transformer->transform($record);
$validator = $this->request_name::runFormRequest($entity);
@ -372,6 +380,11 @@ class BaseImport
$invoices = $this->groupInvoices($invoices, $invoice_number_key);
foreach ($invoices as $raw_invoice) {
if(!is_array($raw_invoice)) {
continue;
}
try {
$invoice_data = $invoice_transformer->transform($raw_invoice);
@ -459,6 +472,11 @@ class BaseImport
foreach ($tasks as $raw_task) {
$task_data = [];
if(!is_array($raw_task)) {
continue;
}
try {
$task_data = $task_transformer->transform($raw_task);
$task_data['user_id'] = $this->company->owner()->id;
@ -527,6 +545,11 @@ class BaseImport
$invoices = $this->groupInvoices($invoices, $invoice_number_key);
foreach ($invoices as $raw_invoice) {
if(!is_array($raw_invoice)) {
continue;
}
try {
$invoice_data = $invoice_transformer->transform($raw_invoice);
$invoice_data['user_id'] = $this->company->owner()->id;
@ -742,6 +765,11 @@ class BaseImport
$quotes = $this->groupInvoices($quotes, $quote_number_key);
foreach ($quotes as $raw_quote) {
if(!is_array($raw_quote)) {
continue;
}
try {
$quote_data = $quote_transformer->transform($raw_quote);
$quote_data['line_items'] = $this->cleanItems(

View File

@ -232,6 +232,11 @@ class Wave extends BaseImport implements ImportInterface
$expenses = $this->groupExpenses($data);
foreach ($expenses as $raw_expense) {
if(!is_array($raw_expense)) {
continue;
}
try {
$expense_data = $expense_transformer->transform($raw_expense);

View File

@ -115,7 +115,7 @@ class BaseTransformer
return isset($data[$field]) && $data[$field] ? $data[$field] : null;
}
public function getCurrencyByCode($data, $key = 'client.currency_id')
public function getCurrencyByCode(array $data, string $key = 'client.currency_id')
{
$code = array_key_exists($key, $data) ? $data[$key] : false;

View File

@ -26,6 +26,7 @@ use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class ProcessBankTransactionsYodlee implements ShouldQueue
{
@ -71,8 +72,8 @@ class ProcessBankTransactionsYodlee implements ShouldQueue
set_time_limit(0);
//Loop through everything until we are up to date
$this->from_date = $this->from_date ?: '2021-01-01';
//Loop through everything until we are up to date - improve handling of delayed accounts
$this->from_date = $this->from_date ? Carbon::parse($this->from_date)->subWeeks(2)->format('Y-m-d') : '2021-01-01';
nlog("Yodlee: Processing transactions for account: {$this->bank_integration->account->key}");

View File

@ -61,7 +61,8 @@ class ClientLedgerBalanceUpdate implements ShouldQueue
->where('id', '<', $company_ledger->id)
->where('client_id', $company_ledger->client_id)
->where('company_id', $company_ledger->company_id)
->where('balance', '!=', 0)
->whereNotNull('balance')
// ->where('balance', '!=', 0)
->orderBy('id', 'DESC')
->first();

View File

@ -23,31 +23,6 @@ class Submit extends Component
public function mount()
{
// $request = new \Illuminate\Http\Request([
// 'sidebar' => 'hidden',
// 'hash' => $this->context['hash'],
// 'action' => 'payment',
// 'invoices' => [
// $this->context['form']['invoice_hashed_id'],
// ],
// 'payable_invoices' => [
// [
// 'amount' => $this->context['form']['payable_amount'],
// 'invoice_id' => $this->context['form']['invoice_hashed_id'],
// ],
// ],
// 'company_gateway_id' => $this->context['form']['company_gateway_id'],
// 'payment_method_id' => $this->context['form']['payment_method_id'],
// 'contact_first_name' => $this->context['contact']['first_name'],
// 'contact_last_name' => $this->context['contact']['last_name'],
// 'contact_email' => $this->context['contact']['email'],
// ]);
// return redirect((new InstantPayment($request))->run());
// dd($this->context);
nlog($this->context);
$this->dispatch(
'purchase.submit',
invoice_hashed_id: $this->context['form']['invoice_hashed_id'],

View File

@ -77,7 +77,7 @@ use Laracasts\Presenter\PresentableTrait;
* @property float $amount
* @property float $balance
* @property float|null $partial
* @property string|null $partial_due_date
* @property \Carbon\Carbon|null $partial_due_date
* @property string|null $last_viewed
* @property int|null $created_at
* @property int|null $updated_at
@ -195,10 +195,10 @@ class Quote extends BaseModel
return $this->dateMutator($value);
}
public function getDueDateAttribute($value)
{
return $value ? $this->dateMutator($value) : null;
}
// public function getDueDateAttribute($value)
// {
// return $value ? $this->dateMutator($value) : null;
// }
// public function getPartialDueDateAttribute($value)
// {

View File

@ -166,6 +166,15 @@ class Vendor extends BaseModel
return $this->hasMany(Activity::class);
}
public function getCurrencyCode(): string
{
if ($this->currency()) {
return $this->currency()->code;
}
return 'USD';
}
public function currency()
{
$currencies = Cache::get('currencies');

View File

@ -166,7 +166,10 @@ class PayPalRestPaymentDriver extends BaseDriver
$data['gateway_type_id'] = $this->gateway_type_id;
$data['currency'] = $this->client->currency()->code;
return render('gateways.paypal.pay', $data);
// return render('gateways.paypal.ppcp.card', $data);
return render('gateways.paypal.pay', $data);
}
@ -324,6 +327,65 @@ class PayPalRestPaymentDriver extends BaseDriver
}
private function getPaymentSource(): array
{
if($this->gateway_type_id == 1) {
return [
"card" => [
"attributes" => [
"verification" => [
"method" => "SCA_WHEN_REQUIRED", //SCA_ALWAYS
],
],
"name" => $this->client->present()->primary_contact_name(),
"email_address" => $this->client->present()->email(),
"address" => [
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
],
"experience_context" => [
"user_action" => "PAY_NOW"
],
"stored_credential" => [
"payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction?
"payment_type" => "UNSCHEDULED",
"usage"=> "DERIVED",
],
],
];
}
return [
"paypal" => [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name(),
],
"email_address" => $this->client->present()->email(),
"address" => [
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
],
"experience_context" => [
"user_action" => "PAY_NOW"
],
],
];
}
private function createOrder(array $data): string
{
@ -338,27 +400,7 @@ class PayPalRestPaymentDriver extends BaseDriver
$order = [
"intent" => "CAPTURE",
"payment_source" => [
"paypal" => [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name(),
],
"email_address" => $this->client->present()->email(),
"address" => [
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
],
"experience_context" => [
"user_action" => "PAY_NOW"
],
],
],
"payment_source" => $this->getPaymentSource(),
"purchase_units" => [
[
"custom_id" => $this->payment_hash->hash,

View File

@ -111,7 +111,7 @@ class QuoteTransformer extends EntityTransformer
'reminder2_sent' => $quote->reminder2_sent ?: '',
'reminder3_sent' => $quote->reminder3_sent ?: '',
'reminder_last_sent' => $quote->reminder_last_sent ?: '',
'due_date' => $quote->due_date ?: '',
'due_date' => $quote->due_date ? $quote->due_date->format('Y-m-d') : '',
'terms' => $quote->terms ?: '',
'public_notes' => $quote->public_notes ?: '',
'private_notes' => $quote->private_notes ?: '',
@ -127,7 +127,7 @@ class QuoteTransformer extends EntityTransformer
'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false),
'footer' => $quote->footer ?: '',
'partial' => (float) ($quote->partial ?: 0.0),
'partial_due_date' => $quote->partial_due_date ?: '',
'partial_due_date' => $quote->partial_due_date ? $quote->partial_due_date->format('Y-m-d') : '',
'custom_value1' => (string) $quote->custom_value1 ?: '',
'custom_value2' => (string) $quote->custom_value2 ?: '',
'custom_value3' => (string) $quote->custom_value3 ?: '',

View File

@ -101,6 +101,12 @@ class Number
if($comma === false) //no comma must be a decimal number already
return (float) $value;
if(!$decimal && substr($value, -3, 1) != ","){
$value = $value.".00";
}
$decimal = strpos($value, '.');
if($decimal < $comma){ //decimal before a comma = euro
$value = str_replace(['.',','], ['','.'], $value);
return (float) $value;
@ -113,6 +119,7 @@ class Number
}
/**
* Formats a given value based on the clients currency
* BACK to a float.
@ -228,9 +235,6 @@ class Number
$code = $currency->code;
$swapSymbol = $currency->swap_currency_symbol;
// App\Models\Client::country() returns instance of BelongsTo.
// App\Models\Company::country() returns record for the country, that's why we check for the instance.
if ($entity instanceof Company) {
$country = $entity->country();
} else {

View File

@ -26,9 +26,7 @@ trait SubscriptionHooker
'X-Requested-With' => 'XMLHttpRequest',
];
$post_purchase_rest_method = &$subscription->webhook_configuration['post_purchase_rest_method'];
if (!isset($subscription->webhook_configuration['post_purchase_url']) && !$post_purchase_rest_method) {
if (!isset($subscription->webhook_configuration['post_purchase_url']) && !isset($subscription->webhook_configuration['post_purchase_rest_method'])) {
return [];
}

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.8.48'),
'app_tag' => env('APP_TAG', '5.8.48'),
'app_version' => env('APP_VERSION', '5.8.49'),
'app_tag' => env('APP_TAG', '5.8.49'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@ -0,0 +1,183 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title' => ''])
@section('gateway_head')
@endsection
@section('gateway_content')
<form action="{{ route('client.payments.response') }}" method="post" id="server_response">
@csrf
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="gateway_type_id" id="gateway_type_id" value="{{ $gateway_type_id }}">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}"/>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div id="paypal-button-container" class="paypal-button-container"></div>
<div id="checkout-form">
<!-- Containers for Card Fields hosted by PayPal -->
<!-- <div id="card-name-field-container"></div> -->
<div id="card-number-field-container"></div>
<div class="expcvv" style="display:flex;">
<div id="card-expiry-field-container" style="width:50%"></div>
<div id="card-cvv-field-container" style="width:50%"></div>
</div>
<!-- <button id="card-field-submit-button" type="button">
{{ ctrans('texts.pay_now') }}
</button> -->
@include('portal.ninja2020.gateways.includes.pay_now')
</div>
@endsection
@section('gateway_footer')
@endsection
@push('footer')
<link rel="stylesheet" type="text/css" href=https://www.paypalobjects.com/webstatic/en_US/developer/docs/css/cardfields.css />
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}&components=card-fields" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
<script>
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;
},
onApprove: function(data, actions) {
var errorDetail = Array.isArray(data.details) && data.details[0];
if (errorDetail && ['INSTRUMENT_DECLINED', 'PAYER_ACTION_REQUIRED'].includes(errorDetail.issue)) {
return actions.restart();
}
console.log("on approve");
console.log(data);
console.log(actions);
document.getElementById("gateway_response").value = JSON.stringify( data );
document.getElementById("server_response").submit();
},
onCancel: function() {
window.location.href = "/client/invoices/";
},
// onError: function(error) {
// document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
// document.getElementById('errors').hidden = false;
// // document.getElementById("gateway_response").value = error;
// // document.getElementById("server_response").submit();
// },
onClick: function (){
}
});
// Render each field after checking for eligibility
if (cardField.isEligible()) {
const numberField = cardField.NumberField({
inputEvents: {
onChange: (event)=> {
console.log("returns a stateObject", event);
}
},
});
numberField.render("#card-number-field-container");
const cvvField = cardField.CVVField({
inputEvents: {
onChange: (event)=> {
console.log("returns a stateObject", event);
}
},
});
cvvField.render("#card-cvv-field-container");
const expiryField = cardField.ExpiryField({
inputEvents: {
onChange: (event)=> {
console.log("returns a stateObject", event);
}
},
});
expiryField.render("#card-expiry-field-container");
document.getElementById("pay-now").addEventListener('click', (e) => {
document.getElementById('errors').textContent = '';
document.getElementById('errors').hidden = true;
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > svg').classList.add('justify-center');
document.querySelector('#pay-now > span').classList.add('hidden');
cardField.submit().then((response) => {
console.log("then");
console.log(response);
// lets goooo
}).catch((error) => {
console.log(error);
document.getElementById('pay-now').disabled = false;
document.querySelector('#pay-now > svg').classList.add('hidden');
document.querySelector('#pay-now > span').classList.remove('hidden');
if(error.message == 'INVALID_NUMBER'){
document.getElementById('errors').textContent = "{{ ctrans('texts.invalid_card_number') }}";
}
else if(error.message == 'INVALID_CVV') {
document.getElementById('errors').textContent = "{{ ctrans('texts.invalid_cvv') }}";
}
else if(error.message == 'INVALID_EXPIRY') {
document.getElementById('errors').textContent = "{{ ctrans('texts.invalid_cvv') }}";
}
document.getElementById('errors').hidden = false;
});
});
}
else {
}
</script>
@endpush

View File

@ -12,16 +12,17 @@
namespace Tests\Feature\Bank;
use App\Factory\BankIntegrationFactory;
use App\Factory\BankTransactionFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory;
use App\Models\BankTransaction;
use Tests\TestCase;
use App\Models\Expense;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
use Tests\TestCase;
use App\Factory\InvoiceFactory;
use App\Models\BankTransaction;
use App\Factory\InvoiceItemFactory;
use App\Factory\BankIntegrationFactory;
use App\Factory\BankTransactionFactory;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class BankTransactionTest extends TestCase
{

View File

@ -54,12 +54,99 @@ class QuoteTest extends TestCase
);
}
public function testQuoteDueDateInjectionValidationLayer()
{
$data = [
'client_id' => $this->client->hashed_id,
'partial_due_date' => now()->format('Y-m-d'),
'partial' => 1,
'amount' => 20,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$arr = $response->json();
// nlog($arr);
$this->assertNotEmpty($arr['data']['due_date']);
}
public function testNullDueDates()
{
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => '',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEmpty($arr['data']['due_date']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/quotes/'.$arr['data']['id'], $arr['data']);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEmpty($arr['data']['due_date']);
}
public function testNonNullDueDates()
{
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => now()->addDays(10),
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertNotEmpty($arr['data']['due_date']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/quotes/'.$arr['data']['id'], $arr['data']);
$response->assertStatus(200);
$arr = $response->json();
$this->assertNotEmpty($arr['data']['due_date']);
}
public function testPartialDueDates()
{
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => now()->format('Y-m-d'),
'due_date' => now()->addDay()->format('Y-m-d'),
];
$response = $this->withHeaders([
@ -73,6 +160,41 @@ class QuoteTest extends TestCase
$this->assertNotNull($arr['data']['due_date']);
$this->assertEmpty($arr['data']['partial_due_date']);
$data = [
'client_id' => $this->client->hashed_id,
'due_date' => now()->addDay()->format('Y-m-d'),
'partial' => 1,
'partial_due_date' => now()->format('Y-m-d'),
'amount' => 20,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/quotes', $data);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals(now()->addDay()->format('Y-m-d'), $arr['data']['due_date']);
$this->assertEquals(now()->format('Y-m-d'), $arr['data']['partial_due_date']);
$this->assertEquals(1, $arr['data']['partial']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/quotes/'.$arr['data']['id'], $arr['data']);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals(now()->addDay()->format('Y-m-d'), $arr['data']['due_date']);
$this->assertEquals(now()->format('Y-m-d'), $arr['data']['partial_due_date']);
$this->assertEquals(1, $arr['data']['partial']);
}
public function testQuoteToProjectConversion2()

View File

@ -28,15 +28,17 @@ class NumberTest extends TestCase
"22000.76" =>"22 000,76",
"22000.76" =>"22.000,76",
"22000.76" =>"22,000.76",
"22000" =>"22 000",
"22000" =>"22,000",
"2201" => "2,201",
"22001" =>"22 001",
"22002" =>"22,002",
"37123" => "37,123",
"22" =>"22.000",
"22000" =>"22.000,",
"22000.76" =>"22000.76",
"22000.76" =>"22000,76",
"1022000.76" =>"1.022.000,76",
"1022000.76" =>"1,022,000.76",
// "1000000" =>"1,000,000",
"1000000" =>"1,000,000",
// "1000000" =>"1.000.000",
"1022000.76" =>"1022000.76",
"1022000.76" =>"1022000,76",
@ -48,7 +50,7 @@ class NumberTest extends TestCase
"1" =>"1.00",
"1" =>"1,00",
"423545" =>"423545 €",
// "423545" =>"423,545 €",
"423545" =>"423,545 €",
// "423545" =>"423.545 €",
"1" =>"1,00 €",
"1.02" =>"€ 1.02",
@ -58,7 +60,8 @@ class NumberTest extends TestCase
"1000.02" =>"1.000,02 EURO",
"9.975" => "9.975",
"9975" => "9.975,",
"9975" => "9.975,00"
"9975" => "9.975,00",
// "0.571" => "0,571",
];