diff --git a/.env.ci b/.env.ci index e4a3a26d66..60c00fc0ea 100644 --- a/.env.ci +++ b/.env.ci @@ -24,4 +24,4 @@ PHANTOMJS_PDF_GENERATION=false CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis -PDF_GENERATOR=hosted_ninja \ No newline at end of file +PDF_GENERATOR=snappdf \ No newline at end of file diff --git a/app/DataMapper/Tax/TaxModel.php b/app/DataMapper/Tax/TaxModel.php index 062a52c16c..c5d4afd185 100644 --- a/app/DataMapper/Tax/TaxModel.php +++ b/app/DataMapper/Tax/TaxModel.php @@ -17,7 +17,7 @@ class TaxModel public string $seller_subregion = 'CA'; /** @var string $version */ - public string $version = 'alpha'; + public string $version = 'beta'; /** @var object $regions */ public object $regions; @@ -28,15 +28,38 @@ class TaxModel * @param TaxModel $model * @return void */ - public function __construct(public ?TaxModel $model = null) + public function __construct(public mixed $model = null) { - if(!$this->model) { + if(!$model) { $this->regions = $this->init(); } else { - $this->regions = $model; + + //@phpstan-ignore-next-line + foreach($model as $key => $value) { + $this->{$key} = $value; + } + } + $this->migrate(); + } + + public function migrate(): self + { + + if($this->version == 'alpha') + { + $this->regions->EU->subregions->PL = new \stdClass(); + $this->regions->EU->subregions->PL->tax_rate = 23; + $this->regions->EU->subregions->PL->tax_name = 'VAT'; + $this->regions->EU->subregions->PL->reduced_tax_rate = 8; + $this->regions->EU->subregions->PL->apply_tax = false; + + $this->version = 'beta'; + } + + return $this; } /** diff --git a/app/Filters/BankTransactionFilters.php b/app/Filters/BankTransactionFilters.php index faaeaa7ade..f786f49e78 100644 --- a/app/Filters/BankTransactionFilters.php +++ b/app/Filters/BankTransactionFilters.php @@ -155,11 +155,13 @@ class BankTransactionFilters extends QueryFilters $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc'; if ($sort_col[0] == 'deposit') { - return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir); + return $this->builder->orderByRaw("(CASE WHEN base_type = 'CREDIT' THEN amount END) $dir")->orderBy('amount', $dir); + // return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir); } if ($sort_col[0] == 'withdrawal') { - return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir); + return $this->builder->orderByRaw("(CASE WHEN base_type = 'DEBIT' THEN amount END) $dir")->orderBy('amount', $dir); + // return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir); } if ($sort_col[0] == 'status') { diff --git a/app/Filters/ClientFilters.php b/app/Filters/ClientFilters.php index ffc3e4fd77..f1b9f1303d 100644 --- a/app/Filters/ClientFilters.php +++ b/app/Filters/ClientFilters.php @@ -41,7 +41,7 @@ class ClientFilters extends QueryFilters */ public function balance(string $balance = ''): Builder { - if (strlen($balance) == 0) { + if (strlen($balance) == 0 || count(explode(":", $balance)) < 2) { return $this->builder; } diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index 76ba72ca5e..9f913c42a8 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -11,16 +11,17 @@ namespace App\Helpers\Invoice; -use App\DataMapper\BaseSettings; -use App\DataMapper\InvoiceItem; -use App\DataMapper\Tax\RuleInterface; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\DataMapper\InvoiceItem; +use App\DataMapper\BaseSettings; +use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSum @@ -313,7 +314,7 @@ class InvoiceItemSum $key = str_replace(' ', '', $tax_name.$tax_rate); - $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.floatval($tax_rate).'%']; + $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%']; $this->tax_collection->push(collect($group_tax)); } diff --git a/app/Helpers/Invoice/InvoiceItemSumInclusive.php b/app/Helpers/Invoice/InvoiceItemSumInclusive.php index da4c3e1f3a..8bcf87fc25 100644 --- a/app/Helpers/Invoice/InvoiceItemSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceItemSumInclusive.php @@ -11,14 +11,15 @@ namespace App\Helpers\Invoice; -use App\DataMapper\Tax\RuleInterface; +use App\Models\Quote; +use App\Utils\Number; use App\Models\Client; use App\Models\Credit; use App\Models\Invoice; use App\Models\PurchaseOrder; -use App\Models\Quote; -use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\RecurringInvoice; +use App\DataMapper\Tax\RuleInterface; use App\Utils\Traits\NumberFormatter; class InvoiceItemSumInclusive @@ -265,7 +266,7 @@ class InvoiceItemSumInclusive $key = str_replace(' ', '', $tax_name.$tax_rate); - $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.$tax_rate.'%']; + $group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%']; $this->tax_collection->push(collect($group_tax)); } diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 32a821445d..c2979f7321 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -131,7 +131,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->invoice->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) { @@ -139,7 +139,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->invoice->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) >= 2) { @@ -147,7 +147,7 @@ class InvoiceSum $tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->invoice->client).'%', 'total' => $tax]; } return $this; diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 6df714231f..8b0b6f95cc 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -12,6 +12,7 @@ namespace App\Helpers\Invoice; use App\Models\Quote; +use App\Utils\Number; use App\Models\Credit; use App\Models\Invoice; use App\Models\PurchaseOrder; @@ -157,19 +158,19 @@ class InvoiceSumInclusive $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->invoice->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) { $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->invoice->client).'%', 'total' => $tax]; } if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) { $tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount); $this->total_taxes += $tax; - $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; + $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->invoice->client).'%', 'total' => $tax]; } return $this; diff --git a/app/Http/Controllers/MailgunWebhookController.php b/app/Http/Controllers/MailgunWebhookController.php index 0585ebc02d..88985e1361 100644 --- a/app/Http/Controllers/MailgunWebhookController.php +++ b/app/Http/Controllers/MailgunWebhookController.php @@ -35,7 +35,7 @@ class MailgunWebhookController extends BaseController } if(\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) { - ProcessMailgunWebhook::dispatch($request->all())->delay(10); + ProcessMailgunWebhook::dispatch($request->all())->delay(rand(2,10)); } return response()->json(['message' => 'Success.'], 200); diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index 69f24d28ab..27b9a4b020 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -64,6 +64,9 @@ class StoreCreditRequest extends Request $user = auth()->user(); $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; // $rules['number'] = new UniqueCreditNumberRule($this->all()); $rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', $user->company()->id)]; diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index d7ece562fb..0733e38913 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -65,6 +65,9 @@ class UpdateCreditRequest extends Request $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('credits')->where('company_id', $user->company()->id)->ignore($this->credit->id)]; $rules['client_id'] = ['bail', 'sometimes',Rule::in([$this->credit->client_id])]; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['line_items'] = 'array'; diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 767ac8db72..34907c6d60 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -38,11 +38,14 @@ class StoreInvoiceRequest extends Request public function rules() { - $rules = []; /** @var \App\Models\User $user */ $user = auth()->user(); + $rules = []; + + $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)]; + if ($this->file('documents') && is_array($this->file('documents'))) { $rules['documents.*'] = $this->fileValidation(); } elseif ($this->file('documents')) { @@ -57,16 +60,16 @@ class StoreInvoiceRequest extends Request $rules['file'] = $this->fileValidation(); } - $rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0'; - - $rules['invitations.*.client_contact_id'] = 'distinct'; - $rules['number'] = ['bail', 'nullable', Rule::unique('invoices')->where('company_id', $user->company()->id)]; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; + $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['is_amount_discount'] = ['boolean']; $rules['date'] = 'bail|sometimes|date:Y-m-d'; + $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; $rules['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric|max:99999999999999'; @@ -79,18 +82,17 @@ class StoreInvoiceRequest extends Request $rules['exchange_rate'] = 'bail|sometimes|numeric'; $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if: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']; - $rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999']; - // $rules['amount'] = ['sometimes', 'bail', 'max:99999999999999']; - // $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); @@ -102,24 +104,24 @@ class StoreInvoiceRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['amount'] = $this->entityTotalAmount($input['line_items']); } - if(isset($input['partial']) && $input['partial'] == 0) { $input['partial_due_date'] = null; - } - - if (array_key_exists('tax_rate1', $input) && is_null($input['tax_rate1'])) { + } + if (!isset($input['tax_rate1'])) { $input['tax_rate1'] = 0; } - if (array_key_exists('tax_rate2', $input) && is_null($input['tax_rate2'])) { + if (!isset($input['tax_rate2'])) { $input['tax_rate2'] = 0; } - if (array_key_exists('tax_rate3', $input) && is_null($input['tax_rate3'])) { + if (!isset($input['tax_rate3'])) { $input['tax_rate3'] = 0; } if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { $input['exchange_rate'] = 1; } - + if(!isset($input['date'])) { + $input['date'] = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d'); + } //handles edge case where we need for force set the due date of the invoice. if((isset($input['partial_due_date']) && strlen($input['partial_due_date']) > 1) && (!array_key_exists('due_date', $input) || (empty($input['due_date']) && empty($this->invoice->due_date)))) { $client = \App\Models\Client::withTrashed()->find($input['client_id']); diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 9d960ef379..1dee8fac1a 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -67,6 +67,9 @@ class UpdateInvoiceRequest extends Request $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])]; $rules['line_items'] = 'array'; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; + $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['tax_rate1'] = 'bail|sometimes|numeric'; diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 3805ed73a9..6e2387ce79 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -50,6 +50,10 @@ class StorePurchaseOrderRequest extends Request $rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', $user->company()->id)]; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.vendor_contact_id'] = 'bail|required|distinct'; + $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['is_amount_discount'] = ['boolean']; $rules['line_items'] = 'array'; diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 5ae69e8d15..6403d1fc38 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -53,6 +53,9 @@ class UpdatePurchaseOrderRequest extends Request $rules['line_items'] = 'array'; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.vendor_contact_id'] = 'bail|required|distinct'; + $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['is_amount_discount'] = ['boolean']; diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 4b2624ee7a..afb6226cfe 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -11,12 +11,13 @@ namespace App\Http\Requests\Quote; -use App\Http\Requests\Request; -use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule; use App\Models\Quote; -use App\Utils\Traits\CleanLineItems; +use App\Http\Requests\Request; use App\Utils\Traits\MakesHash; use Illuminate\Validation\Rule; +use App\Utils\Traits\CleanLineItems; +use App\Http\ValidationRules\Quote\UniqueQuoteNumberRule; +use App\Http\ValidationRules\Project\ValidProjectForClient; class StoreQuoteRequest extends Request { @@ -43,7 +44,7 @@ class StoreQuoteRequest extends Request $rules = []; - $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)]; + $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted',0)]; if ($this->file('documents') && is_array($this->file('documents'))) { $rules['documents.*'] = $this->fileValidation(); @@ -59,15 +60,28 @@ class StoreQuoteRequest extends Request $rules['file'] = $this->fileValidation(); } - $rules['number'] = ['nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)]; + $rules['number'] = ['bail','nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)]; + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; + + $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; + $rules['is_amount_discount'] = ['boolean']; + $rules['date'] = 'bail|sometimes|date:Y-m-d'; + $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; + $rules['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric|max:99999999999999'; - $rules['is_amount_discount'] = ['boolean']; + $rules['tax_rate1'] = 'bail|sometimes|numeric'; + $rules['tax_rate2'] = 'bail|sometimes|numeric'; + $rules['tax_rate3'] = 'bail|sometimes|numeric'; + $rules['tax_name1'] = 'bail|sometimes|string|nullable'; + $rules['tax_name2'] = 'bail|sometimes|string|nullable'; + $rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['exchange_rate'] = 'bail|sometimes|numeric'; - $rules['line_items'] = 'array'; - $rules['date'] = 'bail|sometimes|date:Y-m-d'; + + $rules['partial'] = 'bail|sometimes|nullable|numeric|gte:0'; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if: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']; $rules['amount'] = ['sometimes', 'bail', 'numeric', 'max:99999999999999']; return $rules; @@ -89,19 +103,24 @@ class StoreQuoteRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['amount'] = $this->entityTotalAmount($input['line_items']); } - - if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { - $input['exchange_rate'] = 1; - } - if(isset($input['partial']) && $input['partial'] == 0) { $input['partial_due_date'] = null; } - + if (!isset($input['tax_rate1'])) { + $input['tax_rate1'] = 0; + } + if (!isset($input['tax_rate2'])) { + $input['tax_rate2'] = 0; + } + if (!isset($input['tax_rate3'])) { + $input['tax_rate3'] = 0; + } + if (!isset($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } 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; diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index 77ac6f2f4a..1f5e795ded 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -55,6 +55,9 @@ class UpdateQuoteRequest extends Request } elseif ($this->file('file')) { $rules['file'] = $this->fileValidation(); } + + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $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])]; diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 7dc6ef5fdc..3bdecfa1e6 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -61,7 +61,8 @@ class StoreRecurringInvoiceRequest extends Request $rules['client_id'] = 'required|exists:clients,id,company_id,'.$user->company()->id; - $rules['invitations.*.client_contact_id'] = 'distinct'; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['frequency_id'] = 'required|integer|digits_between:1,12'; diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index a89baa8d2c..6ea38d30ed 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -60,6 +60,8 @@ class UpdateRecurringInvoiceRequest extends Request $rules['number'] = ['bail', 'sometimes', Rule::unique('recurring_invoices')->where('company_id', $user->company()->id)->ignore($this->recurring_invoice->id)]; + $rules['invitations'] = 'sometimes|bail|array'; + $rules['invitations.*.client_contact_id'] = 'bail|required|distinct'; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->recurring_invoice->client_id])]; diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php index 0326f34eff..1124fec9c8 100644 --- a/app/Jobs/Mail/NinjaMailerJob.php +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -297,12 +297,13 @@ class NinjaMailerJob implements ShouldQueue $t->replace(Ninja::transformTranslations($this->nmo->settings)); /** Force free/trials onto specific mail driver */ - // if(Ninja::isHosted() && !$this->company->account->isPaid()) - // { - // $this->mailer = 'mailgun'; - // $this->setHostedMailgunMailer(); - // return $this; - // } + + if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) { + $this->mailer = 'mailgun'; + $this->setHostedMailgunMailer(); + return $this; + } + if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') { //check if outlook. @@ -391,7 +392,7 @@ class NinjaMailerJob implements ShouldQueue $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_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 || diff --git a/app/Jobs/Mailgun/ProcessMailgunWebhook.php b/app/Jobs/Mailgun/ProcessMailgunWebhook.php index 73d32f39e9..69be326cd2 100644 --- a/app/Jobs/Mailgun/ProcessMailgunWebhook.php +++ b/app/Jobs/Mailgun/ProcessMailgunWebhook.php @@ -181,7 +181,7 @@ class ProcessMailgunWebhook implements ShouldQueue $sl = $this->getSystemLog($this->request['MessageID']); /** Prevents Gmail tracking from firing inappropriately */ - if($this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) { + if(!$sl || $this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) { return; } diff --git a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php index 6cc5a6440a..ddf7ce4048 100644 --- a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php +++ b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php @@ -78,7 +78,7 @@ class CleanStaleInvoiceOrder implements ShouldQueue Invoice::query() ->withTrashed() ->where('is_proforma', 1) - ->whereBetween('created_at', [now()->subHours(1), now()->subMinutes(10)]) + ->where('created_at', '<', now()->subHour()) ->cursor() ->each(function ($invoice) use ($repo) { $invoice->is_proforma = false; diff --git a/app/Models/Account.php b/app/Models/Account.php index 9ce846a367..001b05100d 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -364,16 +364,19 @@ class Account extends BaseModel return $this->isProClient() && $this->isPaid(); } + public function isNewHostedAccount() + { + return Ninja::isHosted() && Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2; + } + public function isTrial(): bool { if (!Ninja::isNinja()) { return false; } - //@27-01-2024 - updates for logic around trials return !$this->plan_paid && $this->trial_started && Carbon::parse($this->trial_started)->addDays(14)->gte(now()->subHours(12)); - // $plan_details = $this->getPlanDetails(); - // return $plan_details && $plan_details['trial']; + } public function startTrial($plan): void diff --git a/app/PaymentDrivers/Stripe/BrowserPay.php b/app/PaymentDrivers/Stripe/BrowserPay.php index d468d0a2b7..09d71fcdb1 100644 --- a/app/PaymentDrivers/Stripe/BrowserPay.php +++ b/app/PaymentDrivers/Stripe/BrowserPay.php @@ -153,7 +153,7 @@ class BrowserPay implements MethodInterface $this->stripe->client->company, ); - return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); } /** diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php index 3fbed35932..eab1a98c32 100644 --- a/app/PaymentDrivers/Stripe/CreditCard.php +++ b/app/PaymentDrivers/Stripe/CreditCard.php @@ -160,7 +160,7 @@ class CreditCard } } - return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); } public function processUnsuccessfulPayment($server_response) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 0abd2f2280..c37fb46214 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -48,6 +48,7 @@ use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerPart use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; +use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; class Peppol extends AbstractService @@ -185,6 +186,8 @@ class Peppol extends AbstractService $this->p_invoice->TaxTotal = $this->getTotalTaxes(); $this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal(); + $this->countryLevelMutators(); + // $this->p_invoice->PaymentMeans = $this->getPaymentMeans(); // $payeeFinancialAccount = (new PayeeFinancialAccount()) @@ -573,6 +576,8 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv $contact = new Contact(); $contact->ElectronicMail = $this->invoice->company->owner()->email ?? 'owner@gmail.com'; + $contact->Telephone = ''; + $contact->Name = ''; $party->Contact = $contact; @@ -695,6 +700,7 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv 'PaymentTerms' => 7, ]; + //only scans for top level props foreach($settings as $prop => $visibility){ if($prop_value = PropertyResolver::resolve($this->invoice->client->e_invoice, $prop)) @@ -707,4 +713,249 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv return $this; } + + public function getSetting(object $e_invoice, string $property_path): mixed + { + return PropertyResolver::resolve($e_invoice, $property_path); + } + + public function countryLevelMutators():self + { + + if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) + $this->{$this->invoice->company->country()->iso_3166_2}(); + + return $this; + } + + private function DE(): self + { + // accountingsupplierparty.party.contact MUST be set - Name / Telephone / Electronic Mail + // this is forced by default. + + // ONE payment means MUST be set + + // + return $this; + } + + private function CH(): self + { + //if QR-Bill support required - then special flow required.... optional. + + return $this; + } + + private function AT(): self + { + //special fields for sending to AT:GOV + return $this; + } + + private function AU(): self + { + + //if payment means are included, they must be the same `type` + return $this; + } + + private function ES(): self + { + +// For B2B, provide an ES:DIRE routing identifier and an ES:VAT tax identifier. + // both sender and receiver must be an ES company; + // you must have a "credit_transfer" PaymentMean; + // the "dueDate" property is mandatory. + +// For B2G, provide three ES:FACE identifiers in the routing object, +// as well as the ES:VAT tax identifier in the accountingCustomerParty.publicIdentifiers. +// The invoice will then be routed through the FACe network. The three required ES:FACE identifiers are as follows: +// "routing": { +// "eIdentifiers":[ +// { +// "scheme": "ES:FACE", +// "id": "L01234567", +// "role": "ES-01-FISCAL" +// }, +// { +// "scheme": "ES:FACE", +// "id": "L01234567", +// "role": "ES-02-RECEPTOR" +// }, +// { +// "scheme": "ES:FACE", +// "id": "L01234567", +// "role": "ES-03-PAGADOR" +// } +// ] +// } + + return $this; + } + + private function FI(): self + { + + // For Finvoice, provide an FI:OPID routing identifier and an FI:OVT legal identifier. + // An FI:VAT is recommended. In many cases (depending on the sender/receiver country and the type of service/goods) + // an FI:VAT is required. So we recommend always including this. + + return $this; + } + + private function FR(): self + { + // When sending invoices to the French government (Chorus Pro): + + // All invoices have to be routed to SIRET 0009:11000201100044. There is no test environment for sending to public entities. + + // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + + // The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference) + + // The commitment number must be sent in the invoice.orderReference (deprecated) or the invoice.references array (documentType purchase_order). + + // Invoices to companies (SIRET / 0009 or SIRENE / 0002) are routed directly to that identifier. + return $this; + } + + private function IT(): self + { + // IT Sender, IT Receiver, B2B/B2G + // Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario) + + // IT Sender, IT Receiver, B2C + // Provide the receiver IT:CF and the receiver IT:CUUO (codice destinatario) + + // IT Sender, non-IT Receiver + // Provide the receiver tax identifier and any routing identifier applicable to the receiving country (see Receiver Identifiers). + + // non-IT Sender, IT Receiver, B2B/B2G + // Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario) + + // non-IT Sender, IT Receiver, B2C + // Provide the receiver IT:CF and an optional email. The invoice will be eReported and sent via email. Note that this cannot be a PEC email address. + + return $this; + } + + private function MY(): self + { + //way too much to digest here, delayed. + return $this; + } + + private function NL(): self + { + + // When sending to public entities, the invoice.accountingSupplierParty.party.contact.email is mandatory. + + // Dutch senders and receivers require a legal identifier. For companies, this is NL:KVK, for public entities this is NL:OINO. + + return $this; + } + + private function NZ(): self + { + // New Zealand uses a GLN to identify businesses. In addition, when sending invoices to a New Zealand customer, make sure you include the pseudo identifier NZ:GST as their tax identifier. + return $this; + } + + private function PL(): self + { + + // Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows: + + // "routing": { + // "eIdentifiers": [ + // { + // "scheme": "PL:VAT", + // "id": "PL0101010101" + // } + // ], + // "networks": [ + // { + // "application": "pl-ksef", + // "settings": { + // "enabled": true + // } + // } + // ] + // } + // Note this will only work if your LegalEntity has been setup for this network. + + return $this; + } + + private function RO(): self + { + // Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows: + + // "routing": { + // "eIdentifiers": [ + // { + // "scheme": "RO:VAT", + // "id": "RO010101010" + // } + // ], + // "networks": [ + // { + // "application": "ro-anaf", + // "settings": { + // "enabled": true + // } + // } + // ] + // } + // Note this will only work if your LegalEntity has been setup for this network. + // The county field for a Romania address must use the ISO3166-2:RO codes, e.g. "RO-AB, RO-AR". Don’t omit the country prefix! + // The city field for county RO-B must be SECTOR1 - SECTOR6. + + return $this; + } + + private function SG(): self + { + //delayed - stage 2 + return $this; + } + + //Sweden + private function SE(): self + { + // Deliver invoices to the "Svefaktura" co-operation of local Swedish service providers. + // Routing is through the SE:ORGNR together with a network specification: + + // "routing": { + // "eIdentifiers": [ + // { + // "scheme": "SE:ORGNR", + // "id": "0012345678" + // } + // ], + // "networks": [ + // { + // "application": "svefaktura", + // "settings": { + // "enabled": true + // } + // } + // ] + // } + // Use of the "Svefaktura" co-operation can also be induced by specifying an operator id, as follows: + + // "routing": { + // "eIdentifiers": [ + // { + // "scheme": "SE:ORGNR", + // "id": "0012345678" + // }, + // { + // "scheme": "SE:OPID", + // "id": "1234567890" + // } + // ] + // } + return $this; + } } diff --git a/app/Services/Email/Email.php b/app/Services/Email/Email.php index 04492da8fa..77edc44542 100644 --- a/app/Services/Email/Email.php +++ b/app/Services/Email/Email.php @@ -526,11 +526,11 @@ class Email implements ShouldQueue { /** Force free/trials onto specific mail driver */ - // if(Ninja::isHosted() && !$this->company->account->isPaid()) { - // $this->mailer = 'mailgun'; - // $this->setHostedMailgunMailer(); - // return $this; - // } + if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) { + $this->mailer = 'mailgun'; + $this->setHostedMailgunMailer(); + return $this; + } if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') { @@ -619,7 +619,7 @@ class Email implements ShouldQueue $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_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 || diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index fe27b6979a..093b8b1577 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -739,7 +739,7 @@ class PdfBuilder if ($item->is_amount_discount) { $data[$key][$table_type.'.discount'] = $this->service->config->formatMoney($item->discount); } else { - $data[$key][$table_type.'.discount'] = floatval($item->discount).'%'; + $data[$key][$table_type.'.discount'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->discount)).'%'; } } else { $data[$key][$table_type.'.discount'] = ''; @@ -749,17 +749,17 @@ class PdfBuilder // but that's no longer necessary. if (isset($item->tax_rate1)) { - $data[$key][$table_type.'.tax_rate1'] = floatval($item->tax_rate1).'%'; + $data[$key][$table_type.'.tax_rate1'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate1)).'%'; $data[$key][$table_type.'.tax1'] = &$data[$key][$table_type.'.tax_rate1']; } if (isset($item->tax_rate2)) { - $data[$key][$table_type.'.tax_rate2'] = floatval($item->tax_rate2).'%'; + $data[$key][$table_type.'.tax_rate2'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate2)).'%'; $data[$key][$table_type.'.tax2'] = &$data[$key][$table_type.'.tax_rate2']; } if (isset($item->tax_rate3)) { - $data[$key][$table_type.'.tax_rate3'] = floatval($item->tax_rate3).'%'; + $data[$key][$table_type.'.tax_rate3'] = $this->service->config->formatValueNoTrailingZeroes(floatval($item->tax_rate3)).'%'; $data[$key][$table_type.'.tax3'] = &$data[$key][$table_type.'.tax_rate3']; } diff --git a/composer.lock b/composer.lock index efa6a8ea7e..fa1c4b178c 100644 --- a/composer.lock +++ b/composer.lock @@ -535,16 +535,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.316.3", + "version": "3.316.10", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "e832e594b3c213760e067e15ef2739f77505e832" + "reference": "eeb8df6ff6caa428e8bcd631ad2a96430900a249" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e832e594b3c213760e067e15ef2739f77505e832", - "reference": "e832e594b3c213760e067e15ef2739f77505e832", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/eeb8df6ff6caa428e8bcd631ad2a96430900a249", + "reference": "eeb8df6ff6caa428e8bcd631ad2a96430900a249", "shasum": "" }, "require": { @@ -624,9 +624,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.316.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.316.10" }, - "time": "2024-07-12T18:07:23+00:00" + "time": "2024-07-30T18:10:20+00:00" }, { "name": "bacon/bacon-qr-code", @@ -4552,16 +4552,16 @@ }, { "name": "laravel/framework", - "version": "v11.18.1", + "version": "v11.19.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "b19ba518c56852567e99fbae9321bc436c2cc5a8" + "reference": "5e103d499e9ee5bcfc184412d034c4e516b87085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/b19ba518c56852567e99fbae9321bc436c2cc5a8", - "reference": "b19ba518c56852567e99fbae9321bc436c2cc5a8", + "url": "https://api.github.com/repos/laravel/framework/zipball/5e103d499e9ee5bcfc184412d034c4e516b87085", + "reference": "5e103d499e9ee5bcfc184412d034c4e516b87085", "shasum": "" }, "require": { @@ -4754,7 +4754,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-07-26T10:39:29+00:00" + "time": "2024-07-30T15:22:41+00:00" }, { "name": "laravel/pint", @@ -16199,16 +16199,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.60.0", + "version": "v3.61.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "e595e4e070d17c5d42ed8c4206f630fcc5f401a4" + "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/e595e4e070d17c5d42ed8c4206f630fcc5f401a4", - "reference": "e595e4e070d17c5d42ed8c4206f630fcc5f401a4", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/94a87189f55814e6cabca2d9a33b06de384a2ab8", + "reference": "94a87189f55814e6cabca2d9a33b06de384a2ab8", "shasum": "" }, "require": { @@ -16290,7 +16290,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.60.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.61.1" }, "funding": [ { @@ -16298,7 +16298,7 @@ "type": "github" } ], - "time": "2024-07-25T09:26:51+00:00" + "time": "2024-07-31T14:33:15+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -17347,16 +17347,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.28", + "version": "10.5.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ff7fb85cdf88131b83e721fb2a327b664dbed275" + "reference": "8e9e80872b4e8064401788ee8a32d40b4455318f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ff7fb85cdf88131b83e721fb2a327b664dbed275", - "reference": "ff7fb85cdf88131b83e721fb2a327b664dbed275", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e9e80872b4e8064401788ee8a32d40b4455318f", + "reference": "8e9e80872b4e8064401788ee8a32d40b4455318f", "shasum": "" }, "require": { @@ -17428,7 +17428,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.28" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.29" }, "funding": [ { @@ -17444,7 +17444,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T14:54:16+00:00" + "time": "2024-07-30T11:08:00+00:00" }, { "name": "react/cache", diff --git a/database/migrations/2024_07_29_235430_2024_30_07_tax_model_migration.php b/database/migrations/2024_07_29_235430_2024_30_07_tax_model_migration.php new file mode 100644 index 0000000000..e542bcd2be --- /dev/null +++ b/database/migrations/2024_07_29_235430_2024_30_07_tax_model_migration.php @@ -0,0 +1,36 @@ +cursor() + ->each(function($company){ + + if($company->tax_data?->version == 'alpha') + { + + $company->update(['tax_data' => new \App\DataMapper\Tax\TaxModel($company->tax_data)]); + + } + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +};