diff --git a/VERSION.txt b/VERSION.txt index ad775686352..df99ee43ee3 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.8.48 \ No newline at end of file +5.8.49 \ No newline at end of file diff --git a/app/DataMapper/EDoc/FatturaPA.php b/app/DataMapper/EDoc/FatturaPA.php index a3f3e557d08..4153c3d3127 100644 --- a/app/DataMapper/EDoc/FatturaPA.php +++ b/app/DataMapper/EDoc/FatturaPA.php @@ -13,11 +13,25 @@ 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', + ) { + } } diff --git a/app/DataMapper/EDoc/FatturaPA/DatiAnagraficiVettore.php b/app/DataMapper/EDoc/FatturaPA/DatiAnagraficiVettore.php new file mode 100644 index 00000000000..8ed55a0d0da --- /dev/null +++ b/app/DataMapper/EDoc/FatturaPA/DatiAnagraficiVettore.php @@ -0,0 +1,28 @@ +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 = ''; } \ No newline at end of file diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index b71ff5ca61e..3de96b14ac5 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -235,17 +235,17 @@ public function calculateRates(): self } 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; diff --git a/app/DataProviders/EDocRules.php b/app/DataProviders/EDocRules.php new file mode 100644 index 00000000000..7c7f5ff1f54 --- /dev/null +++ b/app/DataProviders/EDocRules.php @@ -0,0 +1,103 @@ + "", + // "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, + ], + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 2ebf7f50e3e..b446a8d805b 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -207,7 +207,6 @@ private function makePayment(array $ids) //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; diff --git a/app/Http/Controllers/TwilioController.php b/app/Http/Controllers/TwilioController.php index f47a2920ac5..ddd0df7ab76 100644 --- a/app/Http/Controllers/TwilioController.php +++ b/app/Http/Controllers/TwilioController.php @@ -144,7 +144,6 @@ public function confirm(ConfirmSmsRequest $request) public function generate2faResetCode(Generate2faRequest $request) { nlog($request->all()); - nlog($request->headers()); $user = User::where('email', $request->email)->first(); diff --git a/app/Http/Requests/BankTransactionRule/StoreBankTransactionRuleRequest.php b/app/Http/Requests/BankTransactionRule/StoreBankTransactionRuleRequest.php index aff60123fb6..c6ad4e15931 100644 --- a/app/Http/Requests/BankTransactionRule/StoreBankTransactionRuleRequest.php +++ b/app/Http/Requests/BankTransactionRule/StoreBankTransactionRuleRequest.php @@ -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 @@ public function rules() '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; } diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index eb924c22f6e..9baf0c706f4 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -94,6 +94,10 @@ public function prepareForValidation() $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']) : []; diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 79b5b59009a..9dafd1583eb 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -86,6 +86,10 @@ public function prepareForValidation() $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']) : []; } diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index f467277d677..f91faa9cc1f 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -93,8 +93,8 @@ public function prepareForValidation() $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; diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 9f752b24187..f21feb1933f 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -91,8 +91,8 @@ public function prepareForValidation() $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'])) { diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index 0cc1beba756..3c309af4c6f 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -45,9 +45,9 @@ public function rules() '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'], ]; diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 735edd39b72..d21c73cbf68 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -79,6 +79,10 @@ public function prepareForValidation() $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']) : []; } diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 2bec0064e44..a9be48fb7f1 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -83,6 +83,10 @@ public function prepareForValidation() $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']) : []; } diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index e3f5e841442..3fa93335646 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -43,7 +43,7 @@ public function rules() $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 @@ public function rules() $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 @@ public function prepareForValidation() $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); } } diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index dc9a8d0affa..4a8b386c920 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -56,16 +56,16 @@ public function rules() $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 @@ public function prepareForValidation() $input['exchange_rate'] = 1; } + if(isset($input['partial']) && $input['partial'] == 0) { + $input['partial_due_date'] = null; + } $this->replace($input); } diff --git a/app/Http/Requests/Setup/StoreSetupRequest.php b/app/Http/Requests/Setup/StoreSetupRequest.php index 1608c3cbc44..7228e9b4994 100644 --- a/app/Http/Requests/Setup/StoreSetupRequest.php +++ b/app/Http/Requests/Setup/StoreSetupRequest.php @@ -31,14 +31,6 @@ public function rules() /*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', diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php index 11bd12ee1d9..ef36de91aae 100644 --- a/app/Http/Requests/Task/UpdateTaskRequest.php +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -137,6 +137,9 @@ public function prepareForValidation() $input['time_log'] = json_encode([]); } + if(isset($input['user'])) + unset($input['user']); + $this->replace($input); } diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index beb99dcbd66..9411aa79e38 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -255,6 +255,9 @@ public function ingest($data, $entity_type) unset($record['']); + if(!is_array($record)) + continue; + try { $entity = $this->transformer->transform($record); @@ -310,6 +313,11 @@ public function ingestProducts($data, $entity_type) $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 @@ public function ingestRecurringInvoices($invoices, $invoice_number_key) $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 @@ public function ingestTasks($tasks, $task_number_key) 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 @@ public function ingestInvoices($invoices, $invoice_number_key) $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 @@ public function ingestQuotes($quotes, $quote_number_key) $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( diff --git a/app/Import/Providers/Wave.php b/app/Import/Providers/Wave.php index 2367d2f1138..8fdc56f984d 100644 --- a/app/Import/Providers/Wave.php +++ b/app/Import/Providers/Wave.php @@ -232,6 +232,11 @@ public function ingestExpenses($data) $expenses = $this->groupExpenses($data); foreach ($expenses as $raw_expense) { + + if(!is_array($raw_expense)) { + continue; + } + try { $expense_data = $expense_transformer->transform($raw_expense); diff --git a/app/Import/Transformer/BaseTransformer.php b/app/Import/Transformer/BaseTransformer.php index 8d0a4773235..4c6af4aee64 100644 --- a/app/Import/Transformer/BaseTransformer.php +++ b/app/Import/Transformer/BaseTransformer.php @@ -115,7 +115,7 @@ public function getValueOrNull($data, $field) 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; diff --git a/app/Jobs/Bank/ProcessBankTransactionsYodlee.php b/app/Jobs/Bank/ProcessBankTransactionsYodlee.php index b14afe5a27d..3b651fc9a0d 100644 --- a/app/Jobs/Bank/ProcessBankTransactionsYodlee.php +++ b/app/Jobs/Bank/ProcessBankTransactionsYodlee.php @@ -26,6 +26,7 @@ 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 @@ public function handle() 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}"); diff --git a/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php b/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php index 7016f421a8a..7a8724ce15c 100644 --- a/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php +++ b/app/Jobs/Ledger/ClientLedgerBalanceUpdate.php @@ -61,7 +61,8 @@ public function handle(): void ->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(); diff --git a/app/Livewire/BillingPortal/Submit.php b/app/Livewire/BillingPortal/Submit.php index a1324f96a3c..d56340fa8d4 100644 --- a/app/Livewire/BillingPortal/Submit.php +++ b/app/Livewire/BillingPortal/Submit.php @@ -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'], diff --git a/app/Models/Quote.php b/app/Models/Quote.php index ca75f7aa5e9..cf2d27afe2e 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -77,7 +77,7 @@ * @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 @@ public function getDateAttribute($value) 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) // { diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index e17597b776e..834f15a1ecb 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -166,6 +166,15 @@ public function activities(): \Illuminate\Database\Eloquent\Relations\HasMany 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'); diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index 2f43ed73185..cdad6a880ea 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -166,7 +166,10 @@ public function processPaymentView($data) $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 @@ private function getClientToken(): string } + 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 @@ private function createOrder(array $data): string $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, diff --git a/app/Transformers/QuoteTransformer.php b/app/Transformers/QuoteTransformer.php index adbf2467e36..f918a90653e 100644 --- a/app/Transformers/QuoteTransformer.php +++ b/app/Transformers/QuoteTransformer.php @@ -111,7 +111,7 @@ public function transform(Quote $quote) '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 @@ public function transform(Quote $quote) '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 ?: '', diff --git a/app/Utils/Number.php b/app/Utils/Number.php index 96def2edd94..9501df90f81 100644 --- a/app/Utils/Number.php +++ b/app/Utils/Number.php @@ -101,6 +101,12 @@ public static function parseFloat($value) 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 @@ public static function parseFloat($value) } + /** * Formats a given value based on the clients currency * BACK to a float. @@ -228,9 +235,6 @@ public static function formatMoney($value, $entity): string $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 { diff --git a/app/Utils/Traits/SubscriptionHooker.php b/app/Utils/Traits/SubscriptionHooker.php index f8dc0a1b97e..bf33da13a49 100644 --- a/app/Utils/Traits/SubscriptionHooker.php +++ b/app/Utils/Traits/SubscriptionHooker.php @@ -26,9 +26,7 @@ public function sendLoad($subscription, $body) '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 []; } diff --git a/config/ninja.php b/config/ninja.php index 3b4cd01e68f..2bcd182fbda 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ '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), diff --git a/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php b/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php new file mode 100644 index 00000000000..aea037e3667 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php @@ -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') +
+ @csrf + + + + + +
+ + + +
+ +
+ + +
+
+
+
+
+ + @include('portal.ninja2020.gateways.includes.pay_now') +
+ +@endsection + +@section('gateway_footer') +@endsection + +@push('footer') + + + + + +@endpush \ No newline at end of file diff --git a/tests/Feature/Bank/BankTransactionTest.php b/tests/Feature/Bank/BankTransactionTest.php index 93176928f4d..60312d8c9f5 100644 --- a/tests/Feature/Bank/BankTransactionTest.php +++ b/tests/Feature/Bank/BankTransactionTest.php @@ -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 { diff --git a/tests/Feature/QuoteTest.php b/tests/Feature/QuoteTest.php index 921cc0bdb57..25c6639e4b3 100644 --- a/tests/Feature/QuoteTest.php +++ b/tests/Feature/QuoteTest.php @@ -54,12 +54,99 @@ protected function setUp() :void ); } + 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 @@ public function testPartialDueDates() $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() diff --git a/tests/Unit/NumberTest.php b/tests/Unit/NumberTest.php index 191a034517e..1f8e4ba0412 100644 --- a/tests/Unit/NumberTest.php +++ b/tests/Unit/NumberTest.php @@ -28,15 +28,17 @@ public function testRangeOfNumberFormats() "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 @@ public function testRangeOfNumberFormats() "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 @@ public function testRangeOfNumberFormats() "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", ];