diff --git a/.github/workflows/react_release.yml b/.github/workflows/react_release.yml index c4d6f63994aa..ca3a62ce7f64 100644 --- a/.github/workflows/react_release.yml +++ b/.github/workflows/react_release.yml @@ -38,6 +38,9 @@ jobs: sudo php artisan cache:clear sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./ -type d -exec chmod 755 {} \; + - name: Set current date to variable + id: set_date + run: echo "current_date=$(date '+%Y-%m-%d')" >> $GITHUB_ENV - name: Prepare React FrontEnd run: | @@ -46,10 +49,11 @@ jobs: git checkout develop cp .env.example .env cp ../vite.config.ts.react ./vite.config.js + sed -i '/"version"/c\ "version": " Latest Build - ${{ env.current_date }}",' package.json npm i npm run build cp -r dist/* ../public/ - mv dist/index.html ../resources/views/react/index.blade.php + mv ../public/index.html ../resources/views/react/index.blade.php - name: Prepare JS/CSS assets run: | diff --git a/VERSION.txt b/VERSION.txt index d315f4df7571..c0204ea277af 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.10.13 \ No newline at end of file +5.10.16 \ No newline at end of file diff --git a/app/Console/Commands/BackupUpdate.php b/app/Console/Commands/BackupUpdate.php index 33b421ea384d..b2a16bff2502 100644 --- a/app/Console/Commands/BackupUpdate.php +++ b/app/Console/Commands/BackupUpdate.php @@ -177,7 +177,6 @@ class BackupUpdate extends Command $doc_bin = $document->getFile(); } catch(\Exception $e) { nlog("Exception:: BackupUpdate::" . $e->getMessage()); - nlog($e->getMessage()); } if ($doc_bin) { diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 374e7783c3fe..efac973651df 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -172,6 +172,7 @@ class BaseExport 'tax_rate3' => 'invoice.tax_rate3', 'recurring_invoice' => 'invoice.recurring_id', 'auto_bill' => 'invoice.auto_bill_enabled', + 'project' => 'invoice.project', ]; protected array $recurring_invoice_report_keys = [ @@ -1038,6 +1039,10 @@ class BaseExport $recurring_filters = []; + if($this->company->getSetting('report_include_drafts')){ + $recurring_filters[] = RecurringInvoice::STATUS_DRAFT; + } + if (in_array('active', $status_parameters)) { $recurring_filters[] = RecurringInvoice::STATUS_ACTIVE; } diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index d87c49ff943c..39ece67a28a9 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -153,9 +153,9 @@ class InvoiceExport extends BaseExport private function decorateAdvancedFields(Invoice $invoice, array $entity): array { - // if (in_array('invoice.status', $this->input['report_keys'])) { - // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); - // } + if (in_array('invoice.project', $this->input['report_keys'])) { + $entity['invoice.project'] = $invoice->project ? $invoice->project->name : ''; + } if (in_array('invoice.recurring_id', $this->input['report_keys'])) { $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 14a38aebee27..b2b13b523acf 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -265,6 +265,10 @@ class InvoiceItemExport extends BaseExport $entity['invoice.user_id'] = $invoice->user ? $invoice->user->present()->name() : '';// @phpstan-ignore-line } + if (in_array('invoice.project', $this->input['report_keys'])) { + $entity['invoice.project'] = $invoice->project ? $invoice->project->name : '';// @phpstan-ignore-line + } + return $entity; } diff --git a/app/Export/Decorators/InvoiceDecorator.php b/app/Export/Decorators/InvoiceDecorator.php index 35985decba66..b6579b7f546d 100644 --- a/app/Export/Decorators/InvoiceDecorator.php +++ b/app/Export/Decorators/InvoiceDecorator.php @@ -92,6 +92,7 @@ class InvoiceDecorator extends Decorator implements DecoratorInterface { return $invoice->recurring_invoice ? $invoice->recurring_invoice->number : ''; } + public function auto_bill_enabled(Invoice $invoice) { return $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no'); diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 2d2ef7cd06b8..de5a932fb76f 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -153,22 +153,22 @@ class InvoiceFilters extends QueryFilters { return $this->builder->where(function ($query) { - $query->whereIn('invoices.status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) - ->where('invoices.is_deleted', 0) - ->where('invoices.balance', '>', 0) - ->orWhere(function ($query) { + $query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) + ->where('is_deleted', 0) + ->where('balance', '>', 0) + ->where(function ($query) { - $query->whereNull('invoices.due_date') + $query->whereNull('due_date') ->orWhere(function ($q) { - $q->where('invoices.due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', 0); + $q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0); }) ->orWhere(function ($q) { - $q->where('invoices.partial_due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', '>', 0); + $q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0); }); }) - ->orderByRaw('ISNULL(invoices.due_date), invoices.due_date ' . 'desc') - ->orderByRaw('ISNULL(invoices.partial_due_date), invoices.partial_due_date ' . 'desc'); + ->orderByRaw('ISNULL(due_date), due_date ' . 'desc') + ->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc'); }); } diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php index 394e762d9749..07fc7fc238e4 100644 --- a/app/Http/Controllers/ChartController.php +++ b/app/Http/Controllers/ChartController.php @@ -66,7 +66,7 @@ class ChartController extends BaseController return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); } - public function calculatedField(ShowCalculatedFieldRequest $request) + public function calculatedFields(ShowCalculatedFieldRequest $request) { /** @var \App\Models\User auth()->user() */ diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index f1993693a90f..ec30aacc37b6 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -300,7 +300,9 @@ class InvitationController extends Controller 'signature' => false, 'contact_first_name' => $invitation->contact->first_name ?? '', 'contact_last_name' => $invitation->contact->last_name ?? '', - 'contact_email' => $invitation->contact->email ?? '' + 'contact_email' => $invitation->contact->email ?? '', + 'client_city' => $invitation->client->city ?? '', + 'client_postal_code' => $invitation->client->postal_code ?? '', ]; $request->replace($data); diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 728df5eb1de1..94a46bd5cbe5 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -108,11 +108,11 @@ class PaymentController extends Controller */ public function process(Request $request) { - $request->validate([ - 'contact_first_name' => ['required'], - 'contact_last_name' => ['required'], - 'contact_email' => ['required', 'email'], - ]); + // $request->validate([ + // 'contact_first_name' => ['required'], + // 'contact_last_name' => ['required'], + // 'contact_email' => ['required', 'email'], + // ]); return (new InstantPayment($request))->run(); } diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 66eed43075f6..f0b26ad7d7c7 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -85,7 +85,7 @@ class ImportController extends Controller $contents = $this->convertEncoding($contents); // Store the csv in cache with an expiry of 10 minutes - Cache::put($hash.'-'.$entityType, base64_encode($contents), 600); + Cache::put($hash.'-'.$entityType, base64_encode($contents), 1200); // Parse CSV $csv_array = $this->getCsvData($contents); diff --git a/app/Http/ValidationRules/Account/BlackListRule.php b/app/Http/ValidationRules/Account/BlackListRule.php index 1d65de052fa6..0d5e5a13a83f 100644 --- a/app/Http/ValidationRules/Account/BlackListRule.php +++ b/app/Http/ValidationRules/Account/BlackListRule.php @@ -19,8 +19,9 @@ use Illuminate\Contracts\Validation\ValidationRule; */ class BlackListRule implements ValidationRule { - /** Bad domains +/- dispoable email domains */ + /** Bad domains +/- disposable email domains */ private array $blacklist = [ + 'padvn.com', 'anonaddy.me', 'nqmo.com', 'wireconnected.com', diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index b2897352cc4a..fef3ccdd269f 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -98,7 +98,7 @@ class BaseImport } /** @var string $base64_encoded_csv */ - $base64_encoded_csv = Cache::pull($this->hash.'-'.$entity_type); + $base64_encoded_csv = Cache::get($this->hash.'-'.$entity_type); if (empty($base64_encoded_csv)) { return null; @@ -473,6 +473,8 @@ class BaseImport $tasks = $this->groupTasks($tasks, $task_number_key); + nlog($tasks); + foreach ($tasks as $raw_task) { $task_data = []; @@ -702,16 +704,16 @@ class BaseImport ->save(); } - if ($invoice->status_id === Invoice::STATUS_DRAFT) { - } elseif ($invoice->status_id === Invoice::STATUS_SENT) { - $invoice = $invoice - ->service() - ->markSent() - ->save(); - } elseif ( - $invoice->status_id <= Invoice::STATUS_SENT && - $invoice->amount > 0 - ) { + if ($invoice->status_id == Invoice::STATUS_DRAFT) { + return $invoice; + } + + $invoice = $invoice + ->service() + ->markSent() + ->save(); + + if ($invoice->status_id <= Invoice::STATUS_SENT && $invoice->amount > 0) { if ($invoice->balance <= 0) { $invoice->status_id = Invoice::STATUS_PAID; $invoice->save(); diff --git a/app/Import/Providers/Wave.php b/app/Import/Providers/Wave.php index 89c3ef9931a2..1fbd58b0e0c0 100644 --- a/app/Import/Providers/Wave.php +++ b/app/Import/Providers/Wave.php @@ -172,7 +172,7 @@ class Wave extends BaseImport implements ImportInterface { $entity_type = 'expense'; - $data = $this->getCsvData($entity_type); + $data = $this->getCsvData('invoice'); if (!$data) { $this->entity_count['expense'] = 0; @@ -244,14 +244,17 @@ class Wave extends BaseImport implements ImportInterface if (empty($expense_data['vendor_id'])) { $vendor_data['user_id'] = $this->getUserIDForRecord($expense_data); - $vendor_repository->save( - ['name' => $raw_expense['Vendor Name']], - $vendor = VendorFactory::create( - $this->company->id, - $vendor_data['user_id'] - ) - ); - $expense_data['vendor_id'] = $vendor->id; + if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor'])) + { + $vendor_repository->save( + ['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])], + $vendor = VendorFactory::create( + $this->company->id, + $vendor_data['user_id'] + ) + ); + $expense_data['vendor_id'] = $vendor->id; + } } $validator = Validator::make( diff --git a/app/Import/Transformer/Csv/TaskTransformer.php b/app/Import/Transformer/Csv/TaskTransformer.php index edd8737131cb..54636349f050 100644 --- a/app/Import/Transformer/Csv/TaskTransformer.php +++ b/app/Import/Transformer/Csv/TaskTransformer.php @@ -46,6 +46,7 @@ class TaskTransformer extends BaseTransformer 'company_id' => $this->company->id, 'number' => $this->getString($task_data, 'task.number'), 'user_id' => $this->getString($task_data, 'task.user_id'), + 'rate' => $this->getFloat($task_data, 'task.rate'), 'client_id' => $clientId, 'project_id' => $this->getProjectId($projectId, $clientId), 'description' => $this->getString($task_data, 'task.description'), @@ -87,8 +88,7 @@ class TaskTransformer extends BaseTransformer $is_billable = true; } - if(isset($item['task.start_date']) && - isset($item['task.end_date'])) { + if(isset($item['task.start_date'])) { $start_date = $this->resolveStartDate($item); $end_date = $this->resolveEndDate($item); } elseif(isset($item['task.duration'])) { @@ -136,7 +136,7 @@ class TaskTransformer extends BaseTransformer private function resolveEndDate($item) { - $stub_end_date = $item['task.end_date']; + $stub_end_date = isset($item['task.end_date']) ? $item['task.end_date'] : $item['task.start_date']; $stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : ''; try { diff --git a/app/Import/Transformer/Wave/ExpenseTransformer.php b/app/Import/Transformer/Wave/ExpenseTransformer.php index 8f37c94b788a..afd282b80d63 100644 --- a/app/Import/Transformer/Wave/ExpenseTransformer.php +++ b/app/Import/Transformer/Wave/ExpenseTransformer.php @@ -36,18 +36,26 @@ class ExpenseTransformer extends BaseTransformer $total_tax += floatval($record['Sales Tax Amount']); } - $tax_rate = round(($total_tax / $amount) * 100, 3); + $tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0; + + if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1) + $public_notes = $data['Notes / Memo']; + elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1) + $public_notes = $data['Transaction Description']; + else + $public_notes = ''; + $transformed = [ 'company_id' => $this->company->id, 'vendor_id' => $this->getVendorIdOrCreate($this->getString($data, 'Vendor')), 'number' => $this->getString($data, 'Bill Number'), - 'public_notes' => $this->getString($data, 'Notes / Memo'), + 'public_notes' => $public_notes, 'date' => $this->parseDate($data['Transaction Date Added']) ?: now()->format('Y-m-d'), //27-01-2022 'currency_id' => $this->company->settings->currency_id, 'category_id' => $this->getOrCreateExpenseCategry($data['Account Name']), 'amount' => $amount, - 'tax_name1' => $data['Sales Tax Name'], + 'tax_name1' => isset($data['Sales Tax Name']) ? $data['Sales Tax Name'] : '', 'tax_rate1' => $tax_rate, ]; diff --git a/app/Livewire/BillingPortalPurchase.php b/app/Livewire/BillingPortalPurchase.php index 0a15710018c6..079a36325e80 100644 --- a/app/Livewire/BillingPortalPurchase.php +++ b/app/Livewire/BillingPortalPurchase.php @@ -188,6 +188,10 @@ class BillingPortalPurchase extends Component public ?string $contact_email; + public ?string $client_city; + + public ?string $client_postal_code; + public function mount() { MultiDB::setDb($this->db); @@ -203,7 +207,7 @@ class BillingPortalPurchase extends Component if (request()->query('coupon')) { $this->coupon = request()->query('coupon'); $this->handleCoupon(); - } elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { + } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) { $this->price = $this->subscription->promo_price; } @@ -335,10 +339,6 @@ class BillingPortalPurchase extends Component { $this->contact = $contact; - if ($contact->showRff()) { - return $this->rff(); - } - Auth::guard('contact')->loginUsingId($contact->id, true); if ($this->subscription->trial_enabled) { @@ -351,11 +351,20 @@ class BillingPortalPurchase extends Component if ((int)$this->price == 0) { $this->steps['payment_required'] = false; } else { - $this->steps['fetched_payment_methods'] = true; + // $this->steps['fetched_payment_methods'] = true; } $this->methods = $contact->client->service()->getPaymentMethods($this->price); + foreach($this->methods as $method){ + + if($method['is_paypal'] == '1' && !$this->steps['check_rff']){ + $this->rff(); + break; + } + + } + $this->heading_text = ctrans('texts.payment_methods'); return $this; @@ -366,6 +375,8 @@ class BillingPortalPurchase extends Component $this->contact_first_name = $this->contact->first_name; $this->contact_last_name = $this->contact->last_name; $this->contact_email = $this->contact->email; + $this->client_city = $this->contact->client->city; + $this->client_postal_code = $this->contact->client->postal_code; $this->steps['check_rff'] = true; @@ -377,13 +388,20 @@ class BillingPortalPurchase extends Component $validated = $this->validate([ 'contact_first_name' => ['required'], 'contact_last_name' => ['required'], + 'client_city' => ['required'], + 'client_postal_code' => ['required'], 'contact_email' => ['required', 'email'], ]); $this->contact->first_name = $validated['contact_first_name']; $this->contact->last_name = $validated['contact_last_name']; $this->contact->email = $validated['contact_email']; - $this->contact->save(); + $this->contact->client->postal_code = $validated['client_postal_code']; + $this->contact->client->city = $validated['client_city']; + + $this->contact->pushQuietly(); + + $this->steps['fetched_payment_methods'] = true; return $this->getPaymentMethods($this->contact); } @@ -395,13 +413,13 @@ class BillingPortalPurchase extends Component * @param $company_gateway_id * @param $gateway_type_id */ - public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id) + public function handleMethodSelectingEvent($company_gateway_id, $gateway_type_id, $is_paypal = false) { $this->company_gateway_id = $company_gateway_id; $this->payment_method_id = $gateway_type_id; $this->handleBeforePaymentEvents(); - + } /** diff --git a/app/Livewire/BillingPortalPurchasev2.php b/app/Livewire/BillingPortalPurchasev2.php index d707707578b2..5ee7191700ca 100644 --- a/app/Livewire/BillingPortalPurchasev2.php +++ b/app/Livewire/BillingPortalPurchasev2.php @@ -164,6 +164,13 @@ class BillingPortalPurchasev2 extends Component public $payment_confirmed = false; public $is_eligible = true; public $not_eligible_message = ''; + public $check_rff = false; + + public ?string $contact_first_name; + public ?string $contact_last_name; + public ?string $contact_email; + public ?string $client_city; + public ?string $client_postal_code; public function mount() { @@ -472,7 +479,6 @@ class BillingPortalPurchasev2 extends Component */ protected function getPaymentMethods(): self { - nlog("total amount = {$this->float_amount_total}"); if ($this->float_amount_total == 0) { $this->methods = []; @@ -481,10 +487,73 @@ class BillingPortalPurchasev2 extends Component if ($this->contact && $this->float_amount_total >= 1) { $this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total); } + + foreach($this->methods as $method) { + + if($method['is_paypal'] == '1' && !$this->check_rff) { + $this->rff(); + break; + } + + } return $this; } + protected function rff() + { + + $this->contact_first_name = $this->contact->first_name; + $this->contact_last_name = $this->contact->last_name; + $this->contact_email = $this->contact->email; + $this->client_city = $this->contact->client->city; + $this->client_postal_code = $this->contact->client->postal_code; + + if( + strlen($this->contact_first_name ?? '') == 0 || + strlen($this->contact_last_name ?? '') == 0 || + strlen($this->contact_email ?? '') == 0 || + strlen($this->client_city ?? '') == 0 || + strlen($this->client_postal_code ?? '') == 0 + ) + { + $this->check_rff = true; + } + + return $this; + } + + public function handleRff() + { + + $validated = $this->validate([ + 'contact_first_name' => ['required'], + 'contact_last_name' => ['required'], + 'client_city' => ['required'], + 'client_postal_code' => ['required'], + 'contact_email' => ['required', 'email'], + ]); + + $this->check_rff = false; + + $this->contact->first_name = $validated['contact_first_name']; + $this->contact->last_name = $validated['contact_last_name']; + $this->contact->email = $validated['contact_email']; + $this->contact->client->postal_code = $validated['client_postal_code']; + $this->contact->client->city = $validated['client_city']; + + $this->contact->pushQuietly(); + + $this->refreshComponent(); + + return $this; + } + + protected function refreshComponent() + { + $this->dispatch('$refresh'); + } + /** * Middle method between selecting payment method & * submitting the from to the backend. diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index fd47d0e6feb0..0885db2c6c33 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -351,9 +351,9 @@ class ClientContact extends Authenticatable implements HasLocalePreference public function showRff(): bool { - if (\strlen($this->first_name) === 0 || \strlen($this->last_name) === 0 || \strlen($this->email) === 0) { - return true; - } + // if (\strlen($this->first_name ?? '') === 0 || \strlen($this->last_name ?? '') === 0 || \strlen($this->email ?? '') === 0) { + // return true; + // } return false; } diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 9311699f31e3..62ba38ee8e41 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -159,6 +159,11 @@ class CompanyGateway extends BaseModel protected $touches = []; + public function isPayPal() + { + return in_array($this->gateway_key, ['80af24a6a691230bbec33e930ab40666','80af24a6a691230bbec33e930ab40665']); + } + public function getEntityType() { return self::class; diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index b4628cf411b7..43bbfe8fa719 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -141,23 +141,23 @@ class Gateway extends StaticModel case 20: case 56: return [ - GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], - GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']], + GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'charge.refunded', 'payment_intent.payment_failed']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.refunded','charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'charge.refunded', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], - GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], - GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed',]], + GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], + GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::KLARNA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::SEPA => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::PRZELEWY24 => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::GIROPAY => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::EPS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BANCONTACT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], + GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.refunded', 'charge.failed',]], ]; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout diff --git a/app/Models/Project.php b/app/Models/Project.php index d341a3fb8569..c786dcec0836 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -129,7 +129,7 @@ class Project extends BaseModel public function invoices(): HasMany { - return $this->hasMany(Invoice::class); + return $this->hasMany(Invoice::class)->withTrashed(); } public function quotes(): HasMany diff --git a/app/PaymentDrivers/Forte/ACH.php b/app/PaymentDrivers/Forte/ACH.php index aef043d50a9e..8ea313e77200 100644 --- a/app/PaymentDrivers/Forte/ACH.php +++ b/app/PaymentDrivers/Forte/ACH.php @@ -170,6 +170,9 @@ class ACH ]; $payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED); - return redirect('client/invoices')->withSuccess('Invoice paid.'); + // return redirect('client/invoices')->withSuccess('Invoice paid.'); + + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); + } } diff --git a/app/PaymentDrivers/Forte/CreditCard.php b/app/PaymentDrivers/Forte/CreditCard.php index 67c404190137..5a317f4ec8a7 100644 --- a/app/PaymentDrivers/Forte/CreditCard.php +++ b/app/PaymentDrivers/Forte/CreditCard.php @@ -187,6 +187,8 @@ class CreditCard 'gateway_type_id' => GatewayType::CREDIT_CARD, ]; $payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED); - return redirect('client/invoices')->withSuccess('Invoice paid.'); + // return redirect('client/invoices')->withSuccess('Invoice paid.'); + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); + } } diff --git a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php index 56824c840269..e7f6fe01c145 100644 --- a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php +++ b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php @@ -251,11 +251,11 @@ class PayPalBasePaymentDriver extends BaseDriver [ "address" => [ - "address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1, + "address_line_1" => strlen($this->client->shipping_address1 ?? '') > 1 ? $this->client->shipping_address1 : $this->client->address1, "address_line_2" => $this->client->shipping_address2, - "admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city, - "admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state, - "postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code, + "admin_area_2" => strlen($this->client->shipping_city ?? '') > 1 ? $this->client->shipping_city : $this->client->city, + "admin_area_1" => strlen($this->client->shipping_state ?? '') > 1 ? $this->client->shipping_state : $this->client->state, + "postal_code" => strlen($this->client->shipping_postal_code ?? '') > 1 ? $this->client->shipping_postal_code : $this->client->postal_code, "country_code" => $this->client->present()->shipping_country_code(), ], ] diff --git a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php index 36f766dd7303..405f696742e4 100644 --- a/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php +++ b/app/PaymentDrivers/Stripe/Jobs/ChargeRefunded.php @@ -11,18 +11,22 @@ namespace App\PaymentDrivers\Stripe\Jobs; -use App\Libraries\MultiDB; use App\Models\Company; -use App\Models\CompanyGateway; use App\Models\Payment; +use App\Libraries\MultiDB; use App\Models\PaymentHash; -use App\PaymentDrivers\Stripe\Utilities; +use App\Services\Email\Email; use Illuminate\Bus\Queueable; +use App\Models\CompanyGateway; +use App\Services\Email\EmailObject; +use Illuminate\Support\Facades\App; +use Illuminate\Mail\Mailables\Address; +use Illuminate\Queue\SerializesModels; +use App\PaymentDrivers\Stripe\Utilities; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; -use Illuminate\Queue\SerializesModels; class ChargeRefunded implements ShouldQueue { @@ -36,19 +40,10 @@ class ChargeRefunded implements ShouldQueue public $deleteWhenMissingModels = true; - public $stripe_request; - - public $company_key; - - private $company_gateway_id; - public $payment_completed = false; - public function __construct($stripe_request, $company_key, $company_gateway_id) + public function __construct(public array $stripe_request, private string $company_key) { - $this->stripe_request = $stripe_request; - $this->company_key = $company_key; - $this->company_gateway_id = $company_gateway_id; } public function handle() @@ -64,8 +59,8 @@ class ChargeRefunded implements ShouldQueue $payment_hash_key = $source['metadata']['payment_hash'] ?? null; - $company_gateway = CompanyGateway::query()->find($this->company_gateway_id); $payment_hash = PaymentHash::query()->where('hash', $payment_hash_key)->first(); + $company_gateway = $payment_hash->payment->company_gateway; $stripe_driver = $company_gateway->driver()->init(); @@ -79,7 +74,7 @@ class ChargeRefunded implements ShouldQueue ->first(); //don't touch if already refunded - if(!$payment || in_array($payment->status_id, [Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])) { + if(!$payment || $payment->status_id == Payment::STATUS_REFUNDED || $payment->is_deleted){ return; } @@ -94,8 +89,19 @@ class ChargeRefunded implements ShouldQueue return; } - if($payment->status_id == Payment::STATUS_COMPLETED) { + usleep(rand(200000,300000)); + $payment = $payment->fresh(); + if($payment->status_id == Payment::STATUS_PARTIALLY_REFUNDED){ + //determine the delta in the refunded amount - how much has already been refunded and only apply the delta. + + if(floatval($payment->refunded) >= floatval($amount_refunded)) + return; + + $amount_refunded -= $payment->refunded; + + } + $invoice_collection = $payment->paymentables ->where('paymentable_type', 'invoices') ->map(function ($pivot) { @@ -117,9 +123,24 @@ class ChargeRefunded implements ShouldQueue ]; }); - } elseif($invoice_collection->sum('amount') != $amount_refunded) { - //too many edges cases at this point, return early + } + elseif($invoice_collection->sum('amount') != $amount_refunded) { + + $refund_text = "A partial refund was processed for Payment #{$payment_hash->payment->number}.

This payment is associated with multiple invoices, so you will need to manually apply the refund to the correct invoice/s."; + + App::setLocale($payment_hash->payment->company->getLocale()); + + $mo = new EmailObject(); + $mo->subject = "Refund processed in Stripe for multiple invoices, action required."; + $mo->body = $refund_text; + $mo->text_body = $refund_text; + $mo->company_key = $payment_hash->payment->company->company_key; + $mo->html_template = 'email.template.generic'; + $mo->to = [new Address($payment_hash->payment->company->owner()->email, $payment_hash->payment->company->owner()->present()->name())]; + + Email::dispatch($mo, $payment_hash->payment->company); return; + } $invoices = $invoice_collection->toArray(); @@ -131,20 +152,21 @@ class ChargeRefunded implements ShouldQueue 'date' => now()->format('Y-m-d'), 'gateway_refund' => false, 'email_receipt' => false, + 'via_webhook' => true, ]; nlog($data); $payment->refund($data); - $payment->private_notes .= 'Refunded via Stripe'; - return; - } + $payment->private_notes .= 'Refunded via Stripe '; + + $payment->saveQuietly(); } public function middleware() { - return [new WithoutOverlapping($this->company_gateway_id)]; + return [new WithoutOverlapping($this->company_key)]; } } diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index 69920e31e6b5..d91e39fc3763 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -12,54 +12,55 @@ namespace App\PaymentDrivers; -use App\Exceptions\PaymentFailed; -use App\Exceptions\StripeConnectFailure; -use App\Http\Requests\Payments\PaymentWebhookRequest; -use App\Http\Requests\Request; -use App\Jobs\Util\SystemLogger; -use App\Models\Client; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; -use App\Models\Payment; -use App\Models\PaymentHash; -use App\Models\SystemLog; -use App\PaymentDrivers\Stripe\ACH; -use App\PaymentDrivers\Stripe\ACSS; -use App\PaymentDrivers\Stripe\Alipay; -use App\PaymentDrivers\Stripe\BACS; -use App\PaymentDrivers\Stripe\Bancontact; -use App\PaymentDrivers\Stripe\BankTransfer; -use App\PaymentDrivers\Stripe\BECS; -use App\PaymentDrivers\Stripe\BrowserPay; -use App\PaymentDrivers\Stripe\Charge; -use App\PaymentDrivers\Stripe\Connect\Verify; -use App\PaymentDrivers\Stripe\CreditCard; -use App\PaymentDrivers\Stripe\EPS; -use App\PaymentDrivers\Stripe\FPX; -use App\PaymentDrivers\Stripe\GIROPAY; -use App\PaymentDrivers\Stripe\iDeal; -use App\PaymentDrivers\Stripe\ImportCustomers; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook; -use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; -use App\PaymentDrivers\Stripe\Klarna; -use App\PaymentDrivers\Stripe\PRZELEWY24; -use App\PaymentDrivers\Stripe\SEPA; -use App\PaymentDrivers\Stripe\SOFORT; -use App\PaymentDrivers\Stripe\Utilities; -use App\Utils\Traits\MakesHash; use Exception; -use Illuminate\Http\RedirectResponse; -use Laracasts\Presenter\Exceptions\PresenterException; +use Stripe\Stripe; use Stripe\Account; use Stripe\Customer; -use Stripe\Exception\ApiErrorException; +use App\Models\Client; +use App\Models\Payment; +use Stripe\SetupIntent; +use Stripe\StripeClient; +use App\Models\SystemLog; use Stripe\PaymentIntent; use Stripe\PaymentMethod; -use Stripe\SetupIntent; -use Stripe\Stripe; -use Stripe\StripeClient; +use App\Models\GatewayType; +use App\Models\PaymentHash; +use App\Http\Requests\Request; +use App\Jobs\Util\SystemLogger; +use App\Utils\Traits\MakesHash; +use App\Exceptions\PaymentFailed; +use App\Models\ClientGatewayToken; +use App\PaymentDrivers\Stripe\ACH; +use App\PaymentDrivers\Stripe\EPS; +use App\PaymentDrivers\Stripe\FPX; +use App\PaymentDrivers\Stripe\ACSS; +use App\PaymentDrivers\Stripe\BACS; +use App\PaymentDrivers\Stripe\BECS; +use App\PaymentDrivers\Stripe\SEPA; +use App\PaymentDrivers\Stripe\iDeal; +use App\PaymentDrivers\Stripe\Alipay; +use App\PaymentDrivers\Stripe\Charge; +use App\PaymentDrivers\Stripe\Klarna; +use App\PaymentDrivers\Stripe\SOFORT; +use Illuminate\Http\RedirectResponse; +use App\PaymentDrivers\Stripe\GIROPAY; +use Stripe\Exception\ApiErrorException; +use App\Exceptions\StripeConnectFailure; +use App\PaymentDrivers\Stripe\Utilities; +use App\PaymentDrivers\Stripe\Bancontact; +use App\PaymentDrivers\Stripe\BrowserPay; +use App\PaymentDrivers\Stripe\CreditCard; +use App\PaymentDrivers\Stripe\PRZELEWY24; +use App\PaymentDrivers\Stripe\BankTransfer; +use App\PaymentDrivers\Stripe\Connect\Verify; +use App\PaymentDrivers\Stripe\ImportCustomers; +use App\PaymentDrivers\Stripe\Jobs\ChargeRefunded; +use App\Http\Requests\Payments\PaymentWebhookRequest; +use Laracasts\Presenter\Exceptions\PresenterException; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentProcessingWebhook; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentPartiallyFundedWebhook; class StripePaymentDriver extends BaseDriver { @@ -670,31 +671,39 @@ class StripePaymentDriver extends BaseDriver public function processWebhookRequest(PaymentWebhookRequest $request) { + nlog($request->all()); + if ($request->type === 'customer.source.updated') { $ach = new ACH($this); $ach->updateBankAccount($request->all()); } if ($request->type === 'payment_intent.processing') { - PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 12))); + PaymentIntentProcessingWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5)); return response()->json([], 200); } //payment_intent.succeeded - this will confirm or cancel the payment if ($request->type === 'payment_intent.succeeded') { - PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 15))); + PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5)); return response()->json([], 200); } if ($request->type === 'payment_intent.partially_funded') { - PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(10, 15))); + PaymentIntentPartiallyFundedWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(5)); return response()->json([], 200); } if (in_array($request->type, ['payment_intent.payment_failed', 'charge.failed'])) { - PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(5, 10))); + PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(2)); + + return response()->json([], 200); + } + + if ($request->type === 'charge.refunded' && $request->data['object']['status'] == 'succeeded') { + ChargeRefunded::dispatch($request->data, $request->company_key)->delay(now()->addSeconds(5)); return response()->json([], 200); } @@ -702,7 +711,6 @@ class StripePaymentDriver extends BaseDriver if ($request->type === 'charge.succeeded') { foreach ($request->data as $transaction) { - $payment = Payment::query() ->where('company_id', $this->company_gateway->company_id) ->where(function ($query) use ($transaction) { diff --git a/app/Services/Client/PaymentMethod.php b/app/Services/Client/PaymentMethod.php index de5e1622585d..7ca8965d3fc9 100644 --- a/app/Services/Client/PaymentMethod.php +++ b/app/Services/Client/PaymentMethod.php @@ -192,6 +192,7 @@ class PaymentMethod 'label' => ctrans('texts.apply_credit'), 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, 'gateway_type_id' => GatewayType::CREDIT, + 'is_paypal' => false, ]; } @@ -210,12 +211,14 @@ class PaymentMethod 'label' => $gateway->getConfigField('name').$fee_label, 'company_gateway_id' => $gateway->id, 'gateway_type_id' => GatewayType::CREDIT_CARD, + 'is_paypal' => $gateway->isPayPal(), ]; } else { $this->payment_urls[] = [ 'label' => $gateway->getTypeAlias($type).$fee_label, 'company_gateway_id' => $gateway->id, 'gateway_type_id' => $type, + 'is_paypal' => $gateway->isPayPal(), ]; } @@ -236,12 +239,14 @@ class PaymentMethod 'label' => $gateway->getConfigField('name').$fee_label, 'company_gateway_id' => $gateway_id, 'gateway_type_id' => GatewayType::CREDIT_CARD, + 'is_paypal' => $gateway->isPayPal(), ]; } else { $this->payment_urls[] = [ 'label' => $gateway->getTypeAlias($gateway_type_id).$fee_label, 'company_gateway_id' => $gateway_id, 'gateway_type_id' => $gateway_type_id, + 'is_paypal' => $gateway->isPayPal(), ]; } } @@ -259,6 +264,7 @@ class PaymentMethod 'label' => ctrans('texts.apply_credit'), 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, 'gateway_type_id' => GatewayType::CREDIT, + 'is_paypal' => false, ]; } diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index d46052fafb03..f41e4ff1b45d 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -44,17 +44,16 @@ class InstantPayment public function run() { - nlog($this->request->all()); - /** @var \App\Models\ClientContact $cc */ - $cc = auth()->guard('contact')->user(); - $cc->first_name = $this->request->contact_first_name; $cc->last_name = $this->request->contact_last_name; $cc->email = $this->request->contact_email; - - $cc->save(); + $cc->client->postal_code = strlen($cc->client->postal_code ?? '') > 1 ? $cc->client->postal_code : $this->request->client_postal_code; + $cc->client->city = strlen($cc->client->city ?? '') > 1 ? $cc->client->city : $this->request->client_city; + $cc->client->shipping_postal_code = strlen($cc->client->shipping_postal_code ?? '') > 1 ? $cc->client->shipping_postal_code : $cc->client->postal_code; + $cc->client->shipping_city = strlen($cc->client->shipping_city ?? '') > 1 ? $cc->client->shipping_city : $cc->client->city; + $cc->pushQuietly(); $is_credit_payment = false; @@ -73,8 +72,6 @@ class InstantPayment */ $payable_invoices = collect($this->request->payable_invoices); - nlog($payable_invoices); - $invoices = Invoice::query()->whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->withTrashed()->get(); $invoices->each(function ($invoice) { diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index a9c9f415396d..53c51145d859 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -42,6 +42,9 @@ class AutoBillInvoice extends AbstractService public function __construct(private Invoice $invoice, protected string $db) { + + $this->client = $this->invoice->client; + } public function run() @@ -49,8 +52,7 @@ class AutoBillInvoice extends AbstractService MultiDB::setDb($this->db); /* @var \App\Modesl\Client $client */ - $this->client = $this->invoice->client; - + $is_partial = false; /* Is the invoice payable? */ @@ -272,7 +274,7 @@ class AutoBillInvoice extends AbstractService * * @return self */ - private function applyUnappliedPayment(): self + public function applyUnappliedPayment(): self { $unapplied_payments = Payment::query() ->where('client_id', $this->client->id) @@ -284,6 +286,11 @@ class AutoBillInvoice extends AbstractService ->get(); $available_unapplied_balance = $unapplied_payments->sum('amount') - $unapplied_payments->sum('applied'); + + nlog($this->client->id); + nlog($this->invoice->id); + nlog($unapplied_payments->sum('amount')); + nlog($unapplied_payments->sum('applied')); nlog("available unapplied balance = {$available_unapplied_balance}"); @@ -347,7 +354,7 @@ class AutoBillInvoice extends AbstractService * * @return $this */ - private function applyCreditPayment(): self + public function applyCreditPayment(): self { $available_credits = Credit::query()->where('client_id', $this->client->id) ->where('is_deleted', false) diff --git a/app/Services/Payment/RefundPayment.php b/app/Services/Payment/RefundPayment.php index a17baa67051e..ed7b51deed20 100644 --- a/app/Services/Payment/RefundPayment.php +++ b/app/Services/Payment/RefundPayment.php @@ -44,7 +44,6 @@ class RefundPayment ->setStatus() //sets status of payment ->updatePaymentables() //update the paymentable items ->adjustInvoices() - ->finalize() ->save(); if (array_key_exists('email_receipt', $this->refund_data) && $this->refund_data['email_receipt'] == 'true') { @@ -52,10 +51,11 @@ class RefundPayment EmailRefundPayment::dispatch($this->payment, $this->payment->company, $contact); } - $notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : "; - $notes .= $this->refund_data['gateway_refund'] !== false ? ctrans('texts.yes') : ctrans('texts.no'); - + $is_gateway_refund = ($this->refund_data['gateway_refund'] !== false || $this->refund_failed || (isset($this->refund_data['via_webhook']) && $this->refund_data['via_webhook'] !== false)) ? ctrans('texts.yes') : ctrans('texts.no'); + $notes = ctrans('texts.refunded') . " : {$this->total_refund} - " . ctrans('texts.gateway_refund') . " : " . $is_gateway_refund; + $this->createActivity($notes); + $this->finalize(); return $this->payment; } @@ -178,7 +178,7 @@ class RefundPayment */ private function setStatus() { - if ($this->total_refund == $this->payment->amount) { + if ($this->total_refund == $this->payment->amount || floatval($this->payment->amount) == floatval($this->payment->refunded)) { $this->payment->status_id = Payment::STATUS_REFUNDED; } else { $this->payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED; diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index da88156c8d70..af1ea653f64e 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -88,7 +88,7 @@ class SubscriptionService // if we have a recurring product - then generate a recurring invoice - if (strlen($this->subscription->recurring_product_ids) >= 1) { + if (strlen($this->subscription->recurring_product_ids ?? '') >= 1) { if (isset($payment_hash->data->billing_context->bundle)) { $recurring_invoice = $this->convertInvoiceToRecurringBundle($payment_hash->payment->client_id, $payment_hash->data->billing_context->bundle); } else { @@ -1024,10 +1024,10 @@ class SubscriptionService $invoice->subscription_id = $this->subscription->id; $invoice->is_proforma = true; - if (strlen($data['coupon']) >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { + if (strlen($data['coupon'] ?? '') >= 1 && ($data['coupon'] == $this->subscription->promo_code) && $this->subscription->promo_discount > 0) { $invoice->discount = $this->subscription->promo_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount; - } elseif (strlen($this->subscription->promo_code) == 0 && $this->subscription->promo_discount > 0) { + } elseif (strlen($this->subscription->promo_code ?? '') == 0 && $this->subscription->promo_discount > 0) { $invoice->discount = $this->subscription->promo_discount; $invoice->is_amount_discount = $this->subscription->is_amount_discount; } @@ -1118,7 +1118,7 @@ class SubscriptionService */ public function triggerWebhook($context) { - if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) { //@phpstan-ignore-line + if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url'] ?? '') < 1) { //@phpstan-ignore-line return ["message" => "Success", "status_code" => 200]; } @@ -1436,7 +1436,7 @@ class SubscriptionService */ public function handleNoPaymentFlow(Invoice $invoice, $bundle, ClientContact $contact) { - if (strlen($this->subscription->recurring_product_ids) >= 1) { + if (strlen($this->subscription->recurring_product_ids ?? '') >= 1) { $recurring_invoice = $this->convertInvoiceToRecurringBundle($contact->client_id, collect($bundle)->map(function ($bund) { return (object) $bund; })); @@ -1492,7 +1492,7 @@ class SubscriptionService */ private function handleRedirect($default_redirect) { - if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url']) >= 1) { + if (array_key_exists('return_url', $this->subscription->webhook_configuration) && strlen($this->subscription->webhook_configuration['return_url'] ?? '') >= 1) { return method_exists(redirect(), "send") ? redirect($this->subscription->webhook_configuration['return_url'])->send() : redirect($this->subscription->webhook_configuration['return_url']); } diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 5a033c2f5383..13adb454d7f1 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -1023,7 +1023,8 @@ class TemplateService 'vat_number' => $project->client->vat_number ?? '', 'currency' => $project->client->currency()->code ?? 'USD', ] : [], - 'user' => $this->userInfo($project->user) + 'user' => $this->userInfo($project->user), + 'invoices' => $this->processInvoices($project->invoices) ]; } diff --git a/config/ninja.php b/config/ninja.php index c832656df1e2..d2ff07a5bb05 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -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.10.13'), - 'app_tag' => env('APP_TAG', '5.10.13'), + 'app_version' => env('APP_VERSION', '5.10.16'), + 'app_tag' => env('APP_TAG', '5.10.16'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), diff --git a/lang/en/texts.php b/lang/en/texts.php index 7e8a04578b82..eeab2f9c1673 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5124,7 +5124,7 @@ $lang = array( 'all_contacts' => 'All Contacts', 'insert_below' => 'Insert Below', 'nordigen_handler_subtitle' => 'Bank account authentication. Selecting your institution to complete the request with your account credentials.', - 'nordigen_handler_error_heading_unknown' => 'An error has occured', + 'nordigen_handler_error_heading_unknown' => 'An error has occurred', 'nordigen_handler_error_contents_unknown' => 'An unknown error has occurred! Reason:', 'nordigen_handler_error_heading_token_invalid' => 'Invalid Token', 'nordigen_handler_error_contents_token_invalid' => 'The provided token was invalid. Contact support for help, if this issue persists.', diff --git a/public/build/assets/payment-1bdbd169.js b/public/build/assets/payment-1bdbd169.js deleted file mode 100644 index c0133cd9aad5..000000000000 --- a/public/build/assets/payment-1bdbd169.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Invoice Ninja (https://invoiceninja.com) - * - * @link https://github.com/invoiceninja/invoiceninja source repository - * - * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) - * - * @license https://www.elastic.co/licensing/elastic-license - */class s{constructor(t,e,a){this.shouldDisplayTerms=t,this.shouldDisplaySignature=e,this.shouldDisplayRff=a,this.submitting=!1,this.steps=new Map,this.shouldDisplayRff&&this.steps.set("rff",{element:document.getElementById("displayRequiredFieldsModal"),nextButton:document.getElementById("rff-next-step"),callback:()=>{const n={firstName:document.querySelector('input[name="rff_first_name"]'),lastName:document.querySelector('input[name="rff_last_name"]'),email:document.querySelector('input[name="rff_email"]')};n.firstName&&(document.querySelector('input[name="contact_first_name"]').value=n.firstName.value),n.lastName&&(document.querySelector('input[name="contact_last_name"]').value=n.lastName.value),n.email&&(document.querySelector('input[name="contact_email"]').value=n.email.value)}}),this.shouldDisplaySignature&&this.steps.set("signature",{element:document.getElementById("displaySignatureModal"),nextButton:document.getElementById("signature-next-step"),boot:()=>this.signaturePad=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"}),callback:()=>document.querySelector('input[name="signature"').value=this.signaturePad.toDataURL()}),this.shouldDisplayTerms&&this.steps.set("terms",{element:document.getElementById("displayTermsModal"),nextButton:document.getElementById("accept-terms-button")})}handleMethodSelect(t){if(document.getElementById("company_gateway_id").value=t.dataset.companyGatewayId,document.getElementById("payment_method_id").value=t.dataset.gatewayTypeId,this.steps.size===0)return this.submitForm();const e=this.steps.values().next().value;e.element.removeAttribute("style"),e.boot&&e.boot(),console.log(e),e.nextButton.addEventListener("click",()=>{e.element.setAttribute("style","display: none;"),this.steps=new Map(Array.from(this.steps.entries()).slice(1)),e.callback&&e.callback(),this.handleMethodSelect(t)})}submitForm(){this.submitting=!0,document.getElementById("payment-form").submit()}handle(){document.querySelectorAll(".dropdown-gateway-button").forEach(t=>{t.addEventListener("click",()=>{this.submitting||this.handleMethodSelect(t)})})}}const i=document.querySelector('meta[name="require-invoice-signature"]').content,o=document.querySelector('meta[name="show-invoice-terms"]').content,l=document.querySelector('meta[name="show-required-fields-form"]').content;new s(!!+o,!!+i,!!+l).handle(); diff --git a/public/build/assets/payment-292ee4d0.js b/public/build/assets/payment-292ee4d0.js new file mode 100644 index 000000000000..d3f7782bcb95 --- /dev/null +++ b/public/build/assets/payment-292ee4d0.js @@ -0,0 +1,9 @@ +/** + * Invoice Ninja (https://invoiceninja.com) + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */class a{constructor(t,n){this.shouldDisplayTerms=t,this.shouldDisplaySignature=n,this.submitting=!1,this.steps=new Map,this.steps.set("rff",{element:document.getElementById("displayRequiredFieldsModal"),nextButton:document.getElementById("rff-next-step"),callback:()=>{const e={firstName:document.querySelector('input[name="rff_first_name"]'),lastName:document.querySelector('input[name="rff_last_name"]'),email:document.querySelector('input[name="rff_email"]'),city:document.querySelector('input[name="rff_city"]'),postalCode:document.querySelector('input[name="rff_postal_code"]')};e.firstName&&(document.querySelector('input[name="contact_first_name"]').value=e.firstName.value),e.lastName&&(document.querySelector('input[name="contact_last_name"]').value=e.lastName.value),e.email&&(document.querySelector('input[name="contact_email"]').value=e.email.value),e.city&&(document.querySelector('input[name="client_city"]').value=e.city.value),e.postalCode&&(document.querySelector('input[name="client_postal_code"]').value=e.postalCode.value)}}),this.shouldDisplaySignature&&this.steps.set("signature",{element:document.getElementById("displaySignatureModal"),nextButton:document.getElementById("signature-next-step"),boot:()=>this.signaturePad=new SignaturePad(document.getElementById("signature-pad"),{penColor:"rgb(0, 0, 0)"}),callback:()=>document.querySelector('input[name="signature"').value=this.signaturePad.toDataURL()}),this.shouldDisplayTerms&&this.steps.set("terms",{element:document.getElementById("displayTermsModal"),nextButton:document.getElementById("accept-terms-button")})}handleMethodSelect(t){document.getElementById("company_gateway_id").value=t.dataset.companyGatewayId,document.getElementById("payment_method_id").value=t.dataset.gatewayTypeId;const n=document.querySelector('input[name="contact_first_name"').value.length>=1&&document.querySelector('input[name="contact_last_name"').value.length>=1&&document.querySelector('input[name="contact_email"').value.length>=1&&document.querySelector('input[name="client_city"').value.length>=1&&document.querySelector('input[name="client_postal_code"').value.length>=1;if((t.dataset.isPaypal!="1"||n)&&this.steps.delete("rff"),this.steps.size===0)return this.submitForm();const e=this.steps.values().next().value;e.element.removeAttribute("style"),e.boot&&e.boot(),console.log(e),e.nextButton.addEventListener("click",()=>{e.element.setAttribute("style","display: none;"),this.steps=new Map(Array.from(this.steps.entries()).slice(1)),e.callback&&e.callback(),this.handleMethodSelect(t)})}submitForm(){this.submitting=!0,document.getElementById("payment-form").submit()}handle(){document.querySelectorAll(".dropdown-gateway-button").forEach(t=>{t.addEventListener("click",()=>{this.submitting||this.handleMethodSelect(t)})})}}const l=document.querySelector('meta[name="require-invoice-signature"]').content,o=document.querySelector('meta[name="show-invoice-terms"]').content;new a(!!+o,!!+l).handle(); diff --git a/public/build/manifest.json b/public/build/manifest.json index 5052761badb2..f6047f4d4c3c 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -23,7 +23,7 @@ "src": "resources/js/clients/invoices/action-selectors.js" }, "resources/js/clients/invoices/payment.js": { - "file": "assets/payment-1bdbd169.js", + "file": "assets/payment-292ee4d0.js", "isEntry": true, "src": "resources/js/clients/invoices/payment.js" }, @@ -240,7 +240,7 @@ "src": "resources/js/setup/setup.js" }, "resources/sass/app.scss": { - "file": "assets/app-02bc3b96.css", + "file": "assets/app-039bd735.css", "isEntry": true, "src": "resources/sass/app.scss" } diff --git a/resources/js/clients/invoices/payment.js b/resources/js/clients/invoices/payment.js index a384f94dcfa0..1026dadeb593 100644 --- a/resources/js/clients/invoices/payment.js +++ b/resources/js/clients/invoices/payment.js @@ -9,39 +9,47 @@ */ class Payment { - constructor(displayTerms, displaySignature, displayRff) { + constructor(displayTerms, displaySignature) { this.shouldDisplayTerms = displayTerms; this.shouldDisplaySignature = displaySignature; - this.shouldDisplayRff = displayRff; - + this.submitting = false; this.steps = new Map() - if (this.shouldDisplayRff) { - this.steps.set("rff", { - element: document.getElementById('displayRequiredFieldsModal'), - nextButton: document.getElementById('rff-next-step'), - callback: () => { - const fields = { - firstName: document.querySelector('input[name="rff_first_name"]'), - lastName: document.querySelector('input[name="rff_last_name"]'), - email: document.querySelector('input[name="rff_email"]'), - } - - if (fields.firstName) { - document.querySelector('input[name="contact_first_name"]').value = fields.firstName.value; - } - - if (fields.lastName) { - document.querySelector('input[name="contact_last_name"]').value = fields.lastName.value; - } - - if (fields.email) { - document.querySelector('input[name="contact_email"]').value = fields.email.value; - } + this.steps.set("rff", { + element: document.getElementById('displayRequiredFieldsModal'), + nextButton: document.getElementById('rff-next-step'), + callback: () => { + const fields = { + firstName: document.querySelector('input[name="rff_first_name"]'), + lastName: document.querySelector('input[name="rff_last_name"]'), + email: document.querySelector('input[name="rff_email"]'), + city: document.querySelector('input[name="rff_city"]'), + postalCode: document.querySelector('input[name="rff_postal_code"]'), } - }); - } + + if (fields.firstName) { + document.querySelector('input[name="contact_first_name"]').value = fields.firstName.value; + } + + if (fields.lastName) { + document.querySelector('input[name="contact_last_name"]').value = fields.lastName.value; + } + + if (fields.email) { + document.querySelector('input[name="contact_email"]').value = fields.email.value; + } + + if (fields.city) { + document.querySelector('input[name="client_city"]').value = fields.city.value; + } + + if (fields.postalCode) { + document.querySelector('input[name="client_postal_code"]').value = fields.postalCode.value; + } + + } + }); if (this.shouldDisplaySignature) { this.steps.set("signature", { @@ -71,7 +79,17 @@ class Payment { element.dataset.companyGatewayId; document.getElementById("payment_method_id").value = element.dataset.gatewayTypeId; - + + const filledRff = document.querySelector('input[name="contact_first_name"').value.length >=1 && + document.querySelector('input[name="contact_last_name"').value.length >= 1 && + document.querySelector('input[name="contact_email"').value.length >= 1 && + document.querySelector('input[name="client_city"').value.length >= 1 && + document.querySelector('input[name="client_postal_code"').value.length >= 1; + + if (element.dataset.isPaypal != '1' || filledRff) { + this.steps.delete("rff"); + } + if (this.steps.size === 0) { return this.submitForm(); } @@ -124,6 +142,5 @@ const signature = document.querySelector( ).content; const terms = document.querySelector('meta[name="show-invoice-terms"]').content; -const rff = document.querySelector('meta[name="show-required-fields-form"]').content; -new Payment(Boolean(+terms), Boolean(+signature), Boolean(+rff)).handle(); +new Payment(Boolean(+terms), Boolean(+signature)).handle(); diff --git a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php index 3c97e48959be..4af98edc0409 100644 --- a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchase.blade.php @@ -141,12 +141,15 @@ + + + @if($steps['started_payment'] == false) @foreach($this->methods as $method) @@ -189,27 +192,41 @@
@csrf - @if(strlen($contact->first_name) === 0) + @if(strlen($contact->first_name ?? '') === 0)
@endif - @if(strlen($contact->last_name) === 0) + @if(strlen($contact->last_name ?? '') === 0) @endif - @if(strlen($contact->email) === 0) + @if(strlen($contact->email ?? '') === 0) @endif + @if(strlen($client_postal_code ?? '') === 0) + + @endif + + @if(strlen($client_city ?? '') === 0) + + @endif +
+ + @elseif(count($methods) > 0 && $check_rff) + + @if($errors->any()) +
+ @foreach($errors->all() as $error) +

{{ $error }}

+ @endforeach +
+ @endif +
+ @csrf + + @if(strlen($contact->first_name ?? '') === 0) + + @endif + + @if(strlen($contact->last_name ?? '') === 0) + + @endif + + @if(strlen($contact->email ?? '') === 0) + + @endif + + @if(strlen($client_postal_code ?? '') === 0) + + @endif + + @if(strlen($client_city ?? '') === 0) + + @endif + + +
+ @elseif(count($methods) > 0)
@foreach($methods as $method) diff --git a/resources/views/portal/ninja2020/components/livewire/pay-now-dropdown.blade.php b/resources/views/portal/ninja2020/components/livewire/pay-now-dropdown.blade.php index 1f1d02e2ad1f..e8ddde996e39 100644 --- a/resources/views/portal/ninja2020/components/livewire/pay-now-dropdown.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/pay-now-dropdown.blade.php @@ -23,6 +23,7 @@ {{ \App\Models\CompanyGateway::find($method['company_gateway_id'])->firstOrFail()->getConfigField('name') }} @@ -31,6 +32,7 @@ {{ $method['label'] }} diff --git a/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php b/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php index 91520780ad8c..b67881b60065 100644 --- a/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/subscription-plan-switch.blade.php @@ -83,7 +83,7 @@ @if(!$state['payment_initialised']) @foreach($this->methods as $method) diff --git a/resources/views/portal/ninja2020/invoices/includes/required-fields.blade.php b/resources/views/portal/ninja2020/invoices/includes/required-fields.blade.php index 7e213902bb03..161d9c600a8c 100644 --- a/resources/views/portal/ninja2020/invoices/includes/required-fields.blade.php +++ b/resources/views/portal/ninja2020/invoices/includes/required-fields.blade.php @@ -2,7 +2,7 @@ style="display: none" id="displayRequiredFieldsModal" class="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center" - x-data="{ open: true }" + x-data="formValidation()" >
-
+

{{ ctrans('texts.details') }}

@if(strlen(auth()->guard('contact')->user()->first_name) === 0)
- - + + +
@endif @if(strlen(auth()->guard('contact')->user()->last_name) === 0)
- - + + + +
@endif @if(strlen(auth()->guard('contact')->user()->email) === 0)
- + + +
@endif + + @if(strlen(auth()->guard('contact')->user()->client->city) === 0) +
+ + + + +
+ @endif + + @if(strlen(auth()->guard('contact')->user()->client->postal_code) === 0) +
+ + + + +
+ @endif +
-
+
+ class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"> + +
@@ -102,3 +166,64 @@
+ + + diff --git a/resources/views/portal/ninja2020/invoices/payment.blade.php b/resources/views/portal/ninja2020/invoices/payment.blade.php index f11b69c72361..2142c23a8f5c 100644 --- a/resources/views/portal/ninja2020/invoices/payment.blade.php +++ b/resources/views/portal/ninja2020/invoices/payment.blade.php @@ -4,7 +4,6 @@ @push('head') - @endpush @@ -22,6 +21,9 @@ + + +
diff --git a/resources/views/portal/ninja2020/invoices/show.blade.php b/resources/views/portal/ninja2020/invoices/show.blade.php index 172520e9e85a..f2a4e99b5a55 100644 --- a/resources/views/portal/ninja2020/invoices/show.blade.php +++ b/resources/views/portal/ninja2020/invoices/show.blade.php @@ -4,7 +4,6 @@ @push('head') - @include('portal.ninja2020.components.no-cache') @@ -47,6 +46,8 @@ + +
diff --git a/routes/api.php b/routes/api.php index 0b0ffa772b15..2a5a2350f191 100644 --- a/routes/api.php +++ b/routes/api.php @@ -164,6 +164,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('charts/totals_v2', [ChartController::class, 'totalsV2'])->name('chart.totals_v2'); Route::post('charts/chart_summary_v2', [ChartController::class, 'chart_summaryV2'])->name('chart.chart_summary_v2'); + Route::post('charts/calculated_fields', [ChartController::class, 'calculatedFields'])->name('chart.calculated_fields'); Route::post('claim_license', [LicenseController::class, 'index'])->name('license.index'); diff --git a/tests/Feature/Import/Wave/WaveTest.php b/tests/Feature/Import/Wave/WaveTest.php index f8f3edf47fc7..3a194dd0b7d8 100644 --- a/tests/Feature/Import/Wave/WaveTest.php +++ b/tests/Feature/Import/Wave/WaveTest.php @@ -196,7 +196,7 @@ class WaveTest extends TestCase 'import_type' => 'waveaccounting', ]; - Cache::put($hash.'-expense', base64_encode($csv), 360); + Cache::put($hash.'-invoice', base64_encode($csv), 360); $csv_importer = new Wave($data, $this->company);